Compare commits

...

14 Commits

Author SHA1 Message Date
Bruno Postle 78f4190160 Add AGENTS.md contributor guide
Guidelines for external contributors using AI coding tools,
covering licensing, AI disclosure requirements, PR scope,
commit style, code formatting, and testing expectations.

Generated with the assistance of an AI coding tool.
2026-02-13 18:24:46 +00:00
Thomas Krijnen 7978f1fb08 Fix compilation on gcc #7666 2026-02-13 10:17:28 +01:00
ssg3d 7b4889d2ec Update IfcParse.cpp
IfcOpenshell read file, and write file without changes. This round trip introduces truncation noise. It should not hurt to increase the precision to keep this clean.
2026-02-13 10:03:05 +01:00
José Aliste 740fcf7768 Use Blender's angle snap setting in wall.py and profile.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in create_wall_from_2_points()
and create_profile_from_2_points().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste 067e04b564 Use Blender's angle snap setting in model/polyline.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in handle_lock_axis() for:
- Initial angle rounding when locking axis (A key)
- Angle rounding and increments on Shift+Wheel scroll

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste cd95f46db5 Use Blender's angle snap setting in tool/polyline.py
Replace hardcoded 5-degree angle snapping with Blender's
snap_angle_increment setting in calculate_distance_and_angle().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
José Aliste 348e48b49c Add get_angle_snap_value() helper to tool/snap.py
This function retrieves the angle snap increment from Blender's
tool_settings.snap_angle_increment property, which was added in
Blender 4.2. This allows users to configure the angle snap value
through Blender's native UI instead of using hardcoded values.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 22:29:43 -03:00
Thomas Krijnen 6bb8abdc5a Fix for 4.2 schema after a46cdbb907 2026-02-11 12:11:23 +01:00
Thomas Krijnen b7a8c9b330 Fix for 4.2 schema after a46cdbb907 2026-02-11 11:43:58 +01:00
Thomas Krijnen e6780973da arrange_poly: Refactor into logical blocks; add timing 2026-02-10 14:01:10 +01:00
Thomas Krijnen c6072e416c N-Section Lofting for Non-Polygonal (Curved) Shapes #7658 2026-02-10 11:04:43 +01:00
Thomas Krijnen f5686fff26 Simplify destructor by removing null check #7650 2026-02-10 09:25:02 +01:00
Thomas Krijnen a46cdbb907 Ignore site placement also for site geometry - add queue #7654 2026-02-09 21:37:21 +01:00
Thomas Krijnen a4d5d4e19a Ignore site placement also for site geometry #7654 2026-02-09 21:21:10 +01:00
12 changed files with 1108 additions and 1018 deletions
+153
View File
@@ -0,0 +1,153 @@
<!-- This file was generated with the assistance of an AI coding tool. -->
# AGENTS.md
Guidelines for AI coding agents contributing to IfcOpenShell. This file is
intended to be read by all AI agents regardless of platform (Claude Code,
Copilot, Cursor, etc.) in addition to any tool-specific configuration files.
Human contributors using AI tools should also read this document carefully,
as they are responsible for ensuring their contributions comply with these
guidelines.
## Project Overview
IfcOpenShell is an open source library for working with Industry Foundation
Classes (IFC). It provides C++ and Python APIs, geometry processing, and an
ecosystem of tools including IfcConvert and the Bonsai Blender add-on.
## Licensing
All contributions must be compatible with the project's licensing:
- **Library code** (everything except Bonsai): **LGPL-3.0-or-later**
- **Bonsai** (`src/bonsai/`): **GPL-3.0-or-later**
There is no Contributor License Agreement (CLA). By submitting a pull request,
you agree that your contribution is licensed under the applicable license above.
## Indicating AI-Generated Code
Contributors must clearly indicate when code has been generated or
substantially written by an AI tool.
### Commits
Commits that modify existing code must include a note in the **body** of the
commit message (not the subject line) indicating that the change was
AI-generated. For example:
```
Fix off-by-one error in element iteration
The loop termination condition was incorrect when processing
IfcRelAggregates relationships.
Generated with the assistance of an AI coding tool.
```
### New Files
New files that are AI-generated must include a comment near the top of the
file indicating this. Use the appropriate comment syntax for the language:
```python
# This file was generated with the assistance of an AI coding tool.
```
```cpp
// This file was generated with the assistance of an AI coding tool.
```
### Pull Requests
Pull requests containing AI-generated code must indicate in the PR description
which parts of the contribution are AI-generated. If the entire PR is
AI-generated, state that clearly. If only specific commits or files are
AI-generated, identify them.
## Pull Request Guidelines
### Scope and Size
- Each pull request should address a **single issue or feature**.
- Do not mix unrelated changes (e.g., bug fixes with refactoring or style
changes) in the same PR.
- Large pull requests should be broken down into **multiple small, standalone
commits** that are each easy to review independently. Rewrite commit history
for this purpose if necessary.
- PRs that are minimal, focused solutions to a specific problem are much more
likely to be accepted.
### What to Avoid
- **Over-engineering**: Do not add features, abstractions, or configurability
beyond what is needed to solve the immediate problem.
- **Scope creep**: Do not make changes to files or code that are not directly
related to the task at hand.
- **Unnecessary additions**: Do not add docstrings, comments, type annotations,
or error handling to code you did not otherwise need to change.
- **Cosmetic changes**: Do not reformat, rename, or reorganize code that is
unrelated to your change.
## Commit Messages
- The **subject line** must be **50 characters or less**.
- Use the **imperative mood** (e.g., "Fix crash in geometry kernel", not
"Fixed crash" or "Fixes crash").
- A commit message can be a single line if the purpose is obvious from the
subject alone.
- Otherwise, add a blank line after the subject followed by a short explanation
of a few lines in the body.
## Code Style
### Python
- **Line length**: 120 characters
- **Formatter**: black
- **Linter**: ruff
- Configuration is in `pyproject.toml`
### C++
- **Standard**: C++17 minimum
- **Formatter**: clang-format (configuration in `.clang-format`)
- **Linter**: clang-tidy (configuration in `.clang-tidy`)
Run linters and formatters **before submitting** your pull request. Do not rely
on CI to catch formatting issues.
## Testing
- Pull requests with test coverage are **much more likely to be merged**.
- If tests are appropriate and feasible for your change, they should be
included.
- Tests are not required for every change (e.g., documentation-only changes),
but the expectation is that testable code changes come with tests.
- Python tests use **pytest** and are located in `test/` or `tests/` directories
within each package under `src/`.
- Run the existing test suite for the package you modified before submitting.
## Architecture Quick Reference
### Directory Structure
- `src/ifcparse/` — C++ IFC file parsing
- `src/ifcgeom/` — C++ geometry processing (OpenCASCADE and CGAL kernels)
- `src/serializers/` — Output format serializers (glTF, Collada, SVG, etc.)
- `src/ifcwrap/` — SWIG Python bindings
- `src/ifcconvert/` — CLI conversion tool
- `src/ifcopenshell-python/` — Python API (`ifcopenshell` package)
- `src/bonsai/` — Blender add-on (GPL-3.0-or-later)
- `src/ifctester/` — IDS model auditing
- `src/ifcpatch/` — IFC file manipulation scripts
- `src/ifcdiff/` — IFC model comparison
- `src/ifcclash/` — Clash detection
- `src/ifccsv/` — Schedule import/export
### IFC Schema Versions
The library supports IFC2x3 TC1, IFC4 Add2 TC1, IFC4x1, IFC4x2, and
IFC4x3 Add2. Schema-specific code is compiled conditionally. Be aware of
which schema versions your change affects.
@@ -211,22 +211,21 @@ class PolylineOperator:
context.workspace.status_text_set(draw_instructions)
def handle_lock_axis(self, context: bpy.types.Context, event: bpy.types.Event) -> None:
angle_snap = tool.Snap.get_angle_snap_value(context)
if event.value == "PRESS" and event.type == "A":
self.tool_state.lock_axis = False if self.tool_state.lock_axis else True
if self.tool_state.lock_axis:
self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE")
# Round to the closest 5
self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5
self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap
if event.shift and event.type in {"WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
self.tool_state.lock_axis = True
self.tool_state.snap_angle = self.input_ui.get_number_value("WORLD_ANGLE")
# Round to the closest 5
self.tool_state.snap_angle = round(self.tool_state.snap_angle / 5) * 5
self.tool_state.snap_angle = round(self.tool_state.snap_angle / angle_snap) * angle_snap
if event.type in {"WHEELUPMOUSE"}:
self.tool_state.snap_angle += 5
self.tool_state.snap_angle += angle_snap
else:
self.tool_state.snap_angle -= 5
self.tool_state.snap_angle -= angle_snap
self.handle_mouse_move(context, event)
detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox, self.tool_state)
self.snapping_points = tool.Snap.select_snapping_points(context, event, self.tool_state, detected_snaps)
@@ -17,7 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import copy
from math import atan2, degrees, pi
from math import atan2, degrees, pi, radians
from typing import Any, Literal, Optional, Union
import bmesh
@@ -195,8 +195,8 @@ class DumbProfileGenerator:
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (pi / 180) * 5
angle_snap = tool.Snap.get_angle_snap_value(bpy.context)
nearest_degree = radians(angle_snap)
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_profile()
+2 -2
View File
@@ -916,8 +916,8 @@ class DumbWallGenerator:
if should_round:
# Round to nearest 50mm (yes, metric for now)
self.length = 0.05 * round(length / 0.05)
# Round to nearest 5 degrees
nearest_degree = (math.pi / 180) * 5
angle_snap = tool.Snap.get_angle_snap_value(bpy.context)
nearest_degree = math.radians(angle_snap)
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
self.location = coords[0]
data["obj"] = self.create_wall()
+2 -1
View File
@@ -191,7 +191,8 @@ class Polyline(bonsai.core.tool.Polyline):
orientation_angle = 0
if input_ui:
if should_round:
angle = 5 * round(angle / 5) if distance < angle_round_threshold else angle
angle_snap = tool.Snap.get_angle_snap_value(context)
angle = angle_snap * round(angle / angle_snap) if distance < angle_round_threshold else angle
factor = tool.Snap.get_increment_snap_value(context)
distance = factor * round(distance / factor)
input_ui.set_value("X", mouse_vector.x)
+13
View File
@@ -114,6 +114,19 @@ class Snap(bonsai.core.tool.Snap):
return increment
@classmethod
def get_angle_snap_value(cls, context: bpy.types.Context) -> float:
"""Get the angle snap increment from Blender's tool settings.
Uses snap_angle_increment_3d (Blender 5.0+) or snap_angle_increment (Blender 4.x).
:param context: Blender context
:return: Angle snap increment in degrees
"""
if bpy.app.version >= (5, 0, 0):
return math.degrees(context.scene.tool_settings.snap_angle_increment_3d)
return math.degrees(context.scene.tool_settings.snap_angle_increment)
@classmethod
def get_snap_points_on_raycasted_face(cls, context, event, obj, face_index):
matrix = obj.matrix_world.copy()
+2 -5
View File
@@ -12,11 +12,8 @@ ifcopenshell::geometry::Converter::Converter(std::unique_ptr<ifcopenshell::geome
settings_ = mapping_->settings();
}
ifcopenshell::geometry::Converter::~Converter()
{
if (mapping_ != nullptr) {
delete mapping_;
}
ifcopenshell::geometry::Converter::~Converter() {
delete mapping_;
}
namespace {
+65 -35
View File
@@ -82,49 +82,79 @@ bool OpenCascadeKernel::convert(const taxonomy::loft::ptr loft, TopoDS_Shape& re
}
if (non_polygonal) {
if (loft->children.size() == 2) {
BRep_Builder BB;
TopoDS_Shell comp;
BB.MakeShell(comp);
if (loft->children.size() < 2) {
Logger::Error("Not enough sections to loft");
return false;
}
std::vector<std::vector<TopoDS_Wire>> sections;
sections.reserve(loft->children.size());
TopoDS_Shape f0, f1;
if (!convert(std::static_pointer_cast<taxonomy::face>(loft->children.front()), f0) ||
!convert(std::static_pointer_cast<taxonomy::face>(loft->children.back()), f1))
{
TopoDS_Shape f0, f1;
// Convert all children to vectors of wires
for (const auto& child : loft->children) {
TopoDS_Shape shape;
if (!convert(std::static_pointer_cast<taxonomy::face>(child), shape)) {
return false;
}
if (shape.ShapeType() != TopAbs_FACE) {
return false;
}
// At least make sure to have outer wire consistent, but in reality
// this is probably not a concern given how to build up these faces
auto f = TopoDS::Face(shape);
if (child == loft->children.front()) {
f0 = f;
} else if (child == loft->children.back()) {
f1 = f;
}
auto outer = BRepTools::OuterWire(f);
sections.emplace_back();
sections.back().push_back(outer);
for (TopoDS_Iterator it(f); it.More(); it.Next()) {
if (outer != it.Value()) {
sections.back().push_back(TopoDS::Wire(it.Value()));
}
}
}
auto first_wire_count = sections.front().size();
for (auto& section : sections) {
if (section.size() != first_wire_count) {
Logger::Error("Inconsistent number of wires in sections");
return false;
}
if (f0.ShapeType() != TopAbs_FACE || f1.ShapeType() != TopAbs_FACE) {
}
BRep_Builder BB;
TopoDS_Shell comp;
BB.MakeShell(comp);
for (size_t i = 0; i < first_wire_count; ++i) {
// Rule=True uses linear interpolation.
// This is critical for preventing twists in roads/railings.
BRepOffsetAPI_ThruSections builder(false, true);
for (auto& ws : sections) {
builder.AddWire(ws[i]);
}
builder.Build();
if (!builder.IsDone()) {
return false;
}
TopExp_Explorer exp1(f0, TopAbs_WIRE);
TopExp_Explorer exp2(f1, TopAbs_WIRE);
for (; exp1.More() && exp2.More(); exp1.Next(), exp2.Next()) {
const auto& w1 = TopoDS::Wire(exp1.Current());
const auto& w2 = TopoDS::Wire(exp2.Current());
BRepOffsetAPI_ThruSections builder;
builder.AddWire(w1);
builder.AddWire(w2);
builder.Build();
if (!builder.IsDone()) {
return false;
}
for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) {
BB.Add(comp, exp.Current());
}
for (TopExp_Explorer exp(builder.Shape(), TopAbs_FACE); exp.More(); exp.Next()) {
BB.Add(comp, exp.Current());
}
BB.Add(comp, f0.Reversed());
BB.Add(comp, f1);
result = BRepBuilderAPI_MakeSolid(comp).Solid();
return true;
} else {
Logger::Error("Lofting more than two sections is not supported");
return false;
}
BB.Add(comp, f0.Reversed());
BB.Add(comp, f1);
result = BRepBuilderAPI_MakeSolid(comp).Solid();
return true;
}
TopTools_ListOfShape faces;
@@ -21,7 +21,44 @@
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
#include <deque>
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcObjectPlacement* inst) {
if (placement_rel_to_type_ || placement_rel_to_instance_) {
using QueueItem = std::pair<const IfcUtil::IfcBaseEntity*, int>;
std::deque<QueueItem> q = {{inst, 0}};
while (!q.empty()) {
auto [placement_entity, depth] = q.front();
q.pop_front();
auto placement = placement_entity->as<typename IfcSchema::IfcObjectPlacement>();
if (!placement) {
continue;
}
auto self_places = placement->PlacesObject();
for (auto iter = self_places->begin(); iter != self_places->end(); ++iter) {
if ((placement_rel_to_type_ && (*iter)->declaration().is(*placement_rel_to_type_)) ||
(placement_rel_to_instance_ && (*iter)->as<IfcUtil::IfcBaseEntity>() == placement_rel_to_instance_)) {
return taxonomy::make<taxonomy::matrix4>();
}
}
// Look for two levels deep, we want to know if we're at or *above* the
// element we're ignoring, but we don't want to traverse the entire model.
#ifdef SCHEMA_IfcObjectPlacement_HAS_ReferencedByPlacements
if (depth < 2) {
auto refs = placement->ReferencedByPlacements();
for (auto& ref : *refs) {
q.emplace_back(ref, depth + 1);
}
}
#else
Logger::Warning("Using --site-local-placement or --building-local-placement on IFC4.2 might have issues");
#endif
}
}
const IfcSchema::IfcObjectPlacement* relative_to = nullptr;
const IfcUtil::IfcBaseInterface* transform;
+1 -1
View File
@@ -746,7 +746,7 @@ namespace {
static std::string format_double(const double& d) {
std::ostringstream oss;
oss.imbue(std::locale::classic());
oss << std::setprecision(std::numeric_limits<double>::digits10) << d;
oss << std::setprecision(std::numeric_limits<double>::max_digits10) << d;
const std::string str = oss.str();
oss.str("");
std::string::size_type e = str.find('e');
File diff suppressed because it is too large Load Diff
+12
View File
@@ -2,8 +2,10 @@
#define GRAPH_2D_H
#ifdef SVGFILL_DEBUG
#if 0
#include <nlohmann/json.hpp>
#endif
#endif
template <typename Kernel>
class Graph2D {
@@ -334,6 +336,16 @@ public:
return Graph2D(input_adjacency_list);
}
template <typename T>
void to_arrangement(T& arr) {
for (auto it = edges_begin(); it != edges_end(); ++it) {
if (it->first == it->second) {
continue;
}
CGAL::insert(arr, CGAL::Segment_2<Kernel>(it->first, it->second));
}
}
void assert_symmetric() {
#ifdef SVGFILL_DEBUG
#if 0