Compare commits

...

14 Commits

Author SHA1 Message Date
Ryan Schultz 85ae25cd3c Import BoxAlignment into alignment props on edit enable
When enabling text editing, align_vertical and
align_horizontal were not populated from the existing
IFC BoxAlignment value, causing them to revert to
their Blender property defaults.

Generated with the assistance of an AI coding tool.
2026-02-24 11:04:14 -06:00
Ryan Schultz 742debb8f1 Fix #7712: Fix text annotation alignment not saved on edit
edit_text was missing a call to edit_text_alignment,
so align_vertical/align_horizontal prop changes were
never exported to IFC.

Additionally, edit_text_alignment was called before
edit_text_literals, which deletes and recreates the
IFC literal entities — discarding the alignment change
immediately. Moved the call to after edit_text_literals.

EditText operator was also being replayed by Blender's
undo/register stack with stale prop values, overwriting
the correct alignment. Guarded _execute with is_editing
to make replayed calls no-ops.

Also fixed a ValueError in is_camera_moved where
np_frombuffer_legacy returns a flat (9,) array but the
rotation dot product requires (3,3). Fixed with reshape.

Generated with the assistance of an AI coding tool.
2026-02-24 07:51:32 -06:00
Dion Moult dcc25038f9 Fix #7646. Bug with layer thumbnail orientation. 2026-02-24 10:03:33 +11:00
Dion Moult 0d382119dd Fix docs table for selector syntax 2026-02-24 09:53:57 +11:00
Dion Moult 41afaaec0d Revert "Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710)"
This reverts commit 7d8c7a2c3d.
2026-02-24 09:53:28 +11:00
Dion Moult f69ea82789 Revert "Fix #7646: Fix layer thumbnail orientation for IFC types"
This reverts commit 514cbb49cc.
2026-02-24 09:53:27 +11:00
Dion Moult 2f5c71588e Revert "Table was not rendering correctly."
This reverts commit 9ff4a7f0e0.
2026-02-24 09:53:24 +11:00
Ryan Schultz 7d8c7a2c3d Fix #7681: Fix isolate_objects ignoring hide_select/hide_viewport (#7710)
Objects with hide_select=True could not be selected during
isolation, causing hide_view_set to incorrectly hide them.
Objects with hide_viewport=True had their H-key hide state
modified as a side effect of hide_view_clear/hide_view_set.
Both are now left unaffected by bim.activate_drawing.

Generated with the assistance of an AI coding tool.
2026-02-23 07:09:18 -06:00
Ryan Schultz 514cbb49cc Fix #7646: Fix layer thumbnail orientation for IFC types
Use EPset_Parametric.LayerSetDirection exclusively to
determine horizontal vs vertical layer rendering in type
thumbnails, rather than hardcoding IfcSlabType checks.
Also fix line drawing to use the is_horizontal flag
consistently.

Generated with the assistance of an AI coding tool.
2026-02-22 17:46:10 -06:00
Ryan Schultz 9ff4a7f0e0 Table was not rendering correctly.
Fix Sphinx docs: replace csv-table with list-table for formatting functions

The documentation table of formatting/query functions was not rendering
because `.. csv-table::` requires strict RFC4180 CSV escaping. The table
contains nested quotes, inch marks (e.g. `3' - 0"`), backticks, and code
examples, which cause the CSV parser in docutils to treat rows as malformed
and drop the entire directive.

Replaced the directive with `.. list-table::`, which parses reStructuredText
instead of CSV and safely supports inline code, quotes, and multi-line cells.

Also moved the examples text outside the directive block and ensured a blank
line after the table so Sphinx does not interpret following paragraphs as
table rows.

No content changes — documentation now renders correctly.

