스킬 · 기타
라이노 8 건축 모델링·검사
라이노 8에서 파빌리온·구조 프레임·입면 루버 같은 건축 개념 모델을 만들고 모델 품질을 검사하는 원작 스킬. MIT 라이선스.
0번 설치됨
rhino-architectural-modeling/scripts/audit_architectural_model.py
"""Read-only RhinoCommon audit scaffold for Rhino MCP ``run_python``.Edit only CONFIG for the project, then execute this file's contents in a RhinoMCP environment that injects ``__rhino_doc__``. It does not mutate the model."""import jsonfrom collections import Counter, defaultdictimport Rhinoimport Rhino.Geometry as rgCONFIG = { # Empty means include every active object that has a name. "name_prefix": "", # Full layer paths expected to exist among audited objects. "required_layers": [], # Geometry on these layers is reference-only and need not be solid. "reference_layers": [], # Optional physical bounding-box limits: [x, y, z]. Use None to skip. "bbox_min": None, "bbox_max": None, "tolerance": None, # None uses the document absolute tolerance.}doc = __rhino_doc__tolerance = CONFIG["tolerance"] or doc.ModelAbsoluteToleranceprefix = CONFIG["name_prefix"]reference_layers = set(CONFIG["reference_layers"])def layer_path(rhino_object): layer = doc.Layers[rhino_object.Attributes.LayerIndex] return layer.FullPath if layer else "<missing>"def point_values(point): return [round(point.X, 6), round(point.Y, 6), round(point.Z, 6)]def geometry_volume(geometry): try: properties = rg.VolumeMassProperties.Compute(geometry) return properties.Volume if properties else None except Exception: return Nonedef geometry_length(geometry): try: return geometry.GetLength() if isinstance(geometry, rg.Curve) else None except Exception: return Nonedef closed_solid_status(geometry): if isinstance(geometry, rg.Brep): return geometry.IsValid and geometry.IsSolid if isinstance(geometry, rg.Extrusion): return geometry.IsValid and geometry.IsSolid return Falsedef fingerprint(geometry): bbox = geometry.GetBoundingBox(True) volume = geometry_volume(geometry) length = geometry_length(geometry) return ( geometry.GetType().Name, tuple(point_values(bbox.Min)), tuple(point_values(bbox.Max)), None if volume is None else round(volume, 6), None if length is None else round(length, 6), )objects = []for candidate in doc.Objects.GetObjectList(Rhino.DocObjects.ObjectType.AnyObject): if candidate.IsDeleted: continue name = candidate.Attributes.Name or "" if not name or (prefix and not name.startswith(prefix)): continue objects.append(candidate)names = [obj.Attributes.Name for obj in objects]name_counts = Counter(names)duplicate_names = sorted(name for name, count in name_counts.items() if count > 1)layer_counts = Counter(layer_path(obj) for obj in objects)type_counts = Counter(obj.Geometry.GetType().Name for obj in objects)missing_layers = sorted(set(CONFIG["required_layers"]) - set(layer_counts))invalid_or_open = []physical_objects = []reference_objects = []for obj in objects: if layer_path(obj) in reference_layers: reference_objects.append(obj) continue physical_objects.append(obj) if not closed_solid_status(obj.Geometry): invalid_or_open.append(obj.Attributes.Name)fingerprints = defaultdict(list)for obj in objects: fingerprints[fingerprint(obj.Geometry)].append(obj.Attributes.Name)duplicate_geometry_groups = [sorted(group) for group in fingerprints.values() if len(group) > 1]physical_bbox = rg.BoundingBox.Emptyfor obj in physical_objects: physical_bbox.Union(obj.Geometry.GetBoundingBox(True))bbox = Noneif physical_bbox.IsValid: bbox = {"min": point_values(physical_bbox.Min), "max": point_values(physical_bbox.Max)}def bbox_within_contract(actual): if actual is None: return CONFIG["bbox_min"] is None and CONFIG["bbox_max"] is None lower = CONFIG["bbox_min"] upper = CONFIG["bbox_max"] if lower is not None: for actual_value, limit in zip(actual["min"], lower): if actual_value < limit - tolerance: return False if upper is not None: for actual_value, limit in zip(actual["max"], upper): if actual_value > limit + tolerance: return False return Truechecks = { "objects_found": bool(objects), "duplicate_names_zero": not duplicate_names, "duplicate_geometry_zero": not duplicate_geometry_groups, "missing_required_layers_zero": not missing_layers, "physical_objects_closed_solids": not invalid_or_open, "physical_bbox_within_contract": bbox_within_contract(bbox),}report = { "document": doc.Name, "units": str(doc.ModelUnitSystem), "absolute_tolerance": doc.ModelAbsoluteTolerance, "config": CONFIG, "object_count": len(objects), "physical_object_count": len(physical_objects), "reference_object_count": len(reference_objects), "layer_counts": dict(sorted(layer_counts.items())), "geometry_type_counts": dict(sorted(type_counts.items())), "bbox_physical": bbox, "duplicate_names": duplicate_names, "duplicate_geometry_groups": duplicate_geometry_groups, "missing_required_layers": missing_layers, "invalid_or_open": sorted(invalid_or_open), "checks": checks, "overall_pass": all(checks.values()),}print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))