"""Metadata helpers for yapCAD geometry objects."""
from __future__ import annotations
import uuid
from typing import Dict, Tuple, Any, Optional, Iterable
from yapcad.geom3d import issolid, issurface
_SURFACE_META_INDEX = 6
_SOLID_META_INDEX = 4
_DEFAULT_SCHEMA = "metadata-namespace-v1.1"
_ROOT_FIELDS = ("schema", "entityId", "timestamp", "tags", "layer")
_SECTION_KEYS = {
"material",
"manufacturing",
"designHistory",
"constraints",
"analysis",
"assembly",
"operation",
}
# v1.1 enum vocabularies (RFC § 7-8)
_JOINT_KINDS = {"axial", "radial", "mixed", "none"}
_BOLT_RINGS = {"axial", "radial"}
_DATUM_KINDS = {"bolt_circle", "axis", "plane", "point", "edge"}
_SURFACE_KINDS = {"mating", "bearing", "sealing", "datum", "wear"}
_KEEPOUT_KINDS = {"volume", "axis", "ring"}
_OPERATION_KINDS = {"subtract", "intersect", "union"}
_OPERATION_POLICIES = {"strict", "warn", "ignore"}
_FEATURE_KINDS = {
"access_panel", "vent", "parachute_door", "fastener_through",
"wire_pass", "channel", "pocket", "other",
}
_FASTENER_THREADS = {"tap", "clearance", "heat-set", "through"}
_FASTENER_HEADS = {"SHCS", "FHCS", "BHCS", "hex", "stud", "none"}
def _validate_enum(value: Any, allowed: set, *, field: str) -> None:
if value is None:
return
if value not in allowed:
raise ValueError(
f"invalid {field}={value!r}; expected one of {sorted(allowed)}"
)
def _initial_root(entity_id: Optional[str] = None) -> Dict[str, Any]:
data: Dict[str, Any] = {
"schema": _DEFAULT_SCHEMA,
"tags": [],
"layer": "default",
}
if entity_id:
data["entityId"] = entity_id
return data
def _ensure_root(meta: Dict[str, Any], entity_id: Optional[str] = None) -> Dict[str, Any]:
# NOTE: when meta is an empty dict, populate it in place rather than
# returning a detached fresh dict; otherwise downstream namespace
# writes vanish on garbage-collection.
if meta is None:
return _initial_root(entity_id)
if "schema" not in meta:
meta["schema"] = _DEFAULT_SCHEMA
if entity_id and "entityId" not in meta:
meta["entityId"] = entity_id
if "tags" not in meta:
meta["tags"] = []
if "layer" not in meta or not meta["layer"]:
meta["layer"] = "default"
return meta
def _ensure_namespace(meta: Dict[str, Any], namespace: str) -> Dict[str, Any]:
if namespace not in meta or not isinstance(meta[namespace], dict):
meta[namespace] = {}
return meta[namespace]
def _append_tag(meta: Dict[str, Any], tag: str) -> None:
if not tag:
return
tags = meta.setdefault("tags", [])
if isinstance(tags, list) and tag not in tags:
tags.append(tag)
def _ensure_metadata(container: list, index: int, create: bool) -> Dict:
if len(container) <= index:
if not create:
return {}
while len(container) <= index:
container.append({})
meta = container[index]
if not isinstance(meta, dict):
if not create:
return {}
meta = {}
container[index] = meta
return meta
[docs]
def ensure_surface_id(surface: list) -> str:
meta = _ensure_metadata(surface, _SURFACE_META_INDEX, True)
if meta is None:
meta = {}
surface.append(meta)
meta = _ensure_root(meta)
if 'entityId' not in meta:
meta['entityId'] = str(uuid.uuid4())
if 'id' not in meta:
meta['id'] = meta['entityId']
surface[_SURFACE_META_INDEX] = meta
return meta['id']
[docs]
def set_surface_units(surface: list, units: str) -> list:
meta = get_surface_metadata(surface, create=True)
meta['units'] = units
return surface
[docs]
def set_surface_origin(surface: list, origin: str) -> list:
meta = get_surface_metadata(surface, create=True)
meta['origin'] = origin
return surface
[docs]
def ensure_solid_id(sld: list) -> str:
meta = _ensure_metadata(sld, _SOLID_META_INDEX, True)
if meta is None:
meta = {}
sld.append(meta)
meta = _ensure_root(meta)
if 'entityId' not in meta:
meta['entityId'] = str(uuid.uuid4())
if 'id' not in meta:
meta['id'] = meta['entityId']
sld[_SOLID_META_INDEX] = meta
return meta['id']
[docs]
def set_solid_context(sld: list, context: Dict) -> list:
if not isinstance(context, dict):
raise ValueError('context must be a dictionary')
meta = get_solid_metadata(sld, create=True)
meta['context'] = context
return sld
[docs]
def set_layer(meta: Dict[str, Any], layer: str) -> Dict[str, Any]:
if not layer:
layer = "default"
root = _ensure_root(meta)
root["layer"] = layer
return root
# ---------------------------------------------------------------------------
# Metadata namespace helpers
[docs]
def set_material(meta: Dict[str, Any], *, name: Optional[str] = None,
standard: Optional[str] = None, grade: Optional[str] = None,
density_kg_m3: Optional[float] = None,
source: Optional[str] = None) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "material")
if name is not None:
section["name"] = name
if standard is not None:
section["standard"] = standard
if grade is not None:
section["grade"] = grade
if density_kg_m3 is not None:
section["density_kg_m3"] = float(density_kg_m3)
if source is not None:
section["source"] = source
return root
[docs]
def set_manufacturing(meta: Dict[str, Any], *,
process: Optional[str] = None,
instructions: Optional[str] = None,
fixtures: Optional[Iterable[str]] = None,
layers: Optional[Dict[str, Any]] = None,
postprocessing: Optional[Iterable[str]] = None) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "manufacturing")
if process is not None:
section["process"] = process
if instructions is not None:
section["instructions"] = instructions
if fixtures is not None:
section["fixtures"] = list(fixtures)
if layers is not None:
section["layers"] = dict(layers)
if postprocessing is not None:
section["postprocessing"] = list(postprocessing)
return root
[docs]
def add_design_history_entry(meta: Dict[str, Any], *,
author: Optional[str] = None,
source: Optional[str] = None,
context: Optional[str] = None,
tools: Optional[Iterable[str]] = None,
revision: Optional[str] = None,
timestamp: Optional[str] = None,
notes: Optional[str] = None) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "designHistory")
if author is not None:
section["author"] = author
if source is not None:
section["source"] = source
if context is not None:
section["context"] = context
if tools is not None:
section["tools"] = list(tools)
entries = section.setdefault("iterations", [])
entry: Dict[str, Any] = {}
if revision is not None:
entry["revision"] = revision
if timestamp is not None:
entry["timestamp"] = timestamp
if notes is not None:
entry["notes"] = notes
if entry:
entries.append(entry)
return root
[docs]
def set_mass_constraint(meta: Dict[str, Any], *,
max_kg: Optional[float] = None,
target_kg: Optional[float] = None) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "constraints")
mass = section.setdefault("mass", {})
if max_kg is not None:
mass["max_kg"] = float(max_kg)
if target_kg is not None:
mass["target_kg"] = float(target_kg)
return root
[docs]
def set_envelope_constraint(meta: Dict[str, Any], *,
x_mm: Optional[float] = None,
y_mm: Optional[float] = None,
z_mm: Optional[float] = None) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "constraints")
envelope = section.setdefault("envelope", {})
if x_mm is not None:
envelope["x_mm"] = float(x_mm)
if y_mm is not None:
envelope["y_mm"] = float(y_mm)
if z_mm is not None:
envelope["z_mm"] = float(z_mm)
return root
[docs]
def add_compliance(meta: Dict[str, Any], standards: Iterable[str]) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "constraints")
listing = section.setdefault("compliance", [])
for item in standards:
if item and item not in listing:
listing.append(item)
return root
[docs]
def add_analysis_record(meta: Dict[str, Any], record: Dict[str, Any]) -> Dict[str, Any]:
root = _ensure_root(meta)
section = _ensure_namespace(root, "analysis")
simulations = section.setdefault("simulations", [])
simulations.append(dict(record))
return root
# ---------------------------------------------------------------------------
# Assembly namespace (RFC § 7) — v1.1
[docs]
def set_assembly(meta: Dict[str, Any], *,
joint_kind: Optional[str] = None,
no_cut: Optional[bool] = None) -> Dict[str, Any]:
"""Set scalar fields on the ``assembly`` namespace. Returns the namespace dict."""
_validate_enum(joint_kind, _JOINT_KINDS, field="assembly.joint_kind")
root = _ensure_root(meta)
section = _ensure_namespace(root, "assembly")
if joint_kind is not None:
section["joint_kind"] = joint_kind
if no_cut is not None:
section["no_cut"] = bool(no_cut)
return section
def _build_fastener(spec: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
if spec is None:
return None
if not isinstance(spec, dict):
raise ValueError("fastener spec must be a dict")
out: Dict[str, Any] = {}
if "designation" in spec and spec["designation"] is not None:
out["designation"] = str(spec["designation"])
thread = spec.get("thread")
_validate_enum(thread, _FASTENER_THREADS, field="fastener.thread")
if thread is not None:
out["thread"] = thread
head = spec.get("head")
_validate_enum(head, _FASTENER_HEADS, field="fastener.head")
if head is not None:
out["head"] = head
return out
[docs]
def add_bolt_pattern(meta: Dict[str, Any], *,
id: str,
ring: str,
R_mm: float,
z_mm: float,
count: int,
theta0_deg: Optional[float] = None,
fastener: Optional[Dict[str, Any]] = None,
tolerance_mm: Optional[float] = None) -> Dict[str, Any]:
"""Append a bolt pattern entry to ``assembly.bolt_patterns``. Returns the entry."""
if not id:
raise ValueError("bolt_pattern.id is required")
_validate_enum(ring, _BOLT_RINGS, field="bolt_pattern.ring")
section = get_assembly_metadata(meta, create=True)
entry: Dict[str, Any] = {
"id": str(id),
"ring": ring,
"R_mm": float(R_mm),
"z_mm": float(z_mm),
"count": int(count),
}
if theta0_deg is not None:
entry["theta0_deg"] = float(theta0_deg)
fast = _build_fastener(fastener)
if fast is not None:
entry["fastener"] = fast
if tolerance_mm is not None:
entry["tolerance_mm"] = float(tolerance_mm)
section.setdefault("bolt_patterns", []).append(entry)
return entry
[docs]
def add_datum(meta: Dict[str, Any], *,
id: str,
kind: str,
ring: Optional[str] = None,
R_mm: Optional[float] = None,
z_mm: Optional[float] = None,
origin_mm: Optional[Iterable[float]] = None,
direction: Optional[Iterable[float]] = None) -> Dict[str, Any]:
"""Append an explicit datum entry to ``assembly.datums``. Returns the entry.
Two ways to specify the origin (mutually compatible; ``origin_mm``
wins when both are present):
* ``R_mm`` + ``z_mm`` — axisymmetric / cylindrical convention. Origin
is taken as ``(R_mm, 0, z_mm)`` in the part's local frame.
Sufficient for parts whose features all live on a single radial
plane (cylindrical bulkheads, tank domes, etc.).
* ``origin_mm: [x, y, z]`` — explicit 3D origin. Required for parts
whose features aren't axisymmetric (pentagonal prisms, chassis
with off-axis mount points, etc.).
"""
if not id:
raise ValueError("datum.id is required")
_validate_enum(kind, _DATUM_KINDS, field="datum.kind")
if ring is not None:
_validate_enum(ring, _BOLT_RINGS, field="datum.ring")
section = get_assembly_metadata(meta, create=True)
entry: Dict[str, Any] = {"id": str(id), "kind": kind}
if ring is not None:
entry["ring"] = ring
if R_mm is not None:
entry["R_mm"] = float(R_mm)
if z_mm is not None:
entry["z_mm"] = float(z_mm)
if origin_mm is not None:
vec = list(origin_mm)
if len(vec) != 3:
raise ValueError("datum.origin_mm must have length 3")
entry["origin_mm"] = [float(c) for c in vec]
if direction is not None:
vec = list(direction)
if len(vec) != 3:
raise ValueError("datum.direction must have length 3")
entry["direction"] = [float(c) for c in vec]
section.setdefault("datums", []).append(entry)
return entry
[docs]
def add_surface(meta: Dict[str, Any], *,
id: str,
kind: str,
mate_to: Optional[str] = None,
finish: Optional[str] = None,
tolerance_mm: Optional[float] = None) -> Dict[str, Any]:
"""Append a mating/bearing/sealing/datum/wear surface to ``assembly.surfaces``."""
if not id:
raise ValueError("surface.id is required")
_validate_enum(kind, _SURFACE_KINDS, field="surface.kind")
section = get_assembly_metadata(meta, create=True)
entry: Dict[str, Any] = {"id": str(id), "kind": kind}
if mate_to is not None:
entry["mate_to"] = str(mate_to)
if finish is not None:
entry["finish"] = str(finish)
if tolerance_mm is not None:
entry["tolerance_mm"] = float(tolerance_mm)
section.setdefault("surfaces", []).append(entry)
return entry
[docs]
def add_keepout(meta: Dict[str, Any], *,
id: str,
kind: str,
shape: Optional[Dict[str, Any]] = None,
reason: Optional[str] = None) -> Dict[str, Any]:
"""Append a keep-out volume/axis/ring to ``assembly.keepouts``."""
if not id:
raise ValueError("keepout.id is required")
_validate_enum(kind, _KEEPOUT_KINDS, field="keepout.kind")
section = get_assembly_metadata(meta, create=True)
entry: Dict[str, Any] = {"id": str(id), "kind": kind}
if shape is not None:
if not isinstance(shape, dict):
raise ValueError("keepout.shape must be a dict")
entry["shape"] = dict(shape)
if reason is not None:
entry["reason"] = str(reason)
section.setdefault("keepouts", []).append(entry)
return entry
# ---------------------------------------------------------------------------
# Operation namespace (RFC § 8) — v1.1
[docs]
def set_operation(meta: Dict[str, Any], *,
kind: str,
target_filter: Optional[Iterable[str]] = None,
priority: Optional[float] = None,
through: Optional[bool] = None,
consume: Optional[bool] = None,
policy: Optional[str] = None,
feature_id: Optional[str] = None,
feature_kind: Optional[str] = None,
stage: Optional[str] = None) -> Dict[str, Any]:
"""Mark the part as an operation (cutter/intersector/unioner). Returns the namespace."""
_validate_enum(kind, _OPERATION_KINDS, field="operation.kind")
_validate_enum(policy, _OPERATION_POLICIES, field="operation.policy")
_validate_enum(feature_kind, _FEATURE_KINDS, field="operation.feature_kind")
section = get_operation_metadata(meta, create=True)
section["kind"] = kind
if target_filter is not None:
section["target_filter"] = [str(t) for t in target_filter]
if priority is not None:
section["priority"] = float(priority)
if through is not None:
section["through"] = bool(through)
if consume is not None:
section["consume"] = bool(consume)
if policy is not None:
section["policy"] = policy
if feature_id is not None:
section["feature_id"] = str(feature_id)
if feature_kind is not None:
section["feature_kind"] = feature_kind
if stage is not None:
section["stage"] = str(stage)
return section
__all__ = [
# entity-level helpers
'get_surface_metadata',
'set_surface_metadata',
'ensure_surface_id',
'set_surface_units',
'set_surface_origin',
'get_solid_metadata',
'set_solid_metadata',
'ensure_solid_id',
'set_solid_context',
'set_layer',
# v1.0 namespace helpers
'add_tags',
'set_material',
'set_manufacturing',
'add_design_history_entry',
'set_mass_constraint',
'set_envelope_constraint',
'add_compliance',
'add_analysis_record',
# v1.1 assembly namespace
'get_assembly_metadata',
'set_assembly',
'add_bolt_pattern',
'add_datum',
'add_surface',
'add_keepout',
# v1.1 operation namespace
'get_operation_metadata',
'set_operation',
]