mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 16:01:36 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef055669b4 | |||
| f9be61c10b | |||
| b61f809731 | |||
| 948ffce7e9 | |||
| c68e4a0eee | |||
| 6603c8459a | |||
| 824c1fc280 | |||
| 705af7ba3a | |||
| 074fc26e8f | |||
| a90064929b | |||
| 397f13e71c | |||
| 7ebdd046b6 | |||
| 57cfd9d1fd | |||
| e333c1c100 | |||
| 97a85fe5a7 | |||
| 8ee52c466f | |||
| fe4fdd091d | |||
| 56121ca061 | |||
| 7b613a0bcc | |||
| b35f99e63f | |||
| f744753726 | |||
| 2e21fc5a98 | |||
| ca9bbbc4a7 | |||
| 5994fbde27 | |||
| 47dc1a6c68 | |||
| 489084c7be | |||
| 6c590bf008 | |||
| 96e2efebc8 | |||
| d188e3beaf | |||
| 93c0290131 | |||
| f3a7a35acf | |||
| 183e4c47f7 | |||
| 2ac92f01e4 | |||
| 2e9e75c7bd | |||
| 8857396a1f | |||
| f0970b90b0 | |||
| 71a598e63a | |||
| 3d8115ebc5 | |||
| b5a0f1fc74 | |||
| 25441bd816 | |||
| 65811ac7c9 | |||
| d9d1824886 | |||
| 16e5f18553 | |||
| b7a9b7bc5a | |||
| e14397058d | |||
| f0117c60b3 | |||
| 0e5223a30d | |||
| bc41ff78f4 | |||
| 9123d8c183 | |||
| ffd939508c | |||
| 9213b31235 | |||
| 2155e3206f | |||
| 9e0c6cf524 | |||
| d5dc069b2f | |||
| 821cf7b671 |
@@ -55,11 +55,17 @@ jobs:
|
||||
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check
|
||||
id: ty
|
||||
run: |
|
||||
poe ty-venv
|
||||
poe ty
|
||||
- name: ty check (venv setup)
|
||||
run: poe ty-venv
|
||||
|
||||
- name: ty check (bonsai)
|
||||
id: ty-bonsai
|
||||
run: poe ty-bonsai
|
||||
continue-on-error: true
|
||||
|
||||
- name: ty check (ios)
|
||||
id: ty-ios
|
||||
run: poe ty-ios
|
||||
continue-on-error: true
|
||||
|
||||
- name: Ruff check
|
||||
@@ -109,7 +115,10 @@ jobs:
|
||||
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
|
||||
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
|
||||
if [ "${{ steps.ty-bonsai.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (bonsai) failed, see 'ty check (bonsai)' step for the details." && ERROR=1
|
||||
fi
|
||||
if [ "${{ steps.ty-ios.outcome }}" != "success" ]; then
|
||||
echo "::error::ty check (ios) failed, see 'ty check (ios)' step for the details." && ERROR=1
|
||||
fi
|
||||
exit $ERROR
|
||||
|
||||
@@ -18,7 +18,7 @@ and many other libraries, CLI apps, and more. Support is also provided for auxil
|
||||
|
||||
For more information, see:
|
||||
|
||||
* [IfcOpenShell Website](http://ifcopenshell.org)
|
||||
* [IfcOpenShell Website](https://ifcopenshell.org)
|
||||
* [IfcOpenShell Documentation](https://docs.ifcopenshell.org)
|
||||
* [IfcOpenShell C++ Installation](https://docs.ifcopenshell.org/ifcopenshell/installation.html)
|
||||
* [IfcOpenShell Python Installation](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html)
|
||||
|
||||
@@ -68,6 +68,7 @@ def unpack_dependencies(install_dir: Path) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
action = None
|
||||
if len(sys.argv) != 2 or (action := sys.argv[1].lower()) not in ("pack", "unpack"):
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -82,8 +82,6 @@ ignore = [
|
||||
all = "error"
|
||||
|
||||
# Structural rules (no deep type inference needed, easier to adapt).
|
||||
# Has false positives due to ty walrus operator bug.
|
||||
possibly-unresolved-reference = "ignore"
|
||||
# Maybe later, requires to specify element types for all generics.
|
||||
missing-type-argument = "ignore"
|
||||
# Conflicts with `bpy` props defined using annotations.
|
||||
@@ -111,7 +109,6 @@ no-matching-overload = "ignore"
|
||||
not-subscriptable = "ignore"
|
||||
unsupported-dynamic-base = "ignore"
|
||||
unsupported-operator = "ignore"
|
||||
type-assertion-failure = "ignore"
|
||||
|
||||
[tool.ty.environment]
|
||||
extra-paths = [
|
||||
@@ -195,7 +192,6 @@ format.sequence = ["black", "ruff"]
|
||||
cmake-format = "gersemi . --in-place"
|
||||
|
||||
[tool.poe.tasks.ty-ios]
|
||||
# --ignore unresolved-reference: walrus operator false positives in ty.
|
||||
cmd = """
|
||||
ty check
|
||||
nix/
|
||||
@@ -213,7 +209,6 @@ cmd = """
|
||||
src/ifcpatch
|
||||
src/ifctester
|
||||
--python=src/ifcopenshell-python/.venv
|
||||
--ignore unresolved-reference
|
||||
"""
|
||||
|
||||
[tool.poe.tasks.bonsai-deps]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
black==26.3.1
|
||||
ruff==0.15.12
|
||||
poethepoet
|
||||
ty==0.0.59
|
||||
ty==0.0.61
|
||||
gersemi==0.26.1
|
||||
|
||||
@@ -188,6 +188,8 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -206,6 +208,8 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
@@ -222,6 +226,8 @@ class BcfClient:
|
||||
response.raise_for_status()
|
||||
return response.status_code, response.text
|
||||
except requests.exceptions.HTTPError as errh:
|
||||
response = errh.response
|
||||
assert response is not None
|
||||
print(f"message: {response.reason}' '{response.status_code}, {errh}")
|
||||
return response.status_code, response.reason
|
||||
|
||||
|
||||
@@ -24,10 +24,28 @@ a text, a tspan { fill: blue !important; text-decoration: underline;}
|
||||
a:hover { cursor: pointer; }
|
||||
.cut { fill: black; stroke: black; stroke-linecap: 'round'; stroke-width: 0.35; fill-rule: evenodd; }
|
||||
.projection { fill: white; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.surface { stroke: none; fill: #fff; fill-rule: evenodd; }
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.25; }
|
||||
.IfcGeographicElement { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 1; }
|
||||
/* SVG edge classification (issue #3668): see edge-classification.md. These select directly on
|
||||
the <path> element (each classified projection edge carries its own class), so they win over
|
||||
the inherited .projection rule above regardless of specificity. */
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: black; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: black; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: black; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: black; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
|
||||
/* Debug CSS for troubleshooting edge classification */
|
||||
/*
|
||||
path.outline { stroke: black; stroke-width: 0.35; stroke-opacity: 1; }
|
||||
path.boundary { stroke: orange; stroke-width: 0.3; stroke-opacity: 0.9; }
|
||||
path.crease { stroke: green; stroke-width: 0.25; stroke-opacity: 0.85; }
|
||||
path.sharp { stroke: red; stroke-width: 0.18; stroke-opacity: 0.7; }
|
||||
path.flush { stroke: blue; stroke-width: 0.1; stroke-opacity: 0.4; }
|
||||
*/
|
||||
|
||||
.surface {fill: white; stroke-width: 0.1;}
|
||||
.annotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
.IfcAnnotation { fill: none; stroke: black; stroke-linecap: 'round'; stroke-width: 0.3; }
|
||||
/* .IfcGeographicElement { fill: none; stroke: rgb(150, 150, 150); stroke-linecap: 'round'; stroke-dasharray: 1, 2;} */
|
||||
.PredefinedType-LINEWORK { stroke: black; stroke-width: 0.25; }
|
||||
.PredefinedType-LINEWORK.dashed { stroke-dasharray: 3, 2; }
|
||||
.PredefinedType-LINEWORK.fine { stroke-width: 0.18; stroke: #777777; }
|
||||
|
||||
@@ -5,7 +5,7 @@ FILE_NAME('EPset_Drawing.ifc','2020-01-01T00:00:00',$,$,'EPset_Drawing','EPset_D
|
||||
FILE_SCHEMA(('IFC4'));
|
||||
ENDSEC;
|
||||
DATA;
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2));
|
||||
#1=IFCPROPERTYSETTEMPLATE('2JhNIvqZrFnAgxfhK0XVQX',$,'EPset_Drawing','',.PSET_OCCURRENCEDRIVEN.,'IfcAnnotation/DRAWING',(#23,#22,#27,#24,#29,#30,#19,#12,#26,#9,#8,#7,#6,#4,#18,#11,#5,#20,#25,#14,#10,#17,#28,#16,#3,#21,#13,#15,#2,#31,#32,#33,#34,#35,#36));
|
||||
#2=IFCSIMPLEPROPERTYTEMPLATE('23JavTMk98ZxXhrUEnjAcf',$,'TargetView','',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#3=IFCSIMPLEPROPERTYTEMPLATE('1yVWUt5H9DAOuu0OaMMLpe',$,'Scale','The scale of this drawing represented as a numerator and denominator, such as 1/100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
#4=IFCSIMPLEPROPERTYTEMPLATE('3gsuPBtU93b8f0gg1pjkq6',$,'HumanScale','The scale of this drawing in human readable format, such as 1:100',.P_SINGLEVALUE.,'IfcLabel',$,$,$,$,$,.READWRITE.);
|
||||
@@ -35,5 +35,11 @@ DATA;
|
||||
#28=IFCSIMPLEPROPERTYTEMPLATE('1YSnFzurrEyRNtoLdmmddP',$,'BringToFront','The objects with these SVG classes will render in front of all other objects.Ex: IfcBeam, IfcColumn',.P_SINGLEVALUE.,'IfcText',$,$,$,$,$,.READWRITE.);
|
||||
#29=IFCSIMPLEPROPERTYTEMPLATE('0lP6Y8q9v2QhDnR4sT7uVx',$,'PerspectiveShiftX','Horizontal perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#30=IFCSIMPLEPROPERTYTEMPLATE('2mR8b1NcW5EoFyG7hJ9kLp',$,'PerspectiveShiftY','Vertical perspective camera shift stored as drawing metadata using Blender camera shift units.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#31=IFCSIMPLEPROPERTYTEMPLATE('1cFVJnqT13m8ItkMHaI1tp',$,'UseEdgeClassification','Enable the boundary/outline/sharp/crease/flush SVG edge classification scheme (issue #3668). When false, drawings use the original unclassified linework.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#32=IFCSIMPLEPROPERTYTEMPLATE('2kB$mxBgnBUvhjh0Ti0c4P',$,'RenderCreases','Whether to render ''crease'' (concave) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#33=IFCSIMPLEPROPERTYTEMPLATE('3MSIJNW$T8r9Hl12kk0BY$',$,'ValleyAngleMinDegrees','Minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as ''crease'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#34=IFCSIMPLEPROPERTYTEMPLATE('2epSGfC4bFM9gb1X7zBIp4',$,'RenderSharp','Whether to render ''sharp'' (convex) edges. Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
#35=IFCSIMPLEPROPERTYTEMPLATE('3TZwsEjkr5WRDKcgrYzSIA',$,'RidgeAngleMinDegrees','Minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as ''sharp'' rather than ''flush''.',.P_SINGLEVALUE.,'IfcReal',$,$,$,$,$,.READWRITE.);
|
||||
#36=IFCSIMPLEPROPERTYTEMPLATE('2Jua$lO754vgZOkBoHM2gA',$,'RenderFlush','Whether to render ''flush'' edges (dihedral deviation below both ridge/valley thresholds). Only relevant when UseEdgeClassification is enabled.',.P_SINGLEVALUE.,'IfcBoolean',$,$,$,$,$,.READWRITE.);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;
|
||||
|
||||
@@ -1103,12 +1103,14 @@ class IfcImporter:
|
||||
vertices = [[v[i], v[i + 1], v[i + 2], 1] for i in range(0, len(v), 3)]
|
||||
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
|
||||
v2 = None
|
||||
polyline = None
|
||||
for edge in edges:
|
||||
v1 = vertices[edge[0]]
|
||||
if v1 != v2:
|
||||
polyline = curve.splines.new("POLY")
|
||||
polyline.points[-1].co = mathutils.Vector(v1)
|
||||
v2 = vertices[edge[1]]
|
||||
assert polyline is not None
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = mathutils.Vector(v2)
|
||||
edges_item_ids = ifcopenshell.util.shape.get_edges_representation_item_ids(geometry).tolist()
|
||||
|
||||
@@ -843,7 +843,6 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
settings = ifcopenshell.geom.settings()
|
||||
shape = ifcopenshell.geom.create_shape(settings, opening)
|
||||
mat = Matrix(ifcopenshell.util.shape.get_shape_matrix(shape))
|
||||
mat.translation = (0, 0, 0)
|
||||
opening_bm = bmesh.new()
|
||||
verts = ifcopenshell.util.shape.get_vertices(shape.geometry)
|
||||
for vert in verts:
|
||||
@@ -1060,6 +1059,7 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return tool.Ifc.get().createIfcConnectionSurfaceGeometry(surface)
|
||||
|
||||
def export_surface(self, polygon, target_face_matrix):
|
||||
ifc_file = tool.Ifc.get()
|
||||
x_axis = target_face_matrix.col[0][:3]
|
||||
z_axis = target_face_matrix.col[2][:3]
|
||||
p1 = target_face_matrix.translation
|
||||
@@ -1072,18 +1072,20 @@ class AddBoundary(bpy.types.Operator, tool.Ifc.Operator):
|
||||
placement = builder.create_axis2_placement_3d([o / self.unit_scale for o in p1], z_axis, x_axis)
|
||||
surface.BasisSurface = tool.Ifc.get().create_entity("IfcPlane", placement)
|
||||
|
||||
if tool.Ifc.get().schema != "IFC2X3":
|
||||
schema = ifc_file.schema
|
||||
if schema != "IFC2X3":
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in polygon.exterior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
outer_boundary = tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False)
|
||||
|
||||
inner_boundaries = []
|
||||
inner_boundaries: list[ifcopenshell.entity_instance] = []
|
||||
for interior in polygon.interiors:
|
||||
points = [tool.Model.convert_si_to_unit(list(co)) for co in interior.coords]
|
||||
point_list = tool.Ifc.get().createIfcCartesianPointList2D(points)
|
||||
inner_boundaries.append(tool.Ifc.get().createIfcIndexedPolyCurve(point_list, None, False))
|
||||
else:
|
||||
pass # TODO
|
||||
# TODO:
|
||||
raise NotImplementedError(schema)
|
||||
|
||||
surface.OuterBoundary = outer_boundary
|
||||
surface.InnerBoundaries = inner_boundaries
|
||||
|
||||
@@ -400,6 +400,7 @@ class CadOffset(bpy.types.Operator):
|
||||
[verts.update(e.verts) for e in edges]
|
||||
|
||||
# Use the viewport angle to determine the offset direction
|
||||
wp = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
# Don't ask me, I don't know.
|
||||
@@ -409,6 +410,7 @@ class CadOffset(bpy.types.Operator):
|
||||
z = area.spaces.active.region_3d.view_rotation @ Vector((0, 0, 1))
|
||||
wp = Matrix([x, y, z, Vector((0, 0, 0))]).to_4x4().transposed()
|
||||
break
|
||||
assert wp is not None
|
||||
|
||||
rotation = Matrix.Rotation(pi / 2, 2, "Z")
|
||||
rotation_i = Matrix.Rotation(-pi / 2, 2, "Z")
|
||||
|
||||
@@ -478,6 +478,7 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
def execute(self, context):
|
||||
props = tool.Classification.get_classification_props()
|
||||
props.available_library_references.clear()
|
||||
reference = None
|
||||
for reference in IfcStore.classification_file.by_id(self.parent_id).HasReferences:
|
||||
new = props.available_library_references.add()
|
||||
new.identification = reference.Identification or ""
|
||||
@@ -485,6 +486,7 @@ class ChangeClassificationLevel(bpy.types.Operator):
|
||||
new.ifc_definition_id = reference.id()
|
||||
new.has_references = bool(reference.HasReferences)
|
||||
new.referenced_source
|
||||
assert reference
|
||||
if reference.ReferencedSource.is_a("IfcClassificationReference"):
|
||||
props.active_library_referenced_source = reference.ReferencedSource.ReferencedSource.id()
|
||||
else:
|
||||
|
||||
@@ -156,6 +156,8 @@ class CostSchedulesData:
|
||||
values = root_element.CostValues
|
||||
elif root_element.is_a("IfcConstructionResource"):
|
||||
values = root_element.BaseCosts
|
||||
else:
|
||||
assert False, root_element
|
||||
for cost_value in values or []:
|
||||
cls._load_cost_value(root_element, data, cost_value)
|
||||
# data["CostValues"].append(cost_value.id())
|
||||
|
||||
@@ -425,10 +425,12 @@ class BaseDecorator:
|
||||
|
||||
blf.size(font_id, font_size_px)
|
||||
|
||||
w, h = None, None
|
||||
if box_alignment or center or vcenter:
|
||||
w, h = blf.dimensions(font_id, text)
|
||||
|
||||
if box_alignment:
|
||||
assert w is not None and h is not None
|
||||
box_alignment_offset = Vector((0, 0))
|
||||
if "bottom" in box_alignment:
|
||||
pass
|
||||
@@ -450,10 +452,12 @@ class BaseDecorator:
|
||||
else:
|
||||
# horizontal centering
|
||||
if center:
|
||||
assert w is not None
|
||||
pos -= Vector((cos, sin)) * w * 0.5
|
||||
|
||||
# vertical centering
|
||||
if vcenter:
|
||||
assert h is not None
|
||||
pos -= Vector((-sin, cos)) * h * 0.5
|
||||
|
||||
# side-shifting
|
||||
@@ -1001,6 +1005,8 @@ class FallDecorator(BaseDecorator):
|
||||
O = A.copy()
|
||||
O.z = B.z
|
||||
run = (B - O).length
|
||||
|
||||
angle_tg = None
|
||||
if run != 0:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
@@ -1018,6 +1024,7 @@ class FallDecorator(BaseDecorator):
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
assert angle_tg is not None
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
return "NO DATA"
|
||||
|
||||
@@ -1249,6 +1256,7 @@ class SectionLevelDecorator(BaseDecorator):
|
||||
}
|
||||
|
||||
# process edges
|
||||
text_position, text_dir = None, None
|
||||
for edge in edges_original:
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
@@ -1554,32 +1562,39 @@ class SectionDecorator(BaseDecorator):
|
||||
v0, v1 = winspace_verts[edge[0]], winspace_verts[edge[1]]
|
||||
start_i = len(output_verts)
|
||||
|
||||
circle_head = None
|
||||
if display_start_circle or display_end_circle:
|
||||
circle_head = get_circle_head(circle_size)
|
||||
|
||||
if display_start_symbol or display_end_symbol or connect_markers:
|
||||
triangle_head, divider_offset, edge_dir_circle = None, None, None
|
||||
display_symbol = display_start_symbol or display_end_symbol
|
||||
if display_symbol or connect_markers:
|
||||
edge_dir = (v1 - v0).normalized()
|
||||
side = (edge_dir.yx * Vector((1, -1))).to_3d()
|
||||
edge_dir_circle = edge_dir * circle_size
|
||||
|
||||
if display_start_symbol or display_end_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
if display_symbol:
|
||||
triangle_head = get_triangle_head(edge_dir, -side, triangle_length, triangle_width)
|
||||
divider_offset = []
|
||||
divider_offset.append(edge_dir_circle if connect_markers else edge_dir_circle * 3)
|
||||
divider_offset.append(edge_dir_circle)
|
||||
|
||||
if display_start_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_start_symbol:
|
||||
assert divider_offset is not None
|
||||
start_i = add_verts_sequence(
|
||||
[v0 + divider_offset[0], v0 - divider_offset[1]], start_i, **out_kwargs
|
||||
)
|
||||
|
||||
if display_start_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v0 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if display_end_circle:
|
||||
assert circle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in circle_head], start_i, **out_kwargs, closed=True)
|
||||
# circle middle divider
|
||||
if not display_end_symbol:
|
||||
@@ -1588,9 +1603,11 @@ class SectionDecorator(BaseDecorator):
|
||||
)
|
||||
|
||||
if display_end_symbol:
|
||||
assert triangle_head is not None
|
||||
start_i = add_verts_sequence([v + v1 for v in triangle_head], start_i, **out_kwargs, closed=True)
|
||||
|
||||
if connect_markers:
|
||||
assert edge_dir_circle is not None
|
||||
gap = []
|
||||
gap.append(edge_dir_circle if display_start_symbol else Vector((0, 0, 0)))
|
||||
gap.append(edge_dir_circle if display_end_symbol else Vector((0, 0, 0)))
|
||||
@@ -1677,6 +1694,12 @@ class CutDecorator:
|
||||
selected_elements_color = self.addon_prefs.decorator_color_selected
|
||||
self.fallback_colour = (0.3, 0.3, 0.3, 1)
|
||||
|
||||
# Evaluate camera movement once per redraw rather than twice per object: is_camera_moved()
|
||||
# runs eval()/numpy on the camera matrix and, as a side effect, refreshes the stored
|
||||
# checksum on the first True result - so calling it per object also made the second call
|
||||
# (fill) see an already-updated checksum and skip recalculating when it shouldn't.
|
||||
self.camera_moved = self.is_camera_moved()
|
||||
|
||||
all_vertices = []
|
||||
all_edges = []
|
||||
selected_vertices = []
|
||||
@@ -1802,23 +1825,35 @@ class CutDecorator:
|
||||
|
||||
# Currently selected objects must be recalculated as they may be being moved / edited.
|
||||
# If the camera is selected, we also recalculate as the user may be moving the camera.
|
||||
is_selected = obj.select_get()
|
||||
recalc_cut = not has_cut_cache or is_selected or self.camera_moved
|
||||
recalc_fill = not has_fill_cache or is_selected or self.camera_moved
|
||||
if not (recalc_cut or recalc_fill):
|
||||
return
|
||||
|
||||
if not has_cut_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_cut(context, obj, element)
|
||||
if not has_fill_cache or obj.select_get() or self.is_camera_moved():
|
||||
self.recalculate_fill(context, obj, element)
|
||||
# The intersection test builds a bmesh and scans every vertex; both recalculations need
|
||||
# the same answer, so compute it once here rather than once in each.
|
||||
is_intersecting = tool.Drawing.is_intersecting_camera(obj, context.scene.camera)
|
||||
if recalc_cut:
|
||||
self.recalculate_cut(context, obj, element, is_intersecting)
|
||||
if recalc_fill:
|
||||
self.recalculate_fill(context, obj, element, is_intersecting)
|
||||
|
||||
def recalculate_cut(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
if tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
def recalculate_cut(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
if is_intersecting:
|
||||
verts, edges = tool.Drawing.bisect_mesh(obj, context.scene.camera)
|
||||
DecoratorData.cut_cache[element.id()] = (verts, edges)
|
||||
else:
|
||||
DecoratorData.cut_cache[element.id()] = (False, False)
|
||||
|
||||
def recalculate_fill(self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance) -> None:
|
||||
def recalculate_fill(
|
||||
self, context, obj: bpy.types.Object, element: ifcopenshell.entity_instance, is_intersecting: bool
|
||||
) -> None:
|
||||
element_id = element.id()
|
||||
|
||||
if not tool.Drawing.is_intersecting_camera(obj, context.scene.camera):
|
||||
if not is_intersecting:
|
||||
DecoratorData.fill_cache[element_id] = {}
|
||||
return
|
||||
|
||||
@@ -1871,6 +1906,8 @@ class CutDecorator:
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
if len(layer_set.MaterialLayers) == 1:
|
||||
material = layer_set.MaterialLayers[0].Material
|
||||
@@ -1897,6 +1934,8 @@ class CutDecorator:
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
|
||||
|
||||
@@ -225,9 +225,11 @@ def format_distance(
|
||||
unit_system, unit_length, unit_fraction = unit_mapping[custom_unit]
|
||||
|
||||
value *= unit_scale
|
||||
tx_dist = None
|
||||
|
||||
# Imperial Formatting
|
||||
if unit_system == "IMPERIAL":
|
||||
toInches = None
|
||||
if in_unit_length:
|
||||
if unit_length == "INCHES":
|
||||
toInches = 1
|
||||
@@ -241,6 +243,7 @@ def format_distance(
|
||||
toInches = 1550
|
||||
inPerFoot = 144
|
||||
|
||||
assert toInches is not None
|
||||
decInches = value * toInches
|
||||
decFeet = decInches / 12
|
||||
|
||||
@@ -383,6 +386,7 @@ def format_distance(
|
||||
if precision and isinstance(precision, float):
|
||||
value = precision * round(float(value) / precision)
|
||||
|
||||
fmt = None
|
||||
if decimal_places is not None:
|
||||
fmt = "%1." + str(decimal_places) + "f"
|
||||
|
||||
@@ -465,6 +469,7 @@ def format_distance(
|
||||
assert f"Unexpected unit_system - '{unit_system}'."
|
||||
# tx_dist = fmt % value
|
||||
|
||||
assert tx_dist is not None
|
||||
return tx_dist
|
||||
|
||||
|
||||
|
||||
@@ -698,6 +698,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
layer_set = material
|
||||
offset = 0
|
||||
sense_factor = 1
|
||||
else:
|
||||
assert False, material
|
||||
|
||||
camera_matrix_i = context.scene.camera.matrix_world.inverted()
|
||||
|
||||
@@ -722,7 +724,6 @@ class CreateDrawing(bpy.types.Operator):
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.000001)
|
||||
bmesh.ops.triangle_fill(bm, use_dissolve=True, edges=bm.edges)
|
||||
|
||||
prev_co = None
|
||||
if not usage:
|
||||
sense_factor = 1 # Assume the extrusion vector points in the direction sense
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
@@ -739,6 +740,8 @@ class CreateDrawing(bpy.types.Operator):
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = tool.Drawing.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, usage
|
||||
no *= sense_factor
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
@@ -906,6 +909,10 @@ class CreateDrawing(bpy.types.Operator):
|
||||
if os.path.isfile(svg_path) and self.props.should_use_linework_cache:
|
||||
return svg_path
|
||||
|
||||
ifc = tool.Ifc.get()
|
||||
semantics = None
|
||||
pairs = None
|
||||
|
||||
# in case of printing multiple drawings we need to sync just once
|
||||
if self.sync and self.drawing_index == 0:
|
||||
with profile("sync"):
|
||||
@@ -1309,6 +1316,18 @@ class CreateDrawing(bpy.types.Operator):
|
||||
self.svg_settings = ifcopenshell.geom.settings()
|
||||
self.svg_settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
self.svg_settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.NATIVE)
|
||||
# SVG edge classification (issue #3668). See edge-classification.md. Settings are
|
||||
# per-drawing, stored in EPset_Drawing and read into self.cprops by import_camera_props.
|
||||
try:
|
||||
self.svg_settings.set("svg-use-edge-classification", self.cprops.use_edge_classification)
|
||||
self.svg_settings.set("svg-render-crease-edges", self.cprops.render_creases)
|
||||
self.svg_settings.set("svg-valley-angle-min-degrees", self.cprops.valley_angle_min_degrees)
|
||||
self.svg_settings.set("svg-render-sharp-edges", self.cprops.render_sharp)
|
||||
self.svg_settings.set("svg-ridge-angle-min-degrees", self.cprops.ridge_angle_min_degrees)
|
||||
self.svg_settings.set("svg-emit-flush-edges", self.cprops.render_flush)
|
||||
except Exception:
|
||||
# Backwards compatibility with older ifcopenshell builds that don't expose these keys.
|
||||
pass
|
||||
self.svg_buffer = ifcopenshell.geom.serializers.buffer()
|
||||
self.serialiser_settings = ifcopenshell.geom.serializer_settings()
|
||||
self.serialiser = ifcopenshell.geom.serializers.svg(
|
||||
|
||||
@@ -536,6 +536,50 @@ class BIMCameraProperties(PropertyGroup):
|
||||
default=True,
|
||||
update=get_update_layer_callback("has_annotation", "HasAnnotation"),
|
||||
)
|
||||
use_edge_classification: BoolProperty(
|
||||
name="Use Edge Classification",
|
||||
description="Classify projection edges into boundary/outline/sharp/crease/flush "
|
||||
"instead of drawing all linework identically. See edge-classification.md",
|
||||
default=False,
|
||||
update=get_update_layer_callback("use_edge_classification", "UseEdgeClassification"),
|
||||
)
|
||||
render_creases: BoolProperty(
|
||||
name="Render Creases",
|
||||
description="Render 'crease' (concave) projection edges",
|
||||
default=True,
|
||||
update=get_update_layer_callback("render_creases", "RenderCreases"),
|
||||
)
|
||||
valley_angle_min_degrees: FloatProperty(
|
||||
name="Valley Angle Minimum",
|
||||
description="Minimum concave dihedral deviation from flat, in degrees, for a projection "
|
||||
"edge to be classified as 'crease' rather than 'flush'",
|
||||
default=12.0,
|
||||
min=0.0,
|
||||
max=180.0,
|
||||
update=get_update_layer_callback("valley_angle_min_degrees", "ValleyAngleMinDegrees"),
|
||||
)
|
||||
render_sharp: BoolProperty(
|
||||
name="Render Sharp",
|
||||
description="Render 'sharp' (convex) projection edges",
|
||||
default=True,
|
||||
update=get_update_layer_callback("render_sharp", "RenderSharp"),
|
||||
)
|
||||
ridge_angle_min_degrees: FloatProperty(
|
||||
name="Ridge Angle Minimum",
|
||||
description="Minimum convex dihedral deviation from flat, in degrees, for a projection "
|
||||
"edge to be classified as 'sharp' rather than 'flush'",
|
||||
default=45.0,
|
||||
min=0.0,
|
||||
max=180.0,
|
||||
update=get_update_layer_callback("ridge_angle_min_degrees", "RidgeAngleMinDegrees"),
|
||||
)
|
||||
render_flush: BoolProperty(
|
||||
name="Render Flush",
|
||||
description="Render 'flush' projection edges (dihedral deviation below both ridge/valley "
|
||||
"thresholds). Omitted by default",
|
||||
default=False,
|
||||
update=get_update_layer_callback("render_flush", "RenderFlush"),
|
||||
)
|
||||
target_view: EnumProperty(
|
||||
name="Target View",
|
||||
default="PLAN_VIEW",
|
||||
|
||||
@@ -110,12 +110,14 @@ class Scheduler:
|
||||
y = self.margin
|
||||
rows = list(sheet.iter_rows())
|
||||
total_rows = len(rows)
|
||||
x = None
|
||||
for i, row in enumerate(rows):
|
||||
# The last row may contain only null values
|
||||
if i == (total_rows - 1) and not [c for c in row if c.value is not None]:
|
||||
continue
|
||||
|
||||
x = self.margin
|
||||
unmerged_height = None
|
||||
for cell in row:
|
||||
if isinstance(cell, openpyxl.cell.cell.MergedCell):
|
||||
column_letter = openpyxl.utils.get_column_letter(cell.column)
|
||||
@@ -230,8 +232,11 @@ class Scheduler:
|
||||
)
|
||||
|
||||
x += unmerged_width
|
||||
|
||||
assert unmerged_height is not None
|
||||
y += unmerged_height
|
||||
|
||||
assert x is not None
|
||||
total_width = x + self.margin
|
||||
total_height = y + self.margin
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
@@ -375,6 +380,7 @@ class Scheduler:
|
||||
tri = 0
|
||||
stop_iterating_over_rows = False
|
||||
# TODO: row spans support?
|
||||
x = None
|
||||
for tr in table.getElementsByType(TableRow):
|
||||
if stop_iterating_over_rows:
|
||||
break
|
||||
@@ -491,6 +497,7 @@ class Scheduler:
|
||||
tri += 1
|
||||
y += height
|
||||
|
||||
assert x is not None
|
||||
total_width = x + self.margin
|
||||
total_height = y + self.margin
|
||||
self.svg["width"] = "{}mm".format(total_width)
|
||||
|
||||
@@ -102,16 +102,16 @@ void angle_circle_head(
|
||||
in vec4 circle_start, in float circle_angle,
|
||||
in bool counterclockwise,
|
||||
out vec4 head[CIRCLE_SEGS+1], out float angle_segs) {
|
||||
|
||||
|
||||
// 1 added to CIRCLE_SEGS because we're number of vertices
|
||||
// for n segments is n+1
|
||||
|
||||
|
||||
float angle_d;
|
||||
angle_d = PI * 2 / CIRCLE_SEGS; // 30d
|
||||
// need to bottom clamp it to 1, otherwise it causes Blender crash at extruding the curve
|
||||
angle_segs = max(1, ceil(circle_angle / angle_d));
|
||||
angle_d = circle_angle / angle_segs;
|
||||
|
||||
|
||||
for(int i = 0; i < (angle_segs + 1); i++) {
|
||||
float angle = angle_d * i;
|
||||
if (counterclockwise) {
|
||||
@@ -143,7 +143,7 @@ void cross_head(in vec4 dir, in float size, out vec4 head[3]) {
|
||||
#define do_vertex(pos, e) (do_vertex_util(pos, vec2(-(e).y, (e).x) / winsize.xy))
|
||||
#define do_vertex_win(pos, e) ( do_vertex( WIN2CLIP( pos ), e ) )
|
||||
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// if vertex is shared by two segments of the line still need to emit it twice
|
||||
// to avoid smoothing artifacts
|
||||
// don't forget to initialize `vec2 EDGE_DIR` for macro to work
|
||||
// `pos0` / `pos1` - vertex position in clip space
|
||||
@@ -197,10 +197,13 @@ void do_circle_head(vec4 pos_w, vec4 head[CIRCLE_SEGS]) {
|
||||
|
||||
def add_verts_sequence(verts, start_i, output_verts, output_edges, closed=False):
|
||||
"""Add sequence of verts to output lists, returns next vertex index"""
|
||||
i = None
|
||||
for i, v in enumerate(verts[:-1], start_i):
|
||||
output_verts.append(v)
|
||||
output_edges.append((i, i + 1))
|
||||
output_verts.append(verts[-1])
|
||||
assert i is not None
|
||||
|
||||
if closed:
|
||||
output_edges.append((i + 1, start_i))
|
||||
return i + 2
|
||||
@@ -273,7 +276,7 @@ class BaseShader:
|
||||
FRAG_GLSL = """
|
||||
uniform vec4 color;
|
||||
uniform float lineWidth;
|
||||
|
||||
|
||||
in float smoothline;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
|
||||
@@ -1449,6 +1449,7 @@ class SvgWriter:
|
||||
angle_tg = rise / run
|
||||
angle = round(degrees(atan(angle_tg)))
|
||||
else:
|
||||
angle_tg = None
|
||||
angle = 90
|
||||
|
||||
# ues SLOPE_ANGLE as default
|
||||
@@ -1462,6 +1463,7 @@ class SvgWriter:
|
||||
elif object_type == "SLOPE_PERCENT":
|
||||
if angle == 90:
|
||||
return "-"
|
||||
assert angle_tg is not None
|
||||
return f"{round(angle_tg * 100)} %"
|
||||
|
||||
tag = element.Description or get_label_text()
|
||||
|
||||
@@ -113,6 +113,19 @@ class BIM_PT_camera(Panel):
|
||||
row.prop(props, "fill_mode")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "cut_mode")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "use_edge_classification")
|
||||
if props.use_edge_classification:
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_creases")
|
||||
row.prop(props, "valley_angle_min_degrees")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_sharp")
|
||||
row.prop(props, "ridge_angle_min_degrees")
|
||||
row = self.layout.row()
|
||||
row.prop(props, "render_flush")
|
||||
|
||||
row = self.layout.row()
|
||||
row.prop(props, "width")
|
||||
row = self.layout.row()
|
||||
@@ -964,14 +977,14 @@ class BIM_UL_sheets(bpy.types.UIList):
|
||||
|
||||
if self.filter_name:
|
||||
filter_name = self.filter_name.lower()
|
||||
active_sheet = None
|
||||
active_sheet_index = None
|
||||
for sheet in data.sheets:
|
||||
if sheet.is_sheet:
|
||||
active_sheet = sheet
|
||||
active_sheet_index = len(flt_flags)
|
||||
if filter_name in sheet.name.lower() or filter_name in sheet.identification.lower():
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
if not sheet.is_sheet:
|
||||
assert active_sheet_index is not None
|
||||
flt_flags[active_sheet_index] = self.bitflag_filter_item
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
|
||||
@@ -75,9 +75,13 @@ class Helper:
|
||||
for face in bm.faces:
|
||||
if len(face.verts) > 4:
|
||||
potential_faces.append(face)
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
assert face is not None
|
||||
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
@@ -108,10 +112,12 @@ class Helper:
|
||||
if not potential_faces:
|
||||
potential_faces = bm.faces
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
|
||||
assert face is not None
|
||||
profile = [l.vert.index for l in face.loops]
|
||||
extrusion = self.detect_extrusion_edge(bm, face)
|
||||
|
||||
@@ -145,9 +151,12 @@ class Helper:
|
||||
if total_verts > 4:
|
||||
potential_faces.append(face)
|
||||
|
||||
# TODO: replace with next(..., None)
|
||||
face = None
|
||||
for face in potential_faces:
|
||||
if face.normal.z < -0.1:
|
||||
break
|
||||
assert face is not None
|
||||
|
||||
end_faces = []
|
||||
end_face_normal = face.normal
|
||||
|
||||
@@ -581,7 +581,11 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if has_openings and not self.apply_openings:
|
||||
# Meshlike things with openings can only be updated without openings applied.
|
||||
if self.from_ui:
|
||||
self.report({"ERROR"}, f"Object '{obj.name}' has openings - representation cannot be updated.")
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Object '{obj.name}' has openings. "
|
||||
"ALT+click the button to bake the openings into the new representation.",
|
||||
)
|
||||
return
|
||||
|
||||
if not product.is_a("IfcGridAxis"):
|
||||
@@ -3523,12 +3527,15 @@ class EditRepresentationItemShapeAspect(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if props.representation_item_shape_aspect == "NEW":
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
# find IfcProductRepresentationSelect based on current representation
|
||||
product_shape = None
|
||||
if hasattr(element, "Representation"): # IfcProduct
|
||||
product_shape = element.Representation
|
||||
else: # IfcTypeProduct
|
||||
for representation_map in element.RepresentationMaps:
|
||||
if representation_map.MappedRepresentation == active_representation:
|
||||
product_shape = representation_map
|
||||
assert product_shape is not None
|
||||
|
||||
previous_shape_aspect_id = props.active_item.shape_aspect_id
|
||||
# will be None if item didn't had a shape aspect
|
||||
previous_shape_aspect = tool.Ifc.get_entity_by_id(previous_shape_aspect_id)
|
||||
@@ -3878,6 +3885,8 @@ class AddSweptAreaSolidItem(bpy.types.Operator, tool.Ifc.Operator):
|
||||
curve = builder.rectangle(size=Vector((0.5, 0.5)) / unit_scale)
|
||||
elif self.shape == "CYLINDER":
|
||||
curve = builder.circle(radius=0.25 / unit_scale)
|
||||
else:
|
||||
assert False, self.shape
|
||||
item = builder.extrude(
|
||||
curve,
|
||||
magnitude=0.5 / unit_scale,
|
||||
@@ -4115,6 +4124,31 @@ class OverrideMoveSelect(bpy.types.Operator):
|
||||
self.new_active_obj = obj
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get arrays
|
||||
ifc_file = tool.Ifc.get()
|
||||
array_parents_to_move: list[bpy.types.Object] = []
|
||||
for obj in list(context.selected_objects):
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element:
|
||||
continue
|
||||
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||
if not pset:
|
||||
continue
|
||||
parent_element = ifc_file.by_guid(pset["Parent"])
|
||||
parent_obj = tool.Ifc.get_object(parent_element)
|
||||
if parent_obj not in array_parents_to_move:
|
||||
array_parents_to_move.append(parent_obj)
|
||||
if element.GlobalId != pset["Parent"]:
|
||||
obj.select_set(False)
|
||||
|
||||
if array_parents_to_move:
|
||||
for parent_obj in array_parents_to_move:
|
||||
parent_element = tool.Ifc.get_entity(parent_obj)
|
||||
for array_obj in tool.Array.get_all_objects(parent_element):
|
||||
array_obj.select_set(True)
|
||||
self.new_active_obj = parent_obj
|
||||
return {"FINISHED"}
|
||||
|
||||
# Get nests
|
||||
props = tool.Nest.get_nest_props()
|
||||
not_editing_objs = [o.obj for o in props.not_editing_objects]
|
||||
|
||||
@@ -156,6 +156,7 @@ class RadianceRender(bpy.types.Operator):
|
||||
print(f"Quality: {quality}, Detail: {detail}, Variability: {variability}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
hdr_image_path, hdr_mask_path, sky_map_cal_path = None, None
|
||||
if use_hdr:
|
||||
hdr_image = "noon_grass_2k.hdr"
|
||||
hdr_mask = "noon_grass_2k_mask.hdr"
|
||||
@@ -254,6 +255,9 @@ class RadianceRender(bpy.types.Operator):
|
||||
# 4 0 0 -1 180
|
||||
|
||||
if use_hdr and choose_hdr_image == "Noon":
|
||||
assert hdr_image_path is not None
|
||||
assert hdr_mask_path is not None
|
||||
assert sky_map_cal_path is not None
|
||||
|
||||
with open(sky_file_path, "w") as f:
|
||||
f.write(sky_description_str)
|
||||
|
||||
@@ -564,6 +564,7 @@ class SelectAllArrayObjects(bpy.types.Operator):
|
||||
except RuntimeError:
|
||||
self.report({"ERROR"}, f"Objects that don't have an array parent, were deselected.")
|
||||
object.select_set(False)
|
||||
continue
|
||||
|
||||
array_objects = tool.Array.get_all_objects(parent_element)
|
||||
tool.Blender.set_objects_selection(
|
||||
|
||||
@@ -408,6 +408,8 @@ class MEPGenerator:
|
||||
compare = tool.Cad.is_x(requested_value, fitting_value, compare_precision)
|
||||
elif isinstance(fitting_value, list):
|
||||
compare = tool.Cad.are_vectors_equal(requested_value, Vector(fitting_value), precision)
|
||||
else:
|
||||
assert False, f"{key} {second_key}"
|
||||
return compare
|
||||
|
||||
ignore_keys = []
|
||||
@@ -476,11 +478,13 @@ class MEPGenerator:
|
||||
if predefined_type == "OBSTRUCTION":
|
||||
return packed_data
|
||||
|
||||
start_port = None
|
||||
for port in ports:
|
||||
port_local_position = V(*port.ObjectPlacement.RelativePlacement.Location.Coordinates)
|
||||
if tool.Cad.is_x(port_local_position.length, 0.0):
|
||||
start_port = port
|
||||
break
|
||||
assert start_port is not None
|
||||
|
||||
connected_port = tool.System.get_connected_port(start_port)
|
||||
connected_element = tool.System.get_port_relating_element(connected_port)
|
||||
|
||||
@@ -325,7 +325,7 @@ class AddOccurrence(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if self.from_invoke and str(self.relating_type_id) in AuthoringData.data["relating_type_id"]:
|
||||
props.relating_type_id = str(self.relating_type_id)
|
||||
|
||||
building_obj = None
|
||||
building_obj, building_element = None, None
|
||||
if len(context.selected_objects) == 1 and context.active_object:
|
||||
building_obj = context.active_object
|
||||
building_element = tool.Ifc.get_entity(building_obj)
|
||||
|
||||
@@ -593,6 +593,8 @@ class DumbProfileJoiner:
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[1]) - (profile2.matrix_world.inverted() @ axis1[0])
|
||||
elif connection1 == "ATSTART":
|
||||
axisl = (profile2.matrix_world.inverted() @ axis1[0]) - (profile2.matrix_world.inverted() @ axis1[1])
|
||||
else:
|
||||
assert False, connection1
|
||||
xy_angle = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle >= -135 and xy_angle <= -45:
|
||||
closest_plane = "bottom"
|
||||
@@ -617,6 +619,8 @@ class DumbProfileJoiner:
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[1]) - (profile1.matrix_world.inverted() @ axis2[0])
|
||||
elif connection2 == "ATSTART":
|
||||
axisl = (profile1.matrix_world.inverted() @ axis2[0]) - (profile1.matrix_world.inverted() @ axis2[1])
|
||||
else:
|
||||
assert False, connection2
|
||||
xy_angle2 = degrees(Vector((1, 0)).angle_signed(axisl.normalized().to_2d()))
|
||||
if xy_angle2 >= -135 and xy_angle2 <= -45:
|
||||
closest_plane2 = "bottom"
|
||||
@@ -844,6 +848,8 @@ class DumbProfileJoiner:
|
||||
else:
|
||||
y_axis = obj.matrix_world.to_quaternion() @ Vector((0, 1, 0))
|
||||
z_axis = obj.matrix_world.to_quaternion() @ Vector((-1, 0, 0))
|
||||
else:
|
||||
assert False, plane
|
||||
return self.create_matrix(p, x_axis, y_axis, z_axis)
|
||||
|
||||
def create_matrix(self, p: Vector, x: Vector, y: Vector, z: Vector) -> Matrix:
|
||||
|
||||
@@ -508,6 +508,7 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
converter.run()
|
||||
|
||||
profile = tool.Ifc.get().createIfcArbitraryClosedProfileDef("AREA")
|
||||
curve = None
|
||||
for path in converter.paths:
|
||||
points = []
|
||||
lines = path[0]
|
||||
@@ -517,6 +518,7 @@ class EditSketchExtrusionProfile(bpy.types.Operator, tool.Ifc.Operator):
|
||||
points.append(tool.Ifc.get().createIfcCartesianPoint(local_point))
|
||||
points.append(points[0])
|
||||
curve = tool.Ifc.get().createIfcPolyline(points)
|
||||
assert curve
|
||||
profile.OuterCurve = curve
|
||||
|
||||
old_profile = extrusion.SweptArea
|
||||
|
||||
@@ -1577,6 +1577,7 @@ class DumbWallJoiner:
|
||||
# Get the ATEND connection from wall1 to use it in wall2
|
||||
relating_element = None
|
||||
connections = element1.ConnectedTo
|
||||
relating_connection, description = ..., ...
|
||||
for conn in connections:
|
||||
if conn.is_a("IfcRelConnectsPathElements") and conn.RelatingConnectionType == "ATEND":
|
||||
relating_element = conn.RelatedElement
|
||||
@@ -1591,6 +1592,7 @@ class DumbWallJoiner:
|
||||
description = conn.Description
|
||||
bonsai.core.geometry.remove_connection(tool.Geometry, connection=conn)
|
||||
if relating_element:
|
||||
assert relating_connection is not ... and description is not ...
|
||||
ifcopenshell.api.geometry.connect_path(
|
||||
tool.Ifc.get(),
|
||||
relating_element=relating_element,
|
||||
|
||||
@@ -714,6 +714,8 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
representations = element.RepresentationMaps or []
|
||||
elif element.is_a("IfcProduct"):
|
||||
representations = [element.Representation] if element.Representation else []
|
||||
else:
|
||||
assert False, element
|
||||
for representation in representations or []:
|
||||
for element in self.file.traverse(representation):
|
||||
if not element.is_a("IfcRepresentationItem") or not element.StyledByItem:
|
||||
@@ -2029,6 +2031,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
project_props = tool.Project.get_project_props()
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
project_props.use_relative_project_path = self.use_relative_path
|
||||
old_history_size, old_undo_steps = None, None
|
||||
if prefs.should_disable_undo_on_save:
|
||||
old_history_size = tool.Ifc.get().history_size
|
||||
old_undo_steps = context.preferences.edit.undo_steps
|
||||
@@ -2036,6 +2039,7 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
|
||||
context.preferences.edit.undo_steps = 0
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
if prefs.should_disable_undo_on_save:
|
||||
assert old_history_size is not None and old_undo_steps is not None
|
||||
tool.Ifc.get().history_size = old_history_size
|
||||
context.preferences.edit.undo_steps = old_undo_steps
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -113,6 +113,8 @@ class EditPset(bpy.types.Operator, tool.Ifc.Operator):
|
||||
elif props.active_pset_type == "QTO":
|
||||
pset = ifcopenshell.api.pset.add_qto(self.file, product=element, name=props.active_pset_name)
|
||||
props.active_pset_id = pset.id()
|
||||
else:
|
||||
assert False
|
||||
|
||||
if self.properties:
|
||||
properties = json.loads(self.properties)
|
||||
|
||||
@@ -228,6 +228,8 @@ def get_qto_name(self: "PsetProperties", context: bpy.types.Context) -> tool.Ble
|
||||
if "bpy.data.objects" in pset_type:
|
||||
if prop_type == "PsetProperties":
|
||||
results = get_object_qto_name(self, context)
|
||||
else:
|
||||
assert False
|
||||
elif prop_type == "TaskPsetProperties":
|
||||
results = get_task_qto_names(self, context)
|
||||
elif prop_type == "ResourcePsetProperties":
|
||||
|
||||
@@ -480,6 +480,7 @@ class BIM_PT_material_psets(Panel):
|
||||
def draw(self, context):
|
||||
assert self.layout
|
||||
props = tool.Material.get_material_props()
|
||||
ifc_definition_id = None
|
||||
if material := props.active_material:
|
||||
ifc_definition_id = material.ifc_definition_id
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import ifcopenshell.api.material
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.type
|
||||
@@ -129,13 +130,25 @@ class ReassignClass(bpy.types.Operator, tool.Ifc.Operator):
|
||||
same_ifc_product = element.is_a(ifc_product)
|
||||
|
||||
if not same_ifc_product:
|
||||
if not (element.is_a("IfcElement") and ifc_product == "IfcElementType") and not (
|
||||
element.is_a("IfcElementType") and ifc_product == "IfcElement"
|
||||
):
|
||||
self.report(
|
||||
{"ERROR"}, f"Not supported class reassignment for object '{obj.name}' -> {ifc_product}."
|
||||
# A spatial element (e.g. IfcSite) anchors the containment
|
||||
# hierarchy, so only allow reassigning it to another family when
|
||||
# it actually carries geometry - i.e. it's a real modelled thing
|
||||
# (a bench dropped onto IfcSite -> IfcFurniture) rather than an
|
||||
# empty spatial container we'd be turning into a loose element.
|
||||
# IfcSpatialStructureElement covers IFC2X3, which has no
|
||||
# IfcSpatialElement supertype.
|
||||
is_spatial = element.is_a("IfcSpatialElement") or element.is_a("IfcSpatialStructureElement")
|
||||
if is_spatial:
|
||||
has_geometry = (
|
||||
next(ifcopenshell.util.representation.get_representations_iter(element), None) is not None
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
if not has_geometry:
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Cannot reassign '{obj.name}' ({element.is_a()}) to {ifc_product}: "
|
||||
"a spatial element can only be reassigned to another class when it has geometry.",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
props.is_reassigning_class = False
|
||||
@@ -617,10 +630,13 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
local_z = wall_matrix.to_3x3() @ Vector((0, 0, 1))
|
||||
direction_sense = getattr(usage, "DirectionSense", "POSITIVE")
|
||||
|
||||
if usage.LayerSetDirection == "AXIS2":
|
||||
layer_set_direction = usage.LayerSetDirection
|
||||
if layer_set_direction == "AXIS2":
|
||||
z_axis = tuple(local_y) if direction_sense == "POSITIVE" else tuple(-local_y)
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
elif layer_set_direction == "AXIS3":
|
||||
z_axis = tuple(local_z) if direction_sense == "POSITIVE" else tuple(-local_z)
|
||||
else:
|
||||
assert False, layer_set_direction
|
||||
|
||||
item = builder.extrude(
|
||||
profile,
|
||||
@@ -750,6 +766,8 @@ class AddElement(bpy.types.Operator, tool.Ifc.Operator):
|
||||
WebThickness=default_web_thickness / unit_scale,
|
||||
FlangeThickness=default_flange_thickness / unit_scale,
|
||||
)
|
||||
else:
|
||||
assert False, representation_template
|
||||
|
||||
rel = ifcopenshell.api.material.assign_material(
|
||||
tool.Ifc.get(), products=[element], type="IfcMaterialProfileSet"
|
||||
|
||||
@@ -1009,6 +1009,7 @@ class ColourByProperty(Operator):
|
||||
palette = props.palette
|
||||
is_qualitative = palette in ("tab10", "paired")
|
||||
|
||||
colours = None
|
||||
if is_qualitative:
|
||||
colours = tool.Search.get_qualitative_palette(palette)
|
||||
|
||||
@@ -1035,6 +1036,7 @@ class ColourByProperty(Operator):
|
||||
if value in colourscheme:
|
||||
colourscheme[value]["total"] += 1
|
||||
else:
|
||||
assert colours is not None
|
||||
colourscheme[value] = {"colour": next(colours)[0:3], "total": 1}
|
||||
obj.color = (*colourscheme[value]["colour"], 1)
|
||||
else:
|
||||
@@ -1139,6 +1141,7 @@ class SelectByProperty(Operator):
|
||||
|
||||
is_qualitative = palette in ("tab10", "paired")
|
||||
|
||||
values = None
|
||||
if not is_qualitative:
|
||||
values = []
|
||||
for colour in props.colourscheme:
|
||||
|
||||
@@ -281,11 +281,11 @@ class BIM_PT_work_schedules(Panel):
|
||||
def draw_task_operators(self) -> None:
|
||||
row = self.layout.row(align=True)
|
||||
row.alignment = "RIGHT"
|
||||
ifc_definition_id = None
|
||||
task, ifc_definition_id = None, None
|
||||
if self.tprops.tasks and self.props.active_task_index < len(self.tprops.tasks):
|
||||
task = self.tprops.tasks[self.props.active_task_index]
|
||||
ifc_definition_id = task.ifc_definition_id
|
||||
if ifc_definition_id:
|
||||
if task and ifc_definition_id:
|
||||
if self.props.active_task_id:
|
||||
if self.props.editing_task_type == "TASKTIME":
|
||||
row.operator("bim.edit_task_time", text="", icon="CHECKMARK")
|
||||
@@ -341,6 +341,8 @@ class BIM_PT_work_schedules(Panel):
|
||||
row.prop(self.props, "other_columns", text="")
|
||||
column_type, name = self.props.other_columns.split(".")
|
||||
data_type = "string"
|
||||
else:
|
||||
assert False, column_type
|
||||
row.operator("bim.set_task_sort_column", text="", icon="SORTALPHA").column = f"{column_type}.{name}"
|
||||
row.prop(
|
||||
self.props, "is_sort_reversed", text="", icon="SORT_DESC" if self.props.is_sort_reversed else "SORT_ASC"
|
||||
|
||||
@@ -516,7 +516,7 @@ class SetContainerVisibility(bpy.types.Operator):
|
||||
if self.mode == "ISOLATE":
|
||||
if tool.Ifc.get_schema() == "IFC2X3":
|
||||
containers = tool.Ifc.get().by_type("IfcSpatialStructureElement")
|
||||
elif tool.Ifc.get_schema() != "IFC2X3":
|
||||
else:
|
||||
containers = set(tool.Ifc.get().by_type("IfcSpatialElement"))
|
||||
containers -= set(tool.Ifc.get().by_type("IfcSpatialZone"))
|
||||
for container in containers:
|
||||
|
||||
@@ -125,6 +125,7 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
row.label(text="Warning: No Default Container", icon="ERROR")
|
||||
row.operator("bim.import_spatial_decomposition", icon="FILE_REFRESH", text="")
|
||||
|
||||
ifc_definition_id = None
|
||||
if self.props.active_container:
|
||||
ifc_definition_id = self.props.active_container.ifc_definition_id
|
||||
row = self.layout.row(align=True)
|
||||
@@ -170,6 +171,7 @@ class BIM_PT_spatial_decomposition(Panel):
|
||||
|
||||
if not self.props.active_container:
|
||||
return
|
||||
assert ifc_definition_id is not None
|
||||
|
||||
container_has_elements = bool(self.props.total_elements)
|
||||
if container_has_elements:
|
||||
|
||||
@@ -102,6 +102,7 @@ class BIM_PT_styles(Panel):
|
||||
|
||||
# style ui tools
|
||||
if active_style:
|
||||
style = active_style
|
||||
row = self.layout.row(align=True)
|
||||
if material := style.blender_material:
|
||||
msprops = tool.Style.get_material_style_props(material)
|
||||
|
||||
@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.attribute
|
||||
import ifcopenshell.api.material
|
||||
import ifcopenshell.api.type
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.representation
|
||||
@@ -115,51 +116,96 @@ class UnassignType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if TYPE_CHECKING:
|
||||
related_object: str
|
||||
|
||||
def _execute(self, context):
|
||||
@staticmethod
|
||||
def _reattach_styles(
|
||||
file: ifcopenshell.file, copied_entities: dict[int, ifcopenshell.entity_instance]
|
||||
) -> None:
|
||||
"""copy_deep only follows forward references, so IfcStyledItem (an inverse,
|
||||
``StyledByItem``) is not carried onto the copied geometry. Re-create a
|
||||
styled item on each copy that points at the same presentation styles as
|
||||
the original, so the unmapped occurrence keeps its appearance."""
|
||||
for original_id, copied in copied_entities.items():
|
||||
original = file.by_id(original_id)
|
||||
for styled_item in getattr(original, "StyledByItem", None) or []:
|
||||
file.create_entity(
|
||||
"IfcStyledItem",
|
||||
Item=copied,
|
||||
Styles=styled_item.Styles,
|
||||
Name=styled_item.Name,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unassign_and_unmap(obj: bpy.types.Object) -> None:
|
||||
"""Unassign the type from ``obj`` and bake a private copy of any mapped
|
||||
representation onto it, so the occurrence keeps its geometry, styles, and
|
||||
material once the type (the source of all three) is gone."""
|
||||
|
||||
def exclude_callback(attribute):
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
self.file = tool.Ifc.get()
|
||||
file = tool.Ifc.get()
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
return
|
||||
|
||||
# Capture the material inherited from the type before we sever the link,
|
||||
# but only if the occurrence has no material of its own to override it.
|
||||
own_material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
inherited_material = ifcopenshell.util.element.get_material(element, should_inherit=True)
|
||||
|
||||
ifcopenshell.api.type.unassign_type(file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations, carrying over their styles.
|
||||
copied_entities: dict[int, ifcopenshell.entity_instance] = {}
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
file,
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
copied_entities=copied_entities,
|
||||
)
|
||||
UnassignType._reattach_styles(file, copied_entities)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
|
||||
# Bake the inherited material down onto the occurrence now that its type
|
||||
# link (and, in the delete-type case, the type itself) is gone. Usages are
|
||||
# occurrence-specific and never inherited, so they need no handling here.
|
||||
if inherited_material is not None and own_material is None:
|
||||
material_type = inherited_material.is_a()
|
||||
if material_type not in ("IfcMaterialLayerSetUsage", "IfcMaterialProfileSetUsage"):
|
||||
ifcopenshell.api.material.assign_material(
|
||||
file, products=[element], type=material_type, material=inherited_material
|
||||
)
|
||||
|
||||
def _execute(self, context):
|
||||
if self.related_object:
|
||||
related_objects = [bpy.data.objects[self.related_object]]
|
||||
else:
|
||||
related_objects = tool.Blender.get_selected_objects()
|
||||
|
||||
for obj in related_objects:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not element.is_a("IfcObject"):
|
||||
continue
|
||||
ifcopenshell.api.type.unassign_type(self.file, related_objects=[element])
|
||||
|
||||
if element.Representation:
|
||||
new_active_representation = None
|
||||
active_representation = tool.Geometry.get_active_representation(obj)
|
||||
active_context = active_representation.ContextOfItems
|
||||
representations = []
|
||||
for representation in element.Representation.Representations:
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
if representation == resolved_representation:
|
||||
representations.append(representation)
|
||||
else:
|
||||
# We must unmap representations.
|
||||
copied_representation = ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
resolved_representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
representations.append(copied_representation)
|
||||
if representation.ContextOfItems == active_context:
|
||||
new_active_representation = copied_representation
|
||||
element.Representation.Representations = representations
|
||||
|
||||
if new_active_representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
representation=new_active_representation,
|
||||
)
|
||||
self.unassign_and_unmap(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -305,14 +351,82 @@ class SelectTypeObjects(bpy.types.Operator):
|
||||
|
||||
class RemoveType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.remove_type"
|
||||
bl_label = "Remove Type"
|
||||
bl_label = "Delete Type"
|
||||
bl_description = (
|
||||
"Delete this type. Its occurrences are kept but become untyped.\n\n"
|
||||
"SHIFT+Click to also delete every occurrence of this type in the project"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
element: bpy.props.IntProperty()
|
||||
also_delete_instances: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
|
||||
|
||||
if TYPE_CHECKING:
|
||||
element: int
|
||||
also_delete_instances: bool
|
||||
|
||||
@staticmethod
|
||||
def _detach_type_material_set(element: ifcopenshell.entity_instance) -> None:
|
||||
"""Cascade-free removal of the type's IfcMaterialLayerSet / IfcMaterialProfileSet
|
||||
association, called just before the type is deleted.
|
||||
|
||||
``remove_product`` would otherwise route the type's material association
|
||||
through ``unassign_material``, which deletes *every* usage of that set
|
||||
across the model (documented behaviour, with an upstream TODO calling it
|
||||
too aggressive) — stripping the material off the very occurrences we are
|
||||
trying to keep. By unhooking the type<->set link by hand here, the type
|
||||
has no material at delete time, so that cascade never fires and the set
|
||||
plus the occurrences' usages survive intact."""
|
||||
file = tool.Ifc.get()
|
||||
material = ifcopenshell.util.element.get_material(element, should_inherit=False)
|
||||
if not material or material.is_a() not in ("IfcMaterialLayerSet", "IfcMaterialProfileSet"):
|
||||
return
|
||||
for rel in list(getattr(element, "HasAssociations", None) or []):
|
||||
if not (rel.is_a("IfcRelAssociatesMaterial") and rel.RelatingMaterial == material):
|
||||
continue
|
||||
remaining = [o for o in rel.RelatedObjects if o != element]
|
||||
if remaining:
|
||||
rel.RelatedObjects = remaining
|
||||
else:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
if history:
|
||||
ifcopenshell.util.element.remove_deep2(file, history)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.also_delete_instances = event.shift
|
||||
if self.also_delete_instances:
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
count = len(ifcopenshell.util.element.get_types(element))
|
||||
return context.window_manager.invoke_confirm(
|
||||
self,
|
||||
event,
|
||||
title="Delete Type and Occurrences",
|
||||
message=f"This will delete the type and all {count} of its occurrences.",
|
||||
confirm_text="Delete",
|
||||
)
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
element = tool.Ifc.get().by_id(self.element)
|
||||
occurrences = ifcopenshell.util.element.get_types(element)
|
||||
if self.also_delete_instances:
|
||||
for occurrence in occurrences:
|
||||
occurrence_obj = tool.Ifc.get_object(occurrence)
|
||||
if occurrence_obj:
|
||||
tool.Geometry.delete_ifc_object(occurrence_obj)
|
||||
else:
|
||||
# Keep the occurrences: bake their (previously type-mapped) geometry,
|
||||
# styles, and inherited material onto each one so nothing is lost when
|
||||
# the type is deleted...
|
||||
for occurrence in occurrences:
|
||||
occ_obj = tool.Ifc.get_object(occurrence)
|
||||
if occ_obj:
|
||||
UnassignType.unassign_and_unmap(occ_obj)
|
||||
# ...and keep any layer/profile-set material usages alive across the deletion.
|
||||
self._detach_type_material_set(element)
|
||||
obj = tool.Ifc.get_object(element)
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
if obj:
|
||||
tool.Geometry.delete_ifc_object(obj)
|
||||
|
||||
|
||||
class RenameType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
@@ -144,8 +144,10 @@ class BIM_PT_type_attributes(Panel):
|
||||
|
||||
bonsai.bim.helper.draw_attributes(props.type_attributes, layout)
|
||||
else:
|
||||
row = layout.row()
|
||||
row = layout.row(align=True)
|
||||
row.operator("bim.enable_editing_type_attributes", icon="GREASEPENCIL", text="Edit")
|
||||
op = row.operator("bim.remove_type", icon="TRASH", text="")
|
||||
op.element = TypeData.data["relating_type"]["id"]
|
||||
|
||||
for attribute in TypeData.data["relating_type_attributes"]:
|
||||
row = layout.row(align=True)
|
||||
|
||||
@@ -72,6 +72,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
opening_objects = [obj for obj in selected_objects if obj != target_object]
|
||||
|
||||
obj1 = ...
|
||||
for opening_obj in opening_objects:
|
||||
element1 = tool.Ifc.get_entity(target_object)
|
||||
obj1 = target_object
|
||||
@@ -196,6 +197,7 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bpy.data.objects.remove(obj2)
|
||||
|
||||
tool.Model.purge_scene_openings()
|
||||
assert obj1 is not ...
|
||||
context.view_layer.objects.active = obj1
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -284,11 +284,13 @@ class GizmoPreferences(bpy.types.PropertyGroup):
|
||||
draw_gizmos_in_3d_viewport: bool
|
||||
|
||||
|
||||
_gizmo_pref_entry = None
|
||||
for _gizmo_pref_entry in tool.Parametric.EDIT_TYPES:
|
||||
GizmoPreferences.__annotations__[_gizmo_pref_entry.name] = BoolProperty(
|
||||
name=_gizmo_pref_entry.name.replace("_", " ").title(),
|
||||
default=True,
|
||||
)
|
||||
assert _gizmo_pref_entry is not None
|
||||
del _gizmo_pref_entry
|
||||
|
||||
|
||||
@@ -394,12 +396,14 @@ class DefaultParameters(bpy.types.PropertyGroup):
|
||||
and gives the create operator a preset to copy from."""
|
||||
|
||||
|
||||
_default_params_entry = None
|
||||
for _default_params_entry in tool.Parametric.EDIT_TYPES:
|
||||
if not _default_params_entry.has_default_parameters:
|
||||
continue
|
||||
DefaultParameters.__annotations__[_default_params_entry.name] = bpy.props.PointerProperty(
|
||||
type=getattr(_model_prop, _default_params_entry.props_attr),
|
||||
)
|
||||
assert _default_params_entry is not None
|
||||
del _default_params_entry
|
||||
|
||||
|
||||
|
||||
@@ -74,10 +74,11 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
if not relating_type.is_a("IfcCoveringType"):
|
||||
relating_type = None
|
||||
|
||||
ceiling_height = None
|
||||
if selected_objects and active_obj:
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
else:
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
x, y, z, _, _ = spatial.get_x_y_z_h_mat_from_cursor()
|
||||
ceiling_height = covering.get_z_from_ceiling_height()
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
@@ -87,6 +88,7 @@ def add_instance_ceiling_covering_from_cursor(
|
||||
|
||||
obj = spatial.create_object("Covering")
|
||||
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
|
||||
assert ceiling_height is not None
|
||||
spatial.translate_obj_to_z_location(obj, z + ceiling_height)
|
||||
spatial.assign_type_to_obj(obj)
|
||||
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
|
||||
@@ -100,7 +102,9 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
|
||||
selected_objects = spatial.get_selected_objects()
|
||||
|
||||
if selected_objects and active_obj:
|
||||
x, y, z, h, mat = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
x, y, _, _, _ = spatial.get_x_y_z_h_mat_from_obj(active_obj)
|
||||
else:
|
||||
assert False, "Object has to be active and selected."
|
||||
|
||||
space_polygon = spatial.get_space_polygon_from_context_visible_objects(x, y)
|
||||
|
||||
|
||||
@@ -497,6 +497,7 @@ def add_annotation(
|
||||
drawing_tool.show_decorations()
|
||||
obj = drawing_tool.create_annotation_object(drawing, object_type)
|
||||
element = ifc.get_entity(obj)
|
||||
relating_type_rep = None
|
||||
if not element: # Brand new annotation
|
||||
relating_type_rep = drawing_tool.get_annotation_representation(relating_type) if relating_type else None
|
||||
element = drawing_tool.run_root_assign_class(
|
||||
|
||||
@@ -981,6 +981,7 @@ class Cad:
|
||||
has_found_connected_edge = True
|
||||
loops.append(loop)
|
||||
|
||||
new_verts = None
|
||||
for loop in loops:
|
||||
all_verts = {v.index for e in loop for v in e.verts}
|
||||
possible_v1s = []
|
||||
@@ -1084,6 +1085,7 @@ class Cad:
|
||||
break
|
||||
|
||||
v1 = v2
|
||||
assert new_verts is not None
|
||||
|
||||
return new_verts
|
||||
|
||||
|
||||
@@ -280,6 +280,8 @@ class Cost(bonsai.core.tool.Cost):
|
||||
new = props.cost_item_processes.add()
|
||||
elif related_object.is_a("IfcResource"):
|
||||
new = props.cost_item_resources.add()
|
||||
else:
|
||||
assert False, related_object
|
||||
new.ifc_definition_id = related_object.id()
|
||||
new.name = related_object.Name or "Unnamed"
|
||||
|
||||
|
||||
@@ -1072,6 +1072,12 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.has_annotation = True
|
||||
camera_props.target_view = "PLAN_VIEW"
|
||||
camera_props.is_nts = False
|
||||
camera_props.use_edge_classification = False
|
||||
camera_props.render_creases = True
|
||||
camera_props.valley_angle_min_degrees = 12.0
|
||||
camera_props.render_sharp = True
|
||||
camera_props.ridge_angle_min_degrees = 45.0
|
||||
camera_props.render_flush = False
|
||||
camera.shift_x = 0.0
|
||||
camera.shift_y = 0.0
|
||||
|
||||
@@ -1101,6 +1107,18 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
camera_props.has_annotation = bool(pset["HasAnnotation"])
|
||||
if "IsNTS" in pset:
|
||||
camera_props.is_nts = bool(pset["IsNTS"])
|
||||
if "UseEdgeClassification" in pset:
|
||||
camera_props.use_edge_classification = bool(pset["UseEdgeClassification"])
|
||||
if "RenderCreases" in pset:
|
||||
camera_props.render_creases = bool(pset["RenderCreases"])
|
||||
if "ValleyAngleMinDegrees" in pset:
|
||||
camera_props.valley_angle_min_degrees = float(pset["ValleyAngleMinDegrees"])
|
||||
if "RenderSharp" in pset:
|
||||
camera_props.render_sharp = bool(pset["RenderSharp"])
|
||||
if "RidgeAngleMinDegrees" in pset:
|
||||
camera_props.ridge_angle_min_degrees = float(pset["RidgeAngleMinDegrees"])
|
||||
if "RenderFlush" in pset:
|
||||
camera_props.render_flush = bool(pset["RenderFlush"])
|
||||
if "DPI" in pset:
|
||||
camera_props.dpi = int(pset["DPI"])
|
||||
if "LineworkMode" in pset:
|
||||
@@ -2575,16 +2593,15 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
if not obj:
|
||||
continue
|
||||
current_representation = tool.Geometry.get_active_representation(obj)
|
||||
current_representation_subcontext = None
|
||||
if current_representation:
|
||||
subcontext = current_representation.ContextOfItems
|
||||
current_representation_subcontext = tool.Geometry.get_subcontext_parameters(subcontext)
|
||||
|
||||
has_context = False
|
||||
for subcontext in subcontexts:
|
||||
# prioritize already active representation if it matches the subcontext
|
||||
# (element could have multiple representations in the same subcontext)
|
||||
if current_representation and subcontext == current_representation_subcontext:
|
||||
has_context = True
|
||||
if current_representation_subcontext and subcontext == current_representation_subcontext:
|
||||
break
|
||||
priority_representation = ifcopenshell.util.representation.get_representation(element, *subcontext)
|
||||
if priority_representation:
|
||||
@@ -2594,7 +2611,6 @@ class Drawing(bonsai.core.tool.Drawing):
|
||||
obj=obj,
|
||||
representation=priority_representation,
|
||||
)
|
||||
has_context = True
|
||||
break
|
||||
|
||||
linked_handles: set[bpy.types.Object] = set()
|
||||
|
||||
@@ -23,7 +23,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api.feature
|
||||
import ifcopenshell.util.representation
|
||||
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.tool
|
||||
@@ -50,6 +49,7 @@ class Feature(bonsai.core.tool.Feature):
|
||||
has_visible_openings = True
|
||||
break
|
||||
|
||||
element_had_openings = None
|
||||
for feature_obj in feature_objs:
|
||||
feature_element = tool.Ifc.get_entity(feature_obj)
|
||||
|
||||
@@ -58,7 +58,6 @@ class Feature(bonsai.core.tool.Feature):
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=featured_obj)
|
||||
|
||||
element_had_openings = tool.Geometry.has_openings(featured_element)
|
||||
body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body")
|
||||
ifcopenshell.api.feature.add_feature(tool.Ifc.get(), feature=feature_element, element=featured_element)
|
||||
|
||||
if tool.Ifc.is_moved(feature_obj):
|
||||
@@ -73,6 +72,7 @@ class Feature(bonsai.core.tool.Feature):
|
||||
if voided_obj.data:
|
||||
if tool.Ifc.is_edited(voided_obj):
|
||||
voided_element_ = tool.Ifc.get_entity(voided_obj)
|
||||
assert element_had_openings is not None
|
||||
if element_had_openings or (voided_element_ != featured_element and voided_element_.HasOpenings):
|
||||
voided_obj.scale = (1.0, 1.0, 1.0)
|
||||
tool.Ifc.finish_edit(voided_obj)
|
||||
|
||||
@@ -757,6 +757,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
# its centroid not obscured (tested via raycasting) by any other
|
||||
# face.
|
||||
distance = max(obj.dimensions.xyz)
|
||||
min_y, max_z = None, None
|
||||
if axis == "+Z":
|
||||
max_z = max([co[2] for co in obj.bound_box]) + 0.002
|
||||
direction = Vector((0, 0, -1))
|
||||
@@ -771,8 +772,10 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
if direction.dot(face.normal) > 0:
|
||||
continue
|
||||
if axis == "+Z":
|
||||
assert max_z is not None
|
||||
face_centroid_at_max = Vector((*face.calc_center_median().xy, max_z))
|
||||
elif axis == "-Y":
|
||||
assert min_y is not None
|
||||
centroid = face.calc_center_median()
|
||||
face_centroid_at_max = Vector((centroid.x, min_y, centroid.z))
|
||||
face_centroid_at_max = obj.matrix_world @ face_centroid_at_max
|
||||
@@ -1885,6 +1888,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
"""NOTE: we assume that all items belonged to the same representation and to the same shape aspect"""
|
||||
ifc_file = tool.Ifc.get()
|
||||
previous_shape_aspect = None
|
||||
base_representation = None
|
||||
for inverse in ifc_file.get_inverse(representation_items[0]):
|
||||
if inverse.is_a("IfcShapeRepresentation"):
|
||||
if inverse.OfShapeAspect:
|
||||
@@ -1894,6 +1898,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
previous_shape_aspect = inverse.OfShapeAspect[0]
|
||||
else:
|
||||
base_representation = inverse
|
||||
assert base_representation
|
||||
|
||||
# remove item from previous shape aspect
|
||||
if previous_shape_aspect:
|
||||
@@ -2211,6 +2216,7 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
assert item
|
||||
obj.data.clear_geometry()
|
||||
|
||||
cartesian_point_offset = None
|
||||
if item.is_a("IfcHalfSpaceSolid"):
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_grid(bm, size=0.5)
|
||||
|
||||
@@ -1087,18 +1087,21 @@ class Loader(bonsai.core.tool.Loader):
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(mesh)
|
||||
prev_co = None
|
||||
if usage.LayerSetDirection == "AXIS2":
|
||||
layer_set_direction = usage.LayerSetDirection
|
||||
if layer_set_direction == "AXIS2":
|
||||
co = Vector((0.0, offset, 0.0))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = no.cross(Vector([1.0, 0.0, 0.0]))
|
||||
elif usage.LayerSetDirection == "AXIS3":
|
||||
elif layer_set_direction == "AXIS3":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([0.0, 0.0, 1.0])
|
||||
elif usage.LayerSetDirection == "AXIS1":
|
||||
elif layer_set_direction == "AXIS1":
|
||||
co = Vector((0.0, 0.0, offset))
|
||||
no = cls.get_extrusion_vector(element).normalized()
|
||||
no = Vector([1.0, 0.0, 0.0])
|
||||
else:
|
||||
assert False, layer_set_direction
|
||||
no *= sense_factor
|
||||
# Cache this
|
||||
body = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
||||
@@ -1108,6 +1111,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
if style := tool.Ifc.get_entity(material):
|
||||
styles[style] = i
|
||||
last_i = len(layer_set.MaterialLayers) - 1
|
||||
bisect_geom = None
|
||||
for i, layer in enumerate(layer_set.MaterialLayers):
|
||||
if i != last_i:
|
||||
prev_co = co.copy()
|
||||
@@ -1121,6 +1125,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
if (material_index := styles.get(style, None)) is None:
|
||||
material_index = len(mesh.materials)
|
||||
mesh.materials.append(tool.Ifc.get_object(style))
|
||||
assert bisect_geom is not None
|
||||
if i == last_i:
|
||||
for face in bisect_geom["geom"]:
|
||||
if isinstance(face, bmesh.types.BMFace):
|
||||
@@ -1286,6 +1291,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
polyline.material_index = material_index
|
||||
return polyline
|
||||
|
||||
item = None
|
||||
for item_data, item_style in zip(rep_items, item_styles):
|
||||
item = item_data["item"]
|
||||
|
||||
@@ -1313,6 +1319,7 @@ class Loader(bonsai.core.tool.Loader):
|
||||
polyline.points.add(1)
|
||||
polyline.points[-1].co = native_data["matrix"] @ Vector(v2)
|
||||
|
||||
assert item is not None
|
||||
curve.bevel_depth = unit_scale * item.Radius
|
||||
thickness = None
|
||||
if (inner_radius := item.InnerRadius) and (thickness := max(item.Radius - inner_radius, 0)):
|
||||
|
||||
@@ -220,10 +220,12 @@ class Misc(bonsai.core.tool.Misc):
|
||||
related_objects.append((element, ifcopenshell.util.placement.get_storey_elevation(element)))
|
||||
related_objects = sorted(related_objects, key=lambda e: e[1])
|
||||
storey_elevation = None
|
||||
i = None
|
||||
for i, related_object in enumerate(related_objects):
|
||||
if related_object[0] == storey:
|
||||
storey_elevation = related_object[1]
|
||||
break
|
||||
assert i is not None
|
||||
if i + total_storeys < len(related_objects):
|
||||
next_storey_elevation = related_objects[i + total_storeys][1]
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
@@ -641,6 +641,8 @@ del _edit_type_names
|
||||
# call sites can reference ``tool.Parametric.ROOF`` directly. Renaming a
|
||||
# registry entry renames the constant; a typo at the call site surfaces as
|
||||
# AttributeError at module load.
|
||||
_entry = None
|
||||
for _entry in Parametric.EDIT_TYPES:
|
||||
setattr(Parametric, _entry.name.upper(), _entry)
|
||||
assert _entry is not None
|
||||
del _entry
|
||||
|
||||
@@ -168,6 +168,7 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
distance = (mouse_vector - last_point).length
|
||||
if distance < 0:
|
||||
return
|
||||
angle, orientation_angle, angle_round_threshold = None, None
|
||||
if distance > 0:
|
||||
angle = tool.Cad.angle_3_vectors(
|
||||
second_to_last_point, last_point, mouse_vector, new_angle=None, degrees=True
|
||||
@@ -188,6 +189,7 @@ class Polyline(bonsai.core.tool.Polyline):
|
||||
angle = 0
|
||||
orientation_angle = 0
|
||||
if input_ui:
|
||||
assert angle is not None and orientation_angle is not None and angle_round_threshold is not None
|
||||
if should_round:
|
||||
angle_snap = tool.Snap.get_angle_snap_value(context)
|
||||
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
|
||||
|
||||
@@ -370,18 +370,21 @@ class Project(bonsai.core.tool.Project):
|
||||
props = cls.get_project_props()
|
||||
active_library_breadcrumb = props.get_active_library_breadcrumb()
|
||||
change_back = False
|
||||
breadcrumb = None
|
||||
if active_library_breadcrumb:
|
||||
name = active_library_breadcrumb.name
|
||||
breadcrumb_type = active_library_breadcrumb.breadcrumb_type
|
||||
library_id = active_library_breadcrumb.library_id
|
||||
breadcrumb = (name, breadcrumb_type, library_id)
|
||||
change_back = True
|
||||
|
||||
bpy.ops.bim.rewind_library()
|
||||
if change_back:
|
||||
assert breadcrumb
|
||||
bpy.ops.bim.change_library_element(
|
||||
element_name=name,
|
||||
breadcrumb_type=breadcrumb_type,
|
||||
library_id=library_id,
|
||||
element_name=breadcrumb[0],
|
||||
breadcrumb_type=breadcrumb[1],
|
||||
library_id=breadcrumb[2],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -124,6 +124,9 @@ class Pset(bonsai.core.tool.Pset):
|
||||
return bpy.context.scene.GroupPsetProperties
|
||||
elif obj_type == "Zone":
|
||||
return bpy.context.scene.ZonePsetProperties
|
||||
elif obj_type == "Cost":
|
||||
# No psets for cost items currently.
|
||||
assert False, obj_type
|
||||
assert_never(obj_type)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -505,6 +505,8 @@ class Search(bonsai.core.tool.Search):
|
||||
(0.773, 0.922, 0.816),
|
||||
(0.871, 0.957, 0.894),
|
||||
]
|
||||
else:
|
||||
assert False, theme
|
||||
|
||||
if value < min_val:
|
||||
value = min_val
|
||||
@@ -574,8 +576,10 @@ class ImportFilterQueryTransformer(lark.Transformer):
|
||||
new = self.filter_groups.add()
|
||||
global_ids = []
|
||||
is_first_group = len(self.filter_groups) == 1
|
||||
new2 = None
|
||||
for filter_index, arg in enumerate(args):
|
||||
if arg["type"] == "instance" and global_ids:
|
||||
assert new2
|
||||
if "bpy.data.texts" in new2.value:
|
||||
data_name = new2.value.split("bpy.data.texts")[1][2:-2]
|
||||
bpy.data.texts[data_name].write("," + arg["value"])
|
||||
|
||||
@@ -23,7 +23,7 @@ import re
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime
|
||||
from datetime import time as datetime_time
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, assert_never
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
@@ -1127,7 +1127,8 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
|
||||
@classmethod
|
||||
def load_default_animation_color_scheme(cls):
|
||||
groups = {
|
||||
GroupType = Literal["CREATION", "OPERATION", "MOVEMENT_TO", "DESTRUCTION", "MOVEMENT_FROM", "USERDEFINED"]
|
||||
groups: dict[GroupType, dict[str, Any]] = {
|
||||
"CREATION": {
|
||||
"PredefinedType": ["CONSTRUCTION", "INSTALLATION"],
|
||||
"Color": (0.0, 1.0, 0.0),
|
||||
@@ -1167,6 +1168,8 @@ class Sequence(bonsai.core.tool.Sequence):
|
||||
predefined_type_item2 = props.task_output_colors.add()
|
||||
predefined_type_item2.name = predefined_type
|
||||
predefined_type_item2.color = data["Color"]
|
||||
else:
|
||||
assert_never(group)
|
||||
# TO DO: consider cases where users confuses inputs and outputs
|
||||
predefined_type_item.name = predefined_type
|
||||
predefined_type_item.color = data["Color"]
|
||||
|
||||
@@ -225,6 +225,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
# Get axis that are closer than the stick factor threshold
|
||||
elegible_axis = []
|
||||
|
||||
axis = None
|
||||
for axis in snap_axis:
|
||||
if not axis:
|
||||
continue
|
||||
@@ -326,6 +327,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
detected_snaps: list[dict[str, Any]] = []
|
||||
|
||||
def select_plane_method():
|
||||
plane_origin, plane_normal = None, None
|
||||
if not last_polyline_point:
|
||||
plane_origin = Vector((0, 0, 0))
|
||||
plane_normal = Vector((0, 0, 1))
|
||||
@@ -357,6 +359,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
plane_origin = Vector((last_polyline_point.x, last_polyline_point.y, last_polyline_point.z))
|
||||
plane_normal = Vector((1, 0, 0))
|
||||
|
||||
assert plane_origin and plane_normal
|
||||
plane_normal = tool.Polyline.use_transform_orientations(plane_normal)
|
||||
return plane_origin, plane_normal
|
||||
|
||||
@@ -583,6 +586,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
|
||||
snaps_by_group = filter_snapping_points_by_group(detected_snaps)
|
||||
edges = [] # Get edges to create edge-intersection snap
|
||||
axis_start, axis_end = ..., ...
|
||||
for snapping_point in snaps_by_group:
|
||||
if snapping_point["group"] in {"Polyline", "Measure", "Wireframe", "Object"}:
|
||||
if snapping_point["type"] == "Edge":
|
||||
@@ -607,6 +611,7 @@ class Snap(bonsai.core.tool.Snap):
|
||||
if point["type"] == "Axis":
|
||||
if ordered_snaps[0]["type"] not in {"Axis", "Plane"}:
|
||||
obj = ordered_snaps[0]["object"]
|
||||
assert axis_start is not ... and axis_end is not ...
|
||||
mixed_snap = cls.mix_snap_and_axis(ordered_snaps[0], axis_start, axis_end)
|
||||
for mixed_point in mixed_snap:
|
||||
snap_point = {
|
||||
|
||||
@@ -304,12 +304,14 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
|
||||
while True:
|
||||
has_parent = None
|
||||
new_current_results = None
|
||||
for key in current_results:
|
||||
if flat_key.startswith(key):
|
||||
has_parent = True
|
||||
new_current_results = current_results[key]["children"]
|
||||
break
|
||||
if has_parent:
|
||||
assert new_current_results is not None
|
||||
current_results = new_current_results
|
||||
else:
|
||||
break
|
||||
@@ -978,19 +980,24 @@ class Spatial(bonsai.core.tool.Spatial):
|
||||
interiors_list = []
|
||||
|
||||
if union_geom.geom_type == "MultiPolygon":
|
||||
poly = None
|
||||
for poly in union_geom.geoms:
|
||||
interiors_list = cls.get_poly_valid_interior_list(
|
||||
poly=poly, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
|
||||
assert poly
|
||||
new_poly = Polygon(poly.exterior.coords, holes=interiors_list)
|
||||
|
||||
if union_geom.geom_type == "Polygon":
|
||||
elif union_geom.geom_type == "Polygon":
|
||||
interiors_list = cls.get_poly_valid_interior_list(
|
||||
poly=union_geom, min_area=min_area, interiors_list=interiors_list
|
||||
)
|
||||
new_poly = Polygon(union_geom.exterior.coords, holes=interiors_list)
|
||||
|
||||
else:
|
||||
assert False, union_geom.geom_type
|
||||
|
||||
return new_poly
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -360,6 +360,10 @@ class Style(bonsai.core.tool.Style):
|
||||
material_output = tool.Blender.get_material_node(obj, "OUTPUT_MATERIAL", {"is_active_output": True})
|
||||
surface_output = get_input_node(material_output, "Surface")
|
||||
|
||||
# TODO: this variable is not really needed,
|
||||
# just workaround a for ty issue detecting unresolved refs.
|
||||
bsdf = None
|
||||
|
||||
if surface_output and surface_output.type == "MIX_SHADER":
|
||||
mix_shader = surface_output
|
||||
if (
|
||||
@@ -388,6 +392,7 @@ class Style(bonsai.core.tool.Style):
|
||||
and (bsdf := get_input_node(surface_output, input_index=1, of_type="BSDF_PRINCIPLED"))
|
||||
)
|
||||
):
|
||||
assert bsdf
|
||||
report(f"Because of {BLUE}BSDF_PRINCIPLED{R} node reflectance method identified as {BLUE}PHYSICAL{R}")
|
||||
attributes["ReflectanceMethod"] = "NOTDEFINED" if tool.Ifc.get_schema() != "IFC4X3" else "PHYSICAL"
|
||||
|
||||
|
||||
@@ -216,6 +216,7 @@ def update_translations_from_po(po_directory: Path, translations_module: Path):
|
||||
|
||||
|
||||
if BPY_IS_LOADED:
|
||||
import bpy
|
||||
|
||||
class SetupTranslationUI(bpy.types.Operator):
|
||||
bl_idname = "bim.setup_translation_ui"
|
||||
|
||||
@@ -82,6 +82,7 @@ class Generator:
|
||||
}
|
||||
""".replace("{entity}", location.split("#")[-1]))
|
||||
# filter parents for the brick entity
|
||||
parent = None
|
||||
for row in query:
|
||||
parent = row.get("parent").toPython()
|
||||
if "brickschema.org" in parent and parent in references.keys():
|
||||
|
||||
@@ -1036,6 +1036,7 @@ class LibraryGenerator:
|
||||
seat_width_offset = 0.7 * width / 2 if cistern_depth else width / 2
|
||||
seat_start_width_offset = 0.6 * width
|
||||
|
||||
cistern_3d = None
|
||||
if cistern_height:
|
||||
cistern = builder.rectangle(size=V(width, cistern_depth), position=shift_to_center)
|
||||
cistern_3d = ifcopenshell.util.element.copy_deep(self.file, cistern)
|
||||
@@ -1118,6 +1119,7 @@ class LibraryGenerator:
|
||||
|
||||
# cistern
|
||||
if cistern_height:
|
||||
assert cistern_3d
|
||||
cistern_3d = builder.extrude(
|
||||
cistern_3d, cistern_height + seat_level / 2, position=V(0, 0, seat_level / 2)
|
||||
)
|
||||
|
||||
@@ -143,6 +143,7 @@ class LibraryGenerator:
|
||||
if "unused" in ifc_params:
|
||||
del ifc_params["unused"]
|
||||
|
||||
profiles_gap = ...
|
||||
if prof_type == "profile_hollow*_square":
|
||||
ifc_params["YDim"] = ifc_params["XDim"]
|
||||
elif ifc_profile_name == "IfcCircleHollowProfileDef":
|
||||
@@ -160,6 +161,7 @@ class LibraryGenerator:
|
||||
profile = self.file.create_entity(ifc_profile_name, ProfileName=prof_name, ProfileType="AREA", **ifc_params)
|
||||
|
||||
if prof_type == "profile_l*lbeam_2l":
|
||||
assert profiles_gap is not ...
|
||||
profile.ProfileName = None # to avoid name confusion
|
||||
mode = "SLBB" if prof_name.endswith("_SLBB") else "LLBB"
|
||||
profile = self.create_double_l_profile(profile, prof_name, profiles_gap, mode)
|
||||
|
||||
@@ -112,6 +112,51 @@ class TestImportCameraProps(NewFile):
|
||||
assert camera.shift_x == 0.0
|
||||
assert camera.shift_y == 0.0
|
||||
|
||||
def test_defaults_edge_classification_props_when_pset_is_absent(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
|
||||
subject.import_camera_props(drawing, camera)
|
||||
|
||||
props = subject.get_camera_props(camera)
|
||||
assert props.use_edge_classification is False
|
||||
assert props.render_creases is True
|
||||
assert props.valley_angle_min_degrees == pytest.approx(12.0)
|
||||
assert props.render_sharp is True
|
||||
assert props.ridge_angle_min_degrees == pytest.approx(45.0)
|
||||
assert props.render_flush is False
|
||||
|
||||
def test_imports_edge_classification_props_from_drawing_pset(self):
|
||||
ifc = ifcopenshell.file()
|
||||
tool.Ifc.set(ifc)
|
||||
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
|
||||
pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing")
|
||||
ifcopenshell.api.pset.edit_pset(
|
||||
ifc,
|
||||
pset=pset,
|
||||
properties={
|
||||
"UseEdgeClassification": True,
|
||||
"RenderCreases": False,
|
||||
"ValleyAngleMinDegrees": 8.0,
|
||||
"RenderSharp": False,
|
||||
"RidgeAngleMinDegrees": 30.0,
|
||||
"RenderFlush": True,
|
||||
},
|
||||
)
|
||||
camera = bpy.data.cameras.new("Camera")
|
||||
|
||||
subject.import_camera_props(drawing, camera)
|
||||
|
||||
props = subject.get_camera_props(camera)
|
||||
assert props.use_edge_classification is True
|
||||
assert props.render_creases is False
|
||||
assert props.valley_angle_min_degrees == pytest.approx(8.0)
|
||||
assert props.render_sharp is False
|
||||
assert props.ridge_angle_min_degrees == pytest.approx(30.0)
|
||||
assert props.render_flush is True
|
||||
|
||||
|
||||
class TestSyncPerspectiveCameraShifts(NewFile):
|
||||
def test_round_trips_perspective_camera_shifts_through_drawing_pset(self):
|
||||
|
||||
@@ -27,10 +27,12 @@ flatten = itertools.chain.from_iterable
|
||||
|
||||
def get_element_data(model, name, element):
|
||||
if element["geometry_type"] == "Edge":
|
||||
cell_tags, cell_block = None, None
|
||||
for i, cell_block in enumerate(model.cells):
|
||||
if cell_block.type == "line":
|
||||
cell_tags = model.cell_data["cell_tags"][i]
|
||||
break
|
||||
assert cell_tags is not None and cell_block is not None
|
||||
rows = []
|
||||
for i_row, i in enumerate(cell_tags):
|
||||
if i == 0:
|
||||
@@ -59,6 +61,7 @@ def get_element_data(model, name, element):
|
||||
elif element["geometry_type"] == "Face":
|
||||
triangle_cell_tags = None
|
||||
quad_cell_tags = None
|
||||
points, cell_block = None, None
|
||||
for i, cell_block in enumerate(model.cells):
|
||||
if cell_block.type == "triangle":
|
||||
triangle_cell_tags = model.cell_data["cell_tags"][i]
|
||||
@@ -78,8 +81,10 @@ def get_element_data(model, name, element):
|
||||
if not len(rows):
|
||||
points = []
|
||||
else:
|
||||
assert cell_block is not None
|
||||
points = list(flatten([cell_block.data[c] for c in rows]))
|
||||
|
||||
cell_block = None
|
||||
for i, cell_block in enumerate(model.cells):
|
||||
if cell_block.type == "quad":
|
||||
quad_cell_tags = model.cell_data["cell_tags"][i]
|
||||
@@ -97,6 +102,7 @@ def get_element_data(model, name, element):
|
||||
rows.append(i_row)
|
||||
break
|
||||
if len(rows):
|
||||
assert cell_block is not None and points is not None
|
||||
points.extend(list(flatten([cell_block.data[c] for c in rows])))
|
||||
|
||||
points = list(set(points))
|
||||
@@ -172,6 +178,8 @@ def results_to_ifc(ifc_file, ifc_model, rmed_path, global_case, field_types, dat
|
||||
model_cases = data["load_cases"]
|
||||
elif global_case == "COMB":
|
||||
model_cases = data["load_combinations"]
|
||||
else:
|
||||
assert False, global_case
|
||||
for field in field_types:
|
||||
if field == "InternalForces":
|
||||
_parsed_data = internal_forces_to_ifc(ifc_file, ifc_model, result, model_cases, data["elements"])
|
||||
|
||||
+19
-2
@@ -283,7 +283,7 @@ class Ifc2CA:
|
||||
geometry = [x.EdgeStart.VertexGeometry.Coordinates for x in repr_item.Bounds[0].Bound.EdgeList]
|
||||
|
||||
else:
|
||||
print(representation)
|
||||
assert False, representation
|
||||
return geometry
|
||||
|
||||
def parse_material(self, material: ios.entity_instance):
|
||||
@@ -399,6 +399,9 @@ class Ifc2CA:
|
||||
elif element.is_a("IfcStructuralSurfaceMember"):
|
||||
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
|
||||
|
||||
else:
|
||||
assert False, element
|
||||
|
||||
origin, orientation = self.parse_transformation_matrix(placement)
|
||||
data["origin"] = origin
|
||||
data["orientation"] = orientation
|
||||
@@ -436,7 +439,7 @@ class Ifc2CA:
|
||||
for i, v in enumerate(placement[:3]):
|
||||
v[3] = data["geometry"][i]
|
||||
|
||||
if connection.is_a("IfcStructuralCurveConnection"):
|
||||
elif connection.is_a("IfcStructuralCurveConnection"):
|
||||
placement = ifcopenshell.util.placement.a2p(
|
||||
data["geometry"][0],
|
||||
connection.Axis.DirectionRatios,
|
||||
@@ -446,6 +449,9 @@ class Ifc2CA:
|
||||
elif connection.is_a("IfcStructuralSurfaceConnection"):
|
||||
placement = ifcopenshell.util.placement.get_axis2placement(repr_item.FaceSurface.Position)
|
||||
|
||||
else:
|
||||
assert False, connection
|
||||
|
||||
origin, orientation = self.parse_transformation_matrix(placement)
|
||||
data["origin"] = origin
|
||||
data["orientation"] = orientation
|
||||
@@ -552,6 +558,9 @@ class Ifc2CA:
|
||||
},
|
||||
}
|
||||
|
||||
else:
|
||||
assert False, element["geometry_type"]
|
||||
|
||||
for action in actions:
|
||||
self.add_action_loads(element, action, data, load_cases)
|
||||
|
||||
@@ -586,6 +595,7 @@ class Ifc2CA:
|
||||
|
||||
data["actions"].append(action.get_info() | {"AppliedLoad": action.AppliedLoad.get_info()})
|
||||
if element["geometry_type"] in ["Vertex", "Edge"]:
|
||||
force_projection_coeff, moment_projection_coeff = None, None
|
||||
if action.is_a("IfcStructuralPointAction") and load.is_a("IfcStructuralLoadSingleForce"):
|
||||
FX = tempFX = load.ForceX if load.ForceX is not None else 0.0
|
||||
FY = tempFY = load.ForceY if load.ForceY is not None else 0.0
|
||||
@@ -639,8 +649,12 @@ class Ifc2CA:
|
||||
force_projection_coeff = 1.0
|
||||
moment_projection_coeff = 1.0
|
||||
|
||||
else:
|
||||
assert False, action
|
||||
|
||||
for iLC, load_case in enumerate(load_cases):
|
||||
if load_case.id() in active_load_case_ids:
|
||||
assert force_projection_coeff is not None and moment_projection_coeff is not None
|
||||
load_case_coeff = 1.0 if load_case.Coefficient is None else load_case.Coefficient
|
||||
data["loadGroups"].append(load_group.Name)
|
||||
data["loadsLC"]["FX"][iLC] += FX * load_group_coeff * load_case_coeff * force_projection_coeff
|
||||
@@ -672,6 +686,9 @@ class Ifc2CA:
|
||||
else:
|
||||
force_projection_coeff = 1.0
|
||||
|
||||
else:
|
||||
assert False, action
|
||||
|
||||
for iLC, load_case in enumerate(load_cases):
|
||||
if load_case.id() in active_load_case_ids:
|
||||
load_case_coeff = 1.0 if load_case.Coefficient is None else load_case.Coefficient
|
||||
|
||||
@@ -109,6 +109,8 @@ class ifc5D2json:
|
||||
values = root_element.CostValues
|
||||
elif root_element.is_a("IfcConstructionResource"):
|
||||
values = root_element.BaseCosts
|
||||
else:
|
||||
assert False, root_element
|
||||
for cost_value in values or []:
|
||||
self.extract_cost_value(root_element, data, cost_value)
|
||||
# data["CostValues"].append(cost_value.id())
|
||||
|
||||
@@ -478,6 +478,8 @@ class Ifc5DOdsWriter(Ifc5Dwriter):
|
||||
cell.addElement(P(text=value))
|
||||
elif type == "formula":
|
||||
cell = TableCell(formula=value, stylename=style)
|
||||
else:
|
||||
assert False, type
|
||||
row.addElement(cell)
|
||||
|
||||
def add_cost_item_rows(table, cost_data):
|
||||
@@ -715,6 +717,8 @@ if __name__ == "__main__":
|
||||
writer = Ifc5DOdsWriter(args["input"], args["output"])
|
||||
elif args["format"] == "XLSX":
|
||||
writer = Ifc5DXlsxWriter(args["input"], args["output"])
|
||||
else:
|
||||
assert False, args
|
||||
writer.write()
|
||||
|
||||
logger.info("Finished conversion in %ss", time.time() - start)
|
||||
|
||||
@@ -675,7 +675,7 @@ int main(int argc, char** argv) {
|
||||
time_t start, end;
|
||||
time(&start);
|
||||
if (output_extension == XML) {
|
||||
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename), logger);
|
||||
XmlSerializer s(ifc_file, IfcUtil::path::to_utf8(output_temp_filename), &logger);
|
||||
logger.Status("Writing XML output...");
|
||||
s.finalize();
|
||||
} else {
|
||||
@@ -834,14 +834,14 @@ int main(int argc, char** argv) {
|
||||
if (output_extension == OBJ) {
|
||||
// Do not use temp file for MTL as it's such a small file.
|
||||
const path_t mtl_filename = change_extension(output_filename, MTL);
|
||||
serializer = boost::make_shared<WaveFrontOBJSerializer>(IfcUtil::path::to_utf8(output_temp_filename), IfcUtil::path::to_utf8(mtl_filename), geometry_settings, serializer_settings, logger);
|
||||
serializer = boost::make_shared<WaveFrontOBJSerializer>(IfcUtil::path::to_utf8(output_temp_filename), IfcUtil::path::to_utf8(mtl_filename), geometry_settings, serializer_settings, &logger);
|
||||
#ifdef WITH_OPENCOLLADA
|
||||
} else if (output_extension == DAE) {
|
||||
serializer = boost::make_shared<ColladaSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
|
||||
serializer = boost::make_shared<ColladaSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
|
||||
#endif
|
||||
#ifdef WITH_GLTF
|
||||
} else if (output_extension == GLB) {
|
||||
serializer = boost::make_shared<GltfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
|
||||
serializer = boost::make_shared<GltfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
|
||||
#endif
|
||||
#ifdef WITH_USD
|
||||
} else if (output_extension == USD || output_extension == USDA || output_extension == USDC) {
|
||||
@@ -858,15 +858,15 @@ int main(int argc, char** argv) {
|
||||
serializer = boost::make_shared<IgesSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
|
||||
} else if (output_extension == SVG) {
|
||||
geometry_settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
|
||||
serializer = boost::make_shared<SvgSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
|
||||
serializer = boost::make_shared<SvgSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
|
||||
#ifdef WITH_HDF5
|
||||
} else if (output_extension == HDF) {
|
||||
geometry_settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
|
||||
serializer = boost::make_shared<HdfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, false, logger);
|
||||
serializer = boost::make_shared<HdfSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, false, &logger);
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
} else if (output_extension == TTL) {
|
||||
serializer = boost::make_shared<TtlWktSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, logger);
|
||||
serializer = boost::make_shared<TtlWktSerializer>(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings, &logger);
|
||||
} else {
|
||||
cerr_ << "[Error] Unknown output filename extension '" << output_extension << "'\n";
|
||||
write_log(!quiet);
|
||||
@@ -1011,7 +1011,7 @@ int main(int argc, char** argv) {
|
||||
if (!vmap.count("cache-file")) {
|
||||
cache_file = input_filename + CACHE + HDF;
|
||||
}
|
||||
cache.reset(new HdfSerializer(IfcUtil::path::to_utf8(cache_file), geometry_settings, serializer_settings, false, logger));
|
||||
cache.reset(new HdfSerializer(IfcUtil::path::to_utf8(cache_file), geometry_settings, serializer_settings, false, &logger));
|
||||
context_iterator->set_cache(cache.get());
|
||||
}
|
||||
#endif
|
||||
@@ -1290,7 +1290,7 @@ bool init_input_file(const std::string& filename, IfcParse::IfcFile*& ifc_file,
|
||||
|
||||
#ifdef WITH_IFCXML
|
||||
if (boost::ends_with(boost::to_lower_copy(filename), ".ifcxml")) {
|
||||
ifc_file = IfcParse::parse_ifcxml(filename, logger);
|
||||
ifc_file = IfcParse::parse_ifcxml(filename, &logger);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -25,9 +25,10 @@ import re
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union, cast
|
||||
|
||||
import ifcopenshell.util.selector
|
||||
from typing_extensions import assert_never
|
||||
|
||||
try:
|
||||
from openpyxl import Workbook
|
||||
@@ -100,8 +101,9 @@ class Parser:
|
||||
def parse(self, ifc_file: ifcopenshell.file, name=None):
|
||||
for category_name, category_config in self.config["categories"].items():
|
||||
for element in category_config["get_category_elements"](ifc_file):
|
||||
get_element_data: Union[GetElementDataCallBack, dict[str, Any]]
|
||||
get_element_data = category_config["get_element_data"]
|
||||
get_element_data = cast(
|
||||
Union[GetElementDataCallBack, dict[str, Any]], category_config["get_element_data"]
|
||||
)
|
||||
|
||||
if isinstance(get_element_data, dict):
|
||||
data = {}
|
||||
@@ -109,14 +111,18 @@ class Parser:
|
||||
data[key] = ifcopenshell.util.selector.get_element_value(element, query)
|
||||
elif isinstance(get_element_data, Callable):
|
||||
data = get_element_data(ifc_file, element) or {}
|
||||
else:
|
||||
assert_never(get_element_data)
|
||||
|
||||
get_custom_element_data = self.get_custom_element_data.get(category_name, lambda x, y: None)
|
||||
get_custom_element_data = self.get_custom_element_data.get(category_name, lambda *_: None)
|
||||
if isinstance(get_custom_element_data, dict):
|
||||
custom_data = {}
|
||||
for key, query in get_custom_element_data.items():
|
||||
custom_data[key] = ifcopenshell.util.selector.get_element_value(element, query)
|
||||
elif isinstance(get_custom_element_data, Callable):
|
||||
custom_data = get_custom_element_data(ifc_file, element) or {}
|
||||
else:
|
||||
assert_never(get_custom_element_data)
|
||||
|
||||
data.update(custom_data)
|
||||
|
||||
|
||||
@@ -271,6 +271,8 @@ def get_contact_data(ifc_file: ifcopenshell.file, element: ifcopenshell.entity_i
|
||||
pao = the_actor
|
||||
person = the_actor.ThePerson
|
||||
organization = the_actor.TheOrganization
|
||||
else:
|
||||
assert False, the_actor
|
||||
|
||||
email = get_email_from_pao(person, organization)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ dependencies = [
|
||||
"openpyxl",
|
||||
"odfpy",
|
||||
"pandas",
|
||||
"typing-extensions",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -371,6 +371,42 @@ namespace ifcopenshell {
|
||||
static constexpr double defaultvalue = -1.;
|
||||
};
|
||||
|
||||
struct SvgRidgeAngleMinDegrees : public SettingBase<SvgRidgeAngleMinDegrees, double> {
|
||||
static constexpr const char* const name = "svg-ridge-angle-min-degrees";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): minimum convex dihedral deviation from flat, in degrees, for a projection edge to be classified as 'sharp' rather than 'flush'.";
|
||||
static constexpr double defaultvalue = 45.;
|
||||
};
|
||||
|
||||
struct SvgValleyAngleMinDegrees : public SettingBase<SvgValleyAngleMinDegrees, double> {
|
||||
static constexpr const char* const name = "svg-valley-angle-min-degrees";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): minimum concave dihedral deviation from flat, in degrees, for a projection edge to be classified as 'crease' rather than 'flush'.";
|
||||
static constexpr double defaultvalue = 12.;
|
||||
};
|
||||
|
||||
struct SvgEmitFlushEdges : public SettingBase<SvgEmitFlushEdges, bool> {
|
||||
static constexpr const char* const name = "svg-emit-flush-edges";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'flush' projection edges (dihedral deviation below both ridge/valley thresholds). Defaults to false, i.e. flush edges are omitted from the output.";
|
||||
static constexpr bool defaultvalue = false;
|
||||
};
|
||||
|
||||
struct SvgUseEdgeClassification : public SettingBase<SvgUseEdgeClassification, bool> {
|
||||
static constexpr const char* const name = "svg-use-edge-classification";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): enable the 5-class boundary/outline/sharp/crease/flush scheme. When false (the default), falls back to the original unclassified linework.";
|
||||
static constexpr bool defaultvalue = false;
|
||||
};
|
||||
|
||||
struct SvgRenderCreaseEdges : public SettingBase<SvgRenderCreaseEdges, bool> {
|
||||
static constexpr const char* const name = "svg-render-crease-edges";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'crease' (concave) projection edges. Only relevant when svg-use-edge-classification is enabled.";
|
||||
static constexpr bool defaultvalue = true;
|
||||
};
|
||||
|
||||
struct SvgRenderSharpEdges : public SettingBase<SvgRenderSharpEdges, bool> {
|
||||
static constexpr const char* const name = "svg-render-sharp-edges";
|
||||
static constexpr const char* const description = "SVG edge classification (issue #3668): whether to emit 'sharp' (convex) projection edges. Only relevant when svg-use-edge-classification is enabled.";
|
||||
static constexpr bool defaultvalue = true;
|
||||
};
|
||||
|
||||
struct KeepBoundingBoxes : public SettingBase<KeepBoundingBoxes, bool> {
|
||||
static constexpr const char* const name = "keep-bounding-boxes";
|
||||
static constexpr const char* const description =
|
||||
@@ -653,7 +689,7 @@ namespace ifcopenshell {
|
||||
};
|
||||
|
||||
class Settings : public SettingsContainer<
|
||||
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
|
||||
std::tuple<MesherLinearDeflection, MesherAngularDeflection, ReorientShells, LengthUnit, PlaneUnit, Precision, OutputDimensionality, LayersetFirst, DisableBooleanResult, NoWireIntersectionCheck, NoWireIntersectionTolerance, PrecisionFactor, DebugBooleanOperations, BooleanAttempt2d, SurfaceColour, WeldVertices, UseWorldCoords, UnifyShapes, UseMaterialNames, ConvertBackUnits, ContextIds, ContextTypes, ContextIdentifiers, IteratorOutput, DisableOpeningSubtractions, ApplyDefaultMaterials, DontEmitNormals, GenerateUvs, ApplyLayerSets, UseElementHierarchy, ValidateQuantities, EdgeArrows, BuildingLocalPlacement, SiteLocalPlacement, ForceSpaceTransparency, CircleSegments, CgalSmoothAngleDegrees, SvgRidgeAngleMinDegrees, SvgValleyAngleMinDegrees, SvgEmitFlushEdges, SvgUseEdgeClassification, SvgRenderCreaseEdges, SvgRenderSharpEdges, KeepBoundingBoxes, ComputeCurvature, FunctionStepType, FunctionStepParam, NoParallelMapping, PermissiveShapeReuse, ModelOffset, ModelRotation, TriangulationType, CgalEmitOriginalEdges, OcctNoCleanTriangulation, CacheShapes, DeferProcessingFirstElement, MaxOffset, MaxOffsetDeviation, ApplyOffset, MakeVolume>
|
||||
>
|
||||
{};
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
bst.attempt_2d = settings_.get<settings::BooleanAttempt2d>().get();
|
||||
bst.debug = settings_.get<settings::DebugBooleanOperations>().get();
|
||||
bst.precision = settings_.get<settings::Precision>().get();
|
||||
bst.logger = &logger_;
|
||||
|
||||
std::vector< std::pair<double, TopoDS_Shape> > opening_vector;
|
||||
|
||||
@@ -118,7 +119,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
|
||||
auto it3_shape = std::static_pointer_cast<OpenCascadeShape>(it3->Shape())->shape();
|
||||
if (it3_shape.IsNull()) {
|
||||
Logger::Root().Error("GEO", 187, "Null operand");
|
||||
logger_.Error("GEO", 187, "Null operand");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
IfcGeom::util::create_solid_from_faces(list, entity_part, settings_.get<settings::Precision>().get(), true);
|
||||
is_manifold = util::is_manifold(entity_part);
|
||||
if (is_manifold) {
|
||||
Logger::Root().Warning("GEO", 188, "Successfully sewed non-manifold first operand");
|
||||
logger_.Warning("GEO", 188, "Successfully sewed non-manifold first operand");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,17 +162,17 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
failure = "Empty result (no faces) for BOPAlgo_MakerVolume; original was " + std::to_string(IfcGeom::util::count(entity_part, TopAbs_FACE));
|
||||
} else {
|
||||
is_manifold = util::is_manifold(entity_part_2);
|
||||
Logger::Root().Warning("GEO", 189, std::string("Sucessfully detected exterior volume to non-manifold first operand; shape is now ") + (is_manifold ? std::string("manifold") : std::string("non-manifold")));
|
||||
logger_.Warning("GEO", 189, std::string("Sucessfully detected exterior volume to non-manifold first operand; shape is now ") + (is_manifold ? std::string("manifold") : std::string("non-manifold")));
|
||||
entity_part = entity_part_2;
|
||||
}
|
||||
} catch (const Standard_Failure& e) {
|
||||
failure.emplace(e.GetMessageString());
|
||||
}
|
||||
if (failure) {
|
||||
Logger::Root().Warning("GEO", 190, "MakeVolume failed: " + *failure, entity);
|
||||
logger_.Warning("GEO", 190, "MakeVolume failed: " + *failure, entity);
|
||||
}
|
||||
} else {
|
||||
Logger::Root().Warning("GEO", 191, "Non-manifold first operand, use --make-volume to try and make manifold");
|
||||
logger_.Warning("GEO", 191, "Non-manifold first operand, use --make-volume to try and make manifold");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +215,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
if (util::boolean_operation(bst, result, opening_list, BOPAlgo_CUT, intermediate_result)) {
|
||||
result = intermediate_result;
|
||||
} else {
|
||||
Logger::Root().Message(Logger::LOG_ERROR, "GEO", 192, "Opening subtraction failed for " + boost::lexical_cast<std::string>(std::distance(jt, it)) + " openings", entity);
|
||||
logger_.Message(Logger::LOG_ERROR, "GEO", 192, "Opening subtraction failed for " + boost::lexical_cast<std::string>(std::distance(jt, it)) + " openings", entity);
|
||||
}
|
||||
|
||||
jt = it;
|
||||
@@ -235,7 +236,7 @@ bool IfcGeom::OpenCascadeKernel::convert_openings(const IfcUtil::IfcBaseEntity*
|
||||
// where we keep the first operand as is (a compound of faces probably,
|
||||
// unless --orient-shells was activated in which case we're already lost).
|
||||
if (!is_manifold) {
|
||||
Logger::Root().Warning("GEO", 193, "Retrying boolean operation on individual faces");
|
||||
logger_.Warning("GEO", 193, "Retrying boolean operation on individual faces");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -118,14 +118,14 @@ bool OpenCascadeKernel::convert_impl(const taxonomy::boolean_result::ptr br, Con
|
||||
|
||||
const double first_operand_volume = util::shape_volume(a);
|
||||
if (first_operand_volume <= ALMOST_ZERO) {
|
||||
Logger::Root().Message(Logger::LOG_WARNING, "GEO", 119, "Empty solid for:", c->instance);
|
||||
logger_.Message(Logger::LOG_WARNING, "GEO", 119, "Empty solid for:", c->instance);
|
||||
}
|
||||
} else {
|
||||
|
||||
for (auto& r : cr) {
|
||||
auto S = std::static_pointer_cast<OpenCascadeShape>(r.Shape())->shape();
|
||||
if (S.IsNull()) {
|
||||
Logger::Root().Error("GEO", 120, "Null operand");
|
||||
logger_.Error("GEO", 120, "Null operand");
|
||||
continue;
|
||||
}
|
||||
gp_GTrsf trsf;
|
||||
@@ -140,7 +140,7 @@ bool OpenCascadeKernel::convert_impl(const taxonomy::boolean_result::ptr br, Con
|
||||
// #2665 we also set a precision-independent threshold, because in the boolean op routine
|
||||
// the working fuzziness might still be increased.
|
||||
if (d < tol * 20. || d < 0.00002) {
|
||||
Logger::Root().Message(Logger::LOG_WARNING, "GEO", 121, "Halfspace subtraction yields unchanged volume:", c->instance);
|
||||
logger_.Message(Logger::LOG_WARNING, "GEO", 121, "Halfspace subtraction yields unchanged volume:", c->instance);
|
||||
continue;
|
||||
} else {
|
||||
S = result;
|
||||
@@ -159,6 +159,7 @@ bool OpenCascadeKernel::convert_impl(const taxonomy::boolean_result::ptr br, Con
|
||||
bst.attempt_2d = settings_.get<settings::BooleanAttempt2d>().get();
|
||||
bst.debug = settings_.get<settings::DebugBooleanOperations>().get();
|
||||
bst.precision = settings_.get<settings::Precision>().get();
|
||||
bst.logger = &logger_;
|
||||
|
||||
TopoDS_Shape r;
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ bool IfcGeom::util::is_extrusion(const gp_Vec & v, const TopoDS_Shape & s, TopoD
|
||||
return true;
|
||||
}
|
||||
|
||||
int IfcGeom::util::eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c) {
|
||||
int IfcGeom::util::eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c, Logger& logger) {
|
||||
int N = 0;
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(bs);
|
||||
for (; it.More(); it.Next()) {
|
||||
@@ -418,7 +418,7 @@ int IfcGeom::util::eliminate_narrow_operands(double prec, const NCollection_List
|
||||
|
||||
bool is_narrow = min_dimension < prec;
|
||||
|
||||
Logger::Root().Notice("GEO", 122, "Min OBB dimension of operand = " + std::to_string(min_dimension));
|
||||
logger.Notice("GEO", 122, "Min OBB dimension of operand = " + std::to_string(min_dimension));
|
||||
|
||||
if (!is_narrow) {
|
||||
c.Append(it.Value());
|
||||
@@ -573,7 +573,7 @@ int IfcGeom::util::eliminate_touching_operands(double prec, const TopoDS_Shape &
|
||||
return N;
|
||||
}
|
||||
|
||||
bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_input, const NCollection_List<TopoDS_Shape> & b_input, TopoDS_Shape & result, double eps) {
|
||||
bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_input, const NCollection_List<TopoDS_Shape> & b_input, TopoDS_Shape & result, double eps, Logger& logger) {
|
||||
IfcGeom::impl::tree<int> edge_tree;
|
||||
|
||||
NCollection_List<TopoDS_Shape> ab_input = b_input;
|
||||
@@ -703,7 +703,7 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
if (u11 < U1 && U1 < u12 && u21 < U2 && U2 < u22) {
|
||||
// Edge curves belonging to different operands intersect, don't process
|
||||
// using builder.
|
||||
Logger::Root().Notice("GEO", 123, "Intersecting boundaries");
|
||||
logger.Notice("GEO", 123, "Intersecting boundaries");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -750,7 +750,7 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
// any effect and marked as redundant. Feeding it to the builder algo
|
||||
// will likely cause problems.
|
||||
redundant[std::distance(wires.begin(), it)] = true;
|
||||
Logger::Root().Notice("GEO", 124, "Subtraction operand outside of outer bound");
|
||||
logger.Notice("GEO", 124, "Subtraction operand outside of outer bound");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,7 +790,7 @@ bool IfcGeom::util::boolean_subtraction_2d_using_builder(const TopoDS_Shape & a_
|
||||
if (wire_clss[wire_index]->Perform(p2d) == TopAbs_IN) {
|
||||
// A wire is contained within another operand
|
||||
redundant[other_index] = true;
|
||||
Logger::Root().Notice("GEO", 125, "Subtraction operand contained in other");
|
||||
logger.Notice("GEO", 125, "Subtraction operand contained in other");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -848,7 +848,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
std::stringstream ss;
|
||||
ss << "bool-" << std::this_thread::get_id() << "-" << (operation_counter_++);
|
||||
debug_identifier = ss.str();
|
||||
Logger::Root().Notice("GEO", 126, "Boolean debug identifier: " + debug_identifier);
|
||||
settings.log().Notice("GEO", 126, "Boolean debug identifier: " + debug_identifier);
|
||||
}
|
||||
|
||||
if (fuzziness < 0.) {
|
||||
@@ -884,7 +884,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
a = unify(a_input, fuzziness * 1000.);
|
||||
|
||||
Logger::Root().Message(
|
||||
settings.log().Message(
|
||||
Logger::LOG_DEBUG, "GEO", 127,
|
||||
"Simplified operand A from "s +
|
||||
std::to_string(count(a_input, TopAbs_FACE)) +
|
||||
@@ -896,7 +896,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b_input);
|
||||
for (; it.More(); it.Next()) {
|
||||
b.Append(unify(it.Value(), fuzziness));
|
||||
Logger::Root().Message(
|
||||
settings.log().Message(
|
||||
Logger::LOG_DEBUG, "GEO", 128,
|
||||
"Simplified operand B from "s +
|
||||
std::to_string(count(it.Value(), TopAbs_FACE)) +
|
||||
@@ -924,7 +924,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
auto N = bounding_box_overlap(fuzziness, a, b, b_tmp);
|
||||
if (N) {
|
||||
Logger::Root().Notice("GEO", 129, "Eliminated " + std::to_string(N) + " disjoint operands");
|
||||
settings.log().Notice("GEO", 129, "Eliminated " + std::to_string(N) + " disjoint operands");
|
||||
std::swap(b, b_tmp);
|
||||
}
|
||||
}
|
||||
@@ -935,7 +935,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
b_tmp.Clear();
|
||||
auto N = eliminate_touching_operands(fuzziness, a, b, b_tmp);
|
||||
if (N) {
|
||||
Logger::Root().Notice("GEO", 130, "Eliminated " + std::to_string(N) + " touching operands");
|
||||
settings.log().Notice("GEO", 130, "Eliminated " + std::to_string(N) + " touching operands");
|
||||
std::swap(b, b_tmp);
|
||||
}
|
||||
}
|
||||
@@ -944,9 +944,9 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
PERF("boolean subtraction: eliminate narrow");
|
||||
|
||||
b_tmp.Clear();
|
||||
auto N = eliminate_narrow_operands(fuzziness, b, b_tmp);
|
||||
auto N = eliminate_narrow_operands(fuzziness, b, b_tmp, settings.log());
|
||||
if (N) {
|
||||
Logger::Root().Notice("GEO", 131, "Eliminated " + std::to_string(N) + " narrow operands");
|
||||
settings.log().Notice("GEO", 131, "Eliminated " + std::to_string(N) + " narrow operands");
|
||||
std::swap(b, b_tmp);
|
||||
}
|
||||
}
|
||||
@@ -960,21 +960,21 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
if (b.Extent() == 0) {
|
||||
Logger::Root().Warning("GEO", 132, "No other operands remaining, using first operand");
|
||||
settings.log().Warning("GEO", 132, "No other operands remaining, using first operand");
|
||||
result = a;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_2d && Logger::LOG_NOTICE >= Logger::Root().Verbosity()) {
|
||||
if (!is_2d && Logger::LOG_NOTICE >= settings.log().Verbosity()) {
|
||||
PERF("preliminary manifoldness check");
|
||||
|
||||
if (!a.IsNull()) {
|
||||
Logger::Root().Notice("GEO", 133, "Operand A is " + (is_manifold(a) ? ""s : "non-"s) + "manifold");
|
||||
settings.log().Notice("GEO", 133, "Operand A is " + (is_manifold(a) ? ""s : "non-"s) + "manifold");
|
||||
}
|
||||
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (int i = 0; it.More(); it.Next(), ++i) {
|
||||
Logger::Root().Notice("GEO", 134, "Operand B " + std::to_string(i) + " is " + (is_manifold(it.Value()) ? ""s : "non-"s) + "manifold");
|
||||
settings.log().Notice("GEO", 134, "Operand B " + std::to_string(i) + " is " + (is_manifold(it.Value()) ? ""s : "non-"s) + "manifold");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1014,7 +1014,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
const double fuzz = (std::min)(min_length_orig / 3., fuzziness);
|
||||
|
||||
Logger::Root().Notice("GEO", 135, "Used fuzziness: " + std::to_string(fuzz));
|
||||
settings.log().Notice("GEO", 135, "Used fuzziness: " + std::to_string(fuzz));
|
||||
|
||||
const double new_fuzziness = fuzziness * 10.;
|
||||
const bool allow_retry = new_fuzziness - 1e-15 <= settings.precision * 10000. && new_fuzziness < min_length_orig;
|
||||
@@ -1048,7 +1048,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
if (is_extrusion_a) {
|
||||
Logger::Root().Notice("GEO", 136, "Operand A 1/1 is an extrusion");
|
||||
settings.log().Notice("GEO", 136, "Operand A 1/1 is an extrusion");
|
||||
|
||||
NCollection_List<TopoDS_Shape>::Iterator it(b);
|
||||
for (int nb = 1; it.More(); it.Next(), ++nb) {
|
||||
@@ -1064,10 +1064,10 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
if (is_extrusion_b) {
|
||||
Logger::Root().Notice("GEO", 137, "Operand B " + std::to_string(nb) + "/" + std::to_string(b.Extent()) + " is an extrusion");
|
||||
settings.log().Notice("GEO", 137, "Operand B " + std::to_string(nb) + "/" + std::to_string(b.Extent()) + " is an extrusion");
|
||||
|
||||
if (b_interval.first < a_interval.first + (fuzz * 100.) && b_interval.second > a_interval.second - (fuzz * 100.)) {
|
||||
Logger::Root().Notice("GEO", 138, "Operand B creates a through hole");
|
||||
settings.log().Notice("GEO", 138, "Operand B creates a through hole");
|
||||
|
||||
// Align b with a operand
|
||||
gp_Trsf trsf;
|
||||
@@ -1091,7 +1091,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
PERF("boolean operation: 2d builder");
|
||||
// First try using face builder
|
||||
|
||||
boolean_op_2d_success = boolean_subtraction_2d_using_builder(a_face, b_faces, face_result, fuzziness);
|
||||
boolean_op_2d_success = boolean_subtraction_2d_using_builder(a_face, b_faces, face_result, fuzziness, settings.log());
|
||||
}
|
||||
|
||||
if (!boolean_op_2d_success) {
|
||||
@@ -1107,23 +1107,23 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
BRepPrimAPI_MakePrism mp(face_result, gp_Vec(gp::DY()) * (a_interval.second - a_interval.first));
|
||||
if (mp.IsDone()) {
|
||||
if (b_remainder_3d.Extent()) {
|
||||
Logger::Root().Notice("GEO", 139, std::to_string(b_remainder_3d.Extent()) + " operands remaining to process in 3D");
|
||||
settings.log().Notice("GEO", 139, std::to_string(b_remainder_3d.Extent()) + " operands remaining to process in 3D");
|
||||
b = b_remainder_3d;
|
||||
s1s.Clear();
|
||||
s1s.Append(mp.Shape());
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 140, "Processed fully in 2D");
|
||||
settings.log().Notice("GEO", 140, "Processed fully in 2D");
|
||||
result = mp.Shape();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 141, "Failed to extrude 2D boolean result. Retrying in 3D.");
|
||||
settings.log().Notice("GEO", 141, "Failed to extrude 2D boolean result. Retrying in 3D.");
|
||||
}
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 142, "Failed to perform 2D boolean operation. Retrying in 3D.");
|
||||
settings.log().Notice("GEO", 142, "Failed to perform 2D boolean operation. Retrying in 3D.");
|
||||
}
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 143, "No second operands can be processed as 2D inner bounds. Retrying in 3D.");
|
||||
settings.log().Notice("GEO", 143, "No second operands can be processed as 2D inner bounds. Retrying in 3D.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1145,7 +1145,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
if (builder->IsDone()) {
|
||||
if (false && builder->DSFiller()->HasWarning(STANDARD_TYPE(BOPAlgo_AlertAcquiredSelfIntersection))) {
|
||||
Logger::Root().Notice("GEO", 144, "Builder reports self-intersection in output");
|
||||
settings.log().Notice("GEO", 144, "Builder reports self-intersection in output");
|
||||
success = false;
|
||||
|
||||
/*
|
||||
@@ -1159,7 +1159,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
*/
|
||||
} else if(builder->DSFiller()->HasWarning(STANDARD_TYPE(BOPAlgo_AlertBadPositioning)) && !TopoDS_Iterator(*builder).More()) {
|
||||
Logger::Root().Notice("GEO", 145, "Builder reports bad positioning and result is empty");
|
||||
settings.log().Notice("GEO", 145, "Builder reports bad positioning and result is empty");
|
||||
success = false;
|
||||
} else {
|
||||
TopoDS_Shape r = *builder;
|
||||
@@ -1173,7 +1173,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
fix.Perform();
|
||||
r = fix.Shape();
|
||||
} catch (...) {
|
||||
Logger::Root().Error("GEO", 146, "Shape healing failed on boolean result");
|
||||
settings.log().Error("GEO", 146, "Shape healing failed on boolean result");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1184,7 +1184,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
success = ana.IsValid() != 0;
|
||||
|
||||
if (!success) {
|
||||
Logger::Root().Notice("GEO", 147, "Boolean operation yields invalid result");
|
||||
settings.log().Notice("GEO", 147, "Boolean operation yields invalid result");
|
||||
|
||||
std::stringstream str;
|
||||
bool any_emitted = false;
|
||||
@@ -1214,7 +1214,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
dump(r);
|
||||
|
||||
Logger::Root().Notice("GEO", 148, str.str());
|
||||
settings.log().Notice("GEO", 148, str.str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1334,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
|
||||
if (op == BOPAlgo_CUT && has_open_shells && all_faces_included_in_result && result_n_faces > first_op_n_faces) {
|
||||
success = false;
|
||||
Logger::Root().Notice("GEO", 149, "Boolean result discarded because subtractions results in only the addition of faces");
|
||||
settings.log().Notice("GEO", 149, "Boolean result discarded because subtractions results in only the addition of faces");
|
||||
} else {
|
||||
// when there are edges or vertex-edge distances close to the used fuzziness, the
|
||||
// output is not trusted and the operation is attempted with a higher fuzziness.
|
||||
@@ -1380,7 +1380,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
static const char* const reason_strings[] = { "edge length", "vertex-edge", "face-face" };
|
||||
std::stringstream str;
|
||||
str << "Boolean operation result failing " << reason_strings[reason] << " interference check, with fuzziness " << fuzziness << " with length " << v;
|
||||
Logger::Root().Notice("GEO", 150, str.str());
|
||||
settings.log().Notice("GEO", 150, str.str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1389,7 +1389,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
}
|
||||
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 151, "Boolean operation yields non-manifold result");
|
||||
settings.log().Notice("GEO", 151, "Boolean operation yields non-manifold result");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1399,7 +1399,7 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
#if OCC_VERSION_HEX >= 0x70200
|
||||
|
||||
if (builder->HasError(STANDARD_TYPE(BOPAlgo_AlertBOPNotAllowed))) {
|
||||
Logger::Root().Error("GEO", 152, "Invalid operands. Using first operand");
|
||||
settings.log().Error("GEO", 152, "Invalid operands. Using first operand");
|
||||
result = a;
|
||||
success = true;
|
||||
}
|
||||
@@ -1412,14 +1412,14 @@ bool IfcGeom::util::boolean_operation(const boolean_settings& settings, const To
|
||||
#endif
|
||||
std::string str_str = str.str();
|
||||
if (str_str.size()) {
|
||||
Logger::Root().Notice("GEO", 153, str_str);
|
||||
settings.log().Notice("GEO", 153, str_str);
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
if (allow_retry) {
|
||||
return boolean_operation(settings, a, b, op, result, new_fuzziness);
|
||||
} else {
|
||||
Logger::Root().Notice("GEO", 154, "No longer attempting boolean operation with higher fuzziness");
|
||||
settings.log().Notice("GEO", 154, "No longer attempting boolean operation with higher fuzziness");
|
||||
}
|
||||
}
|
||||
return success && !result.IsNull();
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
#include <BOPAlgo_Operation.hxx>
|
||||
|
||||
#include "../../../ifcparse/IfcLogger.h"
|
||||
#include "../ifc_geomlibrary_api.h"
|
||||
|
||||
namespace IfcGeom {
|
||||
@@ -88,13 +89,19 @@ namespace IfcGeom {
|
||||
|
||||
int eliminate_touching_operands(double prec, const TopoDS_Shape& a, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape>& c);
|
||||
|
||||
int eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c);
|
||||
int eliminate_narrow_operands(double prec, const NCollection_List<TopoDS_Shape>& bs, NCollection_List<TopoDS_Shape> & c, Logger& logger = Logger::Root());
|
||||
|
||||
bool boolean_subtraction_2d_using_builder(const TopoDS_Shape& a_input, const NCollection_List<TopoDS_Shape>& b_input, TopoDS_Shape& result, double eps);
|
||||
bool boolean_subtraction_2d_using_builder(const TopoDS_Shape& a_input, const NCollection_List<TopoDS_Shape>& b_input, TopoDS_Shape& result, double eps, Logger& logger = Logger::Root());
|
||||
|
||||
struct boolean_settings {
|
||||
bool debug, attempt_2d;
|
||||
double precision;
|
||||
// Set by callers that carry a per-conversion Logger (e.g. kernels deriving
|
||||
// from AbstractKernel). Falls back to the global Logger::Root() singleton,
|
||||
// which IfcConvert never wires to its --log-file output, so messages logged
|
||||
// through that fallback are effectively silently dropped.
|
||||
Logger* logger = nullptr;
|
||||
Logger& log() const { return logger ? *logger : Logger::Root(); }
|
||||
};
|
||||
|
||||
bool boolean_operation(const boolean_settings& settings, const TopoDS_Shape&, const NCollection_List<TopoDS_Shape>&, BOPAlgo_Operation, TopoDS_Shape&, double fuzziness = -1.);
|
||||
|
||||
@@ -128,7 +128,12 @@ bool OpenCascadeKernel::convert(const taxonomy::sweep_along_curve::ptr scs, Topo
|
||||
}
|
||||
}
|
||||
|
||||
auto w = convert_curve(scs->curve);
|
||||
// Build the wire from curve, which is the directrix offset toward the origin
|
||||
// when applied_temporary_offset is set. Using scs->curve here left the wire
|
||||
// far from the origin yet still translated the result back by +mean, which
|
||||
// misplaced sweeps far from the origin (#4848). When no offset is applied
|
||||
// curve aliases scs->curve, so near-origin geometry is unaffected.
|
||||
auto w = convert_curve(curve);
|
||||
if (w.which() != 2) {
|
||||
Logger::Root().Error("UNS", 9, "Unsupported directrix");
|
||||
return false;
|
||||
|
||||
@@ -55,7 +55,7 @@ PLATFORMTAG:=win_amd64
|
||||
endif
|
||||
|
||||
BINARY_VERSION:=0.8.6
|
||||
BUILD_COMMIT:=3e7b739
|
||||
BUILD_COMMIT:=e333c1c
|
||||
IOS_URL:=https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-$(PYNUMBER)-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
|
||||
IFCCONVERT_URL:=https://s3.amazonaws.com/ifcopenshell-builds/IfcConvert-v$(BINARY_VERSION)-$(BUILD_COMMIT)-$(PLATFORM).zip
|
||||
|
||||
|
||||
@@ -97,7 +97,19 @@ from .file import file as _file
|
||||
from .sql import sqlite, sqlite_entity
|
||||
|
||||
get_log = ifcopenshell_wrapper.get_log
|
||||
logger = getattr(ifcopenshell_wrapper, "logger", None)
|
||||
logger = ifcopenshell_wrapper.logger if hasattr(ifcopenshell_wrapper, "logger") else None
|
||||
if hasattr(ifcopenshell_wrapper, "logger_or_root"):
|
||||
logger_or_root = ifcopenshell_wrapper.logger_or_root
|
||||
else:
|
||||
|
||||
def logger_or_root(_logger: ifcopenshell_wrapper.logger | None) -> None:
|
||||
return None
|
||||
|
||||
|
||||
# TODO: drop this function and all callsites after we migrate to the new build.
|
||||
def optional_logger_args(logger: ifcopenshell_wrapper.logger | None) -> tuple[logger] | tuple[()]:
|
||||
return (logger,) if logger is not None else ()
|
||||
|
||||
|
||||
# explicitly specify available imported symbols
|
||||
# (it's a requirement for a typed library)
|
||||
@@ -194,10 +206,9 @@ def open(
|
||||
raise FileNotFoundError(f"Path does not exist: '{path}'.")
|
||||
if format is None:
|
||||
format = guess_format(path)
|
||||
if logger is None and (logger_type := getattr(ifcopenshell_wrapper, "logger", None)):
|
||||
logger = logger_type.Root()
|
||||
logger = logger_or_root(logger)
|
||||
if format == ".ifcXML":
|
||||
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()), *((logger,) if logger is not None else ()))
|
||||
f = ifcopenshell_wrapper.parse_ifcxml(str(path.absolute()), *optional_logger_args(logger))
|
||||
if f:
|
||||
return file(f)
|
||||
raise OSError(f"Failed to parse .ifcXML file from {path}")
|
||||
@@ -214,11 +225,9 @@ def open(
|
||||
if should_stream:
|
||||
return stream(path)
|
||||
if readonly: # Temporary conditional see #7131. Remove once newer builds don't segfault on Linux.
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly, *((logger,) if logger is not None else ()))
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), readonly, *optional_logger_args(logger))
|
||||
elif bypass_types:
|
||||
f = ifcopenshell_wrapper.file(
|
||||
ifcopenshell_wrapper.uninitialized_tag(), *((logger,) if logger is not None else ())
|
||||
)
|
||||
f = ifcopenshell_wrapper.file(ifcopenshell_wrapper.uninitialized_tag(), *optional_logger_args(logger))
|
||||
for ty in bypass_types:
|
||||
f.bypass_type(ty)
|
||||
if mmap:
|
||||
@@ -233,7 +242,7 @@ def open(
|
||||
kwargs["logger"] = logger
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), **kwargs)
|
||||
else:
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), False, *((logger,) if logger is not None else ()))
|
||||
f = ifcopenshell_wrapper.open(str(path.absolute()), False, *optional_logger_args(logger))
|
||||
return file(f)
|
||||
|
||||
|
||||
@@ -305,12 +314,13 @@ def schema_by_name(
|
||||
you are testing non-ISO IFC releases.
|
||||
:return: Schema definition object.
|
||||
"""
|
||||
import ifcopenshell.util.schema
|
||||
|
||||
assert schema_version or schema, "Either schema or schema_version must be specified."
|
||||
if schema_version:
|
||||
prefixes = ("IFC", "X", "_ADD", "_TC")
|
||||
schema = "".join("".join(map(str, t)) if t[1] else "" for t in zip(prefixes, schema_version))
|
||||
schema = ifcopenshell.util.schema.get_schema_name_from_version(schema_version)
|
||||
else:
|
||||
schema = {"IFC4X3": "IFC4X3_ADD2"}.get(schema, schema)
|
||||
schema = ifcopenshell.util.schema.get_schema_identifier(schema)
|
||||
return ifcopenshell_wrapper.schema_by_name(schema)
|
||||
|
||||
|
||||
|
||||
@@ -159,8 +159,10 @@ def _add_segment_to_curve(
|
||||
else:
|
||||
assert False
|
||||
|
||||
end_point = ...
|
||||
for mapped_segment in mapped_segments:
|
||||
if mapped_segment:
|
||||
end_point = _add_curve_segment_to_composite_curve(file, layout_segment, mapped_segment, curve)
|
||||
|
||||
assert end_point is not ...
|
||||
return end_point
|
||||
|
||||
@@ -308,5 +308,7 @@ def _get_segment_start_point_label(prev_segment: entity_instance, segment: entit
|
||||
label = _cant_callback(prev_segment, segment)
|
||||
else:
|
||||
label = _cant_label(prev_segment, segment)
|
||||
else:
|
||||
assert False, s.DesignParameters
|
||||
|
||||
return label
|
||||
|
||||
@@ -46,6 +46,7 @@ def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points:
|
||||
ifcopenshell.api.nest.assign_object(file, related_objects=alignment_layouts, relating_object=alignment)
|
||||
|
||||
start_dist_along = 0.0
|
||||
gradient = None
|
||||
for p1, p2 in zip(points, points[1:]):
|
||||
x1, y1, z1 = p1.Coordinates
|
||||
x2, y2, z2 = p2.Coordinates
|
||||
@@ -100,6 +101,7 @@ def _create_layout(file: ifcopenshell.file, alignment: entity_instance, points:
|
||||
ifcopenshell.api.nest.assign_object(file, related_objects=[hsegment], relating_object=alignment_layouts[0])
|
||||
|
||||
if include_vertical:
|
||||
assert gradient is not None
|
||||
vsegment = file.createIfcAlignmentSegment(
|
||||
ifcopenshell.guid.new(),
|
||||
DesignParameters=file.createIfcAlignmentVerticalSegment(
|
||||
|
||||
@@ -57,6 +57,7 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
|
||||
:param filepath: path the to CSV file
|
||||
:return: IfcAlignment
|
||||
"""
|
||||
alignment = None
|
||||
with open(filepath, newline="") as csvfile:
|
||||
reader = csv.reader(csvfile)
|
||||
row_count = 0
|
||||
@@ -89,9 +90,14 @@ def create_from_csv(file: ifcopenshell.file, filepath: str) -> entity_instance:
|
||||
)
|
||||
else:
|
||||
# add all subsequent vertical alignments
|
||||
assert alignment is not None
|
||||
vertical_layout = ifcopenshell.api.alignment.add_vertical_layout(file, alignment)
|
||||
ifcopenshell.api.alignment.layout_vertical_alignment_by_pi_method(
|
||||
file, vertical_layout, coordinates, radii
|
||||
)
|
||||
|
||||
if row_count == 0:
|
||||
raise ValueError(f"CSV file '{filepath}' is empty; expected at least one row for the horizontal alignment.")
|
||||
|
||||
assert alignment is not None
|
||||
return alignment
|
||||
|
||||
@@ -182,6 +182,8 @@ class Usecase:
|
||||
if not reference:
|
||||
migrator = ifcopenshell.util.schema.Migrator()
|
||||
|
||||
old_referenced_source = ...
|
||||
existing_classification = None
|
||||
if self.settings["is_lightweight"]:
|
||||
old_referenced_source = self.settings["reference"].ReferencedSource
|
||||
self.settings["reference"].ReferencedSource = None
|
||||
@@ -194,6 +196,7 @@ class Usecase:
|
||||
reference = migrator.migrate(self.settings["reference"], self.file)
|
||||
|
||||
if self.settings["is_lightweight"]:
|
||||
assert old_referenced_source is not ...
|
||||
reference.ReferencedSource = self.settings["classification"]
|
||||
self.settings["reference"].ReferencedSource = old_referenced_source
|
||||
elif existing_classification:
|
||||
|
||||
@@ -88,6 +88,8 @@ def bearing2dd(bearing: str) -> float:
|
||||
elif cY == "S" and cX == "W":
|
||||
angle = 270.0
|
||||
sign = -1.0
|
||||
else:
|
||||
assert False, (cY, cX)
|
||||
|
||||
try:
|
||||
dms = ifcopenshell.util.geolocation.dms2dd(d, m, s, ms)
|
||||
|
||||
@@ -112,76 +112,84 @@ def assign_cost_item_quantity(
|
||||
"""
|
||||
usecase = Usecase()
|
||||
usecase.file = file
|
||||
usecase.settings = {
|
||||
"cost_item": cost_item,
|
||||
"products": products or [],
|
||||
"prop_name": prop_name,
|
||||
"formula": formula,
|
||||
"ifc_class": ifc_class,
|
||||
}
|
||||
return usecase.execute()
|
||||
return usecase.execute(
|
||||
cost_item=cost_item,
|
||||
products=products or [],
|
||||
prop_name=prop_name,
|
||||
formula=formula,
|
||||
ifc_class=ifc_class,
|
||||
)
|
||||
|
||||
|
||||
class Usecase:
|
||||
file: ifcopenshell.file
|
||||
settings: dict[str, Any]
|
||||
|
||||
def execute(self):
|
||||
if self.settings["prop_name"] or self.settings["formula"]:
|
||||
self.quantities = set(self.settings["cost_item"].CostQuantities or [])
|
||||
for product in self.settings["products"]:
|
||||
def execute(
|
||||
self,
|
||||
cost_item: ifcopenshell.entity_instance,
|
||||
products: list[ifcopenshell.entity_instance],
|
||||
prop_name: str,
|
||||
formula: str,
|
||||
ifc_class: str,
|
||||
):
|
||||
self.cost_item = cost_item
|
||||
self.prop_name = prop_name
|
||||
if self.prop_name or formula:
|
||||
self.quantities = set(cost_item.CostQuantities or [])
|
||||
for product in products:
|
||||
if product.is_a("IfcSpatialElement"):
|
||||
continue
|
||||
self.assign_cost_control(related_object=product, cost_item=self.settings["cost_item"])
|
||||
if self.settings["formula"]:
|
||||
tree = ast.parse(self.settings["formula"], mode="eval")
|
||||
ifcopenshell.api.control.assign_control(
|
||||
self.file,
|
||||
related_objects=[product],
|
||||
relating_control=cost_item,
|
||||
)
|
||||
if formula:
|
||||
tree = ast.parse(formula, mode="eval")
|
||||
collector = VariableExtractor()
|
||||
collector.visit(tree)
|
||||
variables = collector.variables
|
||||
|
||||
values: dict[str, float | None] = {}
|
||||
for variable in variables:
|
||||
getter = self.get_value_from_pset if "." in variable else self.get_value_from_qset
|
||||
value = getter(product, variable)
|
||||
values[variable] = value
|
||||
|
||||
if value is None:
|
||||
print(
|
||||
f"WARNING: Variable '{variable}' in product '{product.Name}' "
|
||||
f"is missing (None). Check Pset/Qset or property name."
|
||||
)
|
||||
elif value == 0:
|
||||
print(
|
||||
f"WARNING: Variable '{variable}' in product '{product.Name}' "
|
||||
f"has value 0. Verify if this is correct."
|
||||
)
|
||||
if value is None:
|
||||
print(
|
||||
f"WARNING: Variable '{variable}' in product '{product.Name}' "
|
||||
f"is missing (None). Check Pset/Qset or property name."
|
||||
)
|
||||
elif value == 0:
|
||||
print(
|
||||
f"WARNING: Variable '{variable}' in product '{product.Name}' "
|
||||
f"has value 0. Verify if this is correct."
|
||||
)
|
||||
|
||||
evaluator = FormulaEvaluator(values)
|
||||
result = evaluator.visit(tree.body)
|
||||
|
||||
new_quantity = None
|
||||
for quantity in self.quantities:
|
||||
if (
|
||||
quantity.Formula == self.settings["formula"] and len(self.settings["products"]) == 1
|
||||
): # Todo improve it
|
||||
if quantity.Formula == formula and len(products) == 1: # Todo improve it
|
||||
new_quantity = quantity
|
||||
self.settings["ifc_class"] = quantity.is_a()
|
||||
ifc_class = quantity.is_a()
|
||||
continue
|
||||
if new_quantity is None:
|
||||
new_quantity = self.file.create_entity(self.settings["ifc_class"], Name="Unnamed")
|
||||
new_quantity.Formula = self.settings["formula"]
|
||||
new_quantity = self.file.create_entity(ifc_class, Name="Unnamed")
|
||||
new_quantity.Formula = formula
|
||||
self.quantities.add(new_quantity)
|
||||
|
||||
new_quantity[3] = result
|
||||
continue
|
||||
|
||||
if self.settings["prop_name"]:
|
||||
if (
|
||||
self.settings["cost_item"].CostQuantities
|
||||
and self.settings["cost_item"].CostQuantities[0].Name.lower() != self.settings["prop_name"].lower()
|
||||
):
|
||||
if self.prop_name:
|
||||
if cost_item.CostQuantities and cost_item.CostQuantities[0].Name.lower() != self.prop_name.lower():
|
||||
continue
|
||||
self.add_quantity_from_related_object(product)
|
||||
if self.settings["prop_name"] or self.settings["formula"]:
|
||||
self.settings["cost_item"].CostQuantities = list(self.quantities)
|
||||
if self.prop_name or formula:
|
||||
cost_item.CostQuantities = list(self.quantities)
|
||||
else:
|
||||
self.update_cost_item_count()
|
||||
|
||||
@@ -189,7 +197,7 @@ class Usecase:
|
||||
self,
|
||||
product: ifcopenshell.entity_instance,
|
||||
v: str,
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
pset_name = v.split(".")[0]
|
||||
pset = ifcopenshell.util.element.get_pset(product, pset_name)
|
||||
pset_property_name = v.split(".")[1]
|
||||
@@ -199,20 +207,11 @@ class Usecase:
|
||||
self,
|
||||
product: ifcopenshell.entity_instance,
|
||||
v: str,
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
qtos = ifcopenshell.util.element.get_psets(product, qtos_only=True)
|
||||
quantities = next(iter(qtos.values()), {})
|
||||
return (quantities or {}).get(v, None)
|
||||
|
||||
def assign_cost_control(
|
||||
self, related_object: ifcopenshell.entity_instance, cost_item: ifcopenshell.entity_instance
|
||||
) -> ifcopenshell.entity_instance:
|
||||
return ifcopenshell.api.control.assign_control(
|
||||
self.file,
|
||||
related_objects=[related_object],
|
||||
relating_control=cost_item,
|
||||
)
|
||||
|
||||
def add_quantity_from_related_object(self, element: ifcopenshell.entity_instance) -> None:
|
||||
for relationship in element.IsDefinedBy:
|
||||
if relationship.is_a("IfcRelDefinesByProperties"):
|
||||
@@ -222,23 +221,24 @@ class Usecase:
|
||||
if not qto.is_a("IfcElementQuantity"):
|
||||
return
|
||||
for prop in qto.Quantities:
|
||||
if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.settings["prop_name"].lower():
|
||||
if prop.is_a("IfcPhysicalSimpleQuantity") and prop.Name.lower() == self.prop_name.lower():
|
||||
self.quantities.add(prop)
|
||||
|
||||
def update_cost_item_count(self):
|
||||
cost_item = self.cost_item
|
||||
# This is a bold assumption
|
||||
# https://forums.buildingsmart.org/t/how-does-a-cost-item-know-that-it-is-counting-a-controlled-product/3564
|
||||
if not self.settings["cost_item"].CostQuantities:
|
||||
if not cost_item.CostQuantities:
|
||||
ifcopenshell.api.cost.add_cost_item_quantity(
|
||||
self.file,
|
||||
cost_item=self.settings["cost_item"],
|
||||
cost_item=cost_item,
|
||||
ifc_class="IfcQuantityCount",
|
||||
)
|
||||
if len(self.settings["cost_item"].CostQuantities) == 1:
|
||||
quantity = self.settings["cost_item"].CostQuantities[0]
|
||||
if len(cost_item.CostQuantities) == 1:
|
||||
quantity = cost_item.CostQuantities[0]
|
||||
if quantity.is_a("IfcQuantityCount"):
|
||||
count = 0
|
||||
for rel in self.settings["cost_item"].Controls:
|
||||
for rel in cost_item.Controls:
|
||||
for obj in rel.RelatedObjects:
|
||||
# Only increment if not a resource
|
||||
if not obj.is_a("IfcConstructionResource"):
|
||||
@@ -256,7 +256,7 @@ OPERATORS = {
|
||||
}
|
||||
|
||||
|
||||
def build_full_name(node):
|
||||
def build_full_name(node: ast.expr) -> str:
|
||||
# used for variables with dots
|
||||
parts = []
|
||||
while isinstance(node, ast.Attribute):
|
||||
@@ -270,33 +270,33 @@ def build_full_name(node):
|
||||
|
||||
|
||||
class VariableExtractor(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
self.variables = set()
|
||||
def __init__(self) -> None:
|
||||
self.variables: set[str] = set()
|
||||
|
||||
def visit_Name(self, node):
|
||||
def visit_Name(self, node: ast.Name) -> None:
|
||||
self.variables.add(node.id)
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
def visit_Attribute(self, node: ast.Attribute) -> None:
|
||||
self.variables.add(build_full_name(node))
|
||||
|
||||
|
||||
class FormulaEvaluator(ast.NodeVisitor):
|
||||
def __init__(self, values):
|
||||
def __init__(self, values: dict[str, float | None]):
|
||||
self.values = values
|
||||
|
||||
def visit_BinOp(self, node):
|
||||
def visit_BinOp(self, node: ast.BinOp) -> float:
|
||||
left = self.visit(node.left)
|
||||
right = self.visit(node.right)
|
||||
return OPERATORS[type(node.op)](left, right) # ty: ignore[too-many-positional-arguments]
|
||||
|
||||
def visit_Name(self, node):
|
||||
def visit_Name(self, node: ast.Name) -> float | None:
|
||||
return self.values[node.id]
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
def visit_Attribute(self, node: ast.Attribute) -> float | None:
|
||||
return self.values[build_full_name(node)]
|
||||
|
||||
def visit_Constant(self, node):
|
||||
def visit_Constant(self, node: ast.Constant) -> Any:
|
||||
return node.value
|
||||
|
||||
def generic_visit(self, node):
|
||||
def generic_visit(self, node: ast.AST) -> Any:
|
||||
raise ValueError(f"Operation not permitted: {type(node).__name__}")
|
||||
|
||||
@@ -116,6 +116,8 @@ def add_feature(
|
||||
return ifcopenshell.api.aggregate.assign_object(file, [feature], element)
|
||||
rels = feature.AdheresToElement
|
||||
ifc_class = "IfcRelAdheresToElement"
|
||||
else:
|
||||
assert False, feature
|
||||
|
||||
if rels:
|
||||
if rels[0][4] == element:
|
||||
|
||||
@@ -54,6 +54,8 @@ def remove_feature(file: ifcopenshell.file, feature: ifcopenshell.entity_instanc
|
||||
rels = []
|
||||
else:
|
||||
rels = feature.ProjectsElements
|
||||
else:
|
||||
assert False, feature
|
||||
for rel in rels:
|
||||
history = rel.OwnerHistory
|
||||
file.remove(rel)
|
||||
|
||||
@@ -436,6 +436,7 @@ class Usecase:
|
||||
|
||||
def create_curve_bounded_planes(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]:
|
||||
items = []
|
||||
points = None
|
||||
if self.file.schema != "IFC2X3":
|
||||
points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=False)
|
||||
for polygon in self.settings["geometry"].polygons:
|
||||
@@ -443,6 +444,7 @@ class Usecase:
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=False)
|
||||
else:
|
||||
assert points is not None
|
||||
curve = self.create_curve_from_polygon(points, polygon, is_2d=False)
|
||||
items.append(self.file.createIfcCurveBoundedPlane(BasisSurface=plane, OuterBoundary=curve))
|
||||
return items
|
||||
@@ -457,12 +459,14 @@ class Usecase:
|
||||
|
||||
def create_annotation_fill_areas(self, is_2d: bool = False) -> list[ifcopenshell.entity_instance]:
|
||||
items = []
|
||||
points = None
|
||||
if self.file.schema != "IFC2X3":
|
||||
points = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices, is_2d=is_2d)
|
||||
for polygon in self.settings["geometry"].polygons:
|
||||
if self.file.schema == "IFC2X3":
|
||||
curve = self.create_curve_from_polygon_ifc2x3(polygon, is_2d=is_2d)
|
||||
else:
|
||||
assert points is not None
|
||||
curve = self.create_curve_from_polygon(points, polygon, is_2d=is_2d)
|
||||
items.append(self.file.createIfcAnnotationFillArea(OuterBoundary=curve))
|
||||
return items
|
||||
@@ -813,17 +817,20 @@ class Usecase:
|
||||
|
||||
def create_triangulated_face_set(self) -> ifcopenshell.entity_instance:
|
||||
ifc_raw_items = [None] * self.settings["total_items"]
|
||||
ifc_raw_uv_items = None
|
||||
if self.settings["should_generate_uvs"]:
|
||||
ifc_raw_uv_items = [None] * self.settings["total_items"]
|
||||
for i, value in enumerate(ifc_raw_items):
|
||||
ifc_raw_items[i] = []
|
||||
if self.settings["should_generate_uvs"]:
|
||||
assert ifc_raw_uv_items is not None
|
||||
ifc_raw_uv_items[i] = []
|
||||
for polygon in self.settings["geometry"].polygons:
|
||||
ifc_raw_items[polygon.material_index % self.settings["total_items"]].append(
|
||||
[v + 1 for v in polygon.vertices]
|
||||
)
|
||||
if self.settings["should_generate_uvs"]:
|
||||
assert ifc_raw_uv_items is not None
|
||||
ifc_raw_uv_items[polygon.material_index % self.settings["total_items"]].append(
|
||||
[uv + 1 for uv in polygon.loop_indices]
|
||||
)
|
||||
@@ -831,6 +838,7 @@ class Usecase:
|
||||
coordinates = self.create_cartesian_point_list_from_vertices(self.settings["geometry"].vertices)
|
||||
|
||||
if self.settings["should_generate_uvs"]:
|
||||
assert ifc_raw_uv_items is not None
|
||||
# Blender supports multiple UV layers. We don't. Too bad.
|
||||
tex_coords = self.file.createIfcTextureVertexList(
|
||||
[tuple(x.uv) for x in self.settings["geometry"].uv_layers[0].data]
|
||||
|
||||
@@ -50,6 +50,12 @@ def disconnect_path(
|
||||
for r in relating_element.ConnectedTo
|
||||
if r.is_a("IfcRelConnectsPathElements") and r.RelatedElement == related_element
|
||||
]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Either provide `element` and `connection_type`, or provide `relating_element` and `related_element`. "
|
||||
f"Got: element={element}, connection_type={connection_type}, "
|
||||
f"relating_element={relating_element}, related_element={related_element}."
|
||||
)
|
||||
|
||||
for connection in set(connections):
|
||||
history = connection.OwnerHistory
|
||||
|
||||
@@ -52,6 +52,7 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl
|
||||
# This unsets true north
|
||||
ifcopenshell.api.georeference.edit_true_north(model, true_north=None)
|
||||
"""
|
||||
x, y = None, None
|
||||
if isinstance(true_north, (float, int)):
|
||||
x, y = ifcopenshell.util.geolocation.angle2yaxis(true_north)
|
||||
elif true_north is not None:
|
||||
@@ -65,9 +66,13 @@ def edit_true_north(file: ifcopenshell.file, true_north: Optional[Union[tuple[fl
|
||||
ifcopenshell.util.element.remove_deep2(file, old_true_north)
|
||||
continue
|
||||
|
||||
if true_north is None:
|
||||
continue
|
||||
|
||||
if context.TrueNorth:
|
||||
if file.get_total_inverses(context.TrueNorth) != 1:
|
||||
context.TrueNorth = file.create_entity("IfcDirection")
|
||||
else:
|
||||
context.TrueNorth = file.create_entity("IfcDirection")
|
||||
assert x is not None and y is not None
|
||||
context.TrueNorth.DirectionRatios = (x, y)
|
||||
|
||||
@@ -90,6 +90,8 @@ def edit_wcs(
|
||||
point,
|
||||
file.createIfcDirection((xaxis_x, xaxis_y)),
|
||||
)
|
||||
else:
|
||||
assert False, context
|
||||
context.WorldCoordinateSystem = placement
|
||||
if file.get_total_inverses(old_wcs) == 0:
|
||||
ifcopenshell.util.element.remove_deep2(file, old_wcs)
|
||||
|
||||
+2
@@ -61,6 +61,8 @@ def add_structural_boundary_condition(
|
||||
boundary_class = "IfcBoundaryEdgeCondition"
|
||||
elif related_connection.is_a("IfcStructuralSurfaceConnection"):
|
||||
boundary_class = "IfcBoundaryFaceCondition"
|
||||
else:
|
||||
assert False, related_connection
|
||||
|
||||
condition = file.create_entity(boundary_class, Name=name)
|
||||
connection.AppliedCondition = condition
|
||||
|
||||
@@ -126,6 +126,7 @@ class Usecase:
|
||||
use_style_assignment = self.file.schema == "IFC2X3" or self.settings["should_use_presentation_style_assignment"]
|
||||
replace_previous_same_type_style = self.settings["replace_previous_same_type_style"]
|
||||
|
||||
style: ifcopenshell.entity_instance | None = None
|
||||
for element in self.file.traverse(self.settings["shape_representation"]):
|
||||
if not element.is_a("IfcShapeModel"):
|
||||
continue
|
||||
@@ -137,6 +138,7 @@ class Usecase:
|
||||
if self.settings["styles"]:
|
||||
# If there are more items than styles, fallback to using the last style
|
||||
style = self.settings["styles"].pop(0)
|
||||
assert style is not None
|
||||
name = style.Name
|
||||
current_style_type = style.is_a()
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ def assign_system(
|
||||
# This duct is part of the system
|
||||
ifcopenshell.api.system.assign_system(model, products=[duct], system=system)
|
||||
"""
|
||||
if not all(ifcopenshell.util.system.is_assignable(failed_product := product, system) for product in products):
|
||||
raise TypeError(f"You cannot assign an {failed_product.is_a()} to an {system.is_a()}")
|
||||
for product in products:
|
||||
if not ifcopenshell.util.system.is_assignable(product, system):
|
||||
raise TypeError(f"You cannot assign an {product.is_a()} to an {system.is_a()}")
|
||||
|
||||
return ifcopenshell.api.group.assign_group(file, products=products, group=system)
|
||||
|
||||
@@ -196,13 +196,25 @@ class Usecase:
|
||||
allowed_occurrences = set(
|
||||
ifcopenshell.util.type.get_applicable_entities(relating_type.is_a(), schema=self.file.schema)
|
||||
)
|
||||
schema = ifcopenshell.schema_by_name(self.file.schema)
|
||||
|
||||
# The implementer agreement map has no entry for the abstract
|
||||
# IfcTypeProduct, which Bonsai uses for annotation types. The schema
|
||||
# itself defines IfcTypeProduct.ApplicableOccurrence for exactly this
|
||||
# purpose, so honor it when the leading class token is a valid entity.
|
||||
if applicable_occurrence := getattr(relating_type, "ApplicableOccurrence", None):
|
||||
occurrence_class = applicable_occurrence.split("/", 1)[0]
|
||||
schema = ifcopenshell.schema_by_name(self.file.schema)
|
||||
try:
|
||||
schema.declaration_by_name(occurrence_class)
|
||||
allowed_occurrences.add(occurrence_class)
|
||||
except RuntimeError:
|
||||
pass
|
||||
# The map only covers physical product occurrence/type pairs (e.g.
|
||||
# IfcWallType -> IfcWall). Process and resource types (IfcTaskType,
|
||||
# IfcCrewResourceType, ...) aren't in it, but the schema's universal
|
||||
# Type-suffix naming convention gives the same pairing directly.
|
||||
if (type_class := relating_type.is_a()).endswith("Type"):
|
||||
occurrence_class = type_class[: -len("Type")]
|
||||
try:
|
||||
schema.declaration_by_name(occurrence_class)
|
||||
allowed_occurrences.add(occurrence_class)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user