Files
IfcOpenShell/src/bonsai/test/tool/test_drawing.py
T
Stephen Boddy f3a7a35acf Expose SVG edge classification settings in drawing UI
Add UseEdgeClassification, RenderCreases, ValleyAngleMinDegrees,
RenderSharp, RidgeAngleMinDegrees, and RenderFlush to EPset_Drawing,
following the existing HasUnderlay/DPI/PerspectiveShiftX pattern.
The master toggle defaults off, preserving current linework output;
the three dependent controls only show in the panel once it's on.

Removes the previous dormant, transient operator-redo properties for
the ridge/valley thresholds and flush-edge toggle, which were never
persisted per-drawing or exposed in any panel, replacing them with
the persistent camera properties read in setup_serialiser().

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 01:33:15 +01:00

1110 lines
44 KiB
Python

# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of Bonsai.
#
# Bonsai is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Bonsai is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import os
import xml.etree.ElementTree as ET
from pathlib import Path
import bpy
import ifcopenshell
import ifcopenshell.api.drawing
import ifcopenshell.api.group
import ifcopenshell.api.pset
import ifcopenshell.api.root
import ifcopenshell.guid
import ifcopenshell.util.element
import mathutils
import numpy as np
import pytest
from ifcopenshell.util.shape_builder import ShapeBuilder
from mathutils import Vector
import bonsai.core.tool
import bonsai.tool as tool
from bonsai.bim.module.drawing.data import DecoratorData
from bonsai.tool.drawing import Drawing as subject
from test.bim.bootstrap import NewFile
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(subject(), bonsai.core.tool.Drawing)
class TestCopyRepresentation(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
source = ifc.createIfcAnnotation(Representation=ifc.createIfcProductDefinitionShape())
dest = ifc.createIfcAnnotation()
subject.copy_representation(source, dest)
assert dest.Representation.is_a("IfcProductDefinitionShape")
class TestCreateAnnotationObject(NewFile):
def test_nothing(self):
pass
class TestCreateCamera(NewFile):
def test_run(self):
obj = subject.create_camera("Name", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
assert obj.name == "Name"
assert obj.matrix_world == mathutils.Matrix()
assert isinstance(obj.data, bpy.types.Camera)
assert obj.data.type == "PERSP"
assert obj.data.ortho_scale == 50
assert obj.data.clip_end == 10
assert obj.users_collection == tuple()
class TestImportCameraProps(NewFile):
def test_imports_perspective_camera_shifts_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={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375},
)
camera = bpy.data.cameras.new("Camera")
camera.type = "PERSP"
subject.import_camera_props(drawing, camera)
assert camera.shift_x == pytest.approx(0.125)
assert camera.shift_y == pytest.approx(-0.375)
def test_non_perspective_import_defaults_camera_shifts_to_zero(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={"PerspectiveShiftX": 0.125, "PerspectiveShiftY": -0.375},
)
camera = bpy.data.cameras.new("Camera")
camera.type = "ORTHO"
camera.shift_x = 1.0
camera.shift_y = -1.0
subject.import_camera_props(drawing, camera)
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):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
camera = bpy.data.cameras.new("Camera")
camera.type = "PERSP"
camera.shift_x = 0.25
camera.shift_y = -0.5
subject.sync_perspective_camera_shifts(drawing, camera)
pset = ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing")
assert pset["PerspectiveShiftX"] == pytest.approx(0.25)
assert pset["PerspectiveShiftY"] == pytest.approx(-0.5)
reloaded_camera = bpy.data.cameras.new("ReloadedCamera")
reloaded_camera.type = "PERSP"
subject.import_camera_props(drawing, reloaded_camera)
assert reloaded_camera.shift_x == pytest.approx(0.25)
assert reloaded_camera.shift_y == pytest.approx(-0.5)
def test_ignores_non_perspective_camera_shifts(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
camera = bpy.data.cameras.new("Camera")
camera.type = "ORTHO"
camera.shift_x = 0.25
camera.shift_y = -0.5
subject.sync_perspective_camera_shifts(drawing, camera)
assert ifcopenshell.util.element.get_pset(drawing, "EPset_Drawing") is None
class TestCreateSvgSheet(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcProject")
tool.Ifc.set(ifc)
ifc_path = Path("test/files/temp/test.ifc").absolute()
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
document = ifc.createIfcDocumentInformation(
Identification="X",
Name="FOOBAR",
Scope="SHEET",
Location=(ifc_path.parent / "layouts" / "X - FOOBAR.svg").as_posix(),
)
uri = subject.create_svg_sheet(document, "A1")
assert uri.endswith(".svg")
assert os.path.isfile(uri)
class TestDeleteCollection(NewFile):
def test_run(self):
collection = bpy.data.collections.new("Foobar")
subject.delete_collection(collection)
assert not bpy.data.collections.get("Foobar")
class TestDeleteDrawingElements(NewFile):
def test_run(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", bpy.data.meshes.new("Mesh"))
collection = bpy.data.collections.new("Collection")
bpy.context.scene.collection.children.link(collection)
collection.objects.link(obj)
element = ifc.createIfcAnnotation(GlobalId=ifcopenshell.guid.new())
tool.Ifc.link(element, obj)
element_id = element.id()
subject.delete_drawing_elements([element])
try:
ifc.by_id(element_id)
assert False
except:
pass
assert bpy.data.objects.get("Object") is None
class TestDisableEditingDrawings(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_drawings = True
subject.disable_editing_drawings()
assert props.is_editing_drawings == False
class TestDisableEditingSchedules(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_schedules = True
subject.disable_editing_schedules()
assert props.is_editing_schedules == False
class TestDisableEditingReferences(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_references = True
subject.disable_editing_references()
assert props.is_editing_references == False
class TestDisableEditingSheets(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_sheets = True
subject.disable_editing_sheets()
assert props.is_editing_sheets == False
class TestEditTextLiterals(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcAnnotation()
element.Representation = ifc.createIfcProductDefinitionShape()
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
builder = ShapeBuilder(tool.Ifc.get())
polyline = builder.polyline([(0.0, 0.0, 0.0), (1.0, 0.0, 0.0)])
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item, polyline])
element.Representation.Representations = [representation]
tool.Ifc.link(element, obj)
literal_attributes = [
{
"Literal": "Foo",
"Path": "RIGHT",
"BoxAlignment": "bottom-left",
}
]
subject.edit_text_literals(obj, literal_attributes)
assert len(ifc.by_type("IfcTextLiteralWithExtent")) == 1
literal = ifc.by_type("IfcTextLiteralWithExtent")[0]
assert literal in representation.Items
assert literal.Literal == "Foo"
class TestDisableEditingText(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
props = tool.Drawing.get_text_props(obj)
props.is_editing = True
subject.disable_editing_text(obj)
# Props might get invalidated, since no properties set.
props = tool.Drawing.get_text_props(obj)
assert props.is_editing == False
class TestDisableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
props = subject.get_object_assigned_product_props(obj)
props.is_editing_product = True
subject.disable_editing_assigned_product(obj)
assert props.is_editing_product == False
class TestEnableEditing(NewFile):
def test_run(self):
assert bpy.context.scene
obj = bpy.data.objects.new("Object", None)
bpy.context.scene.collection.objects.link(obj)
subject.enable_editing(obj)
assert obj in bpy.context.selected_objects
class TestEnableEditingDrawings(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_drawings = False
subject.enable_editing_drawings()
assert props.is_editing_drawings == True
class TestEnableEditingSchedules(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_schedules = False
subject.enable_editing_schedules()
assert props.is_editing_schedules == True
class TestEnableEditingReferences(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_references = False
subject.enable_editing_references()
assert props.is_editing_references == True
class TestEnableEditingSheets(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.is_editing_sheets = False
subject.enable_editing_sheets()
assert props.is_editing_sheets == True
class TestEnableEditingText(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
subject.enable_editing_text(obj)
props = tool.Drawing.get_text_props(obj)
assert props.is_editing == True
class TestEnableEditingAssignedProduct(NewFile):
def test_run(self):
obj = bpy.data.objects.new("Object", None)
props = subject.get_object_assigned_product_props(obj)
subject.enable_editing_assigned_product(obj)
assert props.is_editing_product == True
class TestEnsureUniqueDrawingName(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR"
ifc.createIfcAnnotation(Name="FOOBAR", ObjectType="DRAWING")
assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR-X"
ifc.createIfcAnnotation(Name="FOOBAR-X", ObjectType="DRAWING")
assert subject.ensure_unique_drawing_name("FOOBAR") == "FOOBAR-X-X"
class TestEnsureUniqueIdentification(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR"
ifc.createIfcDocumentInformation(Identification="FOOBAR", Scope="SHEET")
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR-X"
ifc.createIfcDocumentInformation(Identification="FOOBAR-X", Scope="SHEET")
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR-X-X"
def test_unique_document_id_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR"
ifc.createIfcDocumentInformation(DocumentId="FOOBAR", Scope="SHEET")
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR-X"
ifc.createIfcDocumentInformation(DocumentId="FOOBAR-X", Scope="SHEET")
assert subject.ensure_unique_identification("FOOBAR") == "FOOBAR-X-X"
class TestExportTextLiteralAttributes(NewFile):
def test_run(self):
TestImportTextAttributes().test_run()
assert subject.export_text_literal_attributes(bpy.data.objects["Object"]) == [
{
"Literal": "Literal",
"Path": "RIGHT",
"BoxAlignment": "bottom-left",
}
]
class TestGetAnnotationContext(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
context = ifc.createIfcGeometricRepresentationSubContext(
ContextType="Plan", ContextIdentifier="Annotation", TargetView="PLAN_VIEW"
)
context2 = ifc.createIfcGeometricRepresentationSubContext(
ContextType="Model", ContextIdentifier="Annotation", TargetView="ELEVATION_VIEW"
)
context3 = ifc.createIfcGeometricRepresentationSubContext(
ContextType="Model", ContextIdentifier="Annotation", TargetView="PLAN_VIEW"
)
tool.Ifc.set(ifc)
assert subject.get_annotation_context("PLAN_VIEW") == context
assert subject.get_annotation_context("ELEVATION_VIEW") == context2
assert subject.get_annotation_context("PLAN_VIEW", "FALL") == context3
class TestGetBodyContext(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
context = ifc.createIfcGeometricRepresentationSubContext(
ContextType="Model", ContextIdentifier="Body", TargetView="MODEL_VIEW"
)
tool.Ifc.set(ifc)
assert subject.get_body_context() == context
class TestGetDocumentUri(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation(
Identification="X", Name="FOOBAR", Scope="SHEET", Location="Location"
)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
def test_get_indirect_locations(self):
ifc = ifcopenshell.file()
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET")
reference = ifc.createIfcDocumentReference(Location="Location", ReferencedDocument=document)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
reference = ifc.createIfcDocumentReference(Location="Location")
document = ifc.createIfcDocumentInformation(
DocumentId="X", Name="FOOBAR", Scope="SHEET", DocumentReferences=[reference]
)
assert subject.get_document_uri(document) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
assert subject.get_document_uri(reference) == os.path.abspath(os.path.join(tool.Ifc.get_path(), "Location"))
class TestGetDrawingCollection(NewFile):
def test_run(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
collection = bpy.data.collections.new("Collection")
bpy.context.scene.collection.children.link(collection)
collection.objects.link(obj)
props = tool.Blender.get_object_bim_props(obj)
props.collection = collection
collection.BIMCollectionProperties.obj = obj
element = ifc.createIfcAnnotation()
tool.Ifc.link(element, obj)
assert subject.get_drawing_collection(element) == collection
class TestGetDrawingGroup(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.group.add_group(ifc)
group.ObjectType = "DRAWING"
ifcopenshell.api.group.assign_group(ifc, products=[element], group=group)
assert subject.get_drawing_group(element) == group
class TestGetDrawingTargetView(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
pset = ifcopenshell.api.pset.add_pset(ifc, product=element, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"})
assert subject.get_drawing_target_view(element) == "PLAN_VIEW"
class TestGetGroupElements(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
element = ifc.createIfcAnnotation()
group = ifcopenshell.api.group.add_group(ifc)
ifcopenshell.api.group.assign_group(ifc, products=[element], group=group)
assert subject.get_group_elements(group) == (element,)
class TestGetIfcRepresentationClass(NewFile):
def test_run(self):
assert subject.get_ifc_representation_class("TEXT") == "IfcTextLiteral"
assert subject.get_ifc_representation_class("TEXT_LEADER") == "IfcGeometricCurveSet/IfcTextLiteral"
assert subject.get_ifc_representation_class("FOOBAR") == ""
class TestGetName(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
assert subject.get_name(ifc.createIfcWall(Name="Foobar")) == "Foobar"
class TestGenerateDrawingMatrix(NewFile):
def test_returning_the_origin_as_a_fallback(self):
assert subject.generate_drawing_matrix("PLAN_VIEW", 0) == mathutils.Matrix()
def test_creating_a_plan_view_at_the_cursor_at_a_storey(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcBuildingStorey()
tool.Ifc.link(element, obj)
bpy.context.scene.collection.objects.link(obj)
obj.matrix_world[2][3] = 3
bpy.context.scene.cursor.location = (1.0, 2.0, 0.0)
m = subject.generate_drawing_matrix("PLAN_VIEW", element.id())
assert round(m[0][3], 3) == 1
assert round(m[1][3], 3) == 2
assert round(m[2][3], 3) == 4.6
def test_creating_an_rcp_at_the_origin(self):
assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", 0) == mathutils.Matrix(
((1, 0, 0, 0), (0, 1, 0, 0), (0, 0, -1, 0), (0, 0, 0, 1))
)
def test_creating_an_rcp_at_the_cursor_at_a_storey(self):
assert bpy.context.scene
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcBuildingStorey()
tool.Ifc.link(element, obj)
bpy.context.scene.collection.objects.link(obj)
obj.matrix_world[2][3] = 3
bpy.context.scene.cursor.location = (1.0, 2.0, 0.0)
assert subject.generate_drawing_matrix("REFLECTED_PLAN_VIEW", element.id()) == mathutils.Matrix(
((1, 0, 0, 1), (0, 1, 0, 2), (0, 0, -1, 3 + 1.6), (0, 0, 0, 1))
)
def test_creating_a_north_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "NORTH") == mathutils.Matrix(
((-1, 0, 0, 1), (0, 0, 1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_south_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "SOUTH") == mathutils.Matrix(
((1, 0, 0, 1), (0, 0, -1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_an_east_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "EAST") == mathutils.Matrix(
((0, 0, 1, 1), (1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_west_elevation_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("ELEVATION_VIEW", "WEST") == mathutils.Matrix(
((0, 0, -1, 1), (-1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_north_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "NORTH") == mathutils.Matrix(
((1, 0, 0, 1), (0, 0, -1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_south_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "SOUTH") == mathutils.Matrix(
((-1, 0, 0, 1), (0, 0, 1, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_an_east_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "EAST") == mathutils.Matrix(
((0, 0, -1, 1), (-1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_creating_a_west_section_at_the_cursor(self):
assert bpy.context.scene
bpy.context.scene.cursor.location = (1.0, 2.0, 3.0)
assert subject.generate_drawing_matrix("SECTION_VIEW", "WEST") == mathutils.Matrix(
((0, 0, 1, 1), (1, 0, 0, 2), (0, 1, 0, 3), (0, 0, 0, 1))
)
def test_aligning_model_view_camera_to_viewport(self):
assert (space := tool.Blender.get_view3d_space())
assert (r3d := space.region_3d)
viewport = r3d.view_matrix.inverted()
generated = subject.generate_drawing_matrix("MODEL_VIEW", "PERSPECTIVE")
assert np.allclose(generated, viewport)
generated = subject.generate_drawing_matrix("MODEL_VIEW", "ORTHOGRAPHIC")
assert np.allclose(generated, viewport)
class TestGenerateSheetIdentification(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
assert subject.generate_sheet_identification() == "A01"
document = ifc.createIfcDocumentInformation(Scope="SHEET")
assert subject.generate_sheet_identification() == "A02"
class TestGetTextLiteral(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcAnnotation()
element.Representation = ifc.createIfcProductDefinitionShape()
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
element.ObjectType = "TEXT" # TODO: double check if it's valid to set this
tool.Ifc.link(element, obj)
assert subject.get_text_literal(obj) == item
class TestGetAssignedProduct(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
label = ifc.createIfcAnnotation()
ifcopenshell.api.drawing.assign_product(ifc, relating_product=wall, related_object=label)
assert subject.get_assigned_product(label) == wall
class TestImportDrawings(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
drawing = ifc.createIfcAnnotation(Name="FOOBAR", ObjectType="DRAWING")
pset = ifcopenshell.api.pset.add_pset(ifc, product=drawing, name="EPset_Drawing")
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"TargetView": "PLAN_VIEW"})
subject.import_drawings()
props = tool.Drawing.get_document_props()
for d in props.drawings:
d.is_expanded = True
subject.import_drawings()
assert props.drawings[1].target_view == "PLAN_VIEW"
assert props.drawings[2].ifc_definition_id == drawing.id()
assert props.drawings[2].name == "FOOBAR"
class TestImportSchedules(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_documents("SCHEDULE")
props = tool.Drawing.get_document_props()
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SCHEDULE")
subject.import_documents("SCHEDULE")
props = tool.Drawing.get_document_props()
assert props.schedules[0].ifc_definition_id == document.id()
assert props.schedules[0].identification == "X"
assert props.schedules[0].name == "FOOBAR"
class TestImportReferences(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="REFERENCE")
subject.import_documents("REFERENCE")
props = tool.Drawing.get_document_props()
assert props.references[0].ifc_definition_id == document.id()
assert props.references[0].identification == "X"
assert props.references[0].name == "FOOBAR"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="REFERENCE")
subject.import_documents("REFERENCE")
props = tool.Drawing.get_document_props()
assert props.references[0].ifc_definition_id == document.id()
assert props.references[0].identification == "X"
assert props.references[0].name == "FOOBAR"
class TestImportSheets(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(Identification="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(Identification="X", Name="FOOBAR", Scope="SHEET")
subject.import_sheets()
props = tool.Drawing.get_document_props()
assert props.sheets[0].ifc_definition_id == document.id()
assert props.sheets[0].identification == "X"
assert props.sheets[0].name == "FOOBAR"
def test_run_ifc2x3(self):
ifc = ifcopenshell.file(schema="IFC2X3")
tool.Ifc.set(ifc)
ifc.createIfcDocumentInformation(DocumentId="Y", Name="FOOBAZ")
document = ifc.createIfcDocumentInformation(DocumentId="X", Name="FOOBAR", Scope="SHEET")
subject.import_sheets()
props = tool.Drawing.get_document_props()
assert props.sheets[0].ifc_definition_id == document.id()
assert props.sheets[0].identification == "X"
assert props.sheets[0].name == "FOOBAR"
class TestImportTextAttributes(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
obj = bpy.data.objects.new("Object", None)
element = ifc.createIfcAnnotation()
element.Representation = ifc.createIfcProductDefinitionShape()
context = ifc.createIfcGeometricRepresentationSubContext(ContextType="Plan", ContextIdentifier="Annotation")
item = ifc.createIfcTextLiteralWithExtent(Literal="Literal", Path="RIGHT", BoxAlignment="bottom-left")
representation = ifc.createIfcShapeRepresentation(ContextOfItems=context, Items=[item])
element.Representation.Representations = [representation]
element.ObjectType = "TEXT"
tool.Ifc.link(element, obj)
subject.import_text_attributes(obj)
props = tool.Drawing.get_text_props(obj)
assert props.font_size == "2.5"
literal_props = props.literals[0]
assert literal_props.ifc_definition_id == item.id()
assert literal_props.attributes["Literal"].string_value == "Literal"
assert literal_props.attributes["Path"].enum_value == "RIGHT"
assert literal_props.attributes["BoxAlignment"].string_value == "bottom-left"
assert literal_props.align_vertical == "bottom"
assert literal_props.align_horizontal == "left"
assert props.align_vertical == "bottom"
assert props.align_horizontal == "left"
class TestReplaceTextLiteralVariables(NewFile):
def test_using_attribute_variables(self):
ifc = ifcopenshell.file()
wall = ifc.create_entity("IfcWall", Name="Baz")
text = "Foo {{Name}} Bar"
updated_text = subject.replace_text_literal_variables(text, wall)
assert updated_text == "Foo Baz Bar"
def test_using_property_variables(self):
ifc = ifcopenshell.file()
wall = ifc.createIfcWall()
pset = ifcopenshell.api.pset.add_pset(ifc, name="Custom_Pset", product=wall)
ifcopenshell.api.pset.edit_pset(ifc, pset=pset, properties={"Key": "Baz"})
text = "Foo {{Custom_Pset.Key}} Bar"
updated_text = subject.replace_text_literal_variables(text, wall)
assert updated_text == "Foo Baz Bar"
class TestEditText(NewFile):
def test_change_text_font_size(self):
TestGetTextLiteral().test_run()
obj = bpy.data.objects["Object"]
assert DecoratorData.get_text_data(obj)["FontSize"] == 2.5
with bpy.context.temp_override(active_object=obj):
bpy.ops.bim.enable_editing_text()
props = tool.Drawing.get_text_props(obj)
props.font_size = "7.0"
bpy.ops.bim.edit_text()
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
assert "title" in annotation_classes
assert DecoratorData.get_text_data(obj)["FontSize"] == 7.0
def test_add_second_literal(self, setup=True):
if setup:
TestGetTextLiteral().test_run()
obj = bpy.data.objects["Object"]
with bpy.context.temp_override(active_object=obj):
bpy.ops.bim.enable_editing_text()
bpy.ops.bim.add_text_literal()
props = tool.Drawing.get_text_props(obj)
literal = props.literals[1]
literal.attributes["Literal"].string_value = "test_value"
bpy.ops.bim.edit_text()
ifc = tool.Ifc.get()
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
class TestDisableTextEditing(NewFile):
def test_disable_text_editing(self):
# add second literal and change font size to test disable editing keeps those changes.
TestEditText.test_change_text_font_size(self) # Set font size to "7.0".
TestEditText.test_add_second_literal(self, setup=False)
obj = bpy.data.objects["Object"]
props = tool.Drawing.get_text_props(obj)
assert obj is not None, obj
with bpy.context.temp_override(active_object=obj):
bpy.ops.bim.enable_editing_text()
bpy.ops.bim.remove_text_literal(literal_prop_id=1)
props.literals[0].attributes["Literal"].string_value = "changed_value"
props.font_size = "2.5"
bpy.ops.bim.disable_editing_text()
ifc = tool.Ifc.get()
# Test font size.
annotation_classes = ifcopenshell.util.element.get_pset(tool.Ifc.get_entity(obj), "EPset_Annotation", "Classes")
assert "title" in annotation_classes
text_data = DecoratorData.get_text_data(obj)
assert text_data["FontSize"] == 7.0
# Test second literal is present.
assert text_data["Literals"][1]["Literal"] == "test_value"
assert ifc.by_type("IfcTextLiteralWithExtent")[1].Literal == "test_value"
# Test first literal value is unchanged.
assert text_data["Literals"][0]["Literal"] == "Literal"
assert ifc.by_type("IfcTextLiteralWithExtent")[0].Literal == "Literal"
class TestImportAssignedProduct(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
wall = ifc.createIfcWall()
label = ifc.createIfcAnnotation()
ifcopenshell.api.drawing.assign_product(ifc, relating_product=wall, related_object=label)
wall_obj = bpy.data.objects.new("Object", None)
label_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(wall, wall_obj)
tool.Ifc.link(label, label_obj)
subject.import_assigned_product(label_obj)
props = subject.get_object_assigned_product_props(label_obj)
assert props.relating_product == wall_obj
def test_doing_nothing_if_no_product_to_import(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
label = ifc.createIfcAnnotation()
label_obj = bpy.data.objects.new("Object", None)
tool.Ifc.link(label, label_obj)
subject.import_assigned_product(label_obj)
props = subject.get_object_assigned_product_props(label_obj)
assert props.relating_product is None
class TestOpenSchedule(NewFile):
def open_spreadsheet(self):
pass
class TestOpenReference(NewFile):
def open_svg(self):
pass
class TestOpenSvg(NewFile):
def test_nothing(self):
pass
class TestRunRootAssignClassOperator(NewFile):
def test_nothing(self):
pass
class TestSetDrawingCollectionName(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
group = ifc.createIfcGroup(Name="Foobaz")
collection = bpy.data.collections.new("Foobar")
subject.set_drawing_collection_name(group, collection)
assert collection.name == "IfcGroup/Foobaz"
class TestSetName(NewFile):
def test_run(self):
ifc = ifcopenshell.file()
drawing = ifc.createIfcAnnotation()
subject.set_name(drawing, "Name")
assert drawing.Name == "Name"
class TestShowDecorations(NewFile):
def test_run(self):
props = tool.Drawing.get_document_props()
props.should_draw_decorations = False
subject.show_decorations()
assert props.should_draw_decorations is True
class TestDrawingMaintainingSheetPosition(NewFile):
def get_sheet_drawing_data(self, layout_path: Path) -> dict[str, tuple[float, ...]]:
SVG = "{http://www.w3.org/2000/svg}"
ET.register_namespace("", SVG)
layout_tree = ET.parse(layout_path)
layout_root = layout_tree.getroot()
drawing_view = layout_root.findall(f'{SVG}g[@data-type="drawing"]')[0]
drawing_data: dict[str, tuple[float, ...]] = {}
for image in drawing_view.findall(f"{SVG}image"):
attribs = ["x", "y", "width", "height"]
image_type = image.attrib["data-type"]
drawing_data[image_type] = tuple([round(float(image.attrib[attr]), 2) for attr in attribs])
return drawing_data
def test_run(self):
props = tool.Drawing.get_document_props()
bpy.ops.bim.create_project()
tool.Project.save_test_project()
ifc = tool.Ifc.get()
sheet_path = Path(tool.Ifc.get_path()).parent / "layouts" / "A01 - UNTITLED.svg"
bpy.ops.mesh.primitive_cube_add(size=10, location=(0, 0, 4))
obj = bpy.data.objects["Cube"]
bpy.ops.bim.assign_class(ifc_class="IfcActuator", predefined_type="ELECTRICACTUATOR", userdefined_type="")
bpy.ops.bim.load_sheets()
bpy.ops.bim.add_sheet()
bpy.ops.bim.load_drawings()
for d in props.drawings:
d.is_expanded = True
bpy.ops.bim.add_drawing()
drawing = ifc.by_type("IfcAnnotation")[0]
for i, d in enumerate(props.drawings):
if d.ifc_definition_id == drawing.id():
props.active_drawing_index = i
bpy.ops.bim.activate_drawing(drawing=drawing.id())
bpy.ops.bim.create_drawing()
bpy.ops.bim.add_drawing_to_sheet()
bpy.ops.bim.open_sheet()
# uncomment if debugging
# without `sleep` `bim.open_sheet` tend to open sheet in browser
# with PLAN_VIEW.svg that will be created only later
# from time import sleep
# sleep(0.1)
# check drawing default position
drawing_data = self.get_sheet_drawing_data(sheet_path)
assert drawing_data["foreground"] == (30.0, 30.0, 500.0, 500.0)
assert drawing_data["view-title"] == (30.0, 535.0, 50.22, 10.0)
assert (scene := bpy.context.scene) and (camera := scene.camera)
props = tool.Drawing.get_camera_props(camera)
props.width = 25
props.height = 25
tool.Blender.force_depsgraph_update()
bpy.ops.bim.create_drawing()
bpy.ops.bim.open_sheet()
assert sheet_path.is_file(), f"Sheet path {sheet_path} doesn't exist"
# check drawing position on the sheet
drawing_data = self.get_sheet_drawing_data(sheet_path)
assert drawing_data["foreground"] == (30.0, 30.0, 500.0, 500.0)
assert drawing_data["view-title"] == (30.0, 535.0, 50.22, 10.0)
class TestDrawingStyles(NewFile):
def setup_project_with_drawing(self):
bpy.ops.bim.create_project()
tool.Project.save_test_project()
bpy.ops.bim.load_drawings()
bpy.ops.bim.add_drawing()
ifc = tool.Ifc.get()
drawing = ifc.by_type("IfcAnnotation")[0]
bpy.ops.bim.toggle_target_view(option="EXPAND", target_view="PLAN_VIEW")
props = tool.Drawing.get_document_props()
props.active_drawing_index = 2
bpy.ops.bim.activate_drawing(drawing=drawing.id())
props = tool.Drawing.get_document_props()
self.drawing_styles = props.drawing_styles
def test_drawing_styles_are_loaded_even_if_underlay_is_inactive(self):
self.setup_project_with_drawing()
assert len(self.drawing_styles) == 3
def test_drawing_styles_reload(self):
self.setup_project_with_drawing()
self.drawing_styles.clear()
bpy.ops.bim.reload_drawing_styles()
assert len(self.drawing_styles) == 3
class TestAddReferenceImage(NewFile):
def test_run(self):
props = tool.Project.get_project_props()
props.template_file = "0"
bpy.ops.bim.create_project()
ifc_path = Path("test/files/temp/test.ifc").absolute()
bpy.ops.bim.save_project(filepath=str(ifc_path), should_save_as=True)
filepath = Path("test/files/image.jpg").absolute()
bpy.ops.bim.add_reference_image(filepath=str(filepath), x_length=3.53982, y_length=2.0)
obj = bpy.data.objects["IfcAnnotation/image"]
assert obj is not None
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
material = obj.active_material
assert material
assert material.name == "image"
assert tool.Blender.get_ifc_definition_id(material) != 0
style = tool.Ifc.get_entity(material)
assert style
styled_items = set(tool.Style.get_styled_items(style))
representation_items = set(tool.Geometry.get_active_representation(obj).Items)
assert styled_items == representation_items
material_nodes = material.node_tree.nodes
texture_filepath = material_nodes["Image Texture"].image.filepath
texture_filepath = Path(tool.Blender.blender_path_to_posix(texture_filepath))
assert texture_filepath == filepath
uv_node = material_nodes["Texture Coordinate"]
assert len(uv_node.outputs["Generated"].links[:]) == 1
class TestIsDrawingActive(NewFile):
def test_no_active_camera(self):
bpy.context.scene.camera = None
assert subject.is_drawing_active() is False
def test_active_camera_without_ifc_definition(self):
bpy.context.scene.camera = subject.create_camera("Camera", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
assert subject.is_drawing_active() is False
def test_ifc_linked_camera_in_background_mode(self):
ifc = ifcopenshell.file()
tool.Ifc.set(ifc)
camera_obj = subject.create_camera("Camera", mathutils.Matrix(), "PERSPECTIVE", "PLAN_VIEW")
drawing = ifc.createIfcAnnotation(ObjectType="DRAWING")
tool.Ifc.link(drawing, camera_obj)
bpy.context.scene.camera = camera_obj
# The test suite itself runs Blender in background mode, where no
# VIEW_3D area can ever exist -- this is exactly the case the fix
# addresses, so this assertion documents that assumption.
assert bpy.app.background is True
assert subject.is_drawing_active() is True