Generated with the assistance of an AI coding tool.
2026-02-22 15:10:29 -06:00
dependabot[bot] 8afe05601e Bump tar from 7.5.7 to 7.5.9 in /src/ifctester/webapp
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 08:56:50 +11:00
falken10vdl 1751c36c67 AddReferenceImage: implement option to show texture in solid mode (#7689) 2026-02-19 21:02:57 +11:00
falken10vdl d4388ec76d AddReferenceImage: fix regression with IFC2X3 support, refactor to no longer depend on add_representation or update_representation, remove legacy style updating functionality
* Enhance AddReferenceImage operator to use file browser instead of independent popup dialogue

* Fix dimensions assertion in TestAddReferenceImage

* Remove error in return in _execute (it is not execute)

* Add  IFC2X3 support to AddReferenceImage

* Adde unit="LENGTH" to the x/y properties (every length dimension everywhere in the UI is in project length units. No need to say it explicitly)

* Manually create the texture always, not just for IFC2X3

* Add poll method to AddReferenceImage operator to check for loaded IFC project

* Refactor AddReferenceImage to add representation manually following pattern in root/operator.py's bim.add_element

* Improve File explorer options between new and select from existing project Ifc Reference Images

* Refactor get_existing_reference_images to use selector for filtering image annotations

* No extra args needed after should_add_representation is False

* Doing clean=True deletes everything

* Don't manually add geometry and materials, don't call bpy.ops. Only create IFC data, then use preexisting loading functions to create geometry.

* Black formatting, also now we can start to remove this operator as it becomes obsolete

* Consolidate duplicate UV generation into Loader.load_generated_uv_map

Replace 3 identical XY-UV baking blocks (create_object IMAGE,
bm_add_image_plane, ImageScalingTool) with a single reusable
classmethod in tool.Loader.

* Fix IFC4 texture display in Solid viewport Texture mode

IFC4 IfcTextureCoordinateGenerator Mode=COORD is used, load_texture_maps
falls back to load_generated_uv_map to bake XY-UV data onto the mesh.

* Fix IFC2X3 texture display

* This looks wrong

* Remove legacy override image feature, because we now have a proper styles and texture manager

* Remove legacy override existing image element, because we now have a dedicated styles texture manager

* Remove unnecessary roundtrip to bmesh and mesh

---------

Co-authored-by: Dion Moult <dion@thinkmoult.com>
2026-02-19 11:56:10 +11:00
falken10vdl 7141f2cf90 Fixes to PR7607 (Linked IFC Projects): Wireframe toggle. More permisive to get has_transformation = False. Show enable_editing_link if link is loaded 2026-02-19 10:53:01 +11:00
13 changed files with 201 additions and 227 deletions
+5 -44
View File
@@ -85,7 +85,7 @@ class MaterialCreator:
if element.is_a("IfcTypeProduct"):
self.parse_element_type_material_styles(element)
self.parsed_meshes.add(self.mesh.name)
if not self.ifc_import_settings.load_indexed_maps:
if self.ifc_import_settings.load_indexed_maps:
self.load_texture_maps(shape_has_openings)
self.assign_material_slots_to_faces()
tool.Geometry.record_object_materials(obj)
@@ -117,7 +117,6 @@ class MaterialCreator:
for texture in texture_style.Textures or []:
if coords := getattr(texture, "IsMappedBy", None):
coords = coords[0]
# IfcTextureCoordinateGenerator handled in the style shader graph
if coords.is_a("IfcIndexedTextureMap"):
return coords
# TODO: support IfcTextureMap
@@ -135,6 +134,10 @@ class MaterialCreator:
if shape_has_openings and coords.is_a("IfcIndexedTextureMap"):
continue
tool.Loader.load_indexed_map(coords, self.mesh)
elif tool.Style.get_texture_style(material):
# No explicit coordinate mapping (e.g. IFC2X3 has no IsMappedBy,
# and IFC4 COORD uses generated UVs). Bake XY→UV as fallback.
tool.Loader.load_generated_uv_map(self.mesh)
def assign_material_slots_to_faces(self) -> None:
if not self.mesh["ios_materials"]:
@@ -892,48 +895,6 @@ class IfcImporter:
obj, tool.Loader.apply_blender_offset_to_matrix_world(obj, self.get_element_matrix(element))
)
if element.is_a("IfcAnnotation") and getattr(element, "ObjectType", None) == "IMAGE":
image = None
if obj.data and obj.data.materials and obj.data.materials[0]:
material = obj.data.materials[0]
if material.use_nodes and material.node_tree:
for node in material.node_tree.nodes:
if node.type == "TEX_IMAGE" and node.image:
image = node.image
break
if image:
import bmesh
bm = bmesh.new()
bm.from_mesh(obj.data)
if not bm.loops.layers.uv:
uv_layer = bm.loops.layers.uv.new()
else:
uv_layer = bm.loops.layers.uv.active
if bm.verts:
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
bm.to_mesh(obj.data)
bm.free()
obj.data.update()
return obj
def load_existing_meshes(self) -> None:
@@ -1788,7 +1788,7 @@ class CutDecorator:
# Handle both old float64 and new float32 checksums for version compatibility
rot_checksum_bytes: bytes = eval(DecoratorData.camera_rotation_checksum)
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9)
rot_check = tool.Blender.np_frombuffer_legacy(rot_checksum_bytes, 9).reshape(3, 3)
rot_real = tool.Blender.np_array_legacy(obj.matrix_world.to_3x3())
rot_dot = np.dot(rot_check, rot_real.T)
angle_rad = np.arccos(np.clip((np.trace(rot_dot) - 1) / 2, -1, 1))
+121 -124
View File
@@ -40,6 +40,7 @@ from typing import (
import bmesh
import bpy
import logging
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.document
@@ -50,6 +51,7 @@ import ifcopenshell.ifcopenshell_wrapper
import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.selector
import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit
import numpy as np
import shapely
@@ -59,6 +61,7 @@ from bpy_extras.io_utils import ImportHelper
from lxml import etree
from mathutils import Color, Matrix, Vector
import bonsai.bim.import_ifc
import bonsai.bim.export_ifc
import bonsai.bim.handler
import bonsai.bim.helper
@@ -138,7 +141,7 @@ class AddAnnotationType(bpy.types.Operator, tool.Ifc.Operator):
element.ApplicableOccurrence = f"IfcAnnotation/{object_type}"
if props.create_representation_for_type and object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
class EnableAddAnnotationType(bpy.types.Operator):
@@ -1759,7 +1762,7 @@ class AddAnnotation(bpy.types.Operator, tool.Ifc.Operator):
enable_editing=True,
)
if props.object_type == "IMAGE":
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", use_existing_object_by_name=obj.name)
bpy.ops.bim.add_reference_image("INVOKE_DEFAULT", existing_object_by_name=obj.name)
class AddSheet(bpy.types.Operator, tool.Ifc.Operator):
@@ -3182,7 +3185,10 @@ class EditText(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
def _execute(self, context):
core.edit_text(tool.Drawing, obj=tool.Blender.get_active_object())
obj = tool.Blender.get_active_object()
if not tool.Drawing.get_text_props(obj).is_editing:
return
core.edit_text(tool.Drawing, obj=obj)
tool.Blender.update_viewport()
@@ -3802,29 +3808,81 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=True)
filter_image: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
filter_folder: bpy.props.BoolProperty(default=True, options={"HIDDEN", "SKIP_SAVE"})
x_length: bpy.props.FloatProperty(
name="X Length",
description="Width of the reference image",
default=1.0,
min=0.001,
soft_min=0.01,
precision=3,
unit="LENGTH",
)
y_length: bpy.props.FloatProperty(
name="Y Length",
description="Height of the reference image",
default=1.0,
min=0.001,
soft_min=0.01,
precision=3,
unit="LENGTH",
)
show_texture_solid_mode: bpy.props.BoolProperty(
name="Show Texture in Solid mode (slow)",
description="Show Texture in Solid mode (slow)",
default=False,
)
override_existing_image: bpy.props.BoolProperty(
name="Override Existing Image",
default=True,
description=(
"Override image if it was previously loaded to Blender. If disabled, will always create a new image"
),
)
use_existing_object_by_name: bpy.props.StringProperty(
name="Use Existing Object By Name",
description="Existing object name to add a style with reference image to. If not provided will create a new object.",
options={"SKIP_SAVE"},
)
size: bpy.props.FloatProperty(name="Size", description="Size of the reference image", default=1.0, unit="LENGTH")
@classmethod
def poll(cls, context):
if not tool.Ifc.get():
cls.poll_message_set("No IFC project is loaded.")
return False
return True
def invoke(self, context, event):
self._last_filepath = ""
return super().invoke(context, event)
def check(self, context):
if not hasattr(self, "_last_filepath"):
self._last_filepath = ""
if self.filepath and self.filepath != self._last_filepath:
self._last_filepath = self.filepath
abs_path = Path(self.filepath).absolute().resolve()
if abs_path.exists() and abs_path.is_file():
image = load_image(abs_path.name, str(abs_path.parent), check_existing=False)
image_width_px = image.size[0]
image_height_px = image.size[1]
aspect_ratio = image_width_px / image_height_px
if aspect_ratio >= 1.0:
self.x_length = 1.0
self.y_length = 1.0 / aspect_ratio
else:
self.x_length = aspect_ratio
self.y_length = 1.0
bpy.data.images.remove(image)
return True
return False
def draw(self, context):
layout = self.layout
if Path(tool.Ifc.get_path()).is_file():
self.layout.prop(self, "use_relative_path")
self.layout.prop(self, "override_existing_image")
self.layout.prop(self, "use_existing_object_by_name")
self.layout.prop(self, "size")
layout.prop(self, "use_relative_path")
else:
self.use_relative_path = False
layout.prop(self, "show_texture_solid_mode")
layout.prop(self, "x_length")
layout.prop(self, "y_length")
def _execute(self, context):
project_props = tool.Project.get_project_props()
project_props.load_indexed_maps = self.show_texture_solid_mode
space = tool.Blender.get_view3d_space()
if space.shading.color_type != "TEXTURE":
space.shading.color_type = "TEXTURE"
@@ -3837,127 +3895,66 @@ class AddReferenceImage(bpy.types.Operator, tool.Ifc.Operator, ImportHelper):
image_filepath = Path(tool.Ifc.get_uri(self.filepath, use_relative_path=self.use_relative_path))
ifc_file = tool.Ifc.get()
if self.override_existing_image:
params = {"check_existing": True, "force_reload": True}
else:
params = {"check_existing": False}
params = {"check_existing": False}
image = load_image(abs_path.name, str(abs_path.parent), **params)
aspect_ratio = image.size[0] / image.size[1]
if aspect_ratio >= 1.0: # Landscape
x_length = self.size
y_length = self.size / aspect_ratio
else:
x_length = self.size / aspect_ratio
y_length = self.size
mesh = bpy.data.meshes.new(image_filepath.stem)
obj = bpy.data.objects.new(image_filepath.stem, mesh)
element = tool.Drawing.run_root_assign_class(
obj=obj, ifc_class="IfcAnnotation", predefined_type="IMAGE", should_add_representation=False
)
def bm_add_image_plane(mesh):
bm = tool.Blender.get_bmesh_for_mesh(mesh, clean=True)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
hx = self.x_length * 0.5 / unit_scale
hy = self.y_length * 0.5 / unit_scale
verts = [(-hx, -hy, 0.0), ( hx, -hy, 0.0), ( hx, hy, 0.0), (-hx, hy, 0.0)]
item = builder.mesh(verts, [[0, 1, 2, 3]])
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
plane_scale = Vector((x_length / 2.0, y_length / 2.0, 1.0))
matrix = Matrix.LocRotScale(None, None, plane_scale)
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, matrix=matrix, calc_uvs=False)
ifc_context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
representation = builder.get_representation(ifc_context, [item])
ifcopenshell.api.geometry.assign_representation(ifc_file, element, representation)
if not bm.loops.layers.uv:
uv_layer = bm.loops.layers.uv.new()
else:
uv_layer = bm.loops.layers.uv.active
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
tool.Blender.apply_bmesh(mesh, bm)
if self.use_existing_object_by_name:
obj = bpy.data.objects[self.use_existing_object_by_name]
bm_add_image_plane(obj.data)
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
else:
temp_mesh = bpy.data.meshes.new("temp_mesh")
bm_add_image_plane(temp_mesh)
obj = bpy.data.objects.new(image_filepath.stem, temp_mesh)
tool.Drawing.run_root_assign_class(
obj=obj,
ifc_class="IfcAnnotation",
predefined_type="IMAGE",
should_add_representation=True,
context=ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW"),
ifc_representation_class=None,
)
tool.Blender.remove_data_block(temp_mesh)
element = tool.Ifc.get_entity(obj)
if element and isinstance(obj.data, bpy.types.Mesh):
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if representation and representation.Items:
item_id = representation.Items[0].id()
num_faces = len(obj.data.polygons)
obj.data["ios_item_ids"] = [item_id] * num_faces
tool.Blender.Attribute.fill_attribute(obj.data, "ios_item_ids", "FACE", "INT", [item_id] * num_faces)
for item in representation.Items:
if item.is_a("IfcPolygonalFaceSet") and item.Coordinates:
new_coords = []
for vertex in obj.data.vertices:
co = obj.matrix_world @ vertex.co
new_coords.append([co.x, co.y, co.z])
item.Coordinates.CoordList = new_coords
tool.Blender.set_active_object(obj)
material = bpy.data.materials.new(name=image_filepath.stem)
obj.data.materials.append(None) # new slot
obj.material_slots[0].material = material
bpy.ops.bim.add_style()
style = tool.Ifc.get_entity(material)
assert style
tool.Style.assign_style_to_object(style, obj)
style = ifcopenshell.api.style.add_style(tool.Ifc.get(), name=image_filepath.stem)
ifcopenshell.api.style.assign_representation_styles(
ifc_file, shape_representation=representation, styles=[style]
)
# TODO: IfcSurfaceStyleRendering is unnecessary here, added it only because
# we don't support IfcSurfaceStyleWithTextures without Rendering yet
shading_attributes = {
"SurfaceColour": {
"Red": 1.0,
"Green": 1.0,
"Blue": 1.0,
},
"SurfaceColour": {"Red": 1.0, "Green": 1.0, "Blue": 1.0},
"Transparency": 0.0,
"ReflectanceMethod": "NOTDEFINED",
}
ifcopenshell.api.style.add_surface_style(
tool.Ifc.get(),
style=style,
ifc_class="IfcSurfaceStyleRendering",
attributes=shading_attributes,
tool.Ifc.get(), style=style, ifc_class="IfcSurfaceStyleRendering", attributes=shading_attributes
)
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
if tool.Ifc.get_schema() == "IFC2X3":
texture = ifc_file.create_entity(
"IfcImageTexture",
RepeatS=True,
RepeatT=True,
TextureType="TEXTURE",
UrlReference=image_filepath.as_posix(),
)
else:
texture = ifc_file.create_entity("IfcImageTexture", Mode="DIFFUSE", URLReference=image_filepath.as_posix())
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=[texture], Mode="COORD")
textures = [texture]
ifc_file.create_entity("IfcTextureCoordinateGenerator", Maps=textures, Mode="COORD") # UV map
ifcopenshell.api.style.add_surface_style(
ifc_file,
style=style,
ifc_class="IfcSurfaceStyleWithTextures",
attributes={"Textures": textures},
ifc_file, style=style, ifc_class="IfcSurfaceStyleWithTextures", attributes={"Textures": textures}
)
tool.Style.reload_material_from_ifc(material)
tool.Geometry.record_object_materials(obj)
logger = logging.getLogger("ImportIFC")
ifc_import_settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
ifc_importer = bonsai.bim.import_ifc.IfcImporter(ifc_import_settings)
ifc_importer.file = tool.Ifc.get()
ifc_importer.create_style(style)
bonsai.core.geometry.switch_representation(tool.Ifc, tool.Geometry, obj=obj, representation=representation)
class ConvertSVGToDXF(bpy.types.Operator):
@@ -1664,12 +1664,16 @@ class ToggleLinkVisibility(bpy.types.Operator):
return {"FINISHED"}
def toggle_wireframe(self, link: "Link") -> None:
linked_collections = self.get_linked_collections()
link.is_wireframe = not link.is_wireframe
display_type = "WIRE" if link.is_wireframe else "TEXTURED"
for collection in self.get_linked_collections():
for collection in linked_collections:
objs = filter(lambda obj: "IfcOpeningElement" not in obj.name, collection.all_objects)
for obj in objs:
obj.display_type = display_type
if handle := tool.Project.get_link_empty_handle(link):
handle.display_type = display_type
def toggle_visibility(self, link: "Link") -> None:
linked_collections = self.get_linked_collections()
@@ -1746,15 +1750,14 @@ class EditLink(bpy.types.Operator, tool.Ifc.Operator):
# obj_matrix is typically calculated as:
# obj_matrix = np.linalg.inv(local_matrix) @ transformation @ global_matrix
# So let's calculate the transformation
transformed_global_matrix = local_matrix @ np.array(new_obj_matrix)
transformation = transformed_global_matrix @ np.linalg.inv(global_matrix)
if np.allclose(transformation, np.eye(4)):
link.has_transformation = True
identity_blender_matrix = np.linalg.inv(local_matrix) @ global_matrix
if np.allclose(np.array(new_obj_matrix), identity_blender_matrix, atol=1e-5):
link.has_transformation = False
transformation = ",".join(map(str, np.eye(4).reshape(-1)))
else:
link.has_transformation = False
transformed_global_matrix = local_matrix @ np.array(new_obj_matrix)
transformation = transformed_global_matrix @ np.linalg.inv(global_matrix)
link.has_transformation = True
transformation = ",".join(map(str, transformation.reshape(-1)))
if tool.Ifc.get():
@@ -3245,29 +3248,9 @@ class ImageScalingTool(bpy.types.Operator, PolylineOperator):
bmesh.ops.scale(bm, vec=(scale_factor, scale_factor, 1.0), verts=bm.verts)
if bm.loops.layers.uv:
uv_layer = bm.loops.layers.uv.active
min_x = min(v.co.x for v in bm.verts)
max_x = max(v.co.x for v in bm.verts)
min_y = min(v.co.y for v in bm.verts)
max_y = max(v.co.y for v in bm.verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
vert = loop.vert
u = (vert.co.x - min_x) / width if width > 0 else 0.5
v = (vert.co.y - min_y) / height if height > 0 else 0.5
u = max(0.0, min(1.0, u))
v = max(0.0, min(1.0, v))
loop[uv_layer].uv = (u, v)
bm.to_mesh(mesh)
bm.free()
tool.Loader.load_generated_uv_map(mesh)
mesh.update()
element = tool.Ifc.get_entity(self.target_object)
+5 -5
View File
@@ -487,12 +487,12 @@ class BIM_PT_links(Panel):
row = self.layout.row(align=True)
row.alignment = "RIGHT"
index = self.props.active_link_index
if self.props.active_link.is_editing:
row.operator("bim.edit_link", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_link", text="", icon="CANCEL")
else:
row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL")
if self.props.active_link.is_loaded:
if self.props.active_link.is_editing:
row.operator("bim.edit_link", text="", icon="CHECKMARK")
row.operator("bim.disable_editing_link", text="", icon="CANCEL")
else:
row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL")
row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index
row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
@@ -87,6 +87,7 @@ class RemoveStyle(bpy.types.Operator, tool.Ifc.Operator):
core.remove_style(tool.Ifc, tool.Style, style=tool.Ifc.get().by_id(self.style), reload_styles_ui=True)
# TODO: remove completely
class AddStyle(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_style"
bl_label = "Add Style"
+1
View File
@@ -44,6 +44,7 @@ def edit_text(drawing: type[tool.Drawing], obj: bpy.types.Object) -> None:
drawing.edit_text_wrap_length(obj, drawing.export_wrap_length(obj))
drawing.edit_text_symbol(obj, drawing.export_symbol(obj))
drawing.edit_text_literals(obj, literal_attributes)
drawing.edit_text_alignment(obj, drawing.export_alignment(obj))
drawing.disable_editing_text(obj)
+11 -1
View File
@@ -1185,7 +1185,7 @@ class Drawing(bonsai.core.tool.Drawing):
ifc_literals = cls.get_text_literal(obj, return_list=True)
assert isinstance(ifc_literals, list)
for ifc_literal in ifc_literals:
for i, ifc_literal in enumerate(ifc_literals):
literal_props = props.literals.add()
bonsai.bim.helper.import_attributes(ifc_literal, literal_props.attributes)
@@ -1196,6 +1196,16 @@ class Drawing(bonsai.core.tool.Drawing):
literal_props.box_alignment = box_alignment_mask # pyright: ignore[reportAttributeAccessIssue]
literal_props.ifc_definition_id = ifc_literal.id()
if i == 0:
if position_string == "center":
props.align_vertical = "middle"
props.align_horizontal = "middle"
else:
parts = position_string.split("-")
if len(parts) == 2:
props.align_vertical = parts[0]
props.align_horizontal = parts[1]
from bonsai.bim.module.drawing.data import DecoratorData
text_data = DecoratorData.get_text_data(obj)
+35 -3
View File
@@ -179,7 +179,8 @@ class Loader(bonsai.core.tool.Loader):
def surface_texture_to_dict(cls, surface_texture):
if isinstance(surface_texture, dict):
return surface_texture
mappings = surface_texture.IsMappedBy or []
# IsMappedBy is an IFC4+ inverse attribute, not available in IFC2X3.
mappings = getattr(surface_texture, "IsMappedBy", None) or []
surface_texture = surface_texture.get_info()
uv_mode = None
if mappings:
@@ -188,7 +189,7 @@ class Loader(bonsai.core.tool.Loader):
uv_mode = "Generated"
elif coordinates.is_a("IfcTextureCoordinateGenerator") and coordinates.Mode == "COORD-EYE":
uv_mode = "Camera"
surface_texture["uv_mode"] = uv_mode or "UV"
surface_texture["uv_mode"] = uv_mode or "Generated"
return surface_texture
@classmethod
@@ -286,6 +287,9 @@ class Loader(bonsai.core.tool.Loader):
for texture in textures:
mode = texture.get("Mode", None)
# IFC2X3 IfcImageTexture has no Mode attribute; default to DIFFUSE.
if mode is None and texture["type"] == "IfcImageTexture":
mode = "DIFFUSE"
node = None
image_url = None
@@ -293,7 +297,8 @@ class Loader(bonsai.core.tool.Loader):
def get_image() -> Union[bpy.types.Image, None]:
# TODO: orphaned textures after shader recreated?
if texture["type"] == "IfcImageTexture":
original_image_url = texture["URLReference"]
# IFC2X3 uses UrlReference, IFC4+ uses URLReference.
original_image_url = texture.get("URLReference") or texture.get("UrlReference", "")
is_relative = not os.path.isabs(original_image_url)
nonlocal image_url
image_url = Path(original_image_url)
@@ -539,6 +544,33 @@ class Loader(bonsai.core.tool.Loader):
for colour in colours:
cls.load_indexed_map(colour, mesh)
@classmethod
def load_generated_uv_map(cls, mesh: bpy.types.Mesh) -> None:
bm = bmesh.new()
bm.from_mesh(mesh)
uv_layer = bm.loops.layers.uv.active or bm.loops.layers.uv.new("UVMap")
all_verts = [v.co for v in bm.verts]
if not all_verts:
bm.free()
return
min_x = min(v.x for v in all_verts)
max_x = max(v.x for v in all_verts)
min_y = min(v.y for v in all_verts)
max_y = max(v.y for v in all_verts)
width = max_x - min_x
height = max_y - min_y
for face in bm.faces:
for loop in face.loops:
u = (loop.vert.co.x - min_x) / width if width > 0 else 0.5
v = (loop.vert.co.y - min_y) / height if height > 0 else 0.5
loop[uv_layer].uv = (max(0.0, min(1.0, u)), max(0.0, min(1.0, v)))
bm.to_mesh(mesh)
bm.free()
@classmethod
def load_indexed_map(cls, index_map: ifcopenshell.entity_instance, mesh: bpy.types.Mesh) -> None:
"""Add data from index map as blender mesh attribute.
+2 -13
View File
@@ -1244,18 +1244,7 @@ class Model(bonsai.core.tool.Model):
height = 100
is_horizontal = False
if element.is_a("IfcSlabType"):
is_horizontal = True
parametric = ifcopenshell.util.element.get_psets(element).get("EPset_Parametric")
if parametric:
layer_set_direction = parametric.get("LayerSetDirection", None)
if layer_set_direction == "AXIS2":
is_horizontal = False
elif layer_set_direction == "AXIS3":
is_horizontal = True
is_horizontal = cls.get_usage_type(element) == "LAYER3"
if is_horizontal:
width, height = height, width
@@ -1266,7 +1255,7 @@ class Model(bonsai.core.tool.Model):
del thicknesses[-1]
for thickness in thicknesses:
current_thickness += thickness
if element.is_a("IfcSlabType"):
if is_horizontal:
y = (current_thickness / total_thickness) * height
line = [x_offset, y_offset + y, x_offset + width, y_offset + y]
else:
+3 -3
View File
@@ -934,11 +934,11 @@ class TestAddReferenceImage(NewFile):
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))
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((1.0, 0.565, 0.0)))
assert tool.Cad.are_vectors_equal(obj.dimensions, Vector((3.53982, 2.0, 0.0)))
material = obj.active_material
assert material
@@ -957,4 +957,4 @@ class TestAddReferenceImage(NewFile):
assert texture_filepath == filepath
uv_node = material_nodes["Texture Coordinate"]
assert len(uv_node.outputs["Generated"].links[:]) == 1
assert len(uv_node.outputs["UV"].links[:]) == 1
@@ -254,7 +254,7 @@ nest formulas, for example ``concat(title("foo"), lower("Bar"))`` will produce
"``imperial_length({{value}}, {{precision}}, {{input_unit}}, {{output_unit}}, {{suppress_zero_inches}})``", "``imperial_length(3.0, 4, ""foot"", ""foot"", true)`` OR ``imperial_length(3.0, 4, ""foot"", ""foot"", false)``", "``3'`` OR ``3' - 0""``", "The ``{{value}}`` may be specified either as ``foot`` or ``inch`` depending on ``{{input_unit}}``. The ``{{value}}`` is then rounded to the nearest ``1/{{precision}}`` inch, then formatted using fractional feet and inches if ``{{output_unit}}`` is set to ``foot``, or just inches if ``{{output_unit}}`` is set to ``inch``. When ``{{suppress_zero_inches}}`` is ``true`` (default), measurements with zero inches will omit the inch portion (e.g., ``3'`` instead of ``3' - 0""``)."
"``sort({{values}})``", "``sort({{mats.Name}})``", "``Name1, Name2``", "Sorts a list of items."
"``reverse({{values}})``", "``reverse({{mats.Name}})``", "``Name2, Name1``", "Reverses a list of items."
"``join({{separator}}, {{values}})``", "``join("-", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
"``join({{separator}}, {{values}})``", "``join(""-"", {{mats.Name}})``", "``Name1-Name2``", "Joins a list of items with a custom separator. By default, all lists a rendered as comma separated."
"``{{value1}}[+-*/]{{value2}}``", "``{{z}}+3``", "``5``", "Does arithmetic. Typical operators such as +, -, \*, and / are allowed and can be mixed with other variables and formatting functions."
When using queries in an IfcAnnotation tag surround with backticks.
+3 -3
View File
@@ -2851,9 +2851,9 @@
}
},
"node_modules/tar": {
"version": "7.5.7",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz",
"integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==",
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
"integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {