mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
Bonsai: support IfcTrimmedCurve arc segments in profile edit (#6929)
Tabbing into an extrusion to edit its profile crashed on Revit-exported geometry. `tool.Model.convert_curve_to_mesh` handled IfcPolyline, IfcCompositeCurve, IfcIndexedPolyCurve and IfcCircle, but not IfcTrimmedCurve. Revit exports a wall/opening profile as an IfcCompositeCurve mixing polyline segments with trimmed-circle arc segments, so the recursion hit an IfcTrimmedCurve, fell through to the else branch, and raised UnsupportedCurveForConversion, aborting the whole tab-into-edit. Add an IfcTrimmedCurve branch that reconstructs a circular arc as a 3-point (start, mid, end) arc into an IFCARCINDEX vertex group, matching how IfcArcIndex segments already round-trip. Trim parameters are read in the project plane-angle unit and converted to radians; both IfcParameterValue and IfcCartesianPoint trims are supported; SenseAgreement is used to unwrap the end angle so the midpoint lands on the swept arc. Non-circular basis curves still raise UnsupportedCurveForConversion rather than emit silently wrong geometry. Verified live in headless Blender on the issue's file: profile #41622 now imports (27 verts, 5 arc groups) with no exception, and each reconstructed arc's endpoints coincide (~1e-6) with the endpoints of the adjacent polyline segments in the composite curve. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import collections.abc
|
||||
import json
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from math import atan, cos, degrees, pi, radians
|
||||
from math import atan, atan2, cos, degrees, pi, radians, sin
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
@@ -660,6 +660,50 @@ class Model(bonsai.core.tool.Model):
|
||||
)
|
||||
cls.circles.append([offset, offset + 1])
|
||||
cls.edges.append((offset, offset + 1))
|
||||
elif curve.is_a("IfcTrimmedCurve"):
|
||||
# A trimmed circular arc, e.g. a fillet segment inside an IfcCompositeCurve
|
||||
# exported by Revit. Reconstruct it as a 3-point (start, mid, end) arc so it
|
||||
# round-trips through the IFCARCINDEX vertex group like IfcArcIndex segments do.
|
||||
basis_curve = curve.BasisCurve
|
||||
if not basis_curve.is_a("IfcCircle"):
|
||||
raise cls.UnsupportedCurveForConversion(f"Profile has unsupported curve type: {curve}.")
|
||||
circle_position = Matrix(ifcopenshell.util.placement.get_axis2placement(basis_curve.Position).tolist())
|
||||
circle_position.translation *= cls.unit_scale
|
||||
radius = cls.convert_unit_to_si(basis_curve.Radius)
|
||||
# Trim parameters on a circle are angles expressed in the project plane angle unit.
|
||||
angle_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get(), "PLANEANGLEUNIT")
|
||||
|
||||
def _trim_angle(trim: tuple[ifcopenshell.entity_instance, ...]) -> Union[float, None]:
|
||||
# Prefer a parameter (angle) trim, else derive the angle from a cartesian point.
|
||||
for select in trim:
|
||||
if select.is_a("IfcParameterValue"):
|
||||
return float(select.wrappedValue) * angle_scale
|
||||
for select in trim:
|
||||
if select.is_a("IfcCartesianPoint"):
|
||||
local = circle_position.inverted() @ Vector(cls.convert_unit_to_si(select.Coordinates)).to_3d()
|
||||
return atan2(local.y, local.x)
|
||||
return None
|
||||
|
||||
angle_1 = _trim_angle(curve.Trim1)
|
||||
angle_2 = _trim_angle(curve.Trim2)
|
||||
if angle_1 is None or angle_2 is None:
|
||||
raise cls.UnsupportedCurveForConversion(f"Profile has unsupported curve type: {curve}.")
|
||||
# SenseAgreement tells us whether the arc runs with (CCW) or against (CW) the
|
||||
# basis curve direction; unwrap the end angle accordingly so the midpoint lands
|
||||
# on the actual swept arc rather than its complement.
|
||||
if curve.SenseAgreement:
|
||||
while angle_2 < angle_1:
|
||||
angle_2 += 2 * pi
|
||||
else:
|
||||
while angle_2 > angle_1:
|
||||
angle_2 -= 2 * pi
|
||||
angle_mid = (angle_1 + angle_2) / 2
|
||||
for angle in (angle_1, angle_mid, angle_2):
|
||||
local_point = Vector((radius * cos(angle), radius * sin(angle), 0.0))
|
||||
cls.vertices.append(position @ circle_position @ local_point)
|
||||
cls.arcs.append([offset, offset + 1, offset + 2])
|
||||
cls.edges.append((offset, offset + 1))
|
||||
cls.edges.append((offset + 1, offset + 2))
|
||||
else:
|
||||
raise cls.UnsupportedCurveForConversion(f"Profile has unsupported curve type: {curve}.")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user