mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
249 lines
8.5 KiB
Python
249 lines
8.5 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from common.assembly_spec import (
|
|
AssemblySpec,
|
|
AssemblySpecError,
|
|
AssemblyNodeSpec,
|
|
IDENTITY_TRANSFORM,
|
|
assembly_spec_children,
|
|
multiply_transforms,
|
|
read_assembly_spec,
|
|
)
|
|
from common.catalog import cad_ref_from_step_path, find_source_by_path, source_from_path
|
|
from common.render import part_glb_path, relative_to_repo
|
|
|
|
|
|
class AssemblyResolutionError(ValueError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CatalogEntry:
|
|
cad_ref: str
|
|
source_ref: str
|
|
kind: str
|
|
source_path: Path
|
|
step_path: Path | None = None
|
|
assembly_spec: AssemblySpec | None = None
|
|
stl_path: Path | None = None
|
|
glb_path: Path | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ResolvedPartInstance:
|
|
instance_path: tuple[str, ...]
|
|
cad_ref: str
|
|
name: str | None
|
|
color: str | None
|
|
use_source_colors: bool
|
|
step_path: Path
|
|
stl_path: Path | None
|
|
glb_path: Path
|
|
transform: tuple[float, ...]
|
|
|
|
|
|
EntryResolver = Callable[[Path], CatalogEntry | None]
|
|
|
|
|
|
def filesystem_entry(source_path: Path) -> CatalogEntry | None:
|
|
resolved = source_path.resolve()
|
|
if resolved.suffix.lower() in {".step", ".stp"}:
|
|
source = find_source_by_path(resolved)
|
|
if source is not None and source.kind == "assembly" and source.script_path is not None:
|
|
try:
|
|
assembly_spec = read_assembly_spec(source.source_path)
|
|
except AssemblySpecError as exc:
|
|
raise AssemblyResolutionError(str(exc)) from exc
|
|
return CatalogEntry(
|
|
cad_ref=source.cad_ref,
|
|
source_ref=source.source_ref,
|
|
kind="assembly",
|
|
source_path=source.source_path,
|
|
step_path=source.step_path,
|
|
assembly_spec=assembly_spec,
|
|
)
|
|
if source is not None and source.step_path is not None:
|
|
return CatalogEntry(
|
|
cad_ref=source.cad_ref,
|
|
source_ref=source.source_ref,
|
|
kind="part",
|
|
source_path=source.source_path,
|
|
step_path=source.step_path,
|
|
stl_path=source.stl_path,
|
|
glb_path=part_glb_path(source.step_path),
|
|
)
|
|
if not resolved.is_file():
|
|
return None
|
|
cad_ref = cad_ref_from_step_path(resolved)
|
|
return CatalogEntry(
|
|
cad_ref=cad_ref,
|
|
source_ref=relative_to_repo(resolved),
|
|
kind="part",
|
|
source_path=resolved,
|
|
step_path=resolved,
|
|
glb_path=part_glb_path(resolved),
|
|
)
|
|
source = source_from_path(resolved) if resolved.exists() else None
|
|
if source is None:
|
|
source = find_source_by_path(source_path)
|
|
if source is None:
|
|
return None
|
|
if source.kind == "assembly" and source.script_path is not None:
|
|
try:
|
|
assembly_spec = read_assembly_spec(source.source_path)
|
|
except AssemblySpecError as exc:
|
|
raise AssemblyResolutionError(str(exc)) from exc
|
|
return CatalogEntry(
|
|
cad_ref=source.cad_ref,
|
|
source_ref=source.source_ref,
|
|
kind="assembly",
|
|
source_path=source.source_path,
|
|
assembly_spec=assembly_spec,
|
|
)
|
|
if source.step_path is None:
|
|
return None
|
|
return CatalogEntry(
|
|
cad_ref=source.cad_ref,
|
|
source_ref=source.source_ref,
|
|
kind="part",
|
|
source_path=source.source_path,
|
|
step_path=source.step_path,
|
|
stl_path=source.stl_path,
|
|
glb_path=part_glb_path(source.step_path),
|
|
)
|
|
|
|
|
|
def flatten_entry(
|
|
entry: CatalogEntry,
|
|
*,
|
|
resolve_entry: EntryResolver,
|
|
parent_transform: tuple[float, ...] = IDENTITY_TRANSFORM,
|
|
parent_color: str | None = None,
|
|
parent_use_source_colors: bool = True,
|
|
instance_path: tuple[str, ...] = (),
|
|
stack: tuple[str, ...] = (),
|
|
) -> tuple[ResolvedPartInstance, ...]:
|
|
if entry.kind == "part":
|
|
if entry.step_path is None or entry.glb_path is None:
|
|
raise AssemblyResolutionError(f"Part source {entry.source_ref} is missing STEP or GLB source paths")
|
|
return (
|
|
ResolvedPartInstance(
|
|
instance_path=instance_path or (entry.cad_ref,),
|
|
cad_ref=entry.cad_ref,
|
|
name=None,
|
|
color=parent_color,
|
|
use_source_colors=parent_use_source_colors,
|
|
step_path=entry.step_path,
|
|
stl_path=entry.stl_path,
|
|
glb_path=entry.glb_path,
|
|
transform=parent_transform,
|
|
),
|
|
)
|
|
|
|
assembly_spec = entry.assembly_spec
|
|
if assembly_spec is None:
|
|
raise AssemblyResolutionError(f"Assembly source {entry.source_ref} is missing assembly spec data")
|
|
if entry.source_ref in stack:
|
|
cycle = " -> ".join((*stack, entry.source_ref))
|
|
raise AssemblyResolutionError(f"Assembly cycle detected: {cycle}")
|
|
|
|
resolved_parts: list[ResolvedPartInstance] = []
|
|
next_stack = (*stack, entry.source_ref)
|
|
for child in assembly_spec_children(assembly_spec):
|
|
resolved_parts.extend(
|
|
_flatten_node(
|
|
child,
|
|
parent_source_ref=entry.source_ref,
|
|
resolve_entry=resolve_entry,
|
|
parent_transform=parent_transform,
|
|
parent_color=parent_color,
|
|
parent_use_source_colors=parent_use_source_colors,
|
|
instance_path=instance_path,
|
|
stack=next_stack,
|
|
)
|
|
)
|
|
return tuple(resolved_parts)
|
|
|
|
|
|
def _flatten_node(
|
|
node: AssemblyNodeSpec,
|
|
*,
|
|
parent_source_ref: str,
|
|
resolve_entry: EntryResolver,
|
|
parent_transform: tuple[float, ...],
|
|
parent_color: str | None,
|
|
parent_use_source_colors: bool,
|
|
instance_path: tuple[str, ...],
|
|
stack: tuple[str, ...],
|
|
) -> tuple[ResolvedPartInstance, ...]:
|
|
child_transform = multiply_transforms(parent_transform, node.transform)
|
|
child_color = parent_color
|
|
child_use_source_colors = parent_use_source_colors and node.use_source_colors
|
|
child_instance_path = (*instance_path, node.instance_id) if instance_path else (node.instance_id,)
|
|
|
|
if node.children:
|
|
resolved_parts: list[ResolvedPartInstance] = []
|
|
for child in node.children:
|
|
resolved_parts.extend(
|
|
_flatten_node(
|
|
child,
|
|
parent_source_ref=parent_source_ref,
|
|
resolve_entry=resolve_entry,
|
|
parent_transform=child_transform,
|
|
parent_color=child_color,
|
|
parent_use_source_colors=child_use_source_colors,
|
|
instance_path=child_instance_path,
|
|
stack=stack,
|
|
)
|
|
)
|
|
return tuple(resolved_parts)
|
|
|
|
if node.source_path is None or node.path is None:
|
|
raise AssemblyResolutionError(f"Assembly source {parent_source_ref} contains node {node.name!r} without a STEP path")
|
|
child_entry = resolve_entry(node.source_path)
|
|
if child_entry is None:
|
|
raise AssemblyResolutionError(
|
|
f"Assembly source {parent_source_ref} references missing CAD source {node.path}"
|
|
)
|
|
if child_entry.kind == "part":
|
|
if child_entry.step_path is None or child_entry.glb_path is None:
|
|
raise AssemblyResolutionError(
|
|
f"Part source {child_entry.source_ref} is missing STEP or GLB source paths"
|
|
)
|
|
return (
|
|
ResolvedPartInstance(
|
|
instance_path=child_instance_path,
|
|
cad_ref=child_entry.cad_ref,
|
|
name=node.name,
|
|
color=child_color,
|
|
use_source_colors=child_use_source_colors,
|
|
step_path=child_entry.step_path,
|
|
stl_path=child_entry.stl_path,
|
|
glb_path=child_entry.glb_path,
|
|
transform=child_transform,
|
|
),
|
|
)
|
|
return flatten_entry(
|
|
child_entry,
|
|
resolve_entry=resolve_entry,
|
|
parent_transform=child_transform,
|
|
parent_color=child_color,
|
|
parent_use_source_colors=child_use_source_colors,
|
|
instance_path=child_instance_path,
|
|
stack=stack,
|
|
)
|
|
|
|
|
|
def flatten_source_path(
|
|
source_path: Path,
|
|
*,
|
|
resolve_entry: EntryResolver = filesystem_entry,
|
|
) -> tuple[ResolvedPartInstance, ...]:
|
|
entry = resolve_entry(source_path)
|
|
if entry is None:
|
|
raise AssemblyResolutionError(f"CAD source not found: {source_path}")
|
|
return flatten_entry(entry, resolve_entry=resolve_entry)
|