Merge branch 'v0.8.0' into ifcmax/initial-refresh

This commit is contained in:
Josef Wienerroither
2026-07-12 07:52:15 +02:00
16 changed files with 281 additions and 31 deletions
+1
View File
@@ -127,6 +127,7 @@ src/ifcopenshell-python/ifcopenshell/express/*.exp.cache.dat
# temp files from AI coding tools # temp files from AI coding tools
*.claude *.claude
CLAUDE.local.md
*.py.tmp* *.py.tmp*
*.json.tmp* *.json.tmp*
+5 -1
View File
@@ -314,8 +314,12 @@ if(WASM_BUILD)
else() else()
# @todo review this, shouldn't this be all possible header-only now? # @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features? # ... or rewritten using C++17 features?
# Boost.System has been header-only since 1.69 and its compiled stub library
# was dropped in newer Boost, so requesting it as a component makes
# find_package fail on Boost 1.70 and up (for example Boost 1.90). It is
# still pulled in transitively by thread / iostreams where needed, so do not
# request it explicitly.
set(BOOST_COMPONENTS set(BOOST_COMPONENTS
system
program_options program_options
regex regex
thread thread
@@ -2343,7 +2343,9 @@ class ActivateDrawingBase(tool.Ifc.Operator):
"Activates the selected drawing view.\n\n" "Activates the selected drawing view.\n\n"
+ "ALT+CLICK to keep the viewport position.\n\n" + "ALT+CLICK to keep the viewport position.\n\n"
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n" + "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views" + "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
) )
drawing: bpy.props.IntProperty() drawing: bpy.props.IntProperty()
@@ -2365,16 +2367,25 @@ class ActivateDrawingBase(tool.Ifc.Operator):
default=False, default=False,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
include_annotations_in_selection: bpy.props.BoolProperty(
name="Include Annotations In Selection",
description="Also select the loaded annotation objects, not just the drawing cameras.",
default=False,
options={"SKIP_SAVE"},
)
if TYPE_CHECKING: if TYPE_CHECKING:
drawing: int drawing: int
should_view_from_camera: bool should_view_from_camera: bool
use_quick_preview: bool use_quick_preview: bool
load_selected_annotations: bool load_selected_annotations: bool
include_annotations_in_selection: bool
def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]: def invoke(self, context, event) -> set["rna_enums.OperatorReturnItems"]:
if event.type == "LEFTMOUSE" and event.shift and event.ctrl: if event.type == "LEFTMOUSE" and event.shift and event.ctrl:
self.load_selected_annotations = True self.load_selected_annotations = True
if event.alt:
self.include_annotations_in_selection = True
return self.execute(context) return self.execute(context)
if event.type == "LEFTMOUSE" and event.alt: if event.type == "LEFTMOUSE" and event.alt:
self.should_view_from_camera = False self.should_view_from_camera = False
@@ -2389,15 +2400,34 @@ class ActivateDrawingBase(tool.Ifc.Operator):
bpy.ops.bim.load_drawings() bpy.ops.bim.load_drawings()
if self.load_selected_annotations: if self.load_selected_annotations:
objs_to_select = []
active_camera = None
for d in props.drawings: for d in props.drawings:
if not (d.is_drawing and d.is_selected): if not (d.is_drawing and d.is_selected):
continue continue
selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id) selected_drawing = tool.Ifc.get().by_id(d.ifc_definition_id)
# Importing the camera (if missing) ensures the drawing's # Importing the camera (if missing) ensures the drawing's
# collection exists so the annotations get collected into it. # collection exists so the annotations get collected into it.
if not tool.Ifc.get_object(selected_drawing): if not (camera := tool.Ifc.get_object(selected_drawing)):
tool.Drawing.import_drawing(selected_drawing) camera = tool.Drawing.import_drawing(selected_drawing)
tool.Drawing.import_annotations_in_group(tool.Drawing.get_drawing_group(selected_drawing)) group = tool.Drawing.get_drawing_group(selected_drawing)
tool.Drawing.import_annotations_in_group(group)
if active_camera is None:
active_camera = camera
objs_to_select.append(camera)
if self.include_annotations_in_selection:
for element in tool.Drawing.get_group_elements(group) or []:
if element.is_a("IfcAnnotation") and element.ObjectType != "DRAWING":
if annotation_obj := tool.Ifc.get_object(element):
objs_to_select.append(annotation_obj)
# Select the checked drawings' objects, with the first drawing's camera as active.
bpy.ops.object.select_all(action="DESELECT")
for obj in objs_to_select:
obj.select_set(True)
if active_camera is not None:
context.view_layer.objects.active = active_camera
return {"FINISHED"} return {"FINISHED"}
drawing = tool.Ifc.get().by_id(self.drawing) drawing = tool.Ifc.get().by_id(self.drawing)
@@ -2486,7 +2516,9 @@ class ActivateDrawing(bpy.types.Operator, ActivateDrawingBase):
"Activates the selected drawing view.\n\n" "Activates the selected drawing view.\n\n"
+ "ALT+CLICK to keep the viewport position.\n\n" + "ALT+CLICK to keep the viewport position.\n\n"
+ "SHIFT+CLICK to load a quick preview of the drawing view.\n\n" + "SHIFT+CLICK to load a quick preview of the drawing view.\n\n"
+ "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views" + "SHIFT+CTRL+CLICK to load the annotations of all selected drawings without switching views, "
+ "then select their cameras (the first selected drawing's camera becomes active).\n\n"
+ "SHIFT+CTRL+ALT+CLICK to do the same but also select the annotations, not just the cameras"
) )
+11
View File
@@ -252,6 +252,10 @@ int main(int argc, char** argv) {
("stderr-progress", "output progress to stderr stream") ("stderr-progress", "output progress to stderr stream")
("yes,y", "answer 'yes' automatically to possible confirmation queries (e.g. overwriting an existing output file)") ("yes,y", "answer 'yes' automatically to possible confirmation queries (e.g. overwriting an existing output file)")
("no-progress", "suppress possible progress bar type of prints that use carriage return") ("no-progress", "suppress possible progress bar type of prints that use carriage return")
("fail-on-error", "return a non-zero exit code when one or more errors were logged during "
"geometry conversion (e.g. an element failed to convert). By default IfcConvert exits "
"successfully as long as an output file could be written, even if some elements were "
"silently dropped. Enable this flag so scripts and CI can detect partial conversions.")
("log-format", po::value<std::string>(&log_format), "log format: plain or json") ("log-format", po::value<std::string>(&log_format), "log format: plain or json")
("log-file", new po::typed_value<path_t, char_t>(&log_file), "redirect log output to file"); ("log-file", new po::typed_value<path_t, char_t>(&log_file), "redirect log output to file");
@@ -449,6 +453,7 @@ int main(int argc, char** argv) {
const bool mmap = vmap.count("mmap") != 0; const bool mmap = vmap.count("mmap") != 0;
const bool no_progress = vmap.count("no-progress") != 0; const bool no_progress = vmap.count("no-progress") != 0;
const bool fail_on_error = vmap.count("fail-on-error") != 0;
const bool quiet = vmap.count("quiet") != 0; const bool quiet = vmap.count("quiet") != 0;
const bool stderr_progress = vmap.count("stderr-progress") != 0; const bool stderr_progress = vmap.count("stderr-progress") != 0;
@@ -885,6 +890,7 @@ int main(int argc, char** argv) {
} }
if (!serializer->ready()) { if (!serializer->ready()) {
logger.Error("SYS", 25, "Unable to open output file '" + IfcUtil::path::to_utf8(output_filename) + "' for writing; check that the directory exists and is writable");
IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename)); IfcUtil::path::delete_file(IfcUtil::path::to_utf8(output_temp_filename));
write_log(!quiet); write_log(!quiet);
return EXIT_FAILURE; return EXIT_FAILURE;
@@ -1220,6 +1226,11 @@ int main(int argc, char** argv) {
successful = false; successful = false;
} }
if (fail_on_error && logger.MaxSeverity() >= Logger::LOG_ERROR) {
logger.Error("SYS", 26, "Errors encountered during processing, failing due to --fail-on-error.");
successful = false;
}
if (logger.Verbosity() == Logger::LOG_PERF) { if (logger.Verbosity() == Logger::LOG_PERF) {
logger.PrintPerformanceStats(); logger.PrintPerformanceStats();
} }
+2 -2
View File
@@ -361,8 +361,8 @@ namespace ifcopenshell {
struct CircleSegments : public SettingBase<CircleSegments, int> { struct CircleSegments : public SettingBase<CircleSegments, int> {
static constexpr const char* const name = "circle-segments"; static constexpr const char* const name = "circle-segments";
static constexpr const char* const description = "Number of segments to approximate full circles in CGAL kernel."; static constexpr const char* const description = "Number of segments to approximate full circles in the CGAL kernel. When 0 (the default) the segment count is derived from mesher-linear-deflection instead, so curves stay within the deflection tolerance regardless of radius.";
static constexpr int defaultvalue = 16; static constexpr int defaultvalue = 0;
}; };
struct CgalSmoothAngleDegrees : public SettingBase<CgalSmoothAngleDegrees, double> { struct CgalSmoothAngleDegrees : public SettingBase<CgalSmoothAngleDegrees, double> {
+35 -1
View File
@@ -391,6 +391,11 @@ namespace {
} }
}; };
// Representative radius used to size the polygonal approximation of a conic.
// For an ellipse the larger semi-axis is the conservative choice.
inline double conic_radius(const taxonomy::circle::ptr& c) { return c->radius; }
inline double conic_radius(const taxonomy::ellipse::ptr& e) { return e->radius > e->radius2 ? e->radius : e->radius2; }
struct cgal_curve_creation_visitor { struct cgal_curve_creation_visitor {
Settings& settings_; Settings& settings_;
parameter_range param; parameter_range param;
@@ -425,7 +430,36 @@ namespace {
if (b <= a) { if (b <= a) {
b += 2 * M_PI; b += 2 * M_PI;
} }
int num_segments = (int)std::ceil(std::fabs(a - b) / (2 * M_PI) * settings_.get<settings::CircleSegments>().get()); const double span = std::fabs(a - b);
// CircleSegments controls how conics (circles, ellipses, arcs) are approximated
// in the CGAL kernel. Two modes, one or the other:
// - CircleSegments == 0 (the default): the segment count is derived from
// MesherLinearDeflection, so the chord deviation stays within the mesher's
// linear deflection regardless of radius. This matches the deflection based
// meshing the OpenCascade kernel already does and fixes issue #8051, where
// large radius arcs (curved curtain wall mullions) collapsed to straight chords
// because a fixed segment count is radius agnostic.
// - CircleSegments > 0: it is used directly as the number of segments for a full
// circle, giving deterministic, radius independent output.
int num_segments;
const int circle_segments = settings_.get<settings::CircleSegments>().get();
if (circle_segments > 0) {
num_segments = (int)std::ceil(span / (2 * M_PI) * circle_segments);
} else {
const double radius = conic_radius(t);
const double deflection = settings_.get<settings::MesherLinearDeflection>().get();
if (deflection > 0. && radius > deflection) {
const double max_segment_angle = 2.0 * std::acos(1.0 - deflection / radius);
num_segments = (int)std::ceil(span / max_segment_angle);
} else {
// Radius within the deflection tolerance (or no deflection set): a chord per
// quarter turn already keeps the deviation within tolerance.
num_segments = (int)std::ceil(span / (M_PI / 2.));
}
}
if (num_segments < 1) {
num_segments = 1;
}
double du = (b - a) / num_segments; double du = (b - a) / num_segments;
taxonomy::point3 P; taxonomy::point3 P;
// @nb for loop is not inclusive of the both end points // @nb for loop is not inclusive of the both end points
+22
View File
@@ -31,6 +31,7 @@
#include <ShapeFix_Shape.hxx> #include <ShapeFix_Shape.hxx>
#include <ShapeFix_ShapeTolerance.hxx> #include <ShapeFix_ShapeTolerance.hxx>
#include <BRep_Tool.hxx> #include <BRep_Tool.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <Standard_Macro.hxx> #include <Standard_Macro.hxx>
#include <TopoDS_Shape.hxx> #include <TopoDS_Shape.hxx>
@@ -356,6 +357,27 @@ bool OpenCascadeKernel::convert(const taxonomy::face::ptr face, TopoDS_Shape& re
return false; return false;
} }
// #527: A face whose inner boundary intersects the outer boundary (or
// another inner boundary) is invalid per the schema. Open Cascade heals or
// drops such a face silently, so the intended hole is lost with no
// diagnostic. The distance between two non-intersecting loops is strictly
// positive; a distance at (or below) the modelling precision means the
// boundaries touch or cross. Emit a clear warning so the invalid input is
// not silently lost. wires() is ordered outer-first, inner-bounds after.
if (fd.wires().size() > 1) {
const auto& fwires = fd.wires();
bool reported = false;
for (size_t i = 1; i < fwires.size() && !reported; ++i) {
for (size_t j = 0; j < i && !reported; ++j) {
BRepExtrema_DistShapeShape dss(fwires[i], fwires[j]);
if (dss.IsDone() && dss.Value() < precision_) {
logger().Warning("GEO", 402, "Face inner boundary intersects another face boundary", face->instance);
reported = true;
}
}
}
}
if (fd.surface().IsNull()) { if (fd.surface().IsNull()) {
// Use the first wire to find a plane manually for polygonal wires // Use the first wire to find a plane manually for polygonal wires
const TopoDS_Wire& wire = fd.wires().front(); const TopoDS_Wire& wire = fd.wires().front();
@@ -0,0 +1,93 @@
// This file was generated with the assistance of an AI coding tool.
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell 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 *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "mapping.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
#include "../profile_helper.h"
// In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of IfcIShapeProfileDef and is
// therefore dispatched (and handled) by the IfcIShapeProfileDef mapping. From IFC4
// onwards it is a standalone subtype of IfcParameterizedProfileDef with its own
// Bottom*/Top* attributes, so nothing mapped it and the extrusion came out empty.
// The presence of the standalone BottomFlangeWidth attribute is the discriminator:
// it is only defined in the schemas where the type is standalone (IFC4 / IFC4X3).
#ifdef SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcAsymmetricIShapeProfileDef* inst) {
// Bottom flange (half width), overall depth (half), web (half thickness).
const double xb = inst->BottomFlangeWidth() / 2.0 * length_unit_;
const double xt = inst->TopFlangeWidth() / 2.0 * length_unit_;
const double y = inst->OverallDepth() / 2.0 * length_unit_;
const double d1 = inst->WebThickness() / 2.0 * length_unit_;
// Bottom flange thickness; top flange thickness defaults to the bottom one.
const double ftb = inst->BottomFlangeThickness() * length_unit_;
const double ftt = inst->TopFlangeThickness().get_value_or(inst->BottomFlangeThickness()) * length_unit_;
// Optional fillet radii (web/flange transition) and flange edge radii.
const double fb = inst->BottomFlangeFilletRadius().get_value_or(0.) * length_unit_;
const double ft_top = inst->TopFlangeFilletRadius().get_value_or(0.) * length_unit_;
const double feb = inst->BottomFlangeEdgeRadius().get_value_or(0.) * length_unit_;
const double fet = inst->TopFlangeEdgeRadius().get_value_or(0.) * length_unit_;
// Optional flange slopes: the inner edge of the flange rises towards the web.
const double bottomSlope = inst->BottomFlangeSlope().get_value_or(0.) * angle_unit_;
const double topSlope = inst->TopFlangeSlope().get_value_or(0.) * angle_unit_;
const double dyb = (xb - d1) * tan(bottomSlope);
const double dyt = (xt - d1) * tan(topSlope);
const double tol = settings_.get<settings::Precision>().get();
if (xb < tol || xt < tol || y < tol || d1 < tol || ftb < tol || ftt < tol) {
logger_.Message(Logger::LOG_NOTICE, "GEO", 264, "Skipping zero sized profile:", inst);
return nullptr;
}
taxonomy::matrix4::ptr m4;
bool has_position = true;
#ifdef SCHEMA_IfcParameterizedProfileDef_Position_IS_OPTIONAL
has_position = !!inst->Position();
#endif
if (has_position) {
m4 = taxonomy::cast<taxonomy::matrix4>(map(inst->Position()));
}
// Twelve corner points, running counter-clockwise from the bottom-left, with the
// bottom flange (xb) possibly wider than the top flange (xt). Fillet/edge radii are
// attached to the corner they round, matching the symmetric IfcIShapeProfileDef.
return profile_helper(m4, {
{{-xb,-y}},
{{xb,-y}},
{{xb,-y + ftb}, {feb}},
{{d1,-y + ftb + dyb},{fb} },
{{d1,y - ftt - dyt},{ft_top} },
{{xt,y - ftt}, {fet}},
{{xt,y}},
{{-xt,y}},
{{-xt,y - ftt}, {fet}},
{{-d1,y - ftt - dyt},{ft_top} },
{{-d1,-y + ftb + dyb},{fb} },
{{-xb,-y + ftb}, {feb}}
});
}
#endif
+22 -11
View File
@@ -39,8 +39,25 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
int max_index = (int)points.size(); int max_index = (int)points.size();
// When the optional PnIndex is present, CoordIndex values do not index into
// CoordList directly but into PnIndex, which in turn remaps to CoordList.
// Both index levels are 1-based per the IFC specification.
auto pn_index = inst->PnIndex();
auto resolve = [&](int idx) -> const taxonomy::point3::ptr& {
if (pn_index) {
if (idx < 1 || idx > (int)pn_index->size()) {
throw IfcParse::IfcException("IfcPolygonalFaceSet PnIndex out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
idx = (*pn_index)[idx - 1];
}
if (idx < 1 || idx > max_index) {
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
return points[idx - 1];
};
auto shell = taxonomy::make<taxonomy::shell>(); auto shell = taxonomy::make<taxonomy::shell>();
for (auto& f : *polygonal_faces) { for (auto& f : *polygonal_faces) {
auto fa = taxonomy::make<taxonomy::face>(); auto fa = taxonomy::make<taxonomy::face>();
shell->children.push_back(fa); shell->children.push_back(fa);
@@ -52,17 +69,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
auto indices = f->CoordIndex(); auto indices = f->CoordIndex();
taxonomy::point3::ptr previous; taxonomy::point3::ptr previous;
for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) { for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) {
if (*jt < 1 || *jt > max_index) { auto current = resolve(*jt);
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
auto current = points[(*jt) - 1];
if (jt != indices.begin()) { if (jt != indices.begin()) {
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current)); loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
} }
previous = current; previous = current;
} }
if (!indices.empty()) { if (!indices.empty()) {
auto current = points[indices.front() - 1]; auto current = resolve(indices.front());
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current)); loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
} }
} }
@@ -77,17 +91,14 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcPolygonalFaceSet* inst) {
loop->external = false; loop->external = false;
for (std::vector<int>::const_iterator jt = li.begin(); jt != li.end(); ++jt) { for (std::vector<int>::const_iterator jt = li.begin(); jt != li.end(); ++jt) {
if (*jt < 1 || *jt > max_index) { auto current = resolve(*jt);
throw IfcParse::IfcException("IfcPolygonalFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
auto current = points[(*jt) - 1];
if (jt != li.begin()) { if (jt != li.begin()) {
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current)); loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
} }
previous = current; previous = current;
} }
if (!li.empty()) { if (!li.empty()) {
auto current = points[li.front() - 1]; auto current = resolve(li.front());
loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current)); loop->children.push_back(taxonomy::make<taxonomy::edge>(previous, current));
} }
} }
+18 -4
View File
@@ -39,6 +39,23 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTriangulatedFaceSet* inst) {
int max_index = (int)points.size(); int max_index = (int)points.size();
// When the optional PnIndex is present, CoordIndex values do not index into
// CoordList directly but into PnIndex, which in turn remaps to CoordList.
// Both index levels are 1-based per the IFC specification.
auto pn_index = inst->PnIndex();
auto resolve = [&](int idx) -> const taxonomy::point3::ptr& {
if (pn_index) {
if (idx < 1 || idx > (int)pn_index->size()) {
throw IfcParse::IfcException("IfcTriangulatedFaceSet PnIndex out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
idx = (*pn_index)[idx - 1];
}
if (idx < 1 || idx > max_index) {
throw IfcParse::IfcException("IfcTriangulatedFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(idx));
}
return points[idx - 1];
};
auto shell = taxonomy::make<taxonomy::shell>(); auto shell = taxonomy::make<taxonomy::shell>();
for (auto& indices : indices_list) { for (auto& indices : indices_list) {
@@ -51,10 +68,7 @@ taxonomy::ptr mapping::map_impl(const IfcSchema::IfcTriangulatedFaceSet* inst) {
loop->external = true; loop->external = true;
taxonomy::point3::ptr first, previous; taxonomy::point3::ptr first, previous;
for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) { for (std::vector<int>::const_iterator jt = indices.begin(); jt != indices.end(); ++jt) {
if (*jt < 1 || *jt > max_index) { const taxonomy::point3::ptr& current = resolve(*jt);
throw IfcParse::IfcException("IfcTriangulatedFaceSet index out of bounds for index " + boost::lexical_cast<std::string>(*jt));
}
const taxonomy::point3::ptr& current = points[(*jt) - 1];
if (jt == indices.begin()) { if (jt == indices.begin()) {
first = current; first = current;
} else { } else {
+5 -1
View File
@@ -89,7 +89,11 @@ BIND(IfcRectangleHollowProfileDef);
BIND(IfcRectangleProfileDef); BIND(IfcRectangleProfileDef);
BIND(IfcTrapeziumProfileDef); BIND(IfcTrapeziumProfileDef);
BIND(IfcCShapeProfileDef); BIND(IfcCShapeProfileDef);
// IfcAsymmetricIShapeProfileDef included // In IFC2X3 IfcAsymmetricIShapeProfileDef is a subtype of IfcIShapeProfileDef and is
// mapped by it; from IFC4 onwards it is a standalone type and needs its own binding.
#ifdef SCHEMA_IfcAsymmetricIShapeProfileDef_HAS_BottomFlangeWidth
BIND(IfcAsymmetricIShapeProfileDef);
#endif
BIND(IfcIShapeProfileDef); BIND(IfcIShapeProfileDef);
BIND(IfcLShapeProfileDef); BIND(IfcLShapeProfileDef);
BIND(IfcTShapeProfileDef); BIND(IfcTShapeProfileDef);
@@ -311,8 +311,12 @@ CLI Manual
output. output.
--force-space-transparency arg Overrides transparency of spaces in --force-space-transparency arg Overrides transparency of spaces in
geometry output. geometry output.
--circle-segments arg (= 16) Number of segments to approximate full --circle-segments arg (= 0) Number of segments to approximate full
circles in CGAL kernel. circles in the CGAL kernel. When 0 (the
default) the segment count is derived from
mesher-linear-deflection instead, so curves
stay within the deflection tolerance
regardless of radius.
--cgal-smooth-angle-degrees arg (= -1) --cgal-smooth-angle-degrees arg (= -1)
Angle in degrees under which adjacent Angle in degrees under which adjacent
facets will have averaged vertex facets will have averaged vertex
@@ -72,6 +72,8 @@ Filtering is typically used to select any IFC element or type.
"``IfcPump, location=""Level 3""``", "Locations bubble up the hierarchy. So if a pump is in a space and that space is on Level 3, then you can say ""all pumps on level 3"" which will include that pump in the space." "``IfcPump, location=""Level 3""``", "Locations bubble up the hierarchy. So if a pump is in a space and that space is on Level 3, then you can say ""all pumps on level 3"" which will include that pump in the space."
"``IfcElement, query:""parent.Name""=""My Site""``", "Only elements *immediately* under ""My Site"" in the spatial hierarchy. Unlike the ``location`` and ``parent`` filters, which both match at any depth, the ``parent`` query key resolves the direct parent only, so nested storeys (and their contents) are excluded."
The filter elements syntax works by specifying one or more groups of filters The filter elements syntax works by specifying one or more groups of filters
separated by a ``+`` character. Each filter group will return a set of filtered separated by a ``+`` character. Each filter group will return a set of filtered
elements, and these are unioned together. elements, and these are unioned together.
@@ -111,6 +113,15 @@ will search through all IfcTypeProducts and IfcProducts in the IFC project.
"Parent", "Filter", "``parent{{=}}{{value}}``", "``parent=Foo`` specifies the criteria that elements must be a direct or indirect child in the spatial hierarchy to an element with a ``Name`` attribute with a value of ``Foo``." "Parent", "Filter", "``parent{{=}}{{value}}``", "``parent=Foo`` specifies the criteria that elements must be a direct or indirect child in the spatial hierarchy to an element with a ``Name`` attribute with a value of ``Foo``."
"Query", "Filter", "``query:{{keys}}{{=}}{{value}}``", "``query:types.count=0`` specifies the criteria that elements must have zero type occurrences. The query keys corresponds to the syntax used in the `Getting element values`_ section" "Query", "Filter", "``query:{{keys}}{{=}}{{value}}``", "``query:types.count=0`` specifies the criteria that elements must have zero type occurrences. The query keys corresponds to the syntax used in the `Getting element values`_ section"
.. note::
The ``location`` and ``parent`` filters both match at **any depth** in the
spatial hierarchy. To match only elements *immediately* contained in (or
aggregated under) a spatial element, use the ``parent`` query key, which
resolves the direct parent only. For example,
``query:"parent.Name"="My Site"`` selects elements directly under ``My
Site`` but excludes anything nested inside its sub-storeys or spaces.
When you specify a filter with a ``{{=}}`` check, you can choose from one of When you specify a filter with a ``{{=}}`` check, you can choose from one of
the following comparison checks: the following comparison checks:
@@ -191,7 +202,7 @@ Valid keys are:
"``storey``", "Gets the first IfcBuildingStorey spatial element that an element is contained in." "``storey``", "Gets the first IfcBuildingStorey spatial element that an element is contained in."
"``building``", "Gets the first IfcBuilding spatial element that an element is contained in." "``building``", "Gets the first IfcBuilding spatial element that an element is contained in."
"``site``", "Gets the first IfcSite spatial element that an element is contained in." "``site``", "Gets the first IfcSite spatial element that an element is contained in."
"``parent``", "Gets the parent element in the spatial hierarchy." "``parent``", "Gets the **immediate** parent element in the spatial hierarchy (the direct spatial container, or the direct aggregate/nest/fill/void parent). Combine with ``.Name`` in a query filter to match only immediate children, e.g. ``query:""parent.Name""=""My Site""``."
"``classification``", "Gets the element's classification reference(s)" "``classification``", "Gets the element's classification reference(s)"
"``group``", "Gets the element's group(s)" "``group``", "Gets the element's group(s)"
"``system``", "Gets the element's system(s). This is a subset of group(s)." "``system``", "Gets the element's system(s). This is a subset of group(s)."
@@ -228,10 +228,10 @@ circle-segments
+------+-----------------------+---------+ +------+-----------------------+---------+
| Type | IfcConvert Option | Default | | Type | IfcConvert Option | Default |
+======+=======================+=========+ +======+=======================+=========+
| INT | ``--circle-segments`` | 16 | | INT | ``--circle-segments`` | 0 |
+------+-----------------------+---------+ +------+-----------------------+---------+
Number of segments to approximate full circles in CGAL kernel. Number of segments to approximate full circles in the CGAL kernel. When 0 (the default) the segment count is derived from mesher-linear-deflection instead, so curves stay within the deflection tolerance regardless of radius.
context-identifiers context-identifiers
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
+9
View File
@@ -187,6 +187,15 @@ void IfcUtil::sanitate_material_name(std::string& str) {
} }
void IfcUtil::escape_xml(std::string& str) { void IfcUtil::escape_xml(std::string& str) {
// Strip characters that are illegal in XML 1.0. Control characters other
// than tab (0x09), newline (0x0A) and carriage return (0x0D) are not valid
// XML 1.0 characters and cannot even be represented as numeric character
// references, so they would otherwise make the serialized XML/SVG output
// non-well-formed. Bytes belonging to a valid UTF-8 multibyte sequence are
// always >= 0x80, so filtering on the low control range leaves them intact.
str.erase(std::remove_if(str.begin(), str.end(), [](unsigned char c) {
return c < 0x20 && c != '\t' && c != '\n' && c != '\r';
}), str.end());
boost::replace_all(str, "&", "&amp;"); boost::replace_all(str, "&", "&amp;");
boost::replace_all(str, "\"", "&quot;"); boost::replace_all(str, "\"", "&quot;");
boost::replace_all(str, "'", "&apos;"); boost::replace_all(str, "'", "&apos;");
@@ -305,7 +305,7 @@ ptree* descend(Logger& logger, ifcopenshell::geometry::abstract_mapping* mapping
<IfcSchema::IfcObject, IfcSchema::IfcRelDefinesByProperties, IfcSchema::IfcPropertySetDefinition> <IfcSchema::IfcObject, IfcSchema::IfcRelDefinesByProperties, IfcSchema::IfcPropertySetDefinition>
(logger, object, &IfcSchema::IfcObject::IsDefinedBy, &IfcSchema::IfcRelDefinesByProperties::RelatingPropertyDefinition); (logger, object, &IfcSchema::IfcObject::IsDefinedBy, &IfcSchema::IfcRelDefinesByProperties::RelatingPropertyDefinition);
#ifdef SCHEMAS_HAS_IfcPropertySetDefinitionSet #ifdef SCHEMA_HAS_IfcPropertySetDefinitionSet
aggregate_of<IfcSchema::IfcPropertySetDefinitionSet>::ptr property_set_sets = get_related aggregate_of<IfcSchema::IfcPropertySetDefinitionSet>::ptr property_set_sets = get_related
<IfcSchema::IfcObject, IfcSchema::IfcRelDefinesByProperties, IfcSchema::IfcPropertySetDefinitionSet> <IfcSchema::IfcObject, IfcSchema::IfcRelDefinesByProperties, IfcSchema::IfcPropertySetDefinitionSet>
(logger, object, &IfcSchema::IfcObject::IsDefinedBy, &IfcSchema::IfcRelDefinesByProperties::RelatingPropertyDefinition); (logger, object, &IfcSchema::IfcObject::IsDefinedBy, &IfcSchema::IfcRelDefinesByProperties::RelatingPropertyDefinition);