Compare commits

...

343 Commits

Author SHA1 Message Date
Dion Moult 9f2f1954f3 Interface mockup 17 2026-05-11 12:57:34 +10:00
Dion Moult b4323ba67a Interface mockup 16 2026-05-11 12:18:38 +10:00
Dion Moult b79070456f Interface mockup 15 2026-05-11 09:17:45 +10:00
Dion Moult 5cd4dbf603 ifcviewer: per-element visibility (H / Shift+H / Alt+H)
Adds VisibilityState, a CPU-only sibling to SelectionState.  It owns
the canonical hidden-id set plus a flat per-object_id byte vector that
the cull's hot path queries inline (bounds check + byte load + compare
per surviving instance).  Hidden elements never reach the visible[]
SSBO so they don't draw or pick — matching Blender/CAD convention.

ViewportWindow registers every streamed and sidecar-cached object_id
with the new state, resets it on clearScene, and connects the changed
signal to invalidate cached cull state.  Three convenience verbs:
hideSelectedElements (union into hidden), isolateSelectedElements
(replace hidden with live-object_ids minus selection, skipping
model-hidden models so element-hide doesn't pile on top of model-hide),
and showAllElements (clears the override; model-hidden models stay
hidden, per the user's spec).

Bound in the View menu: H hide, Shift+H isolate, Alt+H show all.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 07:33:09 +10:00
Dion Moult 2c18aebdcc Interface mockup 14 2026-05-10 22:04:03 +10:00
Dion Moult cb0a70ac7e Configurable navigation bindings 2026-05-10 21:55:06 +10:00
Dion Moult cf3f3c2c00 ifcviewer: promote runtime perf knobs, drop always-on settings
Promotes five env-var-driven knobs to AppSettings + the settings dialog
(min pixel radius, motion min pixel radius, LOD1 pixel threshold, HiZ
resolution, HiZ on/off).  Defaults: motion min pixel radius is now 10
(was 0/disabled) and IFC_HIZ_MOTION is on by default — the strict
view-projection gate reverts via env var =0 when chasing HiZ
correctness bugs.  ViewportWindow connects each *Changed signal so
changes invalidate cached cull state and take effect on the next
frame.

Removes "Load Property Data Source" and "Apply Coordinate Operation"
from the settings dialog: both are now hardcoded on.  The basic-info
property fallback (used when there's no live IFC source for an object,
e.g. .ifcview without a sibling) now triggers organically when
ElementRegistry::findEntity returns null instead of being gated on a
user toggle.  Federation::guessFederatedFalseOrigin lost its
apply_coordinate_operation parameter and now uses
georef.has_coordinate_operation directly.

src/ifcviewer/settings.rst documents the remaining diagnostic env vars
(IFC_HIZ_MOTION, IFC_CULL_THREADS, IFC_SKIP_MDI, IFC_MAX_SUBDRAWS,
IFC_FPS_HITCH_MS, IFC_SUBDRAW_DIAG, IFC_LOD_*) plus a cross-walk from
the old promoted-knob env-var names to their new QSettings keys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 19:37:20 +10:00
Dion Moult 7029feca0a ifcviewer-full: volume tool with HUD + per-object labels
Mirrors the Area tool's display: HUD shows total volume + object count,
each selected object gets a label at its world-AABB centroid showing
its individual volume.  Gated behind ToolMode::Volume (Ctrl+Shift+V) so
it stays out of the way until invoked.

Volume is a passive tool — selection works as in None (multi-select,
modifier toggle, box-select all keep working).  Area / Length still
intercept clicks through surfacePickedInTool.

Adds volumesPerObject() reusing the same mesh-cached readback path as
volumeOfObjects, so the per-object split costs no extra GL readbacks.
computeObjectAabb is promoted to public for the centroid lookup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 08:45:08 +10:00
Dion Moult f75526e6c3 ifcviewer: multi-selection + box-select with active highlight
SelectionState (new) owns the multi-set, the "active" id (last single-
clicked), and a per-object_id flags SSBO bound at binding=3.  Main
shader reads sel_flags[v_object_id] for the in-set tint and a separate
u_active_id uniform for a stronger tint on the active.

Click semantics: plain replaces, Shift/Ctrl toggles.  LMB-drag past 5px
boxes the rect through a pick-pass readback — plain replaces, Shift
adds, Ctrl removes; box-select preserves the active.  Drag promotes
regardless of start point so a press on geometry doesn't disqualify it.

Sidecar fast-path bulk-loads instances, so noteObjectId is also called
from the apply path — without it the flags buffer was sized to 1 slot
while object_ids were in the 100k+ range and the in-set bit was lost.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 22:04:00 +10:00
Dion Moult 6d4fe17975 Add model coordinates settings
Add the federation/model settings dialog and related interface wiring for model coordinate configuration.

Generated with the assistance of an AI coding tool.
2026-05-09 22:00:19 +10:00
Dion Moult c1b4b5c1d0 Rename interface modules
Move interface features from panels into modules, move AddModelDialog into the models module, and rename module Widget surfaces to Panel.

Generated with the assistance of an AI coding tool.
2026-05-09 19:43:26 +10:00
Dion Moult 994fa4bc03 ifcviewer: fix progress bar capping at ~1/n during streaming
The streamer carved [0,100] evenly across N prioritised contexts (and
again across the net/gross passes).  In practice nearly every element
yields from the first (Body) context, so smooth progress only ever
filled range/n of the bar — typically ~20% — before snapping forward.
Drive progress directly from yielded element count over total instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 07:50:32 +10:00
Dion Moult 3e066443be Interface mockup 13 2026-05-09 07:11:58 +10:00
Dion Moult d475553cd0 ifcviewer-full: per-patch area labels + skip redundant 2-pt perpendicular
Area mode now drops a label at every connected coplanar patch — a
BFS sweep over selected_ restricted to each mesh's edge adjacency
identifies the components, then each component gets one label at
its area-weighted centroid in world space.  Two clicks on
different walls now show two distinct numbers; a single BFS-grown
wall face stays one number across all its triangles.

The 2-pt length perpendicular line is now omitted when |perp|
matches any of ΔX/ΔY/ΔZ within 1mm — the surface-aligned-with-
axis case where the perpendicular is already shown by one of the
RGB legs.  Avoids redundant double-readout on axis-aligned walls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 08:55:26 +10:00
Dion Moult 65e40b9923 ifcviewer-full: 1-pt laser, 2-pt XYZ + perpendicular, sharper visuals
Length tool's 1-pt laser is now hybrid:
  - On any surface, a coplanar BFS finds the connected face patch
    around the click and projects its vertices into the surface
    tangent basis to get an exact bounding-box extent.  Stops at
    the face edge by construction — no overshoot into adjacent
    geometry like the previous tangent-raycast did.
  - On near-horizontal surfaces (|n.z| > 0.85, i.e. floors and
    ceilings) it additionally fires one raycast in +n to the
    opposing surface — so a single floor click reports X extent +
    Y extent + ceiling height.
  - Bars are labelled by their dominant world axis (X/Y/Z) instead
    of "vertical/horizontal", which reads cleanly on either kind
    of surface.

The 2-pt readout now draws the world-space XYZ stair-step (red ΔX,
green ΔY, blue ΔZ) with each leg labelled, and a dashed
perpendicular line whenever the two picks landed on near-parallel
surfaces — useful for measuring across walls.

To support multiple line styles per frame, OverlayRenderer's
setOverlayLines takes std::vector<LineGroup> instead of a single
inline style; each group has its own color/halo/width and an
optional dash period.  The line shader gained v_along_px +
u_dash_period uniforms (screen-space dashes), and both line and
point shaders now use a sharp step() for the inner→stroke
transition with AA only on the outer halo edge — much crisper than
the previous soft band.  Default visual style trimmed: 1.5px lines
(0.5px halo), 6px dots (1px halo), opaque black halo.

Also adds ViewportWindow::raycast(origin, dir, RaycastHit&) — CPU
ray traversal of each model's per-instance BVH followed by
Möller-Trumbore against the candidate meshes' triangles (lazily
read back, cached per call).  Used by the floor/ceiling laser path
today and reusable for any future raycast-based feature.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 08:50:02 +10:00
Dion Moult 29fdf629f9 Fix sidecar source loading
Treat .ifcview sources as geometry-only cache inputs, stop guessing sibling data paths, and only start data-source loading for real model sources after a sidecar hit.

Generated with the assistance of an AI coding tool.
2026-05-07 21:41:50 +10:00
Dion Moult d9eb67b7a8 ifcviewer-full: length tool (2/3/4+ point distance, angle, polygon area)
ViewportWindow trades the area_tool_active_ bool for an enum ToolMode
{None, Area, Length}; the existing surfacePickedInTool signal carries
both, the app dispatches on toolMode().  Esc exits any active tool;
Backspace/Delete in length mode emits toolBackspacePressed which the
length tool uses to remove the last point.

LengthMeasurement collects clicked world-space points and adapts the
readout: 2pt → distance + axis-aligned ΔX/ΔY/ΔZ, 3pt → angle at the
middle vertex + triangle area, 4+pt → best-fit-plane PCA + shoelace
when planar (RMS plane distance / bbox diag < 1e-3) else fan
triangulation, with the chosen method labelled in the readout.  Per-
segment lengths float at each midpoint.

OverlayRenderer grows three new pipelines to support this:
  - point sprite shader: gl_PointCoord-based outlined disc with
    fwidth-smoothed inner/stroke bands, a single draw call.
  - line shader: CPU-expand each segment to 6 verts carrying both
    endpoints + (side, along) corner index; vertex shader computes
    the screen-space perpendicular and offsets accordingly.  Real
    outlined lines independent of the driver's glLineWidth clamp.
  - screen-space rect shader: HUD + label backgrounds drawn as raw
    GL quads in NDC.  QPainter::fillRect on QOpenGLPaintDevice was
    silently dropping fills across drivers; bypassing it entirely
    via this shader makes backgrounds reliable.  Cull-face is also
    explicitly disabled here — GL_TRIANGLES respects it but the
    line/point primitives don't, so this was the one path needing
    the fix.

setOverlayLines / setOverlayPoints take an inner color, an outline
color, and an extra-pixels-per-side stroke amount.  Lines + points
draw with GL_ALWAYS so measurement annotations stay visible through
geometry; highlight tris stay depth-aware (GL_LEQUAL) so area
shading still tints the surface in place.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 21:41:47 +10:00
Dion Moult bae1eddda9 Interface mockup 12 2026-05-07 17:10:04 +10:00
Dion Moult ad62dd6d91 ifcviewer: viewport overlay subsystem (highlight tris + HUD text)
New OverlayRenderer module owns every client-supplied overlay primitive
drawn after the main pass: tinted, depth-aware highlight triangles via
its own GL shader, and top-left HUD text via QPainter on a
QOpenGLPaintDevice.  Public surface on ViewportWindow is just two
forwarders (setHighlightTriangles, setHudText).

ViewportWindow's MeshLocalPick now exposes the instance's composed
transform so consumers can map mesh-local geometry back to world
space without re-querying.  AreaMeasurement uses both: its selection
key is now (object_id, tri) so per-instance highlighting works for
two distinct walls sharing a mesh, and on every pick it rebuilds the
world-space tri list and the HUD readout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 16:43:38 +10:00
Dion Moult d565dc3ff3 Interface mockup 11 2026-05-07 14:45:23 +10:00
Dion Moult a340e6cf9a Interface mockup 10 2026-05-07 14:17:06 +10:00
Dion Moult 359693c562 Interface mockup 9 2026-05-07 12:21:55 +10:00
Dion Moult 016c278748 ifcviewer-full: console-print accumulating coplanar-patch area tool
Adds a click-to-measure area mode triggered by Ctrl+Shift+A.  Each LMB
click expands the picked triangle into its connected coplanar patch
(BFS over shared edges, dot(normal, seed) > 0.9999); re-clicking
removes that patch; Alt+LMB skips expansion for a single triangle.
Picks across different meshes accumulate as separate patches.

ViewportWindow gains pickMeshLocalAt (screen pick → mesh-local hit
via inverse composed transform) and a tool-mode pattern mirroring
the section tool (toggleAreaTool, surfacePickedInTool signal,
areaToolToggled signal, Esc to exit).  Per-mesh adjacency is built
lazily on first pick of each mesh and dropped on tool toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 12:03:22 +10:00
Dion Moult f2b655fcf6 ifcviewer-full: print object volume on click via lazy GPU readback
Adds neutral primitives on ViewportWindow (readbackMeshTriangles,
findInstance) so consumers can compute per-object geometry queries
without the library retaining a CPU triangle copy. Measurement.cpp in
ifcviewer-full uses them to sum signed-tetrahedra in mesh-local space,
weighted by |det(placement_3x3)| per instance for mapped-item scaling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 11:25:30 +10:00
Dion Moult 0bb6df6a6b ifcparse: skip flush+compact for read-only RocksDB on destruction
Read-only handles reject Flush/CompactRange, so the destructor's status
assertion always fired on shutdown when the streamer's sidecar was
opened with read_only=true. Track the flag and skip the write path; also
guard against a null db when the initial open failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:16:15 +10:00
Dion Moult f07afda09c Interface mockup 8 2026-05-06 21:35:12 +10:00
Dion Moult 5b2721c1c4 Interface mockup 7 2026-05-06 13:13:37 +10:00
Dion Moult 0d5fa0c20c Interface mockup 6 2026-05-06 12:36:28 +10:00
Dion Moult 802ddf8ff9 Interface mockup 5 2026-05-06 10:47:03 +10:00
Dion Moult e20cea221b Interface mockup 4 2026-05-05 19:59:51 +10:00
Dion Moult ae66550cae Interface mockup 3 2026-05-05 18:02:02 +10:00
Dion Moult 5ccb655e10 Interface mockup 2 2026-05-05 09:40:58 +10:00
Dion Moult 278c1e5068 Interface mockup 2026-05-05 07:29:32 +10:00
Dion Moult 095e4a1677 ifcviewer: nested groups in federation, with cascading visibility
Federation gains a nested Group tree (id, display_name, visible,
children); models reference a single group via Model::group_id.
Visibility cascades: a model is effectively visible only when its own
flag is on and every ancestor group is visible.  Persistence nests
groups directly in the JSON — no parent_id field.

ifcviewer-full surfaces this in the element tree with right-click
menus to create / rename / move / remove groups, move models between
groups, and toggle group visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:39:28 +10:00
Dion Moult de5eb9641f ifcviewer-full: hide and remove model actions
Right-click a model root in the Elements tree to get Hide/Show and
Remove.  Hide flips the federation's per-model visible flag (already
round-tripped to .ifcfed), pushes ViewportWindow::hideModel/showModel,
and italicises + greys the tree root as a visual cue.  Remove drops
the model from the viewport, the SceneLoader (streamer + caches), the
MainWindow UI maps and tree, and the Federation — disabled while the
model is the active load.

Visibility is reapplied on each model's load completion (sidecar or
stream), so a federation saved with hidden models opens with them
hidden.  clearScene() now also drops SceneLoader state so streamers
no longer leak across federation transitions.

API additions:
- Federation::setModelVisible + modelVisibilityChanged signal
- SceneLoader::removeModel + isLoadingModel

Tests cover the setter (dirty + signal + idempotence + unknown id);
extends the existing round-trip test to actually exercise the
visibility load/save it always claimed to.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:22:14 +10:00
Dion Moult 4597be7fda ifcviewer: link Placement.cpp into test_federation
Commit f7add7f4 split getAxis2Placement out of an anonymous helper in
Geolocation.cpp into a shared Placement.{h,cpp}, but the test_federation
target's source list wasn't updated.  The test binary failed to link
with `undefined reference to getAxis2Placement(express::Base const&)`
from Geolocation::getWcs.  Add Placement.cpp to the explicit-source
list — it has no Qt dependency, only ifcparse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:21:50 +10:00
Dion Moult ca2e866c45 serializers: skip_supertypes filter in RocksDbSerializer streaming write
Plumbed through to the Python convert_path_to_rocksdb wrapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 07:28:12 +10:00
Dion Moult e84767c22b ifcviewer-full: multi-select directories in Add Database dialog
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 07:14:52 +10:00
Dion Moult f7add7f412 ifcviewer: auto-guess FederatedFalseOrigin on first model added
When the user adds a model into a fresh, untitled federation that still
has the default (0,0,0, no rotation) FederatedFalseOrigin, derive an
origin from the first instance's placement_transformation (lifted
through CoordinateOperation when enabled) and the helmert grid-north
baked into ModelGeoref::coordinate_operation_meters.  Multi-file batches
naturally settle: whichever load finishes first anchors the federation,
the rest see a non-default origin and skip.  Saved .ifcfeds keep their
authoritative origin.

Adds Placement.{h,cpp} (port of util/placement.py — a2p,
get_axis2placement, get_local_placement) so Geolocation no longer needs
its own anonymous getAxis2Placement, and xaxis2angleDeg in Geolocation
mirroring util/geolocation.xaxis2angle.

SceneLoader captures the first instance's placement_transformation from
either the sidecar's InstanceCpu[0] or the streamer's first
InstanceChunk, so the guess works on both load paths without re-reading
the IFC.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 10:04:39 +10:00
Dion Moult 8ffdb8f0b9 ifcviewer: cache CoordinateOperation in sidecar (v10 -> v11)
Previously, applyCoordinateOperationToViewport — which pushes both
CoordinateOperation and ModelTransformation — was only called on
paths that required the IFC source to be loaded
(onLoadedFromStream and onDataSourceReady).  Sidecar-only loads
(loadDataSource off, or no .ifc/.rdb sibling) silently lost both
stages.

Cache the per-model georef + unit scales in the sidecar itself so
the IFC source isn't needed to apply them:

  SidecarData gains
    coordinate_operation_meters[16]  // column-major
    project_length_to_meters
    map_unit_to_meters
    has_coordinate_operation

  148 B fixed block written/read between instances and elements.
  SIDECAR_VERSION 10 -> 11; existing sidecars rebuild on next load.

  MainWindow::writeSidecarForModel populates the block from
  loader_->modelGeoref(mid) before writeSidecar.

  SceneLoader::applySidecarData restores it into the model's
  ModelGeoref + sets has_georef = true, so subsequent
  loader_->modelGeoref(mid) calls return the cached data without
  needing the IFC.

  MainWindow::onLoadedFromSidecar now calls
  applyCoordinateOperationToViewport(mid) directly — both
  CoordinateOperation and ModelTransformation land at sidecar-load
  time, no longer waiting on a possibly-never-arriving data source.

Edits to the IFC's IfcMapConversion don't invalidate the cache —
delete the .ifcview manually if the source's georef changes.  This
matches the existing cache-invalidation contract.

Tests: round-trip the new fields through the existing sidecar
fixture; assert SIDECAR_VERSION == 11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:08:45 +10:00
Dion Moult e0e143bab5 ifcviewer: F to frame selection from tree, debug coord dump
Tree -> viewport selection was already wired (onTreeSelectionChanged
calls setSelectedObjectId), but pressing F afterwards routed to the
focused tree widget rather than the viewport, so framing didn't fire.
Add a window-level View > Frame Selected QAction with Qt::Key_F that
delegates to ViewportWindow::focusOnSelectedObject — works regardless
of which child widget has focus.  The viewport's own F handler stays
in place for when the viewport itself owns focus.

For debugging coordinate problems, add View > Print Selected Coords
(Ctrl+Shift+P) -> ViewportWindow::printSelectedObjectCoords, which
qInfo's:
  - a sample vertex (first vertex of the selected mesh, decoded on
    demand from the quantised VBO so no extra CPU storage is needed);
  - placement_transformation (the per-instance matrix that maps the
    sample vertex from mesh-local into the model's pre-georef frame);
  - global = CoordinateOperation . placement_transformation (where
    the IFC's own IfcCoordinateOperation has been folded in);
  - the sample vertex transformed through both matrices.

The print is a no-op when nothing is selected or GL hasn't initialised.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 19:01:50 +10:00
Dion Moult 8f3eaa35d7 ifcviewer-full: per-model ModelTransformation editor
ModelTransformationDialog edits one federation model at a time.  Top
combobox picks the model; below it the form covers the four pieces
of authoring intent:

  - AFrame radio: ModelLocal vs ModelGlobal
  - Point A: 3 doubles, label switches between "model project length
    unit" and "model map unit" with the radio
  - Point B: 3 doubles in federation units (label reflects current
    FederationConfig.unit_*)
  - Rotation: rx/ry/rz in degrees, intrinsic XYZ
  - Pivot: 3 doubles in federation units

Switching models discards unsaved form edits — Ok saves the
currently-visible model, Cancel discards.  On Ok calls
Federation::setModelTransformation, which fires
modelTransformationChanged → MainWindow recomposes that model in
the viewport.

Reachable from File > Model Transformations.

End-to-end is now editable: open a federation, edit federation unit /
false origin from one dialog, edit any model's transformation from
the other, watch the viewport recompose live.  Visual verification
on a real model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 08:06:25 +10:00
Dion Moult 81b1efdfd6 ifcviewer-full: federation settings dialog (unit + false origin)
New FederationSettingsDialog edits the federation-wide unit and the
FederatedFalseOrigin (XYZ + Z-rotation in that unit).  On Ok it calls
Federation::setConfig + setFederatedFalseOrigin, which fire the
granular Federation signals MainWindow listens to → viewport
recomposes immediately.

Reachable from File > Federation Settings.  Unit picker is a fixed
combobox of common length units (metres / mm / cm / km / ft / in /
yd / mi); each item carries (prefix, name) in itemData so saving
round-trips correctly.  Per-model ModelTransformation editor still
to come — that's a per-model dialog reachable from the model entry,
not the federation-wide settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 08:02:59 +10:00
Dion Moult 4a9af13626 ifcviewer: wire FederatedFalseOrigin / ModelTransformation to viewport
Federation grows three granular signals so consumers can recompose only
what's affected:
  - configChanged()                        — federation unit changed
  - federatedFalseOriginChanged()          — stage 3 changed
  - modelTransformationChanged(fed_id)     — stage 4 changed for one model

Emitted from setConfig / setFederatedFalseOrigin / setModelTransformation
in addition to the existing dirtyChanged.

MainWindow gains applyFederatedFalseOriginToViewport and
applyModelTransformationToViewport helpers.  Each composes the matrix
from the current federation state (using composeFederatedFalseOrigin /
composeModelTransformation, which already exist on Federation.h) and
pushes to the viewport's setFederatedFalseOrigin /
setModelTransformation.  ModelTransformation reads ModelUnits and the
active CoordinateOperation matrix from SceneLoader::modelGeoref so
ModelLocal-frame `a` lifts correctly through stage 2 when authored.

Wiring:
  - federation.federatedFalseOriginChanged -> applyFederatedFalseOriginToViewport
  - federation.configChanged               -> stage 3 + walk all models for stage 4
  - federation.modelTransformationChanged  -> stage 4 for that one model
  - applyCoordinateOperationToViewport now also re-pushes stage 4 (the
    compose result depends on the active stage 2 when a_frame is ModelLocal)
  - openFederation() pushes the loaded FederatedFalseOrigin once load
    completes; per-model stage 4 falls out of the existing
    onLoadedFromStream / onDataSourceReady path.

End-to-end pipeline is now active under the AppSettings toggle: edit
the federation in memory and the viewport recomposes immediately.  UI
for editing (form-based dialog) still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 07:58:26 +10:00
Dion Moult e9d577890e ifcviewer: gate CoordinateOperation on a settings toggle
AppSettings.applyCoordinateOperation (default false, persisted via
QSettings) controls whether each loaded model's IfcCoordinateOperation
is applied at upload time.  Off keeps models in their local engineering
frame (current behaviour).  On lifts each model into map coordinates
via the stage-2 georef matrix cached on SceneLoader.

MainWindow:
  - applyCoordinateOperationToViewport(mid) reads the toggle, fetches
    the model's ModelGeoref, and pushes either the
    coordinate_operation_meters matrix or identity to the viewport.
  - Called from onLoadedFromStream (streamer path) and onDataSourceReady
    (sidecar-hit path, where the IFC arrives asynchronously).
  - Subscribed to AppSettings::applyCoordinateOperationChanged: a
    runtime toggle walks every loaded model and re-applies, so users
    can flip georef on/off without reloading.

SettingsWindow gains a "Apply Coordinate Operation" checkbox alongside
the existing per-load toggles.

Default-off so the change is opt-in — users with georeferenced models
(UTM coords etc.) can flip the toggle to see them in their map frame
once they're ready.  Visual verification on a real georeferenced
model still pending.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 07:45:40 +10:00
Dion Moult 7f29850022 ifcviewer: compose federation pipeline at SSBO upload
InstanceCpu now carries both placement_transformation (raw streamer
output, the iterator's per-shape transform with vertex-rebasing offset
folded in) and transform (the composed FederatedFalseOrigin ·
ModelTransformation · CoordinateOperation · placement_transformation
result that lands in the SSBO).  World AABBs are recomputed from the
composed transform — frustum/BVH culling sees the actual rendered
position regardless of stage state.

ViewportWindow gains:
  - ModelGpuData::coordinate_operation_meters / model_transformation_meters
  - federated_false_origin_meters_ (federation-wide member)
  - composeInstanceFromPlacement / recomposeAndUploadModel helpers
  - public setFederatedFalseOrigin / setModelCoordinateOperation /
    setModelTransformation

Each setter rewrites the affected model's SSBO, refreshes the
reflection flags, and rebuilds the BVH.  Defaults are identity, so
behaviour is unchanged until something wires a setter up — that's
the next commit (MainWindow listening to Federation::dirtyChanged
and SceneLoader::modelGeoref ready signals).

Sidecar bumped 9 -> 10: InstanceCpu grew 104 B -> 168 B.  Existing
sidecars rebuild on next load.  v10 sidecars store
placement_transformation, so they remain reusable across .ifcfeds —
the composed transform on disk is overwritten with the right one
on load.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:41:25 +10:00
Dion Moult 92c3b4c308 ifcviewer: rename stage1/2/3/4 to their proper IFC-mapped names
Replace the placeholder "stage1/2/3/4" terminology with names that
mirror the IFC concepts each step represents:

  stage 1 -> PlacementTransformation
            (per-instance, derived from IfcObjectPlacement)
  stage 2 -> CoordinateOperation
            (per-model, IfcCoordinateOperation / IfcMapConversion)
  stage 3 -> FederatedFalseOrigin
            (federation-wide, user-nominated)
  stage 4 -> ModelTransformation
            (per-model, user-authored within the federation)

API renames:
  FederationOrigin            -> FederatedFalseOrigin
  ModelTransform              -> ModelTransformation
  composeFederationOrigin     -> composeFederatedFalseOrigin
  composeModelTransform       -> composeModelTransformation
  Federation::setOrigin       -> Federation::setFederatedFalseOrigin
  Federation::setModelTransform -> Federation::setModelTransformation
  Federation::origin()        -> Federation::federatedFalseOrigin()
  Federation::Model::transform_intent -> ::model_transformation
  ModelGeoref::stage2_meters  -> ::coordinate_operation_meters
  ModelGeoref::has_stage2     -> ::has_coordinate_operation

JSON keys in .ifcfed renamed in lockstep:
  origin                -> federated_false_origin
  transform_intent      -> model_transformation

The streamer's per-mesh "stage 1 vertex rebasing" comment is reframed:
the rebase isn't its own stage — it's a precision optimisation applied
inside the PlacementTransformation step.

All 36 ctest cases pass under the new names.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 20:06:06 +10:00
Dion Moult 600d3a3460 ifcviewer: stage 1 mesh-vertex rebasing in the streamer
Per-mesh, when the iterator's first source vertex is more than 1 km
from origin (matching bonsai's distance_limit default), pick that
vertex as a rebase offset.  buildMeshChunk subtracts the offset from
every emitted vertex (in double precision, narrowed to float at the
end), and each instance's placement matrix is post-multiplied by
T(+offset) so world position is preserved by construction:

    T(+offset) · (verts - offset)   ≡   T · verts

The offset is stored on the per-mesh MeshAabb so all instances of the
same mesh apply the same compensation.  When the mesh's first vert is
near origin (the common case), offset is zero and the work is a no-op
beyond a couple of FP ops per vertex.

Improves float32 precision in the vertex buffer for georeferenced
models (UTM coords etc.) where verts would otherwise have to encode
million-metre magnitudes directly — at 1e6 m, float32 resolves about
6 cm, ruining sub-millimetre detail in the buildings themselves.

Visual verification on a real UTM-coords model still pending — the
math preserves world position by construction but precision claims
warrant a hand-test in the viewer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:39:13 +10:00
Dion Moult bea4e38e65 ifcviewer: cache per-model georef in SceneLoader
Adds ModelGeoref { ModelUnits units; Eigen::Matrix4d stage2_meters; bool
has_stage2; } and computeModelGeoref(file*) in Federation.{h,cpp}.  The
helper reads the project length unit, IfcProjectedCRS.MapUnit, helmert
parameters and WCS, and reduces them to a metres-in/metres-out stage 2
matrix using the existing Geolocation + Unit primitives.  When the model
has no IfcMapConversion it returns an identity stage_2 with has_stage2
== false, so the upload pipeline can branch cheaply.

SceneLoader::Model gains a cached ModelGeoref; SceneLoader::modelGeoref
(uint32_t mid) computes lazily on first call (returns nullptr when the
IFC file isn't available yet — happens on the sidecar-hit path before
the data-source thread populates the streamer) and serves from cache
afterwards.

Not yet consumed by the upload pipeline; that's the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:31:08 +10:00
Dion Moult 5386c9ec69 ifcviewer: add stage 3+4 data model and compose helpers to Federation
Adds the structs that were briefly in src/ifcviewer/Federation.{h,cpp}
two commits ago, now folded into the merged Federation alongside the
file persistence layer:

  - FederationConfig: federation-wide unit ({prefix, name}).  Default
    METRE; one-of an IfcSIUnit name with optional prefix or an
    IfcConversionBasedUnit name.
  - FederationOrigin: stage 3 — XYZ in federation unit + Z-rot.
    Composes to R_z · T(-xyz_meters), nominating a point as origin.
  - AFrame + ModelTransform: stage 4 intent — A (model project or
    map unit, per a_frame), B and pivot (federation unit), full
    intrinsic-XYZ Euler rotation in degrees.
  - ModelUnits: per-model project_length_to_meters / map_unit_to_meters
    cached at load time.

Free functions composeFederationOrigin and composeModelTransform
return Eigen::Matrix4d in metres.  composeModelTransform takes the
model's stage-2 georef matrix so it can lift `a` into metres when
authored in ModelLocal.

Federation gains config_, origin_ members + setters that emit
dirtyChanged.  Each Model carries a transform_intent.  JSON I/O
emits config / origin always; transform_intent only when non-default.
Schema stays "ifcfed/1" — additive, optional, sane defaults.

Five new tests: round-trip of the new fields, default-omission
behaviour, two compose smoke tests for FederationOrigin, and one
verifying the "pivot at B preserves A→B" invariant of
composeModelTransform.  All 36 ctest cases pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 15:15:37 +10:00
Dion Moult ecf0a5a4e1 ifcviewer: merge Federation classes into the lib
Move src/ifcviewer-full/Federation.{h,cpp} (and its tests) into
src/ifcviewer/ so the lib stays the single source of truth for the
federation data model.  Restores the original "agnostic lib usable
from ifcviewer-full and ifcviewer-minimal alike" framing.

Drop the unused per-model transform[16] / has_transform field — it
was round-trip-only with no UI to author it, and is being replaced
by an intent-based ModelTransform in the next commit.  No real
.ifcfed in the wild populated this field; old files still load
(unknown JSON keys ignored), they just lose the unused transform.

Replaces the pure-data-model Federation.{h,cpp} that was added a
few commits earlier — that file's structs and compose helpers
return as part of the merged Federation in commit 6.

ifcviewer-full's per-app tests dir is removed (test_federation was
the only one); BUILD_IFCVIEWER_TESTS now wires test_federation in
under src/ifcviewer/tests/, with the Qt6::Core/Gui/Test dependency
declared inline since unlike the other Tier-1 tests it has to pull
Qt in.  All 31 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:41:41 +10:00
Dion Moult 540f3acf52 ifcviewer: add Federation data model in Federation.{h,cpp}
FederationConfig holds the federation-wide display unit (defaults to
METRE; on load the first model's MapUnit becomes the default).
FederationOrigin captures stage 3 — XYZ in federation unit + Z-rot —
and composes to R_z · T(-xyz_meters), nominating a point as the new
origin and rotating around it.  ModelTransform captures stage 4 —
A in model project or map unit (per AFrame), B and pivot in
federation unit, full intrinsic-XYZ Euler rotation — and composes to
T(B - R_pivot · A) · R_pivot, rotating first then translating so the
rotated A lands at B.

ModelUnits caches per-model project/map unit-to-metres scales so the
compose helpers don't need to re-read the IFC each call.

All composed matrices are in metres; user-typed numbers are stored
in source units to round-trip without precision loss, and converted
on compose via Unit.h.

Not yet wired into the streamer or .ifcfed I/O — pure data model and
maths, integrated in subsequent commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:56:23 +10:00
Dion Moult b3d29c4081 ifcviewer: add helmertMetersFromParameters and getMapUnit
helmertMetersFromParameters builds the helmert transformation as a
meter-input/meter-output 4x4 directly from parsed parameters, bypassing
autoLocal2Global's normalisation step.  This preserves
IfcMapConversionScaled.FactorX/Y/Z in the rotation block so the factor
applies to placement translations when the matrix is precomputed
per-model and composed with placements at upload time.  For ordinary
IfcMapConversion (factor = 1) this is bit-identical to
autoLocal2Global; only diverges on rare surveyed models with non-unit
factors, where it is the only correct behaviour.

getMapUnit returns IfcCoordinateOperation.TargetCRS.MapUnit so callers
can resolve the unit-to-metres scale via Unit.h's siScaleFromNamedUnit.

autoLocal2Global is unchanged — kept as a clean port of the python
ifcopenshell.util.geolocation reference impl for one-shot
project-units-in / map-units-out callers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:43:27 +10:00
Dion Moult 0d3849737c ifcviewer: port unit utilities to C++ in Unit.{h,cpp}
Mirrors selected helpers from ifcopenshell.util.unit: SI prefix
multipliers, the conversion-based-unit table (foot/inch/etc -> SI
metres), siScaleFromNamedUnit (walks IfcConversionBasedUnit chains
down to IfcSIUnit), getUnitAssignment / getProjectUnit /
calculateUnitScale, and convert / convertUnit.  Lives in
src/ifcviewer/ for now alongside Geolocation; will move out when
ifcopenshell.util is ported to C++.

Needed by upcoming Geolocation fix (e/n/h on IfcMapConversion are
in MapUnit, must be converted to metres for the meter-by-default
iterator output) and by the federation module (display-unit
conversion when the user changes the federation unit).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 12:12:29 +10:00
Dion Moult a2db0a68a4 ifcviewer: port auto_local2global to C++ in Geolocation.{h,cpp}
Mirrors ifcopenshell.util.geolocation: HelmertTransformation parameters
(IfcMapConversion / IfcMapConversionScaled / IfcRigidOperation, plus
IFC2X3 ePSet_MapConversion), get_wcs from IfcGeometricRepresentationContext,
local2global, and auto_local2global.  Lives in src/ifcviewer/ for now;
will move out when ifcopenshell.util is ported to C++.

Not yet wired into the streamer.  A subsequent commit fixes the
unit handling for the iterator's meter-by-default output.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 11:58:36 +10:00
Dion Moult 3e2869b6aa ifcviewer: re-enable contribution culling in ortho mode
The previous projection-toggle commit short-circuited contribution
culling when projection_ortho_ was set — the formula
r_px = focal_px * r / dist looks like it depends on per-instance
distance, which doesn't apply in ortho.  Result: every frustum-
visible object drew, including sub-pixel ones, and FPS tanked on
top-down plan views.

In ortho the projected pixel size of a bounding sphere is constant:
r_px = pixels_per_world * r, where pixels_per_world equals the
existing focal_px / camera_distance_ (the ortho box was sized to
match perspective at the pivot's distance).  So the same formula
gives the right answer if we replace per-instance dist with
camera_distance_.

cullModelCpu now does that substitution for both contributionPasses
and pixelRadius (the latter feeds LOD1 selection too — sub-pixel
objects pick LOD1 in ortho the same way they do in perspective).
The "camera inside AABB" early-return is kept; it only fires in
perspective where dist→0 would otherwise blow up r_px, and is
harmless in ortho.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 15:05:32 +10:00
Dion Moult db1a2705a3 ifcviewer: edge enhancement post-pass
Adds a per-frame depth-laplacian pass that darkens pixels at sharp
depth discontinuities — silhouettes, overlapping-surface boundaries,
section-cut edges.  Catches the wall-against-wall and slab-against-
ceiling cases that the cavity hint in the lighting shader misses.

Implementation:

- New edge_depth_fbo_ / edge_depth_tex_ — single-sample D24S8 the
  size of the window.  After the main draw, blit the default FB
  depth into it (handles MSAA resolve in the same call).
- Fullscreen triangle generated from gl_VertexID, samples four
  cardinal neighbours, computes |4c - n - s - e - w| on linearized
  depth.  Linearization branches between perspective and ortho via
  u_is_ortho.  Threshold scales with depth so distant edges still
  register.
- Output is multiplicatively blended (GL_DST_COLOR, GL_ZERO) so
  colours just darken; no separate composite step.
- Runs before the pivot/section/axis gizmos so they aren't outlined
  themselves.  HiZ pyramid build still runs after, unchanged.

Per-frame cost is one MSAA depth blit + one fullscreen pass with
five depth samples.  Sub-millisecond at 1080p on a mid GPU.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:34:38 +10:00
Dion Moult e7787b6aad ifcviewer: hemisphere ambient + fill light + cavity hint
Replaces the flat 0.25 ambient + single-Lambert key with three cheap
shape-readability tricks, all in the fragment shader:

- Hemisphere ambient (sky/ground tint mixed by n.z) so floors,
  ceilings, and walls get visibly different ambient colour even when
  shadowed.  +Z is world-up.
- Secondary fill light at 35% intensity from roughly the opposite
  horizontal direction so backs of objects are not pitch black.
- Cavity hint: clamp(length(fwidth(n)) * 1.5, 0, 0.35) darkens
  fragments where adjacent normals diverge sharply.  Catches
  wall-floor seams, column-slab joints, and stair edges as faint
  dark lines without any post-process.

Total cost: ~8 extra ALU ops per fragment, no extra passes, no extra
buffers.  No change to cull/HiZ/MDI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 14:22:08 +10:00
Dion Moult 6f0327d4b5 ifcviewer: orthographic toggle and standard axis-aligned views
- P toggles ortho/perspective.  The ortho box is sized so the
  visible rectangle at the pivot's distance matches what the
  perspective camera would show — toggling at any zoom keeps the
  framing identical, and the wheel keeps working by rescaling the
  box.  Contribution culling is disabled in ortho since its
  r_px = focal_px * r / dist formula assumes perspective; frustum
  and HiZ culling still run.
- X / Y / Z snap the camera to look from +X / +Y / +Z; Shift+X /
  Y / Z snap to the negative side.  Yaw and pitch are set
  directly so top/bottom land on exactly ±90°.
- updateCamera() picks the lookAt up vector dynamically: world +Z
  except within 1° of the pole, where it switches to world +Y.
  That keeps lookAt well-conditioned at the poles and gives top
  views the architectural "Y as north" screen orientation.
- Pan now derives screen-right / screen-up from the real camera
  basis instead of from yaw/pitch alone — the old derivation
  assumed up = world +Z and silently inverted at top/bottom.
- Standard views preserve target and distance — rotate only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 13:30:18 +10:00
Dion Moult 6341d7dd31 ifcviewer: bind Shift+K to clear all section planes
Convenient escape hatch when the user has stacked several cuts
and wants to start over without exiting the tool first.  Also
resets the selection and drag state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:37:37 +10:00
Dion Moult 51971dd493 ifcviewer: section tool — gizmo, drag, K shortcut
Wires up the user-facing section-cut tool on top of the clipping
plumbing landed in the previous commit.

- K toggles the tool.
- LMB while the tool is active:
    * On an existing plane's arrow gizmo (screen-space line-segment
      hit test, 12 px grab radius) → select + start drag.
    * Otherwise on geometry → pickSurfaceAt + addSectionPlaneAt-
      Surface, select the new plane.
    * Otherwise → deselect.
- LMB drag updates the plane's origin by projecting the cursor
  delta onto the screen-space normal axis and converting back to
  metres.  d is rederived from the new origin each frame.
- Delete removes the selected plane; Esc exits the tool.
- Each plane renders a 2x2 m quad outline plus a yellow arrow
  along +n at its origin.  Selected plane draws cyan and
  thicker.

LMB object-pick is suppressed while the tool is active so plane
creation does not also change selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:32:49 +10:00
Dion Moult 309e20009c ifcviewer: add section-plane clipping plumbing
Adds a clip-plane pipeline used by the upcoming section tool:

- Up to 8 SectionPlane{n, d} entries, AND-combined as
  fragment-shader discard against world position.  Main and pick
  fragment shaders both honour the planes, so cut areas are
  neither drawn nor selectable.
- Main vertex shader now passes v_world_pos through.
- Pick FBO grows two attachments (RGB32F world position, RGB16F
  world normal) and the pick shader writes both alongside the
  object id.  pickSurfaceAt() does a single readback of all
  three.  Existing pickObjectAt() still works unchanged for
  callers that just want the id.
- addSectionPlaneAtSurface(point, normal) auto-flips the normal
  toward the camera so the first click immediately cuts the
  camera-facing half.

No UI yet — that's the next commit (gizmo, drag, K shortcut).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 12:13:17 +10:00
Dion Moult a06e5ecdf5 ifcviewer: add Focus-on-Object and View-All camera shortcuts
F (no modifier) re-aims the orbit camera at the selected object's
world AABB centroid and dollies camera_distance_ so the bounding
sphere fits the current viewport.  Home does the same for the union
of all finalized models.  Both preserve yaw/pitch so the user keeps
their orientation; both no-op in FPS mode.

Scene AABB prefers the per-model BVH root when available and falls
back to walking InstanceCpu world AABBs.  Object AABB unions every
matching instance.  Distance accounts for portrait windows by using
the tighter of the horizontal and vertical FOV constraints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 11:34:35 +10:00
Dion Moult 1ca2e12f92 ifcviewer: draw 3D pivot indicator during navigation
A small RGB axis cross is rendered at camera_target_ while the user is
orbiting, panning, or has just zoomed.  Visibility toggles on
middle-mouse press/release; the wheel arms a single-shot QTimer that
hides it 750 ms after the last notch.

Drawn in two passes: GL_GREATER at 30% alpha for the occluded portion
(X-ray cue) and GL_LEQUAL at full alpha for the visible portion.  Arm
length is computed from camera_distance_, fovy, and viewport height so
the cross stays ~30 px on screen across zoom levels.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 10:21:23 +10:00
Dion Moult b9e5739088 ifcviewer: stream geometry per prioritised context
Port get_prioritised_contexts from ifcopenshell.util.representation to
C++ and have GeometryStreamer iterate one context at a time, mirroring
bonsai's create_generic_element loop.  Each pass sets context-ids to a
single context id; elements that yield geometry are dropped from the
include set so lower-priority contexts only pick up leftovers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:00:33 +10:00
Dion Moult 30cdffc27a ifcviewer: include settings for deflection tolerances 2026-04-29 22:37:55 +10:00
Dion Moult 3607fbb762 ifcviewer: include spatial elements in iterator filter
Match bonsai's process_element_filter for the no-filter branch:
IfcSpatialStructureElement on IFC2X3, IfcSpatialElement otherwise.
They flow through the same net/gross split as IfcElement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:20:20 +10:00
Dion Moult b73cdd1a0e ifcviewer: filter iterator to net IfcElements, void-limit setting
Mirror bonsai's IfcImporter.process_element_filter so the streamer
walks only IfcElement (plus IfcProxy on IFC2X3/IFC4), drops
IfcFeatureElement except IfcSurfaceFeature, and routes elements
with more openings than the configurable void limit through a
second iterator pass with disable-opening-subtractions=true.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 08:54:35 +10:00
Dion Moult e21bd1ac96 ifcviewer: add tier-1 unit tests (Catch2 + CTest)
Covers the pure-logic modules with no Qt event loop or GL context: BVH
build, LOD decimation, sidecar round-trip, instanced-geometry layout
constants, and Federation save/load + relative-path policy. Each test
binary compiles only the production source(s) under test, so the unit
tier doesn't pull Qt/OpenCASCADE/IfcGeom into the test build.

Gated behind BUILD_IFCVIEWER_TESTS=OFF; default builds remain offline.
Catch2 v3.5.4 is fetched on demand via FetchContent.
2026-04-28 21:49:44 +10:00
Dion Moult 633c613da2 ifcviewer: drop unused SidecarHeader reserved field, bump v8 -> v9
The reserved uint32_t was always written as 0 and never inspected on
read.  Removing it shrinks the header from 16 to 12 bytes; the version
bump makes pre-existing sidecars fail the version check cleanly rather
than misreading by 4 bytes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:40:00 +10:00
Dion Moult 8282f691e8 ifcviewer: rename SceneLoader::Entry to Model
The struct holds per-model bookkeeping; Model describes its contents
rather than its container relationship.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:31:48 +10:00
Dion Moult 7bc64bef1a ifcviewer-full: add .ifcfed federation save/load
Federation (JSON) tracks an ordered list of model sources plus an
optional home-view camera state. Sources are stored relative when
under the federation file's directory, absolute otherwise.

File menu now exposes New / Open / Save / Save As; Add Files moves
to Ctrl+Shift+O. View menu gains Set/Go to Home View. Window title
binds to dirty state via setWindowModified, and the close-window
prompt offers Save/Discard/Cancel.

Per-model transform (4x4 column-major) and visible round-trip
through load/save but are not yet applied at the viewport — the
georeferencing work uses them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:31:59 +10:00
Dion Moult 81a7c5b50e ifcviewer: add Shift+F fly-mode camera
WASD strafe, Q/E down/up, mouse-look (cursor hidden + recentered),
Shift to sprint, scrollwheel scales speed, click or Esc returns to
orbit.  Exiting drops back to the same viewpoint because rotation
re-pins camera_target_ to keep camera_eye_ stationary.

Movement integrates wall-clock dt inside render() and the next frame
self-schedules via requestUpdate() while any key is held.  A QTimer
would fight Qt's event loop during long swapBuffers blocks and produce
"camera pauses one frame" stalls; render-driven integration keeps
movement phase-locked to vsync and absorbs slow frames in a single
catch-up step.

IFC_FPS_HITCH_MS=<n> logs frames slower than n ms while in fly mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 20:12:52 +10:00
Dion Moult 8f7c8dc1d2 ifcviewer: load rdb/ifc as property data source on sidecar hit
Sidecar hits skipped opening the underlying .rdb/.ifc, so ifcFile() was
null and the property panel only showed cached name/type/guid. Now, after
a sidecar hit, a background thread opens <stem>.rdb (preferred) or
<stem>.ifc and hands the file to GeometryStreamer via setIfcFile(), with
a dataSourceReady signal so the UI refreshes the current selection.

Gated behind a new AppSettings::loadDataSource toggle (default on) so
users can opt into geometry-only viewing; when off, the sidecar-hit
thread is skipped and the stream-path ifc_file_ is released after
the sidecar write completes.

Also adds *.ifcview to the Add Files dialog filter so a cache can be
opened directly without its source file present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 13:45:04 +10:00
Dion Moult eea2398e07 ifcopenshell-python: fix broken imports after upstream refactors
Two upstream commits on this branch landed without updating all their
callers, leaving `import ifcopenshell.geom` unusable:

  89c66f62b "Python import fixes: import from wrapper now which
  inherits from mixins" moved the `file` class out of
  ifcopenshell/file.py into ifcopenshell_wrapper, but missed
  geom/main.py and stream.py which still did `from ..file import file`.

  b022ca7e7 "Some plug-in work" dropped the SWIG exports for
  `serialise`, `tesselate`, `XmlSerializer` (and other serializers)
  with a `// @todo bring back serialization` marker, but left
  geom/main.py referencing them at module-load time.

Fix the `file` imports to come from ifcopenshell_wrapper, and guard
the removed-serializer references behind `hasattr`, matching the
pattern already in use for the other optional serializers (gltf, hdf5,
collada, json, ttl). Revert once upstream fixes this.
2026-04-24 07:33:13 +10:00
Dion Moult 35be7f4190 ifcviewer: load RocksDB-backed IFC models
The viewer can now open a .rdb directory (as produced by
RocksDbSerializer / convert_path_to_rocksdb) anywhere it accepts an
.ifc file. The full GUI gets an "Add Database..." File menu entry
that opens a directory chooser; the streamer lets the file
constructor autodetect the format and opens the store read-only so
multiple viewers can share a database without taking the exclusive
RocksDB lock.

Parallel mapping on RocksDB-backed files still produces
non-deterministic shape counts (the race is outside the instance
cache), so force num_threads=1 for the iterator when the storage is
RocksDB. Serial RocksDB (~2.6s) and parallel SPF (~0.7s) both
produce 107 shapes on AC20-FZK-Haus; @todo in-source points at the
remaining thread-safety work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 4f929e90a7 ifcviewer: key sidecar on path stem, drop staleness check
Previously readSidecar/writeSidecar were keyed on (path, file_size) with
staleness rejected at read time.  Switch to pure path-stem keying: foo.ifc
and foo.ifcdb/ both resolve to foo.ifcview, so the same cache serves either
source format.  Staleness is user-managed (delete the sidecar to force a
rebuild), which also lets sidecars be copied or moved independently of the
source.

v8 header drops the source_file_size field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult f898089b27 Queue viewer ops before GL init
Buffer viewport model mutations until the OpenGL context is initialized so loads that start before first exposure do not silently drop geometry or model state.

Generated with the assistance of an AI coding tool.
2026-04-23 21:32:25 +10:00
Dion Moult 4e4553201b Fix viewer load termination
Handle streamer success, failure, and cancellation as distinct terminal states so failed or cancelled loads do not finalize as successful models. Clean up partial model/UI state in the full and minimal viewer apps when a load is cancelled or fails.

Generated with the assistance of an AI coding tool.
2026-04-23 21:32:25 +10:00
Dion Moult 29f5132510 ifcviewer: extract SceneLoader, remove duplicated load orchestration
MainWindow and MinimalWindow each carried ~150 lines of mirrored
load-queue, sidecar-thread, streamer-wiring, and ID-rebase code. Lift
all of it into a SceneLoader QObject in the library; both apps now
consume it via signals. Sidecar writes stay on the full-app side since
they need the consumer's element metadata strings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult ae938d425b ifcviewer: split into shared library and two app executables
Turn src/ifcviewer into libIfcViewer.so holding the rendering engine +
geometry pipeline (ViewportWindow, GeometryStreamer, BvhAccel,
InstancedGeometry, SidecarCache, LodBuilder, AppSettings).  Move the
existing UI shell (MainWindow, SettingsWindow, main.cpp) into
src/ifcviewer-full as the IfcViewerFull executable.  Add a new
src/ifcviewer-minimal target with a MinimalWindow that hosts only the
viewport and reuses the sidecar fast-path for benchmark/debug runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 5161b0a3f8 ifcviewer: remove meshopt_simplify path, keep only simplifySloppy
Edge-collapse decimation (meshopt_simplify) returns BIM meshes unchanged
due to per-triangle vertex duplication and non-manifold topology. The
sloppy voxel-clustering decimator is faster, needs no shadow index
welding, and produces good results at the sub-30px LOD1 threshold.
Remove the non-sloppy branch, shadow buffer, IFC_LOD_SLOPPY and
IFC_LOD_LOCK_BORDER env vars.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 3015f758ba ifcviewer: shrink vertex format from 16 to 12 bytes (oct i8x2 normals)
Replace i16x2 octahedral normals with i8x2, filling the 2-byte padding
after position and saving 4 bytes per vertex. int8 gives ~1.4 deg
worst-case angular error — invisible for BIM geometry which is
overwhelmingly axis-aligned. 25% VBO reduction; sidecar files shrink
~15% overall (5.4 GB -> 4.6 GB on a 111-model test scene). Bumps
sidecar format to v7.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult dbe68b48f2 Fix kernel/mapping plugin output dir and IfcViewer link dependencies
Use $<TARGET_FILE_DIR:IfcGeom> instead of hardcoded
${CMAKE_BINARY_DIR}/ifcgeom/$<CONFIG> for plugin runtime dirs — the
old path was wrong on non-MSVC generators where $<CONFIG> expands
empty. Add explicit add_dependencies for kernel/mapping plugins so
IfcViewer waits for them to build, and drop the redundant direct link
against ${kernel_libraries}.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 094d96c735 ifcviewer: remove GPU compute cull (IFC_GPU_CULL)
Benchmarks showed negligible gain (52 vs 51 fps) — the CPU BVH path
already culls efficiently, and the GPU path still read back to CPU for
LOD/winding/HiZ. Removes ~570 lines of dead weight: compute shader,
async readback, one-frame-late consume, per-model AABB SSBOs, and
profiling counters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult ed6e8d831e ifcviewer: benchmark CLI, settle recull fix, and Phase 3G documentation
Add --camera tx,ty,tz,dist,yaw,pitch and --benchmark N CLI args for
reproducible performance measurement.  The benchmark orbits the camera
(0.5°/frame yaw) for N frames after a 5-frame warmup, prints
avg/median/p1/p99 frame times, then exits.  Press C during interactive
use to print the current camera as a --camera argument.

Fix settle recull to fire after ANY camera motion (not just when
IFC_MIN_PX_MOTION is set), ensuring HiZ artifacts from motion frames
are always cleared when the camera stops.

Document Phase 3G (motion-adaptive culling + HiZ during motion) in
README with benchmark results from 1.06M-instance scene:
  - Baseline:                    16.3 fps
  - IFC_MIN_PX_MOTION=10:       26.5 fps (1.6x)
  - IFC_HIZ_MOTION=1:           46.6 fps (2.9x)
  - Both combined:              51.0 fps (3.1x)
  - + GPU_CULL:                 52.0 fps (3.2x, negligible gain)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:25 +10:00
Dion Moult 930678e3d2 ifcviewer: motion-adaptive contribution culling + sub-draw diagnostics
During camera motion, use a larger pixel-radius threshold (IFC_MIN_PX_MOTION)
to aggressively cull small objects, dramatically reducing sub_draws and
improving orbit fps (e.g. 29→67 fps on 1M-instance scene).  When the camera
stops, automatically re-cull at the base threshold to restore full detail.

Key behaviors:
- IFC_MIN_PX_MOTION=N sets the motion threshold (0 = disabled)
- Settle recull fires on the first still frame after motion
- HiZ pyramid invalidated on settle (stale from sparse motion frame)
- GPU cull results skipped on settle (dispatched at motion threshold)
- requestUpdate() ensures the settle frame actually runs

Also adds IFC_SUBDRAW_DIAG=1 diagnostic for sub-draw composition analysis
and documents Phase 3E/3F experiment results in README.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 4e3cc63de1 ifcviewer: fix HiZ depth blit and make occlusion test conservative
The HiZ pipeline had two bugs causing false occlusions:

1. The scaling depth blit (glBlitFramebuffer from window-size to HiZ-size)
   produced GL_INVALID_VALUE on some drivers. Replace with a fullscreen-
   triangle shader that samples the resolved depth and writes gl_FragDepth.

2. The resolve texture used GL_DEPTH_COMPONENT24 but Qt's default FBO uses
   D24S8 (depth+stencil). Mismatched formats cause the MSAA resolve blit
   to fail. Fix by using GL_DEPTH24_STENCIL8 for the resolve texture.

Additionally, the occlusion test was too aggressive for scenes with
compressed depth ranges (entire scene in 0.99-1.0). Change from
"max over coarse mip texels" to "reject only if ALL fine-mip texels
agree the AABB is behind them", with early-out on first non-occluding
texel and a 64-sample cap.

Also fix IFC_HIZ_MOTION=0 being treated as enabled (checked env var
existence, not value).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 7b64dd338b ifcviewer: dirty-mesh tracking + consume sub-phase profiling for GPU cull
Only clear and emit mesh buckets that received survivors in the previous
frame, converting both phases from O(total_meshes) to O(active_meshes).
Adds per-sub-phase timing (bin/clr/class/emit) to the stats line.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 71612e0780 ifcviewer: hybrid GPU frustum+contribution cull with async readback
Replace the CPU BVH traversal + frustum + contribution stages with a
GPU compute path (IFC_GPU_CULL=1).  A single scene-wide dispatch tests
all instances against frustum planes and screen-space contribution
threshold, compacting survivors into a flat uint32 buffer via atomicAdd.

Uses one-frame-late async readback: frame N dispatches and fences,
frame N+1 polls the fence (non-blocking) and reads the persistent-
mapped result buffer with zero GPU sync cost.  CPU still handles HiZ,
LOD selection, winding bucketing, and indirect command generation from
the compact survivor list; draw path is unchanged.

On a 1M-instance / 111-model scene (GTX 1650):
  GPU dispatch:  0.70 ms  (frustum + contribution, brute-force)
  Readback:      0.00 ms  (fence already signaled, persistent map)
  CPU consume:   5.7–6.7 ms  (parallel emit across models)
  Cull wall:     5.8–6.9 ms  (vs 9.6–15.2 ms CPU-only path)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 9aae8f0329 Revert "ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1"
This reverts commit 4fe32b54105ca2c5c00290603db17164837211e1.
2026-04-23 21:32:24 +10:00
Dion Moult 175efcfffe Revert "ifcviewer: GPU cull fwd/rev reflection bucketing (step 3b)"
This reverts commit 7defbe982464536e34e80aa85d2cd7eaafbb62ee.
2026-04-23 21:32:24 +10:00
Dion Moult 643a2e1c1f Revert "ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)"
This reverts commit 77cac3ec170b622db6977829f66b62603266a047.
2026-04-23 21:32:24 +10:00
Dion Moult 3c5c8e44cb Revert "ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)"
This reverts commit 9a7a48944f4b62f9ca431149139eb846229f6114.
2026-04-23 21:32:24 +10:00
Dion Moult fc89ffeb19 Revert "ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount"
This reverts commit d5b7b87ba17c90008cf0673c838ce8431ad85e36.
2026-04-23 21:32:24 +10:00
Dion Moult 4bedb40d8a ifcviewer: MDI compaction via glMultiDrawElementsIndirectCount
Pack compute shader compacts non-empty indirect commands into
contiguous fwd/rev ranges, eliminating ~690k empty sub-draws that
dominated command-processor overhead.  GL 4.6 entrypoint loaded via
getProcAddress with ARB fallback; graceful degradation to uncompacted
MDI when unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult a34c36d22e ifcviewer: same-frame HiZ occlusion cull on GPU (step 3d)
Two-phase compute-cull dispatch when IFC_GPU_CULL=1:

  Phase 1  frustum + contribution + LOD, no HiZ  → survivors
  Depth    render survivors depth-only into half-viewport FBO
  Build    GPU compute max-reduce depth → R32F mip pyramid
  Phase 2  same cull + HiZ test                  → final survivors
  Color    render final survivors

The compact shader's new hizOccluded() projects 8 AABB corners to
screen space, picks the mip level where the covered rect fits in ≤2×2
texels, and rejects when the AABB's near-depth exceeds the pyramid's
max depth.

New GPU resources (per-window):
  hiz_gpu_fbo_ / hiz_gpu_depth_tex_  — depth-only FBO at half viewport
  hiz_gpu_pyramid_tex_                — R32F mipmapped pyramid
  hiz_gpu_copy_prog_                  — compute: depth → pyramid L0
  hiz_gpu_reduce_prog_                — compute: max-reduce L(n-1)→L(n)
  hiz_gpu_depth_prog_                 — vertex + trivial fragment

On a dense 18-model BIM dataset:
  survivors:  140k → 65k  (HiZ rejects ~50%)
  triangles:  22M  → 13M
  gpu_cull:   0.06ms → 22.5ms  (depth pre-pass CP overhead)

The depth pre-pass suffers the same empty-sub-draws CP overhead as the
color pass (690k commands, most with instanceCount=0).  Once MDI
compaction lands, both passes will be fast.  For now, net FPS is flat
(savings on color ≈ cost of depth pre-pass).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult e5ed7b53d4 ifcviewer: GPU LOD0/LOD1 selection in compute cull (step 3c)
The compact shader now computes per-instance pixel radius and routes
survivors to LOD1 buckets when the projected sphere falls below the
LOD1 threshold (default 30 px, same as CPU path, tunable via
IFC_LOD1_PX).

Layout expanded from 2 to 4 buckets per mesh:
  [0..M)   fwd_lod0   [M..2M)   fwd_lod1
  [2M..3M) rev_lod0   [3M..4M)  rev_lod1

Two MDIs per model: CCW for [0..2M), CW for [2M..4M).  Per-mesh
has_lod1 flags live in a new gpu_mesh_flags_ssbo (binding 4).

Contribution cull refactored: the compact shader now computes
pixelRadius() once and uses it for both the min_pixel_radius rejection
and LOD routing, matching the CPU path's logic.

Visible-buffer worst case is 2 × total_instances (each LOD bucket
reserves the full fwd/rev capacity per mesh, since LOD selection is
dynamic).

Tri count drops ~60% on the test dataset (53M → 22M) thanks to LOD1
decimated meshes.  FPS recovers from 16 to 36 despite 690k sub_draws
(4M layout).  MDI compaction remains the final perf fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 069ef20c46 ifcviewer: GPU cull fwd/rev reflection bucketing (step 3b)
Extend the GPU-cull indirect buffer from M to 2M commands: the first M
are the forward (non-reflected, CCW) bucket, the second M are the
reverse (reflected, CW) bucket.  The compact shader reads flags bit 0
from the AABB SSBO and routes each survivor to the appropriate bucket
via bucket = reflected ? mesh_id + M : mesh_id.

uploadGpuCullStaticBuffers() now precomputes exact per-mesh fwd/rev
instance counts so each bucket reserves only the slots it needs
(total visible_ssbo size unchanged — sum of fwd + rev = total).

Draw loop issues two MDIs per model under IFC_GPU_CULL: first M
commands CCW, next M commands CW.

Sub-draws doubled (172k → 345k) which further regresses FPS due to
command-processor overhead from zero-instance sub-draws — the same
issue noted in 3a.  MDI compaction remains the fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0b122ae1f5 ifcviewer: GPU cull drives rendering under IFC_GPU_CULL=1
Promote the compute cull from a validation shader to the actual draw
driver.  With the gate on, the CPU cull fan-out is skipped and MDI
consumes gpu_indirect_buffer / gpu_visible_ssbo directly.

- uploadGpuCullStaticBuffers() pre-fills per-mesh DrawElementsIndirect
  commands and a mesh_base prefix sum so the compact shader can scatter
  survivors into a fixed per-mesh range.  Instance count for each
  command is zeroed by a tiny reset dispatch, then the compact shader
  atomically writes survivors and increments instanceCount.
- Draw loop branches on the gate: single CCW MDI with all mesh
  commands.  Fwd/rev winding split, LOD selection, and HiZ are still
  CPU-path-only; reflected instances render with wrong winding under
  this gate (step 3b).
- Once-per-second readback of each model's indirect buffer populates
  the survivor / visible-object / visible-triangle stats so the
  [frame] line reflects what the GPU actually drew.

Known regression: sub_draws is the full mesh count per model (~172k on
the test dataset) vs the handful of non-empty commands the CPU path
produces.  Command-processor overhead from zero-instance sub-draws is
what drives the FPS drop, not the cull itself (0.05 ms).  Compacting
non-empty commands requires glMultiDrawElementsIndirectCount, a GL 4.6
entrypoint not exposed by Qt's QOpenGLFunctions_4_5_Core; deferring to
3a-followup so we don't bolt a getProcAddress loader into the renderer
mid-restructure.

IFC_GPU_CULL is off by default, so this does not affect normal runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult a0cc4b874b ifcviewer: add GPU frustum-cull validation shader (IFC_GPU_CULL=1)
First Phase 3E milestone: a compute shader that reads the per-instance
world-AABB SSBO added in the last commit, tests each instance against
the 6 frustum planes, and atomicAdds a global counter.  No visible list
or indirect-buffer writes yet — the output is just a survivor count,
cross-checked each frame against the CPU cull's numbers in the stats
line (`gpu_cull[Xms in=A surv=B]`) so we can verify the plumbing end-
to-end before we hand the GPU responsibility for the actual render data.

Dispatched from render() after the CPU cull completes, only when
IFC_GPU_CULL=1 and the camera moved (the skipped-cull still-frame path
doesn't re-check either).  The readback is synchronous — that's fine
for a validation path; it'll go away once the GPU writes indirect
commands directly.

Expected invariant: gpu_cull.surv >= cpu_cull.visible_objects, since
the GPU path does frustum-only and CPU adds contribution + HiZ cuts on
top.  A large mismatch (orders of magnitude, or surv < visible) means
the SSBO upload or shader logic is wrong.

No shader/buffer bindings overlap with the draw path (compute uses
bindings 0/1, restored before drawing; draw programs rebind 0/1/2).
2026-04-23 21:32:24 +10:00
Dion Moult 2f88778c9f ifcviewer: upload per-instance world AABBs to a GPU SSBO
Scaffolding for Phase 3E (GPU compute cull).  After finalizeModel /
applyCachedModel, pack each InstanceCpu's world AABB + mesh_id +
reflection bit into a std430-friendly 32 B record and push it to a
per-model aabb_ssbo.  No consumer yet — the CPU cull still drives
rendering — but the next commits will point a compute shader at this
buffer and have it produce the visible list + indirect commands
directly on the GPU.

Cost: 32 B per instance, ~18 MB for the 569 k-instance test scene.
One-shot upload at finalize time; streaming-time appends aren't
mirrored (the CPU cull doesn't need the SSBO, and finalizeModel
rebuilds the whole thing in one go).
2026-04-23 21:32:24 +10:00
Dion Moult 0a752e09eb ifcviewer: README — document HiZ disabled during camera motion
The 'Known caveats' bullet still described the old 1-frame-stale
behavior.  Since 6b496d802 the cull compares hiz_vp_ to the current VP
and drops HiZ rejection whenever they differ, so HiZ only helps on
still frames — orbiting gets no benefit.  Call out the tradeoff and
the planned same-frame-depth-pre-pass fix slated for Phase 3E.
2026-04-23 21:32:24 +10:00
Dion Moult 03662d2016 ifcviewer: fix pick-pass cull corruption and cached-model ID collisions
Two stability bugs:

1. Clicking an object left the scene with wrong shading until the camera
   moved.  The pick pass re-culls every model with its own parameters
   (min_pixel_radius=0, no HiZ) and overwrites each model's visible_ssbo
   and indirect buffer.  The next render() saw an unchanged camera,
   skipped the cull via the have_cached_cull_ shortcut, and drew the
   stale pick-pass buffers.  Fix: invalidate have_cached_cull_ at the
   end of pickObjectAt().

2. Loading two sidecar-cached models made the second model's picked
   properties resolve to the first model's elements.  Sidecars store raw
   object_id / model_id values from the session that wrote them, and
   both files start at object_id=1, so element_map_ entries collided.
   Fix: on load, rebase every PackedElementInfo and InstanceCpu by
   (next_object_id_ - min_id_in_sidecar) and overwrite model_id with
   the freshly-assigned handle before the elements hit element_map_.

Also document both in the README — the pick-pass note under 3A
contribution culling, the sidecar rebase under the sidecar format
section.
2026-04-23 21:32:24 +10:00
Dion Moult 99f409280a ifcviewer: disable HiZ cull when camera has moved
HiZ from last frame encodes depth from last frame's viewpoint. When
the camera moves, projecting a current-frame AABB through the stored
VP answers 'was this occluded last frame?' rather than 'is it occluded
now?' — a self-reinforcing feedback loop where objects culled in
prior frames never appear in any depth buffer and stay permanently
hidden at certain camera angles.

Fix: require hiz_vp_ == current VP for the HiZ test to apply. HiZ
still helps static views (kicks in one frame after camera stops) but
no longer produces false occlusions during orbit. The correct fix for
orbit coverage is a depth pre-pass feeding fresh HiZ — planned as
part of Phase 3E GPU compute cull.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0c9d3ea6d7 ifcviewer: README — document parallel per-model cull (Phase 3D)
Add the parallel cull bullet to the feature list, a Phase 3D section
explaining the fan-out / scratch-ownership design + measured 4x
speedup, and renumber the planned GPU compute cull to Phase 3E so it
can cite 3D as the CPU algorithm being ported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 37fa4e9076 ifcviewer: parallel per-model CPU cull
Split cullAndUploadVisible into cullModelCpu (CPU-only, thread-safe) and
uploadCullResults (GL-only, main thread). render() fans the per-model
culls out via std::async and joins before the serial upload pass.

The cull scratch (vis_fwd/rev_lod0/1, visible_flat, indirect_scratch)
moved onto ModelGpuData so each worker owns its output buffers. Phase
timers and hiz_reject_count_ are atomic since workers fetch_add into
them. A new wall-clock timer around the dispatch block reports the
actual frame-time contribution; the existing clr/trv/emt counters are
now documented as per-thread sums.

Measured on the 18-model / 569k-instance test scene: wall-clock cull
dropped from ~25 ms to ~5 ms while the aggregate CPU work (trv) stayed
~30 ms. Frame time 34 ms -> 19 ms. IFC_CULL_THREADS=0 forces the
single-threaded fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1ec273f508 ifcviewer: README — document event-driven rendering and VBO quantization
Add the event-driven rendering bullet (zero idle cost, in-render frame
timing) and roadmap entries for VBO quantization and event-driven
rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 574bcfa6c5 ifcviewer: quantize VBO to 16 B/vertex (sidecar v6)
Position now u16x3 normalized against each mesh's local AABB; normal
oct-encoded to i16x2; RGBA8 colour unchanged. Per-mesh dequant basis
lives in a new MeshGpu SSBO at binding 2; both main and pick shaders
mix() against it before applying the instance transform.

Drops VBO and sidecar size by ~43 % (28 -> 16 B/vert), which matters
mostly for warm-load downloads of precomputed sidecars and steady-state
VRAM. LodBuilder dequantizes positions into a scratch buffer before
calling meshopt, since meshoptimizer needs float positions.

Also fixes a streaming-time crash in cullAndUploadVisible: bvh_items
was only populated at finalize, but the linear fallback indexes it
during streaming. Mirror BvhItem appends in uploadInstanceChunk so the
hot path stays valid before the BVH is built.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 09ffdd2028 ifcviewer: event-driven rendering, idle scenes cost zero CPU
Replaced the 16ms QTimer with QEvent::UpdateRequest delivered via
requestUpdate(), posted from every state mutator (mouse/wheel, model
lifecycle, selection, visibility, resize).  A static BIM scene — the
common case for a viewer — now does no work at all between user actions.

FPS is now measured as time spent inside render() rather than wall-clock
gap between frames, so idle gaps don't pollute the 1-second window and
the headline number reflects real render throughput.  Headline fps still
caps at vsync; sub-vsync profiling lives in the cull[...] phase timers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d0c5bd5e85 Cull: skip cullAndUploadVisible + HiZ on still frames
render() was re-running the full cull every 16 ms timer tick even when
nothing had changed — the camera matrices, scene state, and therefore
visible set were all identical to the previous frame's.  The GPU was
still happy to redraw from the cached indirect buffer, but the CPU was
burning 21 ms/frame rebuilding the same visible list.

Detect the no-op case by comparing view/proj against last_cull_view_ /
last_cull_proj_ and checking a scene-dirty flag (have_cached_cull_)
that every mutator on models_gpu_ invalidates — finalizeModel,
applyCachedModel, applyLodExtension, hide/show/remove/reset, and
uploadInstanceChunk.  When the check passes we skip both
cullAndUploadVisible and buildHizPyramid (the depth buffer is
bit-identical, so re-reading it produces the same pyramid).

Per-model visible_objects / visible_triangles stats now live on
ModelGpuData so the stats line reports correct numbers on skipped
frames instead of reading from a stale indirect_scratch_.

Measured on a 569k-object overview: still frames go 22 fps → 62 fps;
orbiting goes 23 fps → ~30-50 fps depending on how hard you move the
mouse (the cull only pays its full cost on the ~25 % of frames where
the camera actually moved).  The stats line gains a "skipped N/M"
field so you can see the ratio live.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult c03a7fe117 Cull: read AABBs from compact bvh_items in the hot path
cullAndUploadVisible was reading each instance's AABB through
m.instances[idx] — a 104-byte InstanceCpu struct — for the frustum /
contribution / HiZ tests.  Only 24 of those bytes (the two float[3]
AABBs) are actually used by the tests; the rest (4×4 transform +
header) is pure cache-line waste, and with 569k instances the array
is 59 MB, well past any cache.

bvh_items[idx] already stores a 1:1 compact 28-byte record with the
same AABB, built unconditionally in buildBvhForModel().  Switch the
hot test path to read from it, and only touch InstanceCpu once an
instance has passed all three tests (for mesh_id).  Modest ~20 %
drop in cull-traverse time on a 569k-object overview (26 ms → 21 ms).

Also add four cull-phase timers (clr / trv / emt / upl) to the
per-second stats line so future optimisation work has concrete
numbers to chase.  Confirmed via these timers that bucket clears,
emit and GPU upload are all <1 ms combined; traversal is where the
remaining CPU cost lives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 8596d53a4a Phase 3C: Hierarchical-Z occlusion culling (CPU-side v1)
After the main draw, blit the MSAA default-framebuffer depth to a
single-sample 256×128 depth texture, read it back, and build a CPU
max-reduced mip pyramid.  Next frame's cullAndUploadVisible projects
each BVH node / instance AABB through the previous frame's VP and
compares the AABB's nearest depth against the pyramid's deepest value
at the matching mip level; strictly-beyond AABBs are rejected.

Conservative direction (aabb_near > hiz_max) — never wrongly rejects a
visible instance, so no flicker.  BVH subtree-level test lets a single
8-corner projection reject up to a leaf's worth of instances.

Tuning knobs: IFC_NO_HIZ=1 disables; IFC_HIZ_SIZE overrides base width.
New stats counter hiz_rej shows rejects/frame.

Measured: big win on interior views (GPU-bound), roughly zero net
effect on exterior overviews (CPU-bound on cull traversal, so the
saved GPU work is masked).  Tried a 3-deep PBO ring for async readback
and reverted — the extra frame of staleness produced visible flicker
on fast orbit, and the synchronous readback wasn't actually a measured
bottleneck at 256×128.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult c78e16eafb Phase 3B: per-instance LOD via meshoptimizer simplifySloppy
Decimate each unique mesh once at sidecar-build time and swap to the
reduced index slice per-instance per-frame when projected sphere radius
drops below IFC_LOD1_PX (default 30).  Same VBO, same SSBO, just a
different firstIndex/count in the indirect command.

Extends MeshInfo (48→56 B) with lod1_ebo_byte_offset + lod1_index_count
and bumps the sidecar to v5.  buildLods() runs inside
onStreamingFinished, appends decimated indices to sd.indices,
applyLodExtension pushes the EBO suffix to the live GPU state, and the
sidecar is written with LOD1 baked in.

simplifySloppy (voxel clustering) is used instead of the default
edge-collapse meshopt_simplify because BIM brep output is per-triangle-
unwelded and non-manifold after welding — simplify returned the input
unchanged for every mesh tested.  Sloppy ignores topology.  Knobs
(IFC_LOD_SLOPPY, IFC_LOD_ERROR, IFC_LOD_RATIO, IFC_LOD_MIN_SAVINGS,
IFC_LOD_LOCK_BORDER, IFC_LOD_DEBUG) are available for A/B tuning.

Result on the 128M-tri 10-model test scene (GTX 1650, 2px contribution
cull): 20.2 → 43.2 fps, 40M → 14M visible triangles, no change in
object count.  LOD build adds 100–600 ms per model on first open,
cached thereafter.

README Phase 3B section is now a full writeup of pipeline, selection,
decimator-choice rationale, env vars, and measured numbers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 68fea7bd45 README: mark Phase 3A done with measured numbers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 90366f8236 Phase 3A: screen-space contribution culling
Reject frustum-visible objects whose bounding sphere projects below a
pixel-radius threshold.  Applied at both BVH-node level (whole subtrees
pruned) and per-instance level; short-circuits when the camera is
inside the AABB so nothing-you're-standing-next-to is ever lost.
Pick pass passes threshold 0 so sub-pixel objects stay clickable.

Threshold defaults to 2 px (radius), overridable via IFC_MIN_PX env
var.  Measured on the 128 M-tri test scene (GTX 1650):

  0 px (off):   6.7 fps, 128 M tris
  2 px:        20.2 fps,  40 M tris (31%)
  4 px:        30.3 fps,  15 M tris (12%)

The metric is sphere-based (cheap: one sqrt per test) rather than
AABB-corner projection; loses a little precision on very elongated
bounds but costs ~5x less per test and the BVH-node pre-cull means
the long-tail-of-small-things case is already handled by subtree
pruning before we touch individual instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d3c21d7a81 Pivot Phase 3: diagnose as draw-bound, not upload-bound
Earlier probes pointed at per-frame glNamedBufferSubData uploads as the
bottleneck (60 fps when those two calls were commented out).  That was a
false reading — zeroing the uploads also emptied the indirect buffer, so
MDI drew nothing.  "No upload" and "no draw" were indistinguishable.

Two new diagnostic env vars in render() isolate the real costs:

  IFC_SKIP_MDI=1       keep cull + upload + binds, skip only the MDI
                       draws.  Gives 62 fps with everything else running,
                       confirming the non-draw path fits in ~16 ms.
  IFC_MAX_SUBDRAWS=N   cap each MDI's drawcount.  67k -> 30k sub-draws
                       saves 0 ms, confirming sub-draw count itself is
                       not the bottleneck; the long tail of sub-draws
                       carries ~no triangles.

On a GTX 1650 with 128 M triangles in view, nvidia-smi sits at 95 %
GPU util and FPS scales with triangle work, not sub-draw count.  The
card is simply rasterising at ~850 M tri/s.  No CPU-side or upload
trick recovers it.

Revised Phase 3 is therefore shedding triangles, not bytes:
  3A screen-space contribution culling (next)
  3B LOD
  3C HiZ occlusion
  3D GPU-side compute culling

README Phase 3 section rewritten around the diagnosis, including the
false lead, so future work doesn't re-tread the upload path.  The
aborted staging+resident ring-buffer implementation was reverted (the
uncommitted working tree is gone — pure glNamedBufferSubData retained
for the visible + indirect buffers, which we now know is fine).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult cd77c557e9 Rewrite README for instancing pipeline and refocus Phase 3
The previous README described a pre-instancing world (32-byte world-
coord vertices with per-vertex object_id, ObjectDrawInfo structs, EBO
reordering after BVH build, and a Phase 3 plan built around moving
draw submission to the GPU).  Most of that is either gone or already
solved:

  - Vertices are now 28 B local-coord; per-instance transforms live
    in an SSBO read through a visible-index SSBO and gl_BaseInstanceARB.
  - ObjectDrawInfo is replaced by MeshInfo + InstanceCpu + InstanceGpu.
  - No EBO reorder on BVH build — the BVH is over instance AABBs and
    the mesh/EBO layout is orthogonal.
  - Draw-call submission is already one glMultiDrawElementsIndirect
    per model; the old Phase 3 goal is met.

New content worth keeping:

  - GPU instancing section documents the mesh/instance/visible/indirect
    buffer contract the whole renderer hangs off of.
  - Reflection-aware two-pass draw is documented (det<0 placements,
    forward/reverse slice split, glFrontFace toggle).
  - reorient-shells and backface culling are called out as correctness
    + perf levers with their tradeoffs.
  - Phase 3 is rewritten around the actual bottleneck surfaced by
    profiling: per-frame glNamedBufferSubData stalls on the visible
    and indirect buffers.  Includes the diagnostic methodology (empty-
    screen jump to 60 fps, window/MSAA invariance, upload-comment-out
    experiment) so future-me remembers why this is the next step.
  - 3A (persistent mapped ring buffers, near-term) and 3B (GPU-side
    compute cull, longer-term) split out with scope estimates.
  - Roadmap updated: instancing / MDI / reflections / reorient-shells
    / backface cull all ticked; 3A surfaced as the next open item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 3110c98429 Backface culling with reflection-aware two-pass MDI
Enables GL_CULL_FACE by default (user-toggleable in Settings) so
closed solids skip shading their back halves.  The catch is that
IFC placements can contain reflections (mat4 with det<0 — mirrored
families, symmetric instances).  Naively culling would make every
mirrored instance vanish because the rasterizer sees its screen-space
winding as backwards.

Fix: detect reflections at upload time via determinant sign, bucket
visible instances into forward (det>=0) and reverse (det<0) per mesh
during culling, and issue two glMultiDrawElementsIndirect calls per
model with glFrontFace toggled CCW/CW between them.  The indirect
buffer is still one buffer — just split into a forward slice followed
by a reverse slice, with m.indirect_forward_count recording the split.

Vertex shader flips the normal when the transform has negative
determinant, keeping lighting correct on mirrored instances.  The
fragment shader keeps the gl_FrontFacing fallback as a safety net
when culling is disabled (e.g. for files with open shells).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 0e2a62d3b7 Enable reorient-shells in geometry iterator
IFC files routinely have IfcConnectedFaceSets whose faces point
inconsistently within the same shell — the result under per-vertex
normals is dark inside-out patches, and under GL_CULL_FACE it's
swiss-cheese.  reorient-shells fixes the face winding at geometry
generation time, which is the only place it can be fixed correctly;
no shader trick can recover from a mesh whose triangles disagree
among themselves.

Off by default in IfcOpenShell because it adds iterator time, but
we cache the result in the sidecar so it's a one-shot cost per file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult f532624dc1 Two-sided lighting, rename misleading draw-count stat
Two bugs conflated as "weird colors":

1. Two-sided lighting.  IFC placements often embed reflection
   matrices (mirrored families).  Transforming a_normal by
   mat3(inst.transform) produces a normal pointing the wrong way
   on those instances, and max(n·L, 0) then clamps the surface to
   pure ambient — reads as dark / washed out.  Use gl_FrontFacing
   to flip n in the fragment shader so both winding orientations
   shade correctly.  The proper fix (ship an inverse-transpose
   normal matrix or a det-sign bit per instance) is still owed;
   that would unlock re-enabling GL_CULL_FACE for a big fragment-
   work win on closed solids.

2. Stats label "inst_draws" was counting indirect sub-draws, not
   actual GL draw calls — misleading since MDI collapses N sub-
   draws into one glMultiDrawElementsIndirect.  Split into
   gl_draw_calls (real GL calls, = drawn-model count) and
   indirect_sub_draws (packed sub-commands).  For a BIM model
   with 47k unique meshes at full view this now correctly reads
   "1 gl_draws (47092 sub)" rather than suggesting 47k driver
   dispatches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult f0e3056d0a Collapse per-mesh draws into glMultiDrawElementsIndirect
Each visible model now issues a single glMultiDrawElementsIndirect
call instead of one glDrawElementsInstancedBaseVertex per mesh.  The
CPU BVH cull populates an array of DrawElementsIndirectCommand
records plus the flat visible-instance list, uploads both, and draws
the whole model in one GL call.

Vertex shaders switch from a uniform u_instance_offset to
gl_BaseInstanceARB (ARB_shader_draw_parameters), so per-draw offset
comes from the indirect command's baseInstance field.

Draw-call counts for BIM scenes with hundreds of unique meshes drop
from hundreds-per-frame to one-per-model, cutting driver overhead.
This also sets up the plumbing for the follow-up compute-shader cull
that will populate the indirect buffer entirely on-GPU.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 298eca0ab6 Progressive rendering during streaming
Pre-allocate the instance SSBO on model creation (4 MB, grow-on-demand)
and append each arriving InstanceChunk directly to the GPU-side
InstanceGpu array in uploadInstanceChunk.  This makes a model drawable
as soon as its first mesh + first instance chunk land, rather than
waiting for finalizeModel.

The visible-list architecture already decouples SSBO order from the
draw path, so appending in insertion order is correct — no sorting
required.  finalizeModel collapses to:
  - compute per-mesh instance counts (for stats + sidecar round-trip)
  - build the per-model BVH over instance world AABBs

Render / pick loops now gate on ssbo_instance_count > 0 rather than
the finalized flag.  Stats include in-progress models in totals
(excluding only hidden).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1f17d73f3e BVH frustum culling over instances
Re-wires the BVH acceleration structure on top of the new instanced
renderer.  Per model, build a BVH over per-instance world AABBs at
finalize (and on sidecar apply).  Each frame, traverse the BVH against
the camera frustum to produce a visible-instance index list, bucket by
mesh_id, and upload to a per-model SSBO at binding=1.  The main and
pick vertex shaders do a double-indirection
`instances[visible[u_offset + gl_InstanceID]]` so draws only touch
instances that passed the frustum test.

Models with fewer than BVH_MIN_OBJECTS instances skip the BVH build
and fall back to a linear per-instance frustum test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult ababb49ae7 Sidecar v4: persist instanced geometry + metadata
Commit B of the instancing migration.  The sidecar on-disk format is
reintroduced at version 4 with MeshInfo + InstanceCpu sections in place
of v3's flat per-object draw-info array.

After streaming finishes, MainWindow asks the viewport for a post-
finalise snapshot (VBO + EBO are read back from the GPU, meshes and
instances come from the CPU-side arrays) and writes it alongside
PackedElementInfo + the string table.  On a subsequent load,
readSidecar rehydrates the whole struct and ViewportWindow::
applyCachedModel uploads VBO/EBO/SSBO in a single step, bypassing the
iterator entirely.

Staleness check is still by source file size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 07a5c59359 GPU instancing: streamer, viewport, shaders rewritten
Commit A of the instancing migration (Phase 3a).  The streamer now runs
the iterator with use-world-coords=false and dedupes by the geometry's
representation id, emitting a MeshChunk once per unique geometry and an
InstanceChunk per placement.  The viewport keeps geometry in local
coordinates (28 B/vertex, down from 32) and applies the per-instance
transform in the vertex shader via an std430 SSBO indexed by
gl_InstanceID + a per-draw uniform offset.  After streaming finishes
finalizeModel() stable-sorts instances by mesh_id, assigns each mesh a
contiguous range, and uploads the SSBO; render then issues one
glDrawElementsInstancedBaseVertex per mesh.

BvhAccel is reshaped to operate on a generic BvhItem (world AABB +
model_id) so it can drive instance-level culling, but the path is not
wired in yet -- every instance is drawn every frame in this commit.
Progressive-during-streaming rendering is likewise disabled: a model
appears when its SSBO is uploaded, not incrementally.  Sidecar cache
is stubbed (reads miss, writes are no-ops); the v4 on-disk format with
MeshInfo + InstanceGpu sections lands in Commit B.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 8c8ef5c32b Leaf-batched BVH draw commands
When a BVH leaf passes the frustum test, emit a single glMultiDrawElements
record covering the leaf's entire index range instead of one per object.
Leaves are contiguous in the EBO after reorderEbo, so the range is just
[first_object.index_offset, sum(index_count)]. Cuts draw calls by ~8x
(BVH_MAX_LEAF_SIZE) and shifts the bottleneck from CPU/driver per-draw
overhead toward GPU vertex throughput.

Per-object features (selection highlight, per-vertex color, object_id
picking) are unchanged — they operate on vertex attributes, not draw
state. Future per-object hide/override will use SSBO lookups sampled
by object_id in the fragment shader.

Slight overdraw from skipping per-object frustum tests within a leaf is
negligible given median-split BVH tightness and spare tri throughput.

Also adds visible_objects_ counter so stats still report true object
counts (not leaf counts), plus leaf_draws/model_draws breakdown in the
per-second frame log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 36fa53122a Add profiling for VRAM, FPS ratios, and instancing analysis
Per-second frame log reports fps/ms, visible/total object & triangle
ratios, VRAM breakdown (VBO+EBO), model count, and pending uploads.

Upload-complete log includes per-model VBO/EBO MB and scene total VRAM.

Streamer runs an instancing analysis keyed on geom.id(): total shapes,
unique representations, dedup ratio, theoretical VBO/EBO/SSBO sizes if
instanced, potential savings, and top-5 most-duplicated representations.
Used to validate whether GPU instancing is worth the architectural
rewrite for a given dataset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 1dace18d26 BVH frustum culling, sidecar cache, per-model buffers, progressive upload
Phase 2 performance: BVH acceleration with median-split build, per-model
trees, and EBO re-sorting for GPU cache coherence. Raw binary .ifcview
sidecar stores full geometry + BVH for instant subsequent loads (skip
tessellation entirely).

Per-model GPU buffers (VAO/VBO/EBO per model) eliminate cross-model buffer
copies on growth. Sidecar reads happen on a background thread. Bulk GPU
uploads are progressive (48 MB/frame chunks) so the viewport stays
interactive while multi-GB models stream in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 5b4c1089cf Update README for multi-model support and frustum culling
Reflect current architecture: per-model streamers, glMultiDrawElements
with frustum culling, 32-byte vertex format with color, multiselect
file picker, settings/stats files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 83a3131276 Multi-model project support with sequential loading
Introduce ModelHandle and per-model GeometryStreamers so multiple IFC
files can be loaded simultaneously. Object IDs are globally unique
(monotonically increasing across models). File picker is now multiselect.
Each model gets a top-level tree node. Property lookup uses the correct
model's ifcopenshell::file. ViewportWindow supports hide/show/remove
per model via model_id filtering in the frustum cull pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult 6f6bebf387 Add performance stats overlay in status bar
Show FPS, frame time, visible/total objects, and visible/total
triangles in the status bar. Toggled via Settings > Show Performance
Stats, persisted in app settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult bfac12dbe7 Per-object frustum culling with glMultiDrawElements
Track per-object AABB and index range during upload. Each frame,
extract frustum planes from the view-projection matrix and cull
objects whose AABB is entirely outside any plane. Draw only visible
objects via glMultiDrawElements. Document the three-phase rendering
performance strategy in README.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:24 +10:00
Dion Moult d33055bb72 Plan out performance strategy 2026-04-23 21:32:24 +10:00
Dion Moult d08a4e0706 Update ifcviewer to compile with datamodel refactor 2026-04-23 21:32:24 +10:00
Dion Moult 06eca938d7 Dump of hello world ifc viewer code 2026-04-23 21:32:24 +10:00
Dion Moult 543d9f8588 Local hacks to compile and monkey patch issues in the Python world
All AI generated slop. Do NOT trust these "fixes". It's just to get it
working on my machine.
2026-04-23 21:32:24 +10:00
Thomas Krijnen b40e4378b3 update workflows 2026-04-23 10:58:57 +02:00
Thomas Krijnen 1089de14f0 quotes 2026-04-23 10:51:52 +02:00
Thomas Krijnen 1b35533917 Update workflow for .so copying 2026-04-22 21:40:05 +02:00
Thomas Krijnen c42a7f32d0 The proper id / identity fix for rocksdb 2026-04-22 18:11:01 +02:00
Thomas Krijnen 14e9846e35 identity_ for types; id_ for instances 2026-04-22 12:04:24 +02:00
Thomas Krijnen 37c6aea092 forgot to set goosd 2026-04-22 12:04:09 +02:00
Thomas Krijnen 9b13dc8dd6 Get rid of parse context pool 2026-04-22 12:03:58 +02:00
Thomas Krijnen 89c66f62bf Python import fixes: import from wrapper now which inherits from mixins 2026-04-21 21:59:02 +02:00
Thomas Krijnen 18363c0d19 No soname for plugin.so in CREATE_BUNDLE mode 2026-04-21 21:52:04 +02:00
Thomas Krijnen 782aa4f88f Copy more so to python module 2026-04-21 21:51:21 +02:00
Thomas Krijnen 13c71d7a8a symbol visibility 2026-04-21 21:48:54 +02:00
Thomas Krijnen cb7e7331e6 CREATE_BUNDLE=On to copy .so 2026-04-21 21:48:15 +02:00
Thomas Krijnen 4850e2a07c add manifold to build-all.py 2026-04-21 21:47:38 +02:00
Thomas Krijnen fd980abcc2 Reverse subdir order so that svgfill is a proper target 2026-04-21 21:46:56 +02:00
Thomas Krijnen b022ca7e70 Some plug-in work 2026-04-21 16:18:59 +02:00
Thomas Krijnen 325db2e57f Export templates 2026-04-21 11:55:04 +02:00
Thomas Krijnen 046ceb452a Tighten scope of cmake vars and dirs 2026-04-19 12:32:18 +02:00
Thomas Krijnen 9e19735275 IfcConvert Plug-in discovery for info print 2026-04-19 12:32:10 +02:00
Thomas Krijnen 6ee05b646b swig ignore Base::Base(std::nullopt_t); 2026-04-19 12:24:36 +02:00
Thomas Krijnen e62921171c Remove duplicated attr in wrapper 2026-04-19 11:42:24 +02:00
Thomas Krijnen 6c47123781 Remove C++ references to ifcxml 2026-04-19 10:35:04 +02:00
Thomas Krijnen d448fa95e3 build-all.py use single build dir 2026-04-19 10:21:23 +02:00
Thomas Krijnen 57de09d236 Pointer issue 2026-04-19 09:31:49 +02:00
Thomas Krijnen a25f522cc3 typo 2026-04-19 08:33:48 +02:00
Thomas Krijnen e067c2b834 build-all BUILD_SHARED_LIBS tweak 2026-04-18 21:43:36 +02:00
Thomas Krijnen e2905d6f0e BUILD_SHARED_LIBS=On 2026-04-18 21:32:32 +02:00
Thomas Krijnen ead061a98a svgfill dll link related 2026-04-18 21:13:24 +02:00
Thomas Krijnen fae85e63cc std::optional 2026-04-18 21:06:39 +02:00
Thomas Krijnen bccea6d932 Examples and update virtual bases for new codegen 2026-04-18 21:04:42 +02:00
Thomas Krijnen 1cc93784cd Rerun codegen 2026-04-18 21:03:02 +02:00
Thomas Krijnen f1e93581ec Reuse constructors and return *this from initialize() 2026-04-18 21:01:21 +02:00
Thomas Krijnen 91ae631c7d Merge remote-tracking branch 'origin/v0.8.0' into datamodel-v1.0 2026-04-18 20:15:28 +02:00
Thomas Krijnen b599ee1040 More work on isolating into plug-ins 2026-04-18 15:46:21 +02:00
Thomas Krijnen d2cc66fdf0 tree and document plug-ins 2026-04-17 11:24:09 +02:00
Thomas Krijnen 3824e7b449 First start plug-in architecture 2026-04-15 18:07:28 +02:00
Thomas Krijnen aa10784154 First slice plug-in refactor 2026-04-14 13:51:09 +02:00
Thomas Krijnen 2018bcb3c1 This needs some serious scrutiny 2026-04-13 21:28:55 +02:00
Thomas Krijnen 11006f0ef5 deque 2026-04-13 21:18:58 +02:00
Thomas Krijnen a44f72287b pasta errors 2026-04-13 21:18:49 +02:00
Thomas Krijnen c6849073d9 Reset weights 2026-04-13 21:18:41 +02:00
Andrej730 e6258ab4a8 Bump VERSION to 0.8.6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 19:40:25 +05:00
Andrej730 5db65f4041 maintenance - list all things we do on release 2026-04-13 19:39:14 +05:00
Andrej730 89ce32fdfd Remove redundant docs-deployment.yml workflow
The https://github.com/IfcOpenShell/website repo already has bonsai-docs.yml workflow that does the same thing - builds Bonsai docs from the main repo and deploys to bonsaibim_org_docs, so this workflow is redundant and confusing.
2026-04-13 19:39:14 +05:00
Andrej730 763a31a31d readme: fix ifcsverchok badge filter 2026-04-13 19:38:25 +05:00
Andrej730 4b8c612647 fix ifcmcp package name inconsistency 2026-04-13 18:44:01 +05:00
Andrej730 16723d11ca ci-pyodide-wasm-release - add tag when pushing release 2026-04-13 17:45:37 +05:00
Andrej730 67238c4ac1 ci-pyodide-wasm-release - use BUILD_REPO_TOKEN 2026-04-13 17:40:02 +05:00
Andrej730 20229aa88c README.md: add pyodide-wasm-wheels tag badge 2026-04-13 16:43:20 +05:00
Andrej730 7788ae86c9 build-all.py: descriptive error for missing SSL support 2026-04-13 16:23:41 +05:00
Bruno Postle 002b7c5d6e ifcquery, ifcmcp: better bot selector syntax hints 2026-04-10 22:09:56 +01:00
Thomas Krijnen e7db239647 inverse access in schema 2026-04-10 21:46:39 +02:00
Thomas Krijnen 158756e921 arrange_polygons: settings, simplify based on growing boxes; more... 2026-04-10 21:46:39 +02:00
Andrej730 a3efa7e9ee util.element - fix IfcComplexProperty KeyError when verbose=True (#7921)
Introduced by me in b77df1892
2026-04-10 19:11:42 +05:00
Andrej730 4896946e78 ty ignore some upstream bpy stubs issues 2026-04-10 19:11:41 +05:00
Andrej730 588f365366 Remove unused ty ignores - issue is resolved upsteam in stubs 2026-04-10 19:11:41 +05:00
Andrej730 fa8770c14d ty - drop rules removed from recent version of ty 2026-04-10 19:11:41 +05:00
Andrej730 0cf831133e ci-lint - add ty type check 2026-04-10 19:11:41 +05:00
Andrej730 98338e0831 Rename ci-black-formatting workflow to ci-lint 2026-04-10 18:06:43 +05:00
Andrej730 5a3160eb62 black . 2026-04-10 18:02:47 +05:00
Andrej730 80a9df8f52 Ignore pyright warnings for bpy stubs
See https://github.com/nutti/fake-bpy-module/discussions/440
2026-04-10 18:01:19 +05:00
Andrej730 8eb0060d4a Get rid of pyright ignore reportRedeclaration noise
Welp, it was helping to point out untyped props, but it is getting too noisy now.
2026-04-10 17:55:16 +05:00
Thomas Krijnen b2fc0c00cc Hierarchical index for inverses 2026-04-10 14:54:47 +02:00
falken10vdl 51a338e4c8 Suppress reportRedeclaration in Pyright config 2026-04-10 17:49:14 +05:00
Thomas Krijnen 2d5883f966 Dilation of 2nd operands as a poor mans fuzziness 2026-04-10 14:46:46 +02:00
Andrej730 7169dcd053 Create ci-pyodide-wasm-release.yml 2026-04-10 17:29:46 +05:00
Andrej730 b9d4ea38b0 Script for packing pyodide wheel 2026-04-10 17:29:46 +05:00
Andrej730 c8f46cfb69 build_pyodide.sh - use emsdk from pyodide 2026-04-10 16:22:04 +05:00
Andrej730 6242251d3c Fix typo 2026-04-10 16:22:04 +05:00
Andrej730 1689960257 Maintenence - document ci-bonsai.yml update 2026-04-10 16:20:50 +05:00
dependabot[bot] c509f1d3ee Bump vite from 6.4.1 to 6.4.2 in /src/ifctester/webapp
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 16:16:33 +05:00
Dion Moult 217bfed847 Add py313 to stable build 2026-04-10 19:05:54 +10:00
Thomas Krijnen 4621fc9269 Less useless logging 2026-04-10 09:54:28 +02:00
Thomas Krijnen 1840e3d1a8 Add passthrough kernel 2026-04-09 17:17:53 +02:00
Thomas Krijnen becd38c77d Compilation fixes 2026-04-09 16:18:53 +02:00
Thomas Krijnen 7d6c6bd523 Fix iteration: prevent inserting nullptr equivalents into a set 2026-04-09 11:53:28 +02:00
Bruno Postle b4558f7f75 Fix ruff import ordering complaints 2026-04-09 01:04:14 +01:00
dependabot[bot] ebd5fe854f Bump ruff from 0.15.8 to 0.15.9
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.8 to 0.15.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.8...0.15.9)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.15.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:16 +10:00
dependabot[bot] 06cfd0931c Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:10 +10:00
dependabot[bot] 90bd7d26ac Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-09 09:59:03 +10:00
Thomas Krijnen 3fbf01f446 partial revert of 24acfea 2026-04-08 13:48:23 +02:00
Bruno Postle 26a1955cba Fix compilation failure introduced in 24acfea 2026-04-07 23:34:30 +01:00
Bruno Postle 27d9cae8ff Bonsai, bump ifcmerge.exe to working version with deps
Don't leave a broken repo if ifcmerge is misinstalled.
Fix bug where only local branches could be merged.
Fix gitch where merge commits were not considered relevant.
2026-04-07 22:25:31 +01:00
Thomas Krijnen a425bb6da0 Cache some hotpath inverses regarding context handling 2026-04-07 16:13:01 +02:00
Thomas Krijnen a19d398c78 Vibe code an implementation that uses manifold 2026-04-07 15:47:58 +02:00
Thomas Krijnen a751c1cce3 ifcchat: compaction 2026-04-07 09:46:45 +02:00
Thomas Krijnen 9d4307d343 ifcchat: Throttling of messages based on estimated token counts 2026-04-07 09:46:11 +02:00
Ryan Schultz ab7d9fdf4a Auto-assign aggregate on eyedropper pick
Add update callbacks to the relating_object and related_object
PointerProperties so that selecting an object via the eyedropper
in BIM_PT_aggregate immediately calls aggregate_assign_object
and closes the editing panel, removing the need to click the
checkmark button manually.

Generated with the assistance of an AI coding tool.
2026-04-05 16:43:03 -05:00
Ryan Schultz 5436467fc5 Whoops, this was supposed to be a PR...
Revert "Fix #3742: Remove coplanar boundary lines between adjacent same-material elements in Bonsai SVG drawings"

This reverts commit 1c7e134d78.
2026-04-04 13:49:02 -05:00
Ryan Schultz 1c7e134d78 Fix #3742: Remove coplanar boundary lines between adjacent same-material elements in Bonsai SVG drawings
Adds `remove_coplanar_boundary_lines()` to operator.py (Bonsai uses this
path, not draw.py's main()). After `merge_linework_and_add_metadata()`
assigns material CSS classes, this post-processes the SVG to delete
projection line segments that appear in two or more adjacent, coplanar
elements with the same material and presentation style.

Key design decisions:
- Material identity: compared via sorted IFC material ID tuples from
  `get_materials()`, not CSS class names — avoids false matches between
  unrelated `material-null` elements.
- Presentation style identity: compared via IFC IfcPresentationStyle IDs
  from `StyledByItem` on geometry representation items — handles elements
  with no material but distinct visual styles.
- Physical adjacency: confirmed by a 3D shared-vertex test (tol=0.01 m)
  after a quick AABB guard, rejecting elements whose 2D projections
  overlap but sit at different depths.
- Coplanarity: determined by the dominant (largest-area) face normal of
  each Blender mesh object — area-weighted averages are unreliable for
  slabs whose equal top/bottom faces cancel out. Folded walls sharing an
  edge but meeting at an angle are correctly rejected (normal dot ≪ 1.0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 13:42:49 -05:00
Bruno Perdigão 97ee4eaef0 See #7888 - Fix snap when object changes during modal operator.
Handle cases where the snapped target is modified while a modal operator
is active (e.g., adding a door or window that alters the wall geometry).
2026-04-04 14:30:59 -03:00
Thomas Krijnen 30517770e0 Revert default tool output truncation 2026-04-04 13:12:47 +02:00
DesertSpringsCivil 5b1ec85f75 feat: Reduce token usage in ifcchat and default to IFC4X3
- Add Anthropic prompt caching (cache_control on system prompt and
  tools) to reduce repeated token costs by ~90%
- Truncate large tool results in conversation history (2000 char cap)
  to prevent context bloat from ifc_tree/ifc_select responses
- Add sliding window (40 messages) on conversation history, trimming
  at user message boundaries to avoid breaking tool-call sequences
- Default "New IFC" button to IFC4X3 schema instead of IFC4
- Constrain ifc_new schema parameter with enum to prevent invalid
  schema strings like "IFC4X3ADD2"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:11:14 +02:00
Stephen Boddy 12123baafe Update the pyver as 3.13 is default in 5.1 now 2026-04-04 04:22:12 +01:00
Stephen Boddy 0a1c54cedc Fix the ci-bonsai-daily blender url 2026-04-04 03:56:08 +01:00
Bruno Postle 12144f76d3 Merge branch 'ifcgit-features' into v0.8.0 2026-04-04 00:10:33 +01:00
Bruno Postle ca6e950496 ifcgit: conflict report panel and dry-run merge preview
Parse ifcmerge JSON output and display a per-conflict breakdown in the
panel when merge fails. Ctrl+click on the Merge button previews
conflicts without committing. Add SelectConflictEntity operator to
select and frame the conflicting object in the 3D viewport.

Generated with the assistance of an AI coding tool.
2026-04-03 13:28:14 +01:00
Thomas Krijnen c478da5257 Remove pro 2026-04-03 11:36:56 +02:00
Thomas Krijnen c28251a1b1 Add CNAME file 2026-04-03 11:30:33 +02:00
Thomas Krijnen 9bc0588d21 Update openai model list 2026-04-03 11:30:23 +02:00
Thomas Krijnen 918cc65a0d Provider selection as tabs 2026-04-03 11:22:43 +02:00
Thomas Krijnen 7350ccd25e Tweak header padding 2026-04-03 11:14:40 +02:00
Thomas Krijnen c1f146966c The end of open source? Just regurgitate some markdown parsing code. 2026-04-03 11:03:44 +02:00
Thomas Krijnen 24acfeaf45 Thinking indicator under chat 2026-04-03 10:59:09 +02:00
Thomas Krijnen 5d748c5b04 Add Gemini option 2026-04-03 10:49:36 +02:00
Thomas Krijnen 5bcf8685ab Merge remote-tracking branch 'origin/feat/ifcchat-claude' into v0.8.0 2026-04-03 10:26:29 +02:00
geronimi73 8f64f75abb add favourite models 2026-04-03 09:46:44 +02:00
geronimi73 434ea74f22 format this mess 2026-04-03 09:46:44 +02:00
geronimi73 fbf2946b69 Update index.html 2026-04-03 09:46:44 +02:00
geronimi73 7af0ec13e5 move model to sidebar 2026-04-03 09:46:44 +02:00
geronimi73 29e5e0fd1a chevrons for tool result expansion 2026-04-03 09:46:44 +02:00
geronimi73 d7de4f8df0 dont freeze UI on error 2026-04-03 09:46:44 +02:00
geronimi73 252bd6f4f6 openai by default 2026-04-03 09:46:44 +02:00
geronimi73 3b28c92414 html too big -> styles into sep. file 2026-04-03 09:46:44 +02:00
geronimi73 fcbec74521 spinner 2026-04-03 09:46:44 +02:00
geronimi73 0f7f960b29 let claude code openrouter compatibility 2026-04-03 09:46:44 +02:00
geronimi73 b7fc5daf82 ui: choose openai/openrouter 2026-04-03 09:46:44 +02:00
geronimi73 1d7a8ca249 separate API calls 2026-04-03 09:46:44 +02:00
Ryan Schultz eaf7950677 Fix TypeError in ray_cast_by_proximity_2d degenerate edge
A degenerate edge (zero-length segment) caused an early `return`
of a tuple instead of continuing the loop, resulting in a
TypeError when snap.py iterated the result and tried to assign
`point["group"]` on a float.

Generated with the assistance of an AI coding tool.
2026-04-02 23:24:35 -03:00
DesertSpringsCivil 95851ff94c feat: Add Anthropic Claude API support to ifcchat
Add a provider selector (OpenAI / Anthropic) to the ifcchat web UI,
allowing users to use their Anthropic API key with Claude models
instead of only OpenAI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:50:27 -06:00
Bruno Postle 3a6881ca17 ifcgit: add rename branch button next to working branch label
See #7577

Generated with the assistance of an AI coding tool.
2026-04-03 00:20:22 +01:00
Bruno Postle c5210a4f82 ifcgit: sync load_project post-import steps with project operator
See #7578
2026-04-03 00:03:34 +01:00
Bruno Postle e3cadab406 ifcgit: add clone widget to new project wizard (#7579) 2026-04-02 23:42:39 +01:00
Bruno Postle d5b551874d ifcgit: pre-fill branch name when switching to a remote branch tip
When a remote branch tip is checked out (resulting in detached HEAD),
the new-branch name field is now pre-filled with the local equivalent
of the remote branch name (generating a unique suffix if that name is
already taken), so the commit button is immediately usable.

See #7580

Generated with the assistance of an AI coding tool.
2026-04-02 23:16:40 +01:00
Bruno Postle be3aef59fa ifcgit: move action buttons below revision list in a labelled row
Generated with the assistance of an AI coding tool.
2026-04-02 22:45:33 +01:00
Bruno Postle bcc631bf5f ifcgit: improve colourise to find products via geometry and property changes
Generated with the assistance of an AI coding tool.
2026-04-02 22:44:56 +01:00
Bruno Postle 6c18ac9605 ifcgit: use --prioritise-local flag for ifcmerge-forward mergetool 2026-04-02 22:42:29 +01:00
Bruno Postle 9d42f4c1ee Update Bonsai to latest ifcmerge (#7581 #3096)
This version has some functional differences:
- Structured JSON error message instead of free text (on STDOUT not STDERR)
- New --prioritise-local flag to control which side wins in merge conflicts (not used by Bonsai yet)
- IfcLocalPlacement conflicts now auto-resolve instead of failing the merge (partial solution to #6885)
- Float values are normalised when comparing entities (workaround for #7696)
2026-04-02 07:54:20 +01:00
Ryan Schultz fdb2947345 Add git branch to system info debug output
Include bonsai_git_branch in get_debug_info(). For dev environments
using the GitPython-based update_commit_data() path, the branch is
read from repo.active_branch.name. For built extensions, a 7777777
placeholder is replaced at build time via the Makefile, matching the
existing pattern for bonsai_commit_hash and bonsai_commit_date.

Generated with the assistance of an AI coding tool.
2026-04-01 19:45:22 -05:00
Bruno Postle 5e784e4175 Refactor ifcgit, fix UI bugs and performance
Move all business logic into bonsai core and tool. Performance fixes to
minimise file IO, various minor bug fixes and tests.

Generated with the assistance of an AI coding tool.
2026-04-02 00:03:03 +01:00
Thomas Krijnen fb81c88a5f initial ai chat src 2026-04-01 15:56:13 +02:00
Thomas Krijnen c014ce2b46 initial ai chat src 2026-04-01 15:52:22 +02:00
Thomas Krijnen ff65719074 initial ai chat src 2026-04-01 15:50:24 +02:00
Thomas Krijnen 3491e4c91b initial ai chat src 2026-04-01 15:46:45 +02:00
Thomas Krijnen 9f3adc9154 initial ai chat src 2026-04-01 15:36:27 +02:00
Thomas Krijnen f6c6203408 initial ai chat src 2026-04-01 15:33:43 +02:00
falken10vdl 39a376df95 intersect_edge_region_border: Change return statements to return None, None for no intersection
In order to fix error of the type:
              |     point, _ = cls.intersect_edge_region_border(
                            |     ^^^^^^^^
                            | TypeError: cannot unpack non-iterable NoneType object

a tuple is expected.
2026-04-01 08:46:40 -03:00
Ryan Schultz 70a4fdbf95 Fix #7878: Fix snapping crash with non-mesh objects
Two bugs introduced in 31b571322:
- SnapObj assumed obj.data is always a Mesh; non-mesh
  objects (empties, lights, etc.) have obj.data = None,
  causing an AttributeError on obj.data.edges.
- view3d_utils was used but never imported.

Generated with the assistance of an AI coding tool.
2026-04-01 08:14:51 -03:00
Thomas Krijnen 0a41d2e016 Rename project from 'ifcmcp' to 'ifcopenshell-mcp' 2026-04-01 11:09:49 +02:00
Thomas Krijnen 0b5eab8549 Enable verbose output for PyPI deployment 2026-04-01 09:24:35 +02:00
Bruno Postle 6f9d54c2af ifcquery, ifcedit: update docs for --format ids and foreach subcommand
Add --format ids to the ifcquery.rst format description and a new
"Scripting with ifcedit" section showing composition examples.  Add
the foreach subcommand to ifcedit.rst with usage examples.
2026-04-01 08:53:09 +02:00
Bruno Postle 9ea302cdf1 ifcquery, ifcedit, ifcmcp: add documentation
Add ifcquery, ifcedit and ifcmcp to the README contents table, the
Sphinx docs toctree and introduction utilities table. Add new .rst
pages for each package documenting subcommands, installation, usage,
and parameter types. Fix plot and render CLI examples in ifcquery
README to use -o/--out-format flags. Update ifcmcp README to use the
installed ifcmcp command rather than python3 -m ifcmcp.

Generated with the assistance of an AI coding tool.
2026-04-01 08:53:09 +02:00
Bruno Postle c057e79f17 ifcquery, ifcedit, ifcmcp: add Makefiles and PyPI publish workflows
These three packages were added to src/ but lacked the Makefile needed
by common.mk to build distribution wheels, and the GitHub Actions
workflow to publish them to PyPI.

Adds make dist / make test / make qa targets and ci-*-pypi.yaml
workflows matching the pattern used by ifcpatch, ifcclash, etc.
2026-04-01 08:53:09 +02:00
Thomas Krijnen f616c4049c Add Codex-generated wrappergen 2026-03-31 20:51:46 +02:00
Thomas Krijnen 20ccf2b455 Parameter naming 2026-03-31 18:23:13 +02:00
Thomas Krijnen a07f56db6f Restructure and rename 2026-03-31 15:32:36 +02:00
Thomas Krijnen 724cdb446e Move schemas into schemas/ subfolder 2026-03-31 10:02:00 +02:00
Thomas Krijnen 95b3f7dac4 Don't include dot as special when doing float runs 2026-03-27 21:04:48 +01:00
Thomas Krijnen 603cedc487 Try some things: (a) fewer allocations - parse context pool; lexer string pool (b) SWAR process multiple chars at once in keywords/enums/strs/stc. 2026-03-27 20:45:13 +01:00
Thomas Krijnen fe6e9d86ae Xml serializer proper specialization 2026-03-26 15:49:54 +01:00
Thomas Krijnen 8e42f35db3 Rework variable length token storage to use string pool; eliminate need for rereads 2026-03-26 15:49:28 +01:00
Thomas Krijnen 9d69a712ac Does SWIG prefer std::conditional_t over auto return type? 2026-03-26 11:33:00 +01:00
Thomas Krijnen dc127471c5 Rerun codegen 2026-03-26 10:52:12 +01:00
Thomas Krijnen 95a094d596 Fix some schema generation issues 2026-03-26 10:39:29 +01:00
Thomas Krijnen 4ec643d4e1 schema entity initialize() method, optional argument forwarding in file::create, populate_derived in InstanceData 2026-01-15 19:24:55 +01:00
Thomas Krijnen c089c11cd6 Hack a bit to make stream tests run 2026-01-15 16:22:18 +01:00
Thomas Krijnen 8afba9a665 Hack a bit to make sql tests run 2026-01-15 16:12:25 +01:00
Thomas Krijnen 5c4046e054 declaration as property 2026-01-15 16:11:58 +01:00
Thomas Krijnen d82e1da907 Don't leak parent id into type decl instances 2026-01-15 15:38:49 +01:00
Thomas Krijnen 8c2e1226c9 declaration property 2026-01-15 15:38:32 +01:00
Thomas Krijnen ed8821486d Broaden __eq__ for type decl instances 2026-01-15 14:23:49 +01:00
Thomas Krijnen 742bfac144 _remove, schema_identifier and test_file 2026-01-15 14:13:29 +01:00
Thomas Krijnen 1f4c4204d0 Re-enable setting logical with UNKNOWN in python 2026-01-15 13:13:51 +01:00
Thomas Krijnen 43f78605f2 Small tweak to invocation of test/test_rules.py 2026-01-15 13:00:48 +01:00
Thomas Krijnen 6cd1375cf9 Rerun rule compilation for exists() on indeterminate 2026-01-15 13:00:37 +01:00
Thomas Krijnen 2d1250c18f Global file for rule and derived attributes 2026-01-15 12:39:32 +01:00
Thomas Krijnen bee61cba11 Accept exact schema_identifier in file() 2026-01-15 12:38:36 +01:00
Thomas Krijnen 33f072e0a7 Account for removed instance factory 2026-01-15 12:37:53 +01:00
Thomas Krijnen 490fa92dc0 Rework equality and get_info_2 2026-01-15 12:37:12 +01:00
Thomas Krijnen cf3393b6a0 black 2026-01-14 14:09:40 +01:00
Thomas Krijnen fbbc92c5d7 Hashing solely based on identity 2026-01-14 14:03:55 +01:00
Thomas Krijnen 574827016a Initialization of header and file 2026-01-14 14:03:28 +01:00
Thomas Krijnen 18c9140700 Data types 2026-01-14 14:03:03 +01:00
Thomas Krijnen 9e165319e6 Fix schema passing to file creation 2026-01-14 14:02:48 +01:00
Thomas Krijnen 64bf807baa Fix add entity with id 2026-01-14 14:02:10 +01:00
Thomas Krijnen 136befde86 Fix write() call 2026-01-14 14:01:39 +01:00
Thomas Krijnen 1a4d750ecd Consistency of get_inverse calls 2026-01-14 14:00:50 +01:00
Thomas Krijnen 0a5dd78774 Fix add entity with id 2026-01-14 14:00:00 +01:00
Thomas Krijnen 94427ebcb3 Allow setting of history and future 2026-01-14 13:59:26 +01:00
Thomas Krijnen 9147938c36 Aggregate data types 2026-01-14 13:59:07 +01:00
Thomas Krijnen ffcc02fb99 Remove global create_entity() call 2026-01-14 13:57:26 +01:00
Thomas Krijnen e4e4f31d20 Defer deletion so that traversal still works 2026-01-13 08:45:04 +01:00
Thomas Krijnen 2dd002bcd8 Properly constuct type decl instances 2026-01-13 08:44:44 +01:00
Thomas Krijnen 7e5248da29 Fix create_shape() overloads because SWIG does not map None for us anymore 2026-01-13 08:44:30 +01:00
Thomas Krijnen 07a01bcf54 Process derived attributes that are not redeclared (C++ has no knowledge of them) 2026-01-13 08:43:52 +01:00
Thomas Krijnen 0a9e29ce45 black 2026-01-10 11:52:08 +01:00
Thomas Krijnen 0604db06e9 typename 2026-01-10 11:52:05 +01:00
Thomas Krijnen 991556fbe3 template as 2026-01-10 11:03:26 +01:00
Thomas Krijnen b66b04b001 Remove usage of .wrapped_item and some other fixes 2026-01-10 11:01:18 +01:00
Thomas Krijnen 5c9213426f Hacks and fixes to get python code back in reasonable state 2026-01-10 10:21:09 +01:00
Thomas Krijnen 69a4ad35a1 Remaining cpp changes 2026-01-10 10:20:34 +01:00
Thomas Krijnen 7f4d9e31c3 Add test about deletion 2026-01-08 11:52:04 +01:00
Thomas Krijnen ae79996eb6 Fix running of test/tests.py 2026-01-08 11:49:29 +01:00
Thomas Krijnen f5b2358c2e Make Base::data() private, file::add(..., id) 2026-01-08 10:35:46 +01:00
Thomas Krijnen f9c791de1b Fix examples 2026-01-08 09:44:43 +01:00
Thomas Krijnen 5b85a2c38b Fix examples 2026-01-08 09:44:00 +01:00
Thomas Krijnen bc46a8f85b Fixes 2026-01-08 09:38:46 +01:00
Thomas Krijnen c57209fbce Fixes 2026-01-07 20:37:10 +01:00
Thomas Krijnen 088a6b7204 Reinstate IfcAlignment example 2026-01-07 18:15:23 +01:00
Thomas Krijnen f2f4d626a5 Fix examples mostly 2026-01-07 13:51:48 +01:00
Thomas Krijnen 572a5655cf Fixes for gcc 2026-01-06 13:23:48 +01:00
Thomas Krijnen ccd91c0ff0 No more messing around with specific sfinae as<>() in Select implementations 2026-01-06 11:27:29 +01:00
Thomas Krijnen a1d2c902f3 Add enum to forward declarations for Ifc2x3::IfcNullStyle 2026-01-06 09:56:36 +01:00
Thomas Krijnen 3ae361bb3b Rerun codegen 2026-01-06 09:49:28 +01:00
Thomas Krijnen 2d7521ed88 Move template down to .cpp to prevent use of incomplete type 2026-01-06 09:41:20 +01:00
Thomas Krijnen 7098beb819 Work towards v1.0 data model with encapsulated weak_ptr as basis for instances 2026-01-05 21:42:01 +01:00
Thomas Krijnen f09ca658f1 Initial investigation into lofting open profile with tags 2025-12-02 16:34:24 +01:00
735 changed files with 419244 additions and 363524 deletions
+28 -8
View File
@@ -110,7 +110,20 @@ jobs:
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/10.15/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -125,18 +138,25 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell/*
stage_runtime_payload ifcopenshell
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-macos${{ matrix.oldarch }}64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+27 -8
View File
@@ -86,7 +86,20 @@ jobs:
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -101,18 +114,24 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell/*
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linux64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+28 -8
View File
@@ -86,7 +86,20 @@ jobs:
run: |
VERSION=v`cat VERSION`
cd ./build/`uname`/*/install/ifcopenshell
mkdir ~/output
mkdir -p ~/output
install_root="$PWD"
stage_runtime_payload() {
dest="$1"
while IFS= read -r runtime_file; do
cp -L "$runtime_file" "$dest/"
done < <(
for runtime_dir in "$install_root/bin" "$install_root/lib" "$install_root/lib64"; do
[ -d "$runtime_dir" ] || continue
find "$runtime_dir" -type f \( -name "*.so" -o -name "*.so.*" -o -name "*.dylib" -o -name "*.dll" \)
done
)
}
ls -d python-* | while read py_version; do
postfix=`echo ${py_version: -1} | sed s/[0-9]//`
@@ -101,18 +114,25 @@ jobs:
fi
[ -d ifcopenshell/__pycache__ ] && rm -rf ifcopenshell/__pycache__
find ifcopenshell -name "*.pyc" -delete
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell/*
stage_runtime_payload ifcopenshell
zip -r -qq ifcopenshell-${py_version_major}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip ifcopenshell
mv *.zip ~/output
popd > /dev/null
done
cd bin
rm *.zip || true
ls | while read exe; do
zip -qq -r ${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip $exe
rm -f "$install_root"/bin/*.zip
find "$install_root/bin" -maxdepth 1 -type f -perm /111 ! -name "*.zip" ! -name "*.so" ! -name "*.so.*" ! -name "*.dylib" ! -name "*.dll" | while read exe_path; do
exe=`basename "$exe_path"`
package_dir="$install_root/.package-${exe}"
rm -rf "$package_dir"
mkdir -p "$package_dir"
cp "$exe_path" "$package_dir/"
stage_runtime_payload "$package_dir"
pushd "$package_dir" > /dev/null
zip -qq -r "$HOME/output/${exe}-${VERSION}-${GITHUB_SHA:0:7}-linuxarm64.zip" .
popd > /dev/null
rm -rf "$package_dir"
done
mv *.zip ~/output
cd ..
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
+58
View File
@@ -68,6 +68,64 @@ jobs:
cd win
python build-all-win.py
- name: Repack .zip Archives With Runtime Plugins
shell: powershell
run: |
$version = (Get-Content VERSION | Select-Object -First 1).Trim()
$sha = $env:GITHUB_SHA.Substring(0, 7)
$outputDir = Join-Path $env:USERPROFILE "output"
$installDir = Get-ChildItem -Path . -Directory | Where-Object { $_.Name -like "_installed*" } | Select-Object -First 1
if (-not $installDir) {
throw "Install directory not found."
}
$runtimeFiles = @()
foreach ($runtimeDir in @((Join-Path $installDir.FullName "bin"), (Join-Path $installDir.FullName "lib"))) {
if (Test-Path $runtimeDir) {
$runtimeFiles += Get-ChildItem -Path $runtimeDir -Recurse -File | Where-Object { $_.Extension -ieq ".dll" }
}
}
$runtimeFiles = $runtimeFiles | Sort-Object FullName -Unique
Get-ChildItem -Path $outputDir -Filter *.zip -ErrorAction SilentlyContinue | Remove-Item -Force
$binDir = Join-Path $installDir.FullName "bin"
foreach ($exe in Get-ChildItem -Path $binDir -File -Filter *.exe) {
$stageDir = Join-Path $env:RUNNER_TEMP ("package-" + $exe.BaseName)
Remove-Item -Path $stageDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $stageDir | Out-Null
Copy-Item -Path $exe.FullName -Destination $stageDir
foreach ($runtimeFile in $runtimeFiles) {
Copy-Item -Path $runtimeFile.FullName -Destination $stageDir -Force
}
$zipPath = Join-Path $outputDir ("{0}-v{1}-{2}-win64.zip" -f $exe.BaseName, $version, $sha)
Compress-Archive -Path (Join-Path $stageDir '*') -DestinationPath $zipPath -Force
}
$depsRoot = Join-Path (Get-Location) "_deps"
if (Test-Path $depsRoot) {
foreach ($pythonRoot in Get-ChildItem -Path $depsRoot -Directory | Where-Object { $_.Name -like "python.*" }) {
$pythonVersion = $pythonRoot.Name.Substring("python.".Length)
$pythonVersionMajorMinor = ($pythonVersion.Split(".")[0..1] -join "")
$sitePackages = Join-Path $pythonRoot.FullName "tools\\Lib\\site-packages"
$packagePath = Join-Path $sitePackages "ifcopenshell"
if (!(Test-Path $packagePath)) {
continue
}
Get-ChildItem -Path $packagePath -Recurse -Filter *.pyc -ErrorAction SilentlyContinue | Remove-Item -Force
foreach ($runtimeFile in $runtimeFiles) {
Copy-Item -Path $runtimeFile.FullName -Destination $packagePath -Force
}
$zipPath = Join-Path $outputDir ("ifcopenshell-python-{0}-v{1}-{2}-win64.zip" -f $pythonVersionMajorMinor, $version, $sha)
Push-Location $sitePackages
try {
Compress-Archive -Path "ifcopenshell" -DestinationPath $zipPath -Force
} finally {
Pop-Location
}
}
}
- name: Pack Dependencies
run: |
cd ${{ matrix.deps_dir }}
+2 -2
View File
@@ -109,7 +109,7 @@ jobs:
# Ensure Bonsai and ifcsverchok enable/disable works before uploading to extensions repo.
# Download Blender.
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.0/blender-5.1.0-linux-x64.tar.xz
wget -q -O blender.tar.xz https://download.blender.org/release/Blender5.1/blender-5.1.0-linux-x64.tar.xz
tar -xf blender.tar.xz
# Setup Blender.
@@ -122,7 +122,7 @@ jobs:
pip install -r requirements.txt
python setup_extensions_repo.py --last-tag
cd ..
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py311*-linux-x64.zip)"
bonsai_zip="$(pwd)/$(ls bonsai_unstable_repo/bonsai_py313*-linux-x64.zip)"
# Install Bonsai.
blender --command extension install-file -r user_default -e $bonsai_zip
+6 -1
View File
@@ -24,7 +24,7 @@ jobs:
strategy:
fail-fast: false
matrix:
pyver: [py311, py312]
pyver: [py311, py312, py313]
config:
- {
name: "Windows Build",
@@ -42,6 +42,11 @@ jobs:
name: "MacOS ARM Build",
short_name: macosm1,
}
exclude:
# Python 3.13 is needed for Blender 5.1+ and Blender dropped Intel Mac support in 5.0.
- pyver: py313
config:
short_name: macos
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6 # https://github.com/actions/setup-python
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcedit-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcedit &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcedit/dist
+36
View File
@@ -0,0 +1,36 @@
name: ci-ifcmcp-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcmcp &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcmcp/dist
verbose: true
+35
View File
@@ -0,0 +1,35 @@
name: ci-ifcquery-pypi
on:
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Compile
run: |
pip install build
cd src/ifcquery &&
make dist IS_STABLE=TRUE
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
packages_dir: src/ifcquery/dist
@@ -1,4 +1,4 @@
name: ci-black-formatting
name: ci-lint
on:
push:
@@ -30,6 +30,7 @@ jobs:
uv tool install ruff
uv tool install black
uv tool install poethepoet
uv tool install ty
# black doesn't catch all syntax errors, so we check them explicitly.
- name: Check syntax errors
@@ -57,6 +58,13 @@ jobs:
black --diff --check . | black-codeclimate | python .github/workflows/black_to_github_annotations.py
continue-on-error: true
- name: ty check
id: ty
run: |
poe ty-venv
poe ty
continue-on-error: true
- name: Ruff check
id: ruff
run: |
@@ -105,4 +113,7 @@ jobs:
if [ "${{ steps.ruff.outcome }}" != "success" ]; then
echo "::error::Ruff check failed, see Summary or 'ruff' step for the details." && ERROR=1
fi
if [ "${{ steps.ty.outcome }}" != "success" ]; then
echo "::error::ty check failed, see 'ty check' step for the details." && ERROR=1
fi
exit $ERROR
@@ -0,0 +1,46 @@
name: Release Pyodide WASM Wheel
on:
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout IfcOpenShell
uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build wheel
working-directory: pyodide
run: uv run pack_wheel.py --build
- name: Find wheel
id: wheel
run: |
WHEEL=$(ls pyodide/dist/ifcopenshell-*.whl)
echo "path=$WHEEL" >> $GITHUB_OUTPUT
echo "name=$(basename $WHEEL)" >> $GITHUB_OUTPUT
- name: Checkout wasm-wheels
uses: actions/checkout@v6
with:
repository: IfcOpenShell/wasm-wheels
path: wasm-wheels
token: ${{ secrets.BUILD_REPO_TOKEN }}
- name: Commit and push wheel to wasm-wheels
run: |
WHEEL_NAME="${{ steps.wheel.outputs.name }}"
cp "${{ steps.wheel.outputs.path }}" "wasm-wheels/$WHEEL_NAME"
cd wasm-wheels
git config user.name "IfcOpenBot"
git config user.email "ifcopenbot@ifcopenshell.org"
git add "$WHEEL_NAME"
git commit -m "Add $WHEEL_NAME"
VERSION=$(cat ../VERSION)
git tag "v${VERSION}"
git push origin main
git push origin "v${VERSION}"
-36
View File
@@ -1,36 +0,0 @@
name: Build and Deploy Stable Documentation
on:
workflow_dispatch: # Manual trigger
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install dependencies
run: |
cd src/bonsai/docs
pip install -r requirements.txt # Run pip install from the docs directory
- name: Build documentation
run: |
cd src/bonsai/docs
make html
- name: Deploy to GitHub Pages (Stable)
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: IfcOpenShell/bonsaibim_org_docs
publish_branch: main
cname: docs.bonsaibim.org
publish_dir: src/bonsai/docs/_build/html
+65
View File
@@ -0,0 +1,65 @@
name: Deploy AI chat App to static page repo
permissions:
id-token: write
pages: write
on:
push:
paths:
- 'src/ifcchat/**'
- '.github/workflows/publish-aichat-app.yaml'
branches:
- v0.8.0
workflow_dispatch:
jobs:
activate:
runs-on: ubuntu-latest
if: |
github.repository == 'IfcOpenShell/IfcOpenShell'
steps:
- name: Set env
run: echo ok go
build:
needs: activate
runs-on: ubuntu-latest
steps:
- name: Checkout (recursive)
uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Checkout intermediate Pages repo
uses: actions/checkout@v6
with:
repository: IfcOpenShell/aichat_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/ifcchat/ output/
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
- name: Download wheels
working-directory: output/
run: |
pip download ifcquery==0.8.5 ifcopenshell-mcp==0.8.5 ifcedit==0.8.5 lark==1.3.1 isodate==0.7.2 --no-deps -d ./dist
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "$(git log --oneline -1)"
git push origin gh-pages
+24 -17
View File
@@ -1,4 +1,4 @@
name: Deploy Pyodide Demo App to GitHub Pages
name: Deploy Pyodide Demo App to static page repo
permissions:
id-token: write
@@ -11,6 +11,7 @@ on:
- '.github/workflows/publish-pyodide-demo-app.yml'
branches:
- v0.8.0
workflow_dispatch:
jobs:
activate:
@@ -30,21 +31,27 @@ jobs:
with:
submodules: recursive
fetch-depth: 0
- name: Setup Pages
uses: actions/configure-pages@v6
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v4
- name: Checkout intermediate Pages repo
uses: actions/checkout@v6
with:
path: src/pyodide/demo-app/
repository: IfcOpenShell/wasm_ifcopenshell_org_static_html
ref: gh-pages
path: output
token: ${{ secrets.WEBSITE_PUBLISH }}
- name: Sync demo app into target subfolder
run: |
rsync -av --delete --exclude='.git/' src/pyodide/demo-app/ output/
- name: Commit and push if changed
working-directory: output
run: |
git config --global user.name 'IfcOpenBot'
git config --global user.email 'IfcOpenBot@users.noreply.github.com'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
git add .
if git diff --cached --quiet; then
echo "No changes to commit"
exit 0
fi
git commit -m "$(git log --oneline -1)"
git push origin gh-pages
+5 -2
View File
@@ -50,11 +50,14 @@ Contents
| [ifcconvert](https://docs.ifcopenshell.org/ifcconvert.html) | CLI app to convert IFC to many other formats | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcconvert/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcconvert-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcconvert&expanded=true)
| [ifccsv](https://docs.ifcopenshell.org/ifccsv.html) | Library and CLI app to export and import schedules from IFC | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifccsv?label=PyPI&color=006dad)](https://pypi.org/project/ifccsv/) |
| [ifcdiff](https://docs.ifcopenshell.org/ifcdiff.html) | Compare changes between IFC models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcdiff?label=PyPI&color=006dad)](https://pypi.org/project/ifcdiff/) |
| [ifcedit](https://docs.ifcopenshell.org/ifcedit.html) | CLI wrapper for ifcopenshell.api IFC model mutation functions | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcedit?label=PyPI&color=006dad)](https://pypi.org/project/ifcedit/) |
| [ifcfm](https://docs.ifcopenshell.org/ifcfm.html) | Extract IFC data for FM handover requirements | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcfm?label=PyPI&color=006dad)](https://pypi.org/project/ifcfm/) |
| [ifcmax](https://docs.ifcopenshell.org/ifcmax.html) | Historic extension for IFC support in 3DS Max | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcmax.html)
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [Pyodide WASM Wheels](https://github.com/IfcOpenShell/wasm-wheels#pyodide-test-wheels) |
| [ifcmcp](https://docs.ifcopenshell.org/ifcmcp.html) | MCP server for querying and editing IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcopenshell-mcp?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell-mcp/) |
| [ifcopenshell-python](https://docs.ifcopenshell.org/ifcopenshell-python.html) | Python library for IFC manipulation | LGPL-3.0-or-later\* | [![Official](https://img.shields.io/badge/IfcOpenShell.org-Download-70ba35)](https://docs.ifcopenshell.org/ifcopenshell-python/installation.html) [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcopenshell-python-*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcopenshell-python&expanded=true) [![PyPI](https://img.shields.io/pypi/v/ifcopenshell?label=PyPI&color=006dad)](https://pypi.org/project/ifcopenshell/) [![Anaconda](https://img.shields.io/conda/vn/conda-forge/ifcopenshell?label=Anaconda&color=43b02a)](https://anaconda.org/conda-forge/ifcopenshell) [![Anaconda](https://img.shields.io/conda/vn/ifcopenshell/ifcopenshell?label=Anaconda-Unstable&color=43b02a)](https://anaconda.org/ifcopenshell/ifcopenshell) [![Docker](https://img.shields.io/docker/pulls/aecgeeks/ifcopenshell?label=Docker&color=1D63ED)](https://hub.docker.com/r/aecgeeks/ifcopenshell) [![AUR](https://img.shields.io/aur/version/ifcopenshell?label=AUR&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell) [![AUR Unstable](https://img.shields.io/aur/version/ifcopenshell-git?label=AUR-Unstable&color=1793d1)](https://aur.archlinux.org/packages/ifcopenshell-git) [![Pyodide WASM Wheels tag](https://img.shields.io/github/v/tag/ifcopenshell/wasm-wheels?sort=semver&label=pyodide-wasm-wheels)](https://github.com/IfcOpenShell/wasm-wheels) |
| [ifcpatch](https://docs.ifcopenshell.org/ifcpatch.html) | Utility to run pre-packaged scripts to manipulate IFCs | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcpatch?label=PyPI&color=006dad)](https://pypi.org/project/ifcpatch/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub Unstable](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*.*&label=GitHub-Unstable&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifcquery](https://docs.ifcopenshell.org/ifcquery.html) | CLI tool for querying and inspecting IFC building models | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifcquery?label=PyPI&color=006dad)](https://pypi.org/project/ifcquery/) |
| [ifcsverchok](https://docs.ifcopenshell.org/ifcsverchok.html) | Blender Add-on for visual node programming with IFC | GPL-3.0-or-later | [![GitHub](https://img.shields.io/github/v/release/ifcopenshell/ifcopenshell?filter=ifcsverchok-*.*.*&label=GitHub&color=f6f8fa)](https://github.com/IfcOpenShell/IfcOpenShell/releases?q=ifcsverchok&expanded=true)
| [ifctester](https://docs.ifcopenshell.org/ifctester.html) | Library, CLI and webapp for IDS model auditing | LGPL-3.0-or-later | [![PyPI](https://img.shields.io/pypi/v/ifctester?label=PyPI&color=006dad)](https://pypi.org/project/ifctester/) |
The IfcOpenShell C++ codebase is split into multiple interal libraries:
+1 -1
View File
@@ -1 +1 @@
0.8.5
0.8.6
Executable
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
set -e
mkdir -p build && cd build
cmake ../cmake \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DPython_EXECUTABLE=/home/dion/Projects/env/bin/python3.11 \
-DPython_INCLUDE_DIR=/usr/include/python3.11 \
-DBUILD_IFCPYTHON=ON \
-DBUILD_IFCGEOM=ON \
-DBUILD_CONVERT=ON \
-DBUILD_GEOMSERVER=OFF \
-DBUILD_EXAMPLES=OFF \
-DWITH_OPENCASCADE=ON \
-DWITH_CGAL=ON \
-DWITH_MANIFOLD=ON \
-DHDF5_SUPPORT=OFF \
-DGLTF_SUPPORT=ON \
-DIFCXML_SUPPORT=OFF \
-DCOLLADA_SUPPORT=OFF \
-DSCHEMA_VERSIONS="2x3;4;4x3_add2" \
-DOCC_INCLUDE_DIR=/usr/include/opencascade \
-DOCC_LIBRARY_DIR=/usr/lib64/opencascade
ninja
cp ifcwrap/_ifcopenshell_wrapper*.so ifcwrap/ifcopenshell_wrapper.py \
../src/ifcopenshell-python/ifcopenshell/
+204 -204
View File
@@ -18,28 +18,19 @@
################################################################################
cmake_minimum_required(VERSION 3.21)
if(NOT DEFINED CMAKE_CXX_STANDARD)
if (NOT DEFINED CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_CXX_STANDARD LESS 17)
if (CMAKE_CXX_STANDARD LESS 17)
message(FATAL_ERROR "C++17 or newer is required.")
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON) # not necessary, but encouraged
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
if(POLICY CMP0141) # 3.25+
# Has to be set before `project` to take effect.
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
cmake_policy(SET CMP0141 NEW) # Support for `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT`.
endif()
if(POLICY CMP0144) # 3.27
cmake_policy(SET CMP0144 NEW) # find_package() uses upper-case <PACKAGENAME>_ROOT variables.
@@ -48,8 +39,6 @@ if(POLICY CMP0167) # 3.30
cmake_policy(SET CMP0167 OLD)
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
endif()
@@ -65,64 +54,59 @@ endif()
option(MINIMAL_BUILD "The build is to make a minimal version of IFC converter from OCCT into IFC." OFF)
option(WASM_BUILD "Build a WebAssembly binary." OFF)
option(
ENABLE_BUILD_OPTIMIZATIONS
"Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds."
OFF
)
option(BUILD_SHARED_LIBS "Build IfcParse and IfcGeom as shared libs (SO/DLL)." OFF)
option(ENABLE_BUILD_OPTIMIZATIONS "Enable certain compiler and linker optimizations on RelWithDebInfo and Release builds." OFF)
option(BUILD_SHARED_LIBS "Build IfcOpenShell as shared libraries (required)." ON)
option(MSVC_PARALLEL_BUILD "Multi-threaded compilation in Microsoft Visual Studio (/MP)" OFF)
option(USE_VLD "Use Visual Leak Detector for debugging memory leaks, MSVC-only." OFF)
option(USE_MMAP "Adds a command line options to parse IFC files from memory mapped files using Boost.Iostreams" OFF)
option(NO_WARN "Disable all warnings" OFF)
option(CREATE_BUNDLE "Copy .so files and don't create RPATHS or SOVERSION symlinks" )
option(BUILD_IFCGEOM "Build IfcGeom." ON)
option(BUILD_IFCPYTHON "Build IfcPython." ON)
option(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER "Build the experimental Clang-generated ifcparse Python wrapper." OFF)
option(BUILD_CONVERT "Build IfcConvert executable." ON)
option(BUILD_DOCUMENTATION "Build IfcOpenShell Documentation." OFF)
option(BUILD_EXAMPLES "Build example applications." OFF)
option(BUILD_EXAMPLES "Build example applications." ON)
option(BUILD_GEOMSERVER "Build IfcGeomServer executable (Open CASCADE is required)." ON)
option(BUILD_IFCMAX "Build IfcMax, a 3ds Max plug-in, Windows-only." OFF)
option(BUILD_QTVIEWER "Build IfcOpenShell Qt GUI Viewer" OFF) # QtViewer requires Qt6
option(BUILD_IFCVIEWER "Build IfcViewer, a high-performance IFC viewer" OFF) # Requires Qt6 + OpenGL 4.5
option(BUILD_IFCVIEWER_TESTS "Build unit tests for IfcViewer (fetches Catch2 v3)" OFF)
option(BUILD_PACKAGE "" OFF)
# Most users probably need just common schemas,
# but we're keeping it `OFF` by default to avoid disruption
# (e.g. all Python distribution would need to adapt this option to be set).
option(
BUILD_ONLY_COMMON_SCHEMAS
"Build only common IFC schemas (2x3, 4, 4x3_add2). By default all schemas will be built."
OFF
)
option(SCHEMA_VERSIONS "Explicitly specify schemas to build." "")
option(WITH_OPENCASCADE "Enable geometry interpretation using Open CASCADE" ON)
option(WITH_CGAL "Enable geometry interpretation using CGAL" ON)
option(WITH_MANIFOLD "Enable geometry interpretation using Manifold" OFF)
option(COLLADA_SUPPORT "Build IfcConvert with COLLADA support (requires OpenCOLLADA)." ON)
option(GLTF_SUPPORT "Build IfcConvert with glTF support (requires json.hpp)." OFF)
option(HDF5_SUPPORT "Enable HDF5 support (requires HDF5, zlib)" ON)
option(WITH_PROJ "Enable output of Earth-Centered Earth-Fixed glTF output using the PROJ library" OFF)
option(IFCXML_SUPPORT "Build IfcParse with ifcXML support (requires libxml2)." ON)
option(USD_SUPPORT "Build IfcConvert with USD support (requires pixar's USD library)." OFF)
option(WITH_RELATIONSHIP_VALIDATION "Build IfcConvert with option to validate geometrical relationships." OFF)
option(WITH_ROCKSDB "Support a RocksDB key-value store as a file backend in IfcOpenShell" OFF)
option(WITH_ZSTD "Use Zstd compression in RocksDB writes" OFF)
option(USERSPACE_PYTHON_PREFIX "Installs IfcPython for the current user only instead of system-wide." OFF)
option(USE_DEBUG_PYTHON "Use debug binaries when building Debug IfcPython on Windows." OFF)
option(ADD_COMMIT_SHA "Add commit sha and branch in version number, requires git" OFF)
option(
VERSION_OVERRIDE
"Override the version defined in buildinfo.cpp with the file VERSION in the repository root"
OFF
)
option(USE_CCACHE "Enable use of ccache if it's available from PATH." ON)
option(VERSION_OVERRIDE "Override the version defined in buildinfo.cpp with the file VERSION in the repository root" OFF)
set(PYTHON_MODULE_INSTALL_DIR
""
CACHE PATH
set(
PYTHON_MODULE_INSTALL_DIR
"" CACHE PATH
"Directory to install IfcPython package to. By default package is installed in found Python's site-packages."
)
if (VERSION_OVERRIDE)
file(READ "../VERSION" "RELEASE_VERSION_")
string(STRIP "${RELEASE_VERSION_}" RELEASE_VERSION)
message(STATUS "Detected version '${RELEASE_VERSION}'")
else()
set(RELEASE_VERSION "0.8.0")
endif()
project(IfcOpenShell VERSION ${RELEASE_VERSION})
# Make sure CMake modules in this project are found first
list(PREPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR})
@@ -134,17 +118,20 @@ if(MINIMAL_BUILD)
set(COLLADA_SUPPORT OFF)
set(GLTF_SUPPORT OFF)
set(HDF5_SUPPORT OFF)
set(IFCXML_SUPPORT OFF)
set(USD_SUPPORT OFF)
endif()
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND (NOT BUILD_IFCGEOM))
if(NOT BUILD_SHARED_LIBS)
message(FATAL_ERROR "BUILD_SHARED_LIBS=OFF is not supported. Configure with -DBUILD_SHARED_LIBS=ON.")
endif()
if((BUILD_CONVERT OR BUILD_GEOMSERVER OR BUILD_IFCPYTHON) AND(NOT BUILD_IFCGEOM))
message(STATUS "'IfcGeom' is required with current outputs")
set(BUILD_IFCGEOM ON)
endif()
find_program(CCACHE_FOUND ccache)
if(USE_CCACHE AND CCACHE_FOUND)
if(CCACHE_FOUND)
message(STATUS "`ccache` is found, using it as a compiler launcher.")
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_FOUND}")
if(MSVC)
@@ -153,15 +140,17 @@ if(USE_CCACHE AND CCACHE_FOUND)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>")
# Not needed for Ninja.
if(CMAKE_GENERATOR MATCHES "Visual Studio")
file(COPY_FILE ${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS "CLToolExe=cl.exe" "CLToolPath=${CMAKE_BINARY_DIR}" "UseMultiToolTask=true")
file(COPY_FILE
${CCACHE_FOUND} ${CMAKE_BINARY_DIR}/cl.exe
ONLY_IF_DIFFERENT)
set(CMAKE_VS_GLOBALS
"CLToolExe=cl.exe"
"CLToolPath=${CMAKE_BINARY_DIR}"
"UseMultiToolTask=true"
)
endif()
endif()
endif()
mark_as_advanced(CCACHE_FOUND)
# Variable to accumulate swig definitions from various submodules.
set(SWIG_DEFINES "")
if(MSVC AND MSVC_PARALLEL_BUILD)
add_definitions("/MP")
@@ -177,28 +166,23 @@ endif()
include(GNUInstallDirs)
set(IFCOPENSHELL_EXPORT_TARGETS "${PROJECT_NAME}Targets")
# On Windows Release and Debug binaries are not compatible.
# So we add a postfix to avoid issues and allow release and debug installations to coexist.
if(WIN32)
set(CMAKE_DEBUG_POSTFIX "_d")
if(NOT INCLUDEDIR)
set(INCLUDEDIR include)
endif()
if(NOT IS_ABSOLUTE ${INCLUDEDIR})
set(INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR})
endif()
message(STATUS "INCLUDEDIR: ${INCLUDEDIR}")
if(BUILD_SHARED_LIBS)
add_definitions(-DIFC_SHARED_BUILD)
if(MSVC)
message(
WARNING
"Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed."
)
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
add_definitions(-wd4251)
endif()
if(MSVC)
message(WARNING "Building DLLs against the static VC run-time. This is not recommended if the DLLs are to be redistributed.")
# C4521: 'identifier' : class 'type' needs to have dll-interface to be used by clients of class 'type2'
# There will be couple hundreds of these so suppress them away, https://msdn.microsoft.com/en-us/library/esew7y1w.aspx
add_definitions(-wd4251)
endif()
UNIFY_ENVVARS_AND_CACHE(BOOST_ROOT)
UNIFY_ENVVARS_AND_CACHE(BOOST_LIBRARYDIR)
if(NOT MINIMAL_BUILD)
UNIFY_ENVVARS_AND_CACHE(PYTHON_INCLUDE_DIR)
@@ -214,71 +198,75 @@ foreach(option_flag IN LISTS option_flags)
convert_env_var_to_bool("${option_flag}")
endforeach()
if(WITH_CGAL)
find_package(CGAL REQUIRED)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_CGAL)
list(APPEND GEOMETRY_KERNELS cgal)
endif()
if(BUILD_IFCGEOM AND WITH_OPENCASCADE)
find_package(OpenCASCADE REQUIRED)
add_definitions(-DIFOPSH_WITH_OPENCASCADE)
# Map OpenCASCADE_LIBRARIES variable from OpenCASCADEConfig.cmake to OpenCASCADE_LIBRARIES used by kernel generic cmake file
set(OpenCASCADE_LIBRARIES ${OpenCASCADE_LIBRARIES})
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_OPENCASCADE)
list(APPEND GEOMETRY_KERNELS opencascade)
endif()
set(GLTF_LIBRARIES "")
message(STATUS "BUILD_IFCGEOM WITH_MANIFOLD: ${BUILD_IFCGEOM} ${WITH_MANIFOLD}")
if(BUILD_IFCGEOM AND WITH_MANIFOLD)
find_package(manifold CONFIG REQUIRED)
if(TARGET manifold::manifold)
set(MANIFOLD_LIBRARIES manifold::manifold)
elseif(TARGET manifold)
set(MANIFOLD_LIBRARIES manifold)
else()
message(FATAL_ERROR "Unable to determine manifold target")
endif()
list(APPEND GEOMETRY_KERNELS manifold)
endif()
if(BUILD_IFCGEOM)
list(APPEND GEOMETRY_KERNELS passthrough)
endif()
if(GLTF_SUPPORT)
find_package(nlohmann_json REQUIRED)
set(GLTF_LIBRARIES nlohmann_json::nlohmann_json)
add_definitions(-DWITH_GLTF)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_GLTF)
endif()
# Add USD support to serializers
set(USD_LIBRARIES "")
if(USD_SUPPORT)
find_package(USD REQUIRED)
set(USD_LIBRARIES pxr::USD)
endif(USD_SUPPORT)
set(ROCKSDB_LIBRARIES "")
if(WITH_ROCKSDB)
if (WITH_ROCKSDB)
# Temporaily mess with CMAKE_FIND_PACKAGE_PREFER_CONFIG to help RocksDB
# find it's zstd dependency on Windows.
# Only do it on Windows, otherwise it might create problems as
# findzstd and zstd-config target names do not match.
# https://github.com/facebook/rocksdb/pull/13975
if(WIN32)
set(TEMP CMAKE_FIND_PACKAGE_PREFER_CONFIG)
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
endif()
find_package(RocksDB CONFIG REQUIRED)
mark_as_advanced(RocksDB_DIR)
if(WIN32)
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ${TEMP})
endif()
message(STATUS "RocksDB: found at '${RocksDB_DIR}'.")
add_library(IFCOPENSHELL_RocksDB INTERFACE)
set(ROCKSDB_LIBRARIES "IFCOPENSHELL_RocksDB")
target_compile_definitions(IFCOPENSHELL_RocksDB INTERFACE IFOPSH_WITH_ROCKSDB)
set(SWIG_DEFINES ${SWIG_DEFINES} -DIFOPSH_WITH_ROCKSDB)
# Shared binaries for `rocksdb` only support limited API (only `c.h`), but we use `db.h` API.
# So rocksdb supported only as a static library.
# See https://github.com/facebook/rocksdb/issues/981.
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE RocksDB::rocksdb)
if(WITH_ZSTD)
if (WITH_ZSTD)
# @todo do we actually need the zstd include dir or rather just pass
# the libzstd.a along with the rocksdb library when needed and feature
# detect based on rocksdb API?
find_package(zstd CONFIG REQUIRED)
mark_as_advanced(zstd_DIR)
message(STATUS "zstd: found at '${zstd_DIR}'.")
target_link_libraries(IFCOPENSHELL_RocksDB INTERFACE zstd::libzstd_static)
endif()
install(TARGETS IFCOPENSHELL_RocksDB EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
endif()
# Find Boost: On win32 the (hardcoded) default is to use static libraries and
# runtime, when doing running conda-build we pick what conda prepared for us.
if(WIN32 AND NOT DEFINED ENV{CONDA_BUILD})
if(WIN32 AND("$ENV{CONDA_BUILD}" STREQUAL ""))
set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_STATIC_RUNTIME OFF)
set(Boost_USE_MULTITHREADED ON)
@@ -309,14 +297,8 @@ if(WASM_BUILD)
else()
# @todo review this, shouldn't this be all possible header-only now?
# ... or rewritten using C++17 features?
set(BOOST_COMPONENTS
system
program_options
regex
thread
date_time
iostreams
)
# set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
endif()
if(USE_MMAP)
@@ -326,17 +308,6 @@ if(USE_MMAP)
else()
set(BOOST_COMPONENTS ${BOOST_COMPONENTS} iostreams)
endif()
add_definitions(-DUSE_MMAP)
endif()
# Handle CGAL after Boost settings are set, since CGAL will use them too.
# Do `find_package(Boost)` only after this, to make sure `FindBoost` finds correct components.
# Otherwise it will find components needed for CGAL and we might some libraries.
if(WITH_CGAL)
find_package(CGAL REQUIRED)
set(CGAL_LIBRARIES IFCOPENSHELL_CGAL)
list(APPEND GEOMETRY_KERNELS cgal)
endif()
find_package(Boost REQUIRED COMPONENTS ${BOOST_COMPONENTS})
@@ -345,15 +316,10 @@ message(STATUS "Boost libraries found in ${Boost_LIBRARY_DIRS}")
if(COLLADA_SUPPORT)
find_package(OpenCOLLADA REQUIRED)
add_definitions(-DWITH_OPENCOLLADA)
endif()
if(HDF5_SUPPORT)
find_package(HDF5 REQUIRED COMPONENTS C CXX)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} hdf5::hdf5_cpp)
add_definitions(-DWITH_HDF5)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_HDF5)
endif(HDF5_SUPPORT)
if(ENABLE_BUILD_OPTIMIZATIONS)
@@ -368,20 +334,12 @@ if(ENABLE_BUILD_OPTIMIZATIONS)
# Linker
# /OPT:REF enables also /OPT:ICF and disables INCREMENTAL
set(LINKER_FLAGS_RELEASE "/LTCG /OPT:REF")
# /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx)
set(LINKER_FLAGS_RELWITHDEBINFO "/DEBUG /OPT:NOICF")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
"${CMAKE_SHARED_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
)
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}")
set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELEASE}")
set(CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
"${CMAKE_MODULE_LINKER_FLAGS_RELEASE} ${LINKER_FLAGS_RELWITHDEBINFO}"
)
# /OPT:NOICF is recommended when /DEBUG is used (http://msdn.microsoft.com/en-us/library/xe4t6fc1.aspx)
set(CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /OPT:REF")
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /DEBUG /OPT:NOICF")
else()
# GCC-like: Release should use O3 but RelWithDebInfo 02 so enforce 03. Anything other useful that could be added here?
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
@@ -430,7 +388,7 @@ if(MSVC)
endif()
# Enforce standards-conformance on VS > 2015, older Boost versions fail to compile with this
if(MSVC_VERSION GREATER 1900 AND (Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
if(MSVC_VERSION GREATER 1900 AND(Boost_MAJOR_VERSION GREATER 1 OR Boost_MINOR_VERSION GREATER 66))
add_definitions(-permissive-)
endif()
@@ -449,11 +407,11 @@ if(MSVC)
# endforeach()
# endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
# See #5158.
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
endif()
add_definitions(-D_ENABLE_EXTENDED_ALIGNED_STORAGE)
# See #5158.
if(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.40)
add_definitions(-D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
endif()
else()
add_definitions(-Wall -Wextra)
@@ -463,10 +421,7 @@ else()
add_definitions(-Wno-maybe-uninitialized)
endif()
if(
CMAKE_CXX_COMPILER_ID MATCHES "GNU"
AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0)
)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 9.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 9.0))
# OpenCascade spews a lot of deprecated-copy warnings
add_definitions(-Wno-deprecated-copy)
endif()
@@ -477,45 +432,35 @@ else()
endif()
endif(MSVC)
include_directories(${OPENCOLLADA_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${HDF5_INCLUDE_DIR})
include_directories(${INCLUDE_DIRECTORIES}
${Boost_INCLUDE_DIRS} ${HDF5_INCLUDE_DIR}
${CGAL_INCLUDE_DIR} ${GMP_INCLUDE_DIR} ${MPFR_INCLUDE_DIR}
)
if(NOT SCHEMA_VERSIONS)
# `WASM_BUILD` - super arbitrarily try to keep size down at least a little bit
if(BUILD_ONLY_COMMON_SCHEMAS OR WASM_BUILD)
if(WASM_BUILD)
# super arbitrarily try to keep size down at least a little bit
set(SCHEMA_VERSIONS "2x3" "4" "4x3_add2")
else()
set(SCHEMA_VERSIONS
"2x3"
"4"
"4x1"
"4x2"
"4x3"
"4x3_tc1"
"4x3_add1"
"4x3_add2"
)
set(SCHEMA_VERSIONS "2x3" "4" "4x1" "4x2" "4x3" "4x3_tc1" "4x3_add1" "4x3_add2")
endif()
endif()
message(STATUS "IFC SCHEMA_VERSIONS that will be used for the build: ${SCHEMA_VERSIONS}.")
set(SCHEMA_DEFINITIONS "")
foreach(schema ${SCHEMA_VERSIONS})
list(APPEND SCHEMA_DEFINITIONS "-DHAS_SCHEMA_${schema}")
add_definitions(-DHAS_SCHEMA_${schema})
endforeach()
string(REPLACE ";" ")(" schema_version_seq "(${SCHEMA_VERSIONS})")
list(APPEND SCHEMA_DEFINITIONS "-DSCHEMA_SEQ=${schema_version_seq}")
add_definitions(-DSCHEMA_SEQ=${schema_version_seq})
if(COMPILE_SCHEMA)
# @todo, this appears to be untested at the moment
find_package(PythonInterp)
if(NOT PYTHONINTERP_FOUND)
message(
FATAL_ERROR
"A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed."
)
message(FATAL_ERROR "A Python interpreter is necessary when COMPILE_SCHEMA is enabled. Disable COMPILE_SCHEMA or fix Python paths to proceed.")
endif()
set(IFC_RELEASE_NOT_USED ${SCHEMA_VERSIONS})
@@ -535,10 +480,7 @@ if(COMPILE_SCHEMA)
if("${PYPARSING_FOUND}" STREQUAL "-1")
message(STATUS "Installing pyparsing")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing
RESULT_VARIABLE SUCCESS
)
execute_process(COMMAND ${PYTHON_EXECUTABLE} -m pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
if(NOT "${SUCCESS}" STREQUAL "0")
execute_process(COMMAND pip "install" --user pyparsing RESULT_VARIABLE SUCCESS)
@@ -553,23 +495,19 @@ if(COMPILE_SCHEMA)
# Bootstrap the parser
message(STATUS "Compiling schema, this will take a while...")
execute_process(
COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
execute_process(COMMAND ${PYTHON_EXECUTABLE} bootstrap.py express.bnf
WORKING_DIRECTORY ../src/ifcexpressparser
OUTPUT_FILE express_parser.py
RESULT_VARIABLE SUCCESS
)
RESULT_VARIABLE SUCCESS)
if(NOT "${SUCCESS}" STREQUAL "0")
message(FATAL_ERROR "Failed to bootstrap parser. Make sure pyparsing is installed")
endif()
# Generate code
execute_process(
COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
execute_process(COMMAND ${PYTHON_EXECUTABLE} ../ifcexpressparser/express_parser.py ../../${COMPILE_SCHEMA}
WORKING_DIRECTORY ../src/ifcparse
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME
)
OUTPUT_VARIABLE COMPILED_SCHEMA_NAME)
# Prevent the schema that had just been compiled from being excluded
foreach(schema ${SCHEMA_VERSIONS})
@@ -584,16 +522,39 @@ if(NOT Boost_VERSION LESS 105800)
add_definitions(-DBOOST_OPTIONAL_USE_OLD_DEFINITION_OF_NONE)
endif()
add_subdirectory(../src/plugin plugin)
add_subdirectory(../src/ifcparse ifcparse)
set(IFCOPENSHELL_LIBRARIES IfcParse)
if(BUILD_IFCPARSE_EXPERIMENTAL_WRAPPER)
add_subdirectory(../src/wrappergen wrappergen)
endif()
if(BUILD_IFCGEOM)
# CGAL::CGAL target already has dependencies resolved.
if(WITH_CGAL AND CGAL_DIR)
set(CGAL_LIBRARIES CGAL::CGAL)
message(STATUS "Using found CGAL package at '${CGAL_DIR}'")
elseif(WITH_CGAL AND NOT CGAL_DIR)
find_library(libGMP NAMES gmp mpir PATHS ${GMP_LIBRARY_DIR} NO_DEFAULT_PATH)
find_library(libMPFR NAMES mpfr PATHS ${MPFR_LIBRARY_DIR} NO_DEFAULT_PATH)
if(NOT libGMP)
message(FATAL_ERROR "Unable to find GMP library files, aborting")
endif()
if(NOT libMPFR)
message(FATAL_ERROR "Unable to find MPFR library files, aborting")
endif()
list(APPEND CGAL_LIBRARIES "${libMPFR}")
list(APPEND CGAL_LIBRARIES "${libGMP}")
endif()
add_subdirectory(../src/ifcgeom ifcgeom)
endif(BUILD_IFCGEOM)
if(BUILD_CONVERT OR BUILD_IFCPYTHON)
add_subdirectory(../src/serializers serializers)
set(IFCOPENSHELL_LIBRARIES ${IFCOPENSHELL_LIBRARIES} ${SERIALIZER_SCHEMA_LIBRARIES})
endif(BUILD_CONVERT OR BUILD_IFCPYTHON)
if(BUILD_CONVERT)
@@ -613,8 +574,8 @@ if(ADD_COMMIT_SHA)
endif()
if(GIT_FOUND)
if(VERSION_OVERRIDE)
set(git_branch ${RELEASE_VERSION})
if (VERSION_OVERRIDE)
set (git_branch ${RELEASE_VERSION})
else()
message("git found: ${GIT_EXECUTABLE} with version ${GIT_VERSION_STRING}")
execute_process(
@@ -626,8 +587,8 @@ if(ADD_COMMIT_SHA)
string(REPLACE "\n" ";" git_branch_list "${git_branches}")
foreach(git_branch_candidate IN ITEMS ${git_branch_list})
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
string(REPLACE "*" "" git_branch_candidate_temp "${git_branch_candidate}")
string(STRIP "${git_branch_candidate_temp}" git_branch_candidate_2)
if(NOT git_branch_candidate_2 MATCHES "^HEAD$")
string(REPLACE "/" ";" git_branch_candidate_2_list "${git_branch_candidate_2}")
list(GET git_branch_candidate_2_list -1 git_branch)
@@ -645,13 +606,13 @@ if(ADD_COMMIT_SHA)
message(STATUS "IfcOpenShell branch: \"${git_branch}\"")
message(STATUS "IfcOpenShell commit: \"${git_sha}\"")
if("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
if ("${git_branch}" STREQUAL "" OR "${git_sha}" STREQUAL "")
message(FATAL_ERROR "Unable to determine commit sha and/or branch")
endif()
target_compile_definitions(
IfcParse
PRIVATE -DIFCOPENSHELL_BRANCH=${git_branch} -DIFCOPENSHELL_COMMIT=${git_sha}
target_compile_definitions(IfcParse PRIVATE
-DIFCOPENSHELL_BRANCH=${git_branch}
-DIFCOPENSHELL_COMMIT=${git_sha}
)
endif()
endif(ADD_COMMIT_SHA)
@@ -669,10 +630,6 @@ if(BUILD_DOCUMENTATION)
add_subdirectory(../docs docs)
endif()
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(../src/examples examples)
endif()
@@ -685,32 +642,82 @@ if(BUILD_IFCPYTHON AND WITH_CGAL)
add_subdirectory(../src/svgfill svgfill)
endif()
if(BUILD_IFCPYTHON)
add_subdirectory(../src/ifcwrap ifcwrap)
endif()
if(BUILD_QTVIEWER)
add_subdirectory(../src/qtviewer qtviewer)
endif()
if(BUILD_IFCGEOM)
# install(FILES ${IFCGEOM_H_FILES}
# DESTINATION ${INCLUDEDIR}/ifcgeom
# )
install(FILES ${SCHEMA_AGNOSTIC_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom
)
file(GLOB SERIALIZATION_H_FILES ../src/ifcgeom/serialization/*.h)
install(FILES ${SERIALIZATION_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/serialization
)
foreach(kernel ${GEOMETRY_KERNELS})
file(GLOB IFCGEOM_H_FILES ../src/ifcgeom/kernels/${kernel}/*.h)
install(FILES ${IFCGEOM_H_FILES}
DESTINATION ${INCLUDEDIR}/ifcgeom/kernels/${kernel}
)
endforeach()
install(TARGETS ${IFCGEOM_SCHEMA_LIBRARIES} ${kernel_libraries} IfcGeom)
endif(BUILD_IFCGEOM)
if(BUILD_IFCVIEWER)
if(BUILD_IFCVIEWER_TESTS)
# Catch2 v3 — fetched on demand. Test option is OFF by default so the
# default build remains offline-capable.
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.4
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(Catch2)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(CTest)
include(Catch)
enable_testing()
endif()
add_subdirectory(../src/ifcviewer ifcviewer)
add_subdirectory(../src/ifcviewer-minimal ifcviewer-minimal)
add_subdirectory(../src/ifcviewer-full ifcviewer-full)
endif()
# Cmake uninstall target
if(NOT TARGET uninstall)
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake"
IMMEDIATE
@ONLY
)
IMMEDIATE @ONLY)
add_custom_target(uninstall COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
add_custom_target(uninstall
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)
endif()
# Packaging
list(APPEND CPACK_SOURCE_IGNORE_FILES "/\\\\.git" "/build/" "/.pytest_cache/" "/__pycache__/")
list(APPEND CPACK_SOURCE_IGNORE_FILES
"/\\\\.git"
"/build/"
"/.pytest_cache/"
"/__pycache__/"
)
set(CPACK_SOURCE_INSTALLED_DIRECTORIES "${CMAKE_SOURCE_DIR}/..;/")
set(CPACK_PACKAGE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}"
)
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}${EXTRA_VERSION}")
set(CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}"
)
SET(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${EXTRA_VERSION}-${CMAKE_SYSTEM_NAME}")
set(CPACK_PACKAGE_DIRECTORY "${PROJECT_BINARY_DIR}/assets")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IfcOpenShell")
set(CPACK_PACKAGE_DESCRIPTION "IfcOpenShell.")
@@ -723,7 +730,6 @@ set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
set(CPACK_GENERATOR "TGZ;DEB")
set(CPACK_SOURCE_GENERATOR "TGZ")
set(BOOST_DEPS "")
foreach(COMPONENT IN ITEMS ${BOOST_COMPONENTS})
string(REPLACE "_" "-" COMP ${COMPONENT})
set(BOOST_DEPS "${BOOST_DEPS}, libboost-${COMP}-dev")
@@ -731,19 +737,13 @@ endforeach(COMPONENT)
set(CPACK_DEBIAN_PACKAGE_NAME "${PROJECT_NAME}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${CPACK_PACKAGE_CONTACT}")
set(CPACK_DEBIAN_PACKAGE_DEPENDS
"python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}"
)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3, libxml2, libocct-foundation-dev, libocct-modeling-algorithms-dev, libocct-modeling-data-dev, libocct-ocaf-dev, libocct-visualization-dev, libocct-data-exchange-dev, libhdf5-serial-dev, libpython3-dev, python3-pytest ${BOOST_DEPS}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION_SUMMARY "${CPACK_PACKAGE_DESCRIPTION_SUMMARY}")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_SECTION "science")
set(CPACK_DEBIAN_PACKAGE_VERSION
"${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}"
)
set(CPACK_DEBIAN_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}${EXTRA_VERSION}")
set(CPACK_DEBIAN_ARCHITECTURE "${CMAKE_SYSTEM_PROCESSOR}")
# set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/cmake/debian/postinst")
include(CPack)
include(package_export.cmake)
+4 -3
View File
@@ -139,7 +139,8 @@ if(NOT OpenCOLLADA_DIR)
endif()
endif(NOT OpenCOLLADA_DIR)
if(OPENCOLLADA_FOUND)
add_definitions(-DWITH_OPENCOLLADA)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_OPENCOLLADA)
if(OPENCOLLADA_FOUND AND NOT TARGET OpenCOLLADA::OpenCOLLADA)
add_library(OpenCOLLADA::OpenCOLLADA INTERFACE IMPORTED)
target_include_directories(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_INCLUDE_DIRS})
target_link_libraries(OpenCOLLADA::OpenCOLLADA INTERFACE ${OPENCOLLADA_LIBRARIES})
endif()
+12 -6
View File
@@ -6,7 +6,7 @@
# Input variables could also be provided as environment variables.
#
# Output targets:
# - `PROJ::proj`
# - `proj::proj`
#
# To avoid cyclic calls to this file
@@ -34,10 +34,12 @@ if((NOT PROJ_INCLUDE_DIR AND NOT PROJ_LIBRARIES))
message(FATAL_ERROR "Unable to find PROJ libraries in: ${PROJ_LIBRARY_DIR}")
endif()
add_library(PROJ::proj INTERFACE IMPORTED)
target_include_directories(PROJ::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(PROJ::proj INTERFACE ${PROJ_LIBRARIES})
target_link_directories(PROJ::proj INTERFACE "${PROJ_LIBRARY}")
if(NOT TARGET proj::proj)
add_library(proj::proj INTERFACE IMPORTED)
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
target_link_directories(proj::proj INTERFACE "${PROJ_LIBRARY}")
endif()
endif()
else()
find_library(PROJ_LIBRARY NAMES proj PATHS ${PROJ_LIBRARY_DIR})
@@ -50,7 +52,11 @@ else()
set(PROJ_INCLUDE_DIR ${PROJ_INCLUDE_DIR} CACHE FILEPATH "PROJ header files")
message(STATUS "Looking for PROJ include files in: ${PROJ_INCLUDE_DIR}")
include_directories(${PROJ_INCLUDE_DIR})
if(NOT TARGET proj::proj)
add_library(proj::proj INTERFACE IMPORTED)
target_include_directories(proj::proj INTERFACE "${PROJ_INCLUDE_DIR}")
target_link_libraries(proj::proj INTERFACE ${PROJ_LIBRARIES})
endif()
endif()
list(PREPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR})
-3
View File
@@ -64,7 +64,6 @@ set(USD_LIBRARIES
find_library(USD_LIBRARY NAMES ${USD_LIBRARIES} PATHS ${USD_LIBRARY_DIR})
if(USD_LIBRARY)
message(STATUS "USD libraries ${USD_LIBRARIES} found in: ${USD_LIBRARY_DIR}")
link_directories(${USD_LIBRARY_DIR})
else()
message(FATAL_ERROR "Unable to find USD libraries in: ${USD_LIBRARY_DIR}")
endif()
@@ -82,5 +81,3 @@ if(MSVC)
endif()
target_compile_definitions(pxr::USD INTERFACE PXR_STATIC WITH_USD)
set(SWIG_DEFINES ${SWIG_DEFINES} -DWITH_USD)
-1
View File
@@ -37,7 +37,6 @@ cmake -G "Ninja" ^
-D GLTF_SUPPORT:BOOL=ON ^
-D BUILD_CONVERT:BOOL=ON ^
-D BUILD_IFCMAX:BOOL=OFF ^
-D IFCXML_SUPPORT:BOOL=ON ^
-D Boost_LIBRARY_DIR:FILEPATH="%LIBRARY_PREFIX%\lib" ^
-D Boost_INCLUDE_DIR:FILEPATH="%LIBRARY_PREFIX%\include" ^
-D Boost_USE_STATIC_LIBS:BOOL=OFF ^
-1
View File
@@ -34,7 +34,6 @@ cmake ${CMAKE_ARGS} -G Ninja \
-DEIGEN_DIR:FILEPATH=$PREFIX/include/eigen3 \
-DCOLLADA_SUPPORT:BOOL=OFF \
-DBUILD_EXAMPLES:BOOL=OFF \
-DIFCXML_SUPPORT:BOOL=ON \
-DGLTF_SUPPORT:BOOL=ON \
-DBUILD_CONVERT:BOOL=ON \
-DBUILD_IFCPYTHON:BOOL=ON \
+317
View File
@@ -0,0 +1,317 @@
# Build fix: remove `boost_system` from CMake components
`Boost.System` became header-only in Boost 1.69. Boost 1.90.0 no longer ships a compiled library or CMake config for it, so `find_package(Boost REQUIRED COMPONENTS system ...)` fails.
## Fix
`cmake/CMakeLists.txt`:
```diff
- set(BOOST_COMPONENTS system program_options regex thread date_time iostreams)
+ set(BOOST_COMPONENTS program_options regex thread date_time iostreams)
```
The headers are still available; no linking is needed.
# Build fix: add `template` keyword for dependent template member calls
Calling a template member function through a dependent expression (e.g. `storage->has_attribute_value<T>(...)` where `storage`'s type depends on a template parameter) requires the `template` keyword to disambiguate from a less-than comparison.
## Error
```
src/ifcparse/IfcParse.cpp:1856:67: error: expected primary-expression before '>' token
1856 | if (storage->has_attribute_value<express::Base>(attr_index)) {
| ^
```
Six identical errors at lines 1856, 1865, 1896, 1905, 1934, 1943.
## Fix
`src/ifcparse/IfcParse.cpp`:
```diff
-storage->has_attribute_value<express::Base>(attr_index)
+storage->template has_attribute_value<express::Base>(attr_index)
-storage->has_attribute_value<Blank>(attr_index)
+storage->template has_attribute_value<Blank>(attr_index)
```
Applied at all six call sites in `in_memory_file_storage::read_from_stream`.
# Linker fix: missing explicit template instantiations for `InstanceStreamer`
`InstanceStreamer` is a class template with methods defined in `IfcParse.cpp`, not the header. Without explicit instantiations, the linker can't find the symbols when the SWIG wrapper loads.
## Error
```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPS3_PNS_7IfcFileE
(IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcFile*))
```
## Fix
Cannot use `template class InstanceStreamer<...>` because some constructors have `static_assert` guards that reject certain reader types. Instead, instantiate each member function individually per reader type, only including the constructors valid for that type.
`src/ifcparse/IfcParse.cpp` (after the last `InstanceStreamer` method definition):
```cpp
// FullBufferImpl
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(const std::string&, bool, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcParse::IfcFile*);
template IfcParse::InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(FileReader<FullBufferImpl>*, IfcParse::IfcFile*);
// ... plus ensure_header, initialize_header, hasSemicolon, semicolonCount,
// pushPage, bypassTypes, readInstance
// PushedSequentialImpl — same pattern, different valid constructors
// MMapFileReader (ifdef USE_MMAP) — same pattern
```
# Linker fix: `FullBufferImpl` missing buffer constructor
SWIG's `stream_from_string` calls `InstanceStreamer<FileReader<FullBufferImpl>>(void*, int, IfcFile*)`, but the `(void*, int)` constructor previously hit a `static_assert` for `FullBufferImpl` — it only allowed `PushedSequentialImpl`.
## Error
```
ImportError: undefined symbol: _ZN8IfcParse16InstanceStreamerINS_10FileReaderINS_14FullBufferImplEEEEC1EPviPNS_7IfcFileE
(InstanceStreamer<FileReader<FullBufferImpl>>::InstanceStreamer(void*, int, IfcFile*))
```
## Fix
Three changes to make `FullBufferImpl` support buffer-based and default construction:
`src/ifcparse/FileReader.h` — add buffer constructor to `FullBufferImpl`:
```diff
class IFC_PARSE_API FullBufferImpl {
public:
explicit FullBufferImpl(const std::string& fn);
+ FullBufferImpl(void* data, size_t length);
```
`src/ifcparse/FileReader.h` — add `FileReader(void*, size_t)` forwarding constructor:
```diff
+ FileReader(void* data, size_t length)
+ : cursor_(0) {
+ if constexpr (std::is_same_v<Impl, FullBufferImpl>) {
+ impl_ = std::make_shared<Impl>(data, length);
+ } else {
+ static_assert(...);
+ }
+ }
```
`src/ifcparse/FileReader.cpp` — implement the constructor:
```cpp
FullBufferImpl::FullBufferImpl(void* data, size_t length)
: buf_(static_cast<char*>(data), static_cast<char*>(data) + length)
, size_(length) {
}
```
`src/ifcparse/IfcParse.cpp` — extend the two `InstanceStreamer` constructors to accept `FullBufferImpl`:
```diff
// InstanceStreamer(IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(nullptr, (size_t)0);
// InstanceStreamer(void*, int, IfcFile*):
+ } else if constexpr (std::is_same_v<Reader, FileReader<FullBufferImpl>>) {
+ owned_stream_ = std::make_unique<Reader>(data, (size_t)length);
```
# Runtime fix: segfault in `parse_context::push()` due to vector reallocation
`parse_context_pool` stores nodes in a `std::vector<parse_context>`. During parsing, `load()` takes a `parse_context&` parameter and calls `context.push()`, which calls `pool_->make()`. If the pool's vector reallocates (via `emplace_back`), all existing references into the vector — including the `context` reference held by the caller — become dangling. Subsequent access through the dangling reference causes a segfault.
Triggered by larger IFC files (e.g. `ISSUE_159_kleine_Wohnung_R22.ifc`, 9.5 MB) that cause enough pool growth to trigger reallocation.
## Error
```
Thread 1 received signal SIGSEGV, Segmentation fault.
0x... in IfcParse::parse_context::push()
#1 in_memory_file_storage::load(...) // context& is dangling after reallocation
#2 in_memory_file_storage::load(...) // parent call
#3 InstanceStreamer::readInstance()
```
## Fix
`src/ifcparse/storage.h` — change the pool container from `std::vector` to `std::deque`, which does not invalidate references on `push_back`/`emplace_back`:
```diff
+#include <deque>
struct parse_context_pool {
- std::vector<parse_context> nodes_;
+ std::deque<parse_context> nodes_;
```
# Runtime fix: `express::Base` comparison operators throw on null/expired instances
`express::Base::operator<` and `operator==` called `data()`, which throws `std::runtime_error("Trying to access deleted instance reference")` when the internal `weak_ptr` is expired. A default-constructed `express::Base` (the value-type equivalent of a null pointer) always has an expired `weak_ptr`.
## Why this model triggers it
The bug requires two conditions to coincide:
1. A representation is shared by **more than one product** (via `IfcRepresentationMap` / `IfcMappedItem`).
2. At least one of those products has **no material association**, so `get_single_material_association()` returns `express::Base{}` (the null equivalent).
In `advanced_model.ifc`, Body representations like `#449` (Body/Brep) have a single `IfcRepresentationMap` (`#453`) with 13 `IfcMappedItem` usages, meaning 13 products share the geometry. Some of those products (e.g. `IfcFlowTerminal` instances) have no `IfcRelAssociatesMaterial`, so `get_single_material_association` returns `express::Base{}`.
Smaller or simpler models don't hit this because either:
- Every representation maps to only 1 product → `reuse_ok_` short-circuits at `products.size() == 1` before reaching the material check.
- Every product has a material association → no null `express::Base` is ever inserted into the set.
## Exact call sequence
```
Iterator::initialize()
try {
mapping::get_representations(reps, filters_)
addRepresentationsFromDefaultContexts(representations)
→ collects reps from subcontexts in order:
Axis (#115): 143 reps
Body (#117): 7550 reps
FootPrint (#119): 12 reps
for (auto representation : representations):
── Axis reps (indices 0142) ──────────────────────────
products_represented_by(rep, rmap)
→ OfProductRepresentation: 1 product each
filter_products(products, filters) → 1 product
reuse_ok_(ifcproducts)
→ products.size() == 1 → return true ← SHORT-CIRCUIT, no material check
representation_mapped_to(rep) → null (no MappedItem)
→ task created. 143 tasks accumulated.
── First Body rep #449 (Body/Brep) ────────────────────
products_represented_by(#449, rmap)
→ OfProductRepresentation: empty
→ RepresentationMap: 1 map (#453)
→ MapUsage: 13 MappedItems → traces through to 13 IfcProducts
filter_products(products, filters) → 13 products
reuse_ok_(ifcproducts) ← CRASH HERE
→ products.size() == 1? NO (13 products)
→ for each product:
find_openings(product) → OK
get_single_material_association(product)
→ some products have no IfcRelAssociatesMaterial
→ returns express::Base{} (expired weak_ptr)
associated_single_materials.insert(result)
→ std::set::insert calls operator<
→ operator< calls data()
→ data() calls data_.lock() → expired → THROWS
"Trying to access deleted instance reference"
} catch (const std::exception& e) {
Logger::Error(e) ← exception caught here, get_representations aborted
}
→ reps contains only the 143 Axis tasks created before the throw
→ all 143 Axis reps have Curve2D geometry → map(representation) returns null
→ no valid elements produced → initialize() returns false
```
In the old pointer-based code, `reuse_ok_` used `std::set<const IfcUtil::IfcBaseEntity*>` and `get_single_material_association` returned `nullptr`. Inserting `nullptr` into a `std::set<T*>` is a plain pointer comparison — no dereference, no throw. The refactoring to `std::set<express::Base>` changed the comparison from pointer comparison to `express::Base::operator<`, which unconditionally dereferences through `data()`.
## Error
```
[Error] Trying to access deleted instance reference
[Notice] Created 143 tasks for 143 products ← only Axis reps; all Body reps lost
initialize() returned: False
```
## Fix
`src/ifcparse/express.h` — use `weak_ptr::lock().get()` instead of `data()` so that expired pointers compare as `nullptr` (matching old raw-pointer semantics):
```diff
bool operator<(const Base& other) const {
- return data() < other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() < b.get();
}
bool operator==(const Base& other) const {
- return data() == other.data();
+ auto a = data_.lock();
+ auto b = other.data_.lock();
+ return a.get() == b.get();
}
```
# Runtime fix: `entity_instance` missing `get_inverse` due to SWIG `%rename` collision
Accessing inverse attributes (e.g. `element.IsDecomposedBy`) on any entity raises `AttributeError: entity instance of type 'IFC2X3.IfcProject' has no attribute 'get_inverse'`.
## Why
`entity_instance_mixin.__getattr__` (line 106 of `entity_instance.py`) calls `self.get_inverse(name)` when it detects an inverse attribute. Since the mixin inherits into the SWIG-generated `entity_instance` class (via the `object = custom_base` hack in `IfcParseWrapper.i:936`), `self.get_inverse` must resolve to a method on the SWIG class.
However, `IfcParseWrapper.i:70` has a global rename:
```
%rename("get_inverses_by_declaration") get_inverse;
```
This was intended for `ifcopenshell::file::get_inverse` (which takes an entity + declaration and returns instances by reference), but SWIG `%rename` is global — it also renames the `%extend express::Base` method `get_inverse(const std::string& a)` at line 551. So the Python-side `entity_instance` class exposes the method as `get_inverses_by_declaration`, not `get_inverse`.
The old code (`v0.8.0`) didn't hit this because `__getattr__` called `self.wrapped_data.get_inverse(name)` on an inner `ifcopenshell_wrapper.entity_instance` object — but in that old layout, the inner object was constructed differently and the rename didn't apply the same way (or the method had a different path). In the new mixin approach, `self` **is** the SWIG object, so the rename is directly visible.
## Fix
`src/ifcwrap/IfcParseWrapper.i` — override the global rename specifically for `express::Base::get_inverse`, restoring the original name on entity instances:
```diff
+%rename("get_inverse") express::Base::get_inverse;
%rename("get_inverses_by_declaration") get_inverse;
```
Add this line **before** the global rename (or anywhere before the `%extend express::Base` block). This scoped rename takes precedence for `express::Base`, so:
- `entity_instance.get_inverse(name)` works as the mixin expects
- `file.get_inverses_by_declaration(...)` keeps its intended name
## Python-side workaround
`entity_instance.py:106` — call the method by its SWIG-renamed name:
```diff
- vs = self.get_inverse(name)
+ vs = self.get_inverses_by_declaration(name)
```
# Runtime fix: `entity_instance` class no longer importable from `entity_instance` module
The class rename from `entity_instance` to `entity_instance_mixin` broke external code that does `from ifcopenshell.entity_instance import entity_instance`.
## Error
```
ImportError: cannot import name 'entity_instance' from 'ifcopenshell.entity_instance'
```
Triggered at import time via `ifcopenshell.util.pset` (and likely other modules).
## Fix
`src/ifcopenshell-python/ifcopenshell/entity_instance.py` — add a backwards-compatible alias at the bottom of the module:
```python
entity_instance = entity_instance_mixin
```
+95 -51
View File
@@ -162,6 +162,8 @@ USD_VERSION = "23.05"
TBB_VERSION = "2021.9.0"
ROCKSDB_VERSION = "9.11.2"
ZSTD_VERSION = "1.5.7"
MANIFOLD_VERSION = "3.2.1"
# binaries
cp = "cp"
bash = "bash"
@@ -330,7 +332,7 @@ cecho(""" - IFC Schemas to compile. If not provided, fallback to default provide
dependency_tree: "dict[str, tuple[str, ...]]" = {
"IfcParse": ("boost", "libxml2", "hdf5", "rocksdb"),
"IfcGeom": ("IfcParse", "occ", "json", "cgal", "eigen", "OpenCOLLADA"),
"IfcGeom": ("IfcParse", "occ", "manifold", "json", "cgal", "eigen", "OpenCOLLADA"),
"IfcConvert": ("IfcGeom",),
"OpenCOLLADA": ("libxml2", "pcre"),
"IfcGeomServer": ("IfcGeom",),
@@ -347,6 +349,7 @@ dependency_tree: "dict[str, tuple[str, ...]]" = {
"eigen": (),
"rocksdb": ("zstd",),
"zstd": (),
"manifold": (),
# 'usd': ('boost', 'oneTBB')
}
@@ -583,6 +586,11 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
]
)
if not any("BUILD_SHARED_LIBS" in f for f in cmake_args):
cmake_flags.append(
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
)
run(
[
*wasm,
@@ -591,7 +599,6 @@ def run_cmake(arg1, cmake_args: "list[str]", cmake_dir: Union[str, None] = None,
*cmake_flags,
*cmake_args,
f"-DCMAKE_BUILD_TYPE={BUILD_CFG}",
f"-DBUILD_SHARED_LIBS={OFF_ON[not BUILD_STATIC]}",
f"-DCMAKE_SHARED_LINKER_FLAGS={os.environ['LDFLAGS']}",
],
cwd=cwd,
@@ -994,6 +1001,29 @@ elif "occ" in targets:
download_name=f"OCE-{OCE_VERSION}.tar.gz",
)
if "manifold" in targets:
dependency_name = f"manifold-{MANIFOLD_VERSION}"
build_dependency(
name=dependency_name,
mode="cmake",
build_tool_args=[
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/{dependency_name}",
"-DMANIFOLD_PAR=OFF",
"-DMANIFOLD_CROSS_SECTION=OFF",
"-DMANIFOLD_PYBIND=OFF",
"-DMANIFOLD_JSBIND=OFF",
"-DMANIFOLD_CBIND=OFF",
"-DMANIFOLD_TEST=OFF",
"-DMANIFOLD_EXPORT=OFF",
"-DMANIFOLD_DOWNLOADS=OFF",
*MAC_CROSS_COMPILE_INTEL_ARGS,
],
download_url="https://github.com/elalish/manifold.git",
download_name="manifold",
download_tool=download_tool_git,
revision=f"v{MANIFOLD_VERSION}",
)
if "libxml2" in targets:
OLD_CC = ""
if MAC_CROSS_COMPILE_INTEL:
@@ -1094,10 +1124,19 @@ if "python" in targets and not USE_CURRENT_PYTHON_VERSION and "wasm" not in flag
f"http://www.python.org/ftp/python/{PYTHON_VERSION}/",
f"Python-{PYTHON_VERSION}.tgz",
)
python_bin = INSTALL_DIR / f"python-{PYTHON_VERSION}" / "bin" / "python3"
python_install = INSTALL_DIR / f"python-{PYTHON_VERSION}"
python_bin = python_install / "bin" / "python3"
# `_ssl` module is present -> we will be able to install `numpy` later
# to verify IfcOpenShell installation
run([str(python_bin), "-c", "import _ssl"])
try:
run([str(python_bin), "-c", "import _ssl"])
except RuntimeError:
print(
"ERROR: Python was built without SSL support (_ssl module is missing). "
f"To fix this: remove the installed Python at {python_install}; "
"install OpenSSL development libraries and re-run."
)
raise
if MAC_CROSS_COMPILE_INTEL:
assert original_path
@@ -1295,16 +1334,17 @@ if os.environ.get("NO_CLEAN", "").lower() not in {"1", "on", "true"}:
if os.path.exists(IFCOS_DIR):
shutil.rmtree(IFCOS_DIR)
os.makedirs(IFCOS_DIR, exist_ok=True)
executables_dir = os.path.join(IFCOS_DIR, "executables")
os.makedirs(executables_dir, exist_ok=True)
ifcos_build_dir = os.path.join(IFCOS_DIR, "build")
os.makedirs(ifcos_build_dir, exist_ok=True)
cmake_args = [
"-DUSE_MMAP=OFF",
"-DBUILD_EXAMPLES=OFF",
"-DBUILD_SHARED_LIBS=" + OFF_ON[not BUILD_STATIC],
"-DBUILD_SHARED_LIBS=ON",
"-DGLTF_SUPPORT=ON",
"-DBoost_NO_BOOST_CMAKE=On",
"-DCREATE_BUNDLE=On",
"-DADD_COMMIT_SHA=" + ("On" if ADD_COMMIT_SHA else "Off"),
"-DVERSION_OVERRIDE=" + ("On" if ADD_COMMIT_SHA else "Off"),
*MAC_CROSS_COMPILE_INTEL_ARGS,
@@ -1354,6 +1394,10 @@ elif "occ" in targets:
occ_library_dir = f"{DEPS_DIR}/install/oce-{OCE_VERSION}/lib"
cmake_args.extend(["-DOCC_INCLUDE_DIR=" + occ_include_dir, "-DOCC_LIBRARY_DIR=" + occ_library_dir])
if "manifold" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/manifold-{MANIFOLD_VERSION}")
cmake_args.append("-DWITH_MANIFOLD=On")
if "OpenCOLLADA" in targets:
# pcre is a dependency of OpenCOLLADA, but since we `find_package`,
# we don't need to add it explicitly here as cmake will find it from the config.
@@ -1399,40 +1443,33 @@ if "rocksdb" in targets:
if "swig" in targets:
cmake_args_prefix_path.append(f"{DEPS_DIR}/install/swig-{SWIG_VERSION}")
ifcos_build_args = [
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
if not WASM and (not explicit_targets or {"IfcGeom", "IfcConvert", "IfcGeomServer"} & set(explicit_targets)):
logger.info("\rConfiguring executables...")
exec_args = [
f"-DBUILD_IFCGEOM={OFF_ON['IfcGeom' in targets]}",
f"-DBUILD_GEOMSERVER={OFF_ON['IfcGeomServer' in targets]}",
f"-DBUILD_CONVERT={OFF_ON['IfcConvert' in targets]}",
*ifcos_build_args,
f"-DBUILD_IFCPYTHON=OFF",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell",
]
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=executables_dir)
run_cmake("", exec_args + cmake_args + get_cmake_args_prefix_path(), cmake_dir=CMAKE_DIR, cwd=ifcos_build_dir)
logger.info("\rBuilding executables... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=executables_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=executables_dir)
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/strip" if BUILD_CFG == "Release" else "install"], cwd=ifcos_build_dir)
if "IfcOpenShell-Python" in targets:
# On OSX the actual Python library is not linked against.
ADDITIONAL_ARGS = ""
wrapper_ldflags = ""
if platform.system() == "Darwin":
ADDITIONAL_ARGS = "-Wl,-undefined,dynamic_lookup"
# NOTE: We don't use `CXXFLAGS` for wrappers, so wrapper is compiled with different flags
# (e.g. ` -fdata-sections` is missing, which is set by default for executables)
# So cache doesn't match and running build-all.py builds most of ifcopenshell libraries twice.
os.environ["CPPFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CXXFLAGS"] = f"{CXXFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["CFLAGS"] = f"{CFLAGS_MINIMAL} {ADDITIONAL_ARGS}"
os.environ["LDFLAGS"] = f"{LDFLAGS} {ADDITIONAL_ARGS}"
python_dir = os.path.join(IFCOS_DIR, "pythonwrapper")
os.makedirs(python_dir, exist_ok=True)
# On OSX the actual Python library is not linked against.
wrapper_ldflags = "-Wl,-undefined,dynamic_lookup"
def compile_python_wrapper(
python_version: str,
@@ -1447,10 +1484,6 @@ if "IfcOpenShell-Python" in targets:
logger.info(f"\rConfiguring python {python_version} wrapper...")
cache_path = os.path.join(python_dir, "CMakeCache.txt")
if os.path.exists(cache_path):
os.remove(cache_path)
if python_path:
# We couldn't just prefix PATH and have to provide all variables explicitly,
# see ifcwrap/cmake for the details.
@@ -1464,27 +1497,38 @@ if "IfcOpenShell-Python" in targets:
)
assert python_include
run_cmake(
"",
cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=python_dir,
)
old_ldflags = os.environ["LDFLAGS"]
if wrapper_ldflags:
os.environ["LDFLAGS"] = f"{old_ldflags} {wrapper_ldflags}"
try:
run_cmake(
"",
ifcos_build_args
+ [
"-DBUILD_IFCPYTHON=ON",
]
+ cmake_args
+ get_cmake_args_prefix_path()
+ [
*([f"-DPYTHON_EXECUTABLE={python_executable}"] if python_executable else []),
# Needed because pyodide is expecting setup.py to be in the root.
*([f"-DPYTHON_MODULE_INSTALL_DIR={REPO_PATH}"] * WASM),
f"-DPYTHON_INCLUDE_DIR={python_include}",
f"-DCMAKE_INSTALL_PREFIX={DEPS_DIR}/install/ifcopenshell/tmp",
"-DUSERSPACE_PYTHON_PREFIX="
+ ["Off", "On"][os.environ.get("PYTHON_USER_SITE", "").lower() in {"1", "on", "true"}],
],
cmake_dir=CMAKE_DIR,
cwd=ifcos_build_dir,
)
finally:
os.environ["LDFLAGS"] = old_ldflags
logger.info(f"\rBuilding python {python_version} wrapper... ")
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=python_dir)
run([make, "install/local"], cwd=os.path.join(python_dir, "ifcwrap"))
run([make, f"-j{IFCOS_NUM_BUILD_PROCS}", "ifcopenshell_wrapper", "VERBOSE=1"], cwd=ifcos_build_dir)
run([make, "install/local"], cwd=os.path.join(ifcos_build_dir, "ifcwrap"))
if python_executable:
run([python_executable, "-m", "ensurepip"])
@@ -1515,7 +1559,7 @@ if "IfcOpenShell-Python" in targets:
)
# Copy setup.py where pyodide build system expects it.
shutil.copy(REPO_PATH / "pyodide" / "setup.py", REPO_PATH)
# Empty pyproject so it's contents won't affect the resulting wheelthe the
# Empty pyproject so it's contents won't affect the resulting wheel
# otherwise the wheel will use version and dependencies from toml, not setup.py.
(REPO_PATH / "pyproject.toml").write_text("")
+3 -10
View File
@@ -14,18 +14,11 @@ source .venv/bin/activate
uv pip install pyodide-build
# `uv run` is required, so xbuildenv would skip using `pip`.
uv run pyodide xbuildenv install
uv run pyodide xbuildenv install-emscripten
# Emscripten doesn't come with xbuildenv.
if [ ! -d emsdk ]; then
git clone https://github.com/emscripten-core/emsdk
fi
pushd emsdk
PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)
./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}
./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}
source emsdk_env.sh
EMSDK_ROOT=$(pyodide config get emscripten_dir)
source ${EMSDK_ROOT}/emsdk_env.sh
which emcc
popd
mkdir -p packages/ifcopenshell
VERSION=`cat IfcOpenShell/VERSION`
+232
View File
@@ -0,0 +1,232 @@
#
# /// script
# # Latest Pyodide build env versions are listed here:
# # https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json
# # https://github.com/pyodide/pyodide-build/blob/main/pyodide_build/xbuildenv_releases.py
# requires-python = "==3.13.2"
# dependencies = [
# "requests",
# "setuptools",
# ]
# ///
"""
Pack an IfcOpenShell WASM wheel using Pyodide build system.
Usage:
uv run make_wheel.py # Show this help
uv run make_wheel.py --build # Build wheel
uv run make_wheel.py --clean # Clean build artifacts and exit
"""
import argparse
import os
import re
import shutil
import subprocess
import time
import zipfile
from pathlib import Path
from urllib.parse import quote
import requests
# Get repo root (parent of this script's parent directory)
REPO_ROOT = Path(__file__).parent.parent
PYODIDE_DIR = REPO_ROOT / "pyodide"
BUILD_DIR = PYODIDE_DIR / "build"
# Hardcoded path (Windows packing workaround with --dev flag)
PYODIDE_BUILD = Path(r"L:\Projects\Github\pyodide-build")
# Wheel platform tag (from PYODIDE_EMSCRIPTEN_VERSION in pyodide-build/Makefile.envs)
WHEEL_PLATFORM_TAG = "emscripten_4_0_9_wasm32"
# Location where ifcopenshell will be extracted
IFCOPENSHELL_DIR = PYODIDE_DIR / "ifcopenshell"
class WheelBuilder:
@staticmethod
def extract_ifcopenshell_from_git(dst: Path) -> None:
"""Extract ifcopenshell directory from git repo into destination."""
Tools.rmrf(dst)
print(f"Extracting ifcopenshell from git to {dst}...")
# Use git ls-files piped to git checkout-index to avoid copying
# untracked or ignored files from the actual repo.
ls_proc = subprocess.Popen(
["git", "ls-files", "-z", "src/ifcopenshell-python/ifcopenshell"],
cwd=REPO_ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
checkout_proc = subprocess.Popen(
["git", "checkout-index", "-z", "--prefix", "pyodide/", "--stdin"],
cwd=REPO_ROOT,
stdin=ls_proc.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert ls_proc.stdout is not None
ls_proc.stdout.close()
checkout_proc.communicate()
if checkout_proc.returncode != 0:
assert checkout_proc.stderr is not None
raise RuntimeError(f"Failed to extract: {checkout_proc.stderr.decode()}")
# Move src/ifcopenshell-python/ifcopenshell to ifcopenshell.
temp_src = PYODIDE_DIR / "src" / "ifcopenshell-python" / "ifcopenshell"
shutil.move(temp_src, dst)
# Clean up temporary src directory.
Tools.rmrf(PYODIDE_DIR / "src")
print("✓ Extracted ifcopenshell from git")
@staticmethod
def get_wheel_url(makefile_path: Path) -> str:
"""Get S3 wheel URL based on BINARY_VERSION and BUILD_COMMIT from Makefile."""
def parse_makefile_vars() -> dict[str, str]:
content = makefile_path.read_text()
vars: dict[str, str] = {}
for match in re.finditer(r"^(BINARY_VERSION|BUILD_COMMIT):=(.+)$", content, re.MULTILINE):
vars[match.group(1)] = match.group(2).strip()
return vars
vars: dict[str, str] = parse_makefile_vars()
binary_version = vars["BINARY_VERSION"]
build_commit = vars["BUILD_COMMIT"]
filename = f"ifcopenshell-{binary_version}+{build_commit}-cp313-cp313-pyodide_2025_0_wasm32.whl"
encoded_filename = quote(filename, safe="")
return f"https://s3.amazonaws.com/ifcopenshell-builds/{encoded_filename}"
@staticmethod
def download_and_extract_so(url: str, build_dir: Path) -> tuple[Path, Path]:
"""Download wheel from URL and extract .so and .py files."""
py_wrapper_filename = "ifcopenshell_wrapper.py"
build_dir.mkdir(parents=True, exist_ok=True)
wheel_path = build_dir / url.rsplit("/", 1)[-1]
if wheel_path.exists():
print(f"Using cached wheel: {wheel_path}")
else:
print(f"Downloading {url}...")
response = requests.get(url)
response.raise_for_status()
wheel_path.write_bytes(response.content)
print("Extracting _ifcopenshell_wrapper files...")
with zipfile.ZipFile(wheel_path) as zf:
so_files = [f for f in zf.namelist() if f.endswith(".so")]
py_files = [f for f in zf.namelist() if f.endswith(py_wrapper_filename)]
assert so_files, "No .so file found in wheel"
assert py_files, f"No {py_wrapper_filename} file found in wheel"
so_file = so_files[0]
so_dst = build_dir / Path(so_file).name
so_dst.write_bytes(zf.read(so_file))
py_file = py_files[0]
py_dst = build_dir / Path(py_file).name
py_dst.write_bytes(zf.read(py_file))
return so_dst, py_dst
class Tools:
@staticmethod
def run(
cmd: list[str],
cwd: Path | None = None,
) -> None:
print(f"$ {' '.join(cmd)}")
subprocess.check_call(cmd, cwd=cwd)
@staticmethod
def create_symlink(dst: Path, src: Path) -> None:
Tools.rmrf(dst)
dst.symlink_to(src)
@staticmethod
def rmrf(path: Path) -> None:
if path.exists() or path.is_symlink():
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
def clean() -> None:
"""Remove build artifacts."""
paths_to_remove = (
BUILD_DIR,
PYODIDE_DIR / ".pyodide_build",
PYODIDE_DIR / "dist",
PYODIDE_DIR / "ifcopenshell.egg-info",
PYODIDE_DIR / "src",
IFCOPENSHELL_DIR,
)
for path in paths_to_remove:
if path.exists() or path.is_symlink():
print(f"Removing {path}...")
Tools.rmrf(path)
print("✓ Clean complete")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, add_help=False)
parser.add_argument("--build", action="store_true", help="Build the wheel")
parser.add_argument("--clean", action="store_true", help="Clean build folder")
parser.add_argument(
"--dev",
action="store_true",
help="Use editable pyodide-build from hardcoded path (Windows packing workaround)",
)
args = parser.parse_args()
if not args.build and not args.clean:
print(__doc__)
return
if args.clean:
clean()
return
start_time = time.time()
WheelBuilder.extract_ifcopenshell_from_git(IFCOPENSHELL_DIR)
print("Downloading and extracting _ifcopenshell_wrapper files...")
makefile = REPO_ROOT / "src" / "ifcopenshell-python" / "Makefile"
wheel_url = WheelBuilder.get_wheel_url(makefile)
so_file, py_file = WheelBuilder.download_and_extract_so(wheel_url, BUILD_DIR)
Tools.create_symlink(IFCOPENSHELL_DIR / Path(so_file).name, so_file)
Tools.create_symlink(IFCOPENSHELL_DIR / Path(py_file).name, py_file)
print("Installing pyodide-build...")
if args.dev:
Tools.run(["uv", "pip", "install", "-e", str(PYODIDE_BUILD)])
else:
Tools.run(["uv", "pip", "install", "pyodide-build"])
print("Building with pyodide...")
# Use --no-isolation due to pyodide-build Windows support issues:
# symlink_unisolated_packages fails with missing `_sysconfigdata_$(CPYTHON_ABI_FLAGS)_emscripten_wasm32-emscripten.py`.
# Hardcode platform name since pyodide doesn't yet support overriding wheel tags on Windows.
#
# Use `LEGACY_PLATFORM` since pyodide 0.34.1 introduced new tag for wheels `pyemscripten`,
# which doesn't work with pyodide itself yet - https://github.com/pyodide/pyodide/issues/6177.
os.environ["USE_LEGACY_PLATFORM"] = "1"
Tools.run(["pyodide", "build", f"-C--build-option=--plat-name={WHEEL_PLATFORM_TAG}"])
elapsed = time.time() - start_time
print(f"\n✓ Done! ({elapsed:.1f}s)")
if __name__ == "__main__":
main()
+39 -1
View File
@@ -2,12 +2,16 @@
# because `tool.setuptools.ext-modules` is still experimental in pyproject.toml
# and we need it to get the wheel suffix right.
import os
import sys
from pathlib import Path
import tomllib
from setuptools import Extension, find_packages, setup
from setuptools.command.build_ext import build_ext
REPO_FOLDER = Path(__file__).parent
# Detect repo folder: if setup.py is in pyodide folder, go to parent
SETUP_DIR = Path(__file__).parent
REPO_FOLDER = SETUP_DIR.parent if SETUP_DIR.name == "pyodide" else SETUP_DIR
def get_version() -> str:
@@ -25,6 +29,39 @@ def get_dependencies() -> list[str]:
return dependencies
class UnixBuildExt(build_ext):
"""Customize ``build_ext`` to support packing on Windows."""
def finalize_options(self):
from distutils import sysconfig
super().finalize_options()
if sys.platform == "win32":
self.compiler = "unix"
# Configure sysconfig for Windows builds
# CCSHARED is the only variable that's not customizable with env vars.
# Basically avoiding this:
# File ".venv\Lib\site-packages\setuptools\_distutils\sysconfig.py", line 366, in customize_compiler
# compiler_so=cc_cmd + ' ' + ccshared,
# ~~~~~~~~~~~~~^~~~~~~~~~
# TypeError: can only concatenate str (not "NoneType") to str
sysconfig.get_config_vars() # Initialize config cache
if sysconfig._config_vars.get("CCSHARED") is None:
sysconfig._config_vars["CCSHARED"] = "-fPIC"
# Override compiler type before it's instantiated
# Set Emscripten compiler environment variables
os.environ["CC"] = "emcc"
os.environ["CXX"] = "em++"
os.environ["CFLAGS"] = ""
os.environ["CXXFLAGS"] = ""
os.environ["LDSHARED"] = "emcc -shared"
os.environ["AR"] = "emar"
os.environ["ARFLAGS"] = "rcs"
os.environ["SETUPTOOLS_EXT_SUFFIX"] = ".cpython-313-wasm32-emscripten.so"
setup(
name="ifcopenshell",
version=get_version(),
@@ -44,4 +81,5 @@ setup(
},
# Has to provide extension to get the correct wheel suffix.
ext_modules=[Extension("ifcopenshell._ifcopenshell_wrapper", sources=[])],
cmdclass={"build_ext": UnixBuildExt},
)
+6 -4
View File
@@ -3,8 +3,9 @@ name = "IfcOpenShell"
version = "0.0.0"
dependencies = [
"black==26.3.1",
"ruff==0.15.8",
"ruff==0.15.9",
"poethepoet",
"ty==0.0.29",
"gersemi==0.26.1",
]
@@ -28,6 +29,9 @@ extend-exclude = '''
reportInvalidTypeForm = false
disableBytesTypePromotions = true
reportUnnecessaryTypeIgnoreComment = true
reportRedeclaration = false
# Ignore warnings from bpy stubs missing actual source files.
reportMissingModuleSource = false
# Pylance doesn't respect gitignore, so we have to exclude files manually here
# to avoid VS Code slowing down.
# https://github.com/microsoft/pylance-release/issues/5169
@@ -84,7 +88,6 @@ all = "ignore"
# Structural rules (no deep type inference needed, easier to adapt).
abstract-method-in-final-class = "error"
ambiguous-protocol-member = "error"
byte-string-type-annotation = "error"
conflicting-declarations = "error"
conflicting-metaclass = "error"
cyclic-class-definition = "error"
@@ -96,7 +99,6 @@ empty-body = "error"
escape-character-in-forward-annotation = "error"
final-on-non-method = "error"
final-without-value = "error"
fstring-type-annotation = "error"
ignore-comment-unknown-rule = "error"
implicit-concatenated-string-type-annotation = "error"
inconsistent-mro = "error"
@@ -224,7 +226,7 @@ ty.sequence = ["ty-bonsai", "ty-ios"]
ty.help = "Run ty type checker. Requires ty-venv to be set up first."
ty-bonsai = "ty check src/bonsai --python=src/bonsai/.venv"
ty-venv.sequence = ["ty-venv-bonsai", "ty-venv-ios"]
ty-venv.sequence = ["bonsai-deps", "ty-venv-bonsai", "ty-venv-ios"]
ty-venv-bonsai.sequence = [
{cmd = "uv venv src/bonsai/.venv --python=3.11 --allow-existing"},
+1 -1
View File
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
Returns:
The BCF viewpoint definition.
"""
ifc_file = element.wrapped_data.file
ifc_file = element.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
+1 -1
View File
@@ -316,7 +316,7 @@ def build_viewpoint(element: entity_instance) -> mdl.VisualizationInfo:
Returns:
The BCF viewpoint definition.
"""
ifc_file = element.wrapped_data.file
ifc_file = element.file
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
elem_placement = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
elem_placement[:3, 3] *= unit_scale
+5 -3
View File
@@ -48,6 +48,7 @@ VERSION_PATCH:=$(shell cat '../../VERSION' | cut -d '.' -f 3)
VERSION_DATE:=$(shell date '+%y%m%d')
LAST_COMMIT_HASH:=$(shell git rev-parse HEAD)
LAST_COMMIT_DATE:=$(shell git show -s --format=%cI)
LAST_GIT_BRANCH:=$(shell git rev-parse --abbrev-ref HEAD)
PYPI_IMP:=cp
ifdef PYVERSION
@@ -63,6 +64,7 @@ PYNUMBER:=3$(PYMINOR)
PYPI_VERSION:=3.$(PYMINOR)
endif # def PYVERSION
IFCMERGE_VERSION:=2026-04-07
ifdef PLATFORM
SUPPORTED_PLATFORMS := linux macos macosm1 win
@@ -239,10 +241,9 @@ endif
# required for three-way git merging
ifeq ($(PLATFORM), win)
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/2025-01-26/ifcmerge.zip
cd build/bonsai/libs/bin && unzip ifcmerge.zip && rm ifcmerge.zip
cd build/bonsai/libs/bin && wget https://github.com/brunopostle/ifcmerge/releases/download/$(IFCMERGE_VERSION)/ifcmerge.exe
else
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge && chmod +x ifcmerge
cd build/bonsai/libs/bin && wget https://raw.githubusercontent.com/brunopostle/ifcmerge/$(IFCMERGE_VERSION)/ifcmerge && chmod +x ifcmerge
endif
# Generate translations module for Bonsai build
@@ -261,6 +262,7 @@ else
$(SED) "s/0.0.0/$(VERSION)-alpha$(VERSION_DATE)/" build/bonsai/blender_manifest.toml
$(SED) "s/8888888/$(LAST_COMMIT_HASH)/" build/bonsai/__init__.py
$(SED) "s/9999999/$(LAST_COMMIT_DATE)/" build/bonsai/__init__.py
$(SED) "s/7777777/$(LAST_GIT_BRANCH)/" build/bonsai/__init__.py
$(SED) 's/version = "0.0.0"/version = "$(VERSION)-alpha$(VERSION_DATE)"/' build/pyproject.toml
endif
+13
View File
@@ -43,6 +43,7 @@ from typing import TYPE_CHECKING, Any, Union
last_commit_hash = "8888888"
last_commit_date = "9999999"
last_git_branch = "7777777"
def get_last_commit_hash() -> Union[str, None]:
@@ -60,6 +61,15 @@ def get_last_commit_date() -> Union[str, None]:
return last_commit_date
def get_git_branch() -> Union[str, None]:
# Using this weird way to write 7777777,
# so makefile won't accidentally replace it here
# we'll be able to distinguish branch from placeholder value.
if last_git_branch == str(7_777777):
return None
return last_git_branch
# Accessed from bonsai extension:
bbim_semver: dict[str, Any] = {}
@@ -125,6 +135,7 @@ def get_debug_info(*, bonsai_failed_to_load: bool = False) -> dict[str, Any]:
"bonsai_version": bbim_version,
"bonsai_commit_hash": get_last_commit_hash(),
"bonsai_commit_date": get_last_commit_date(),
"bonsai_git_branch": get_git_branch(),
"last_actions": last_actions,
"last_error": last_error,
}
@@ -251,10 +262,12 @@ if IN_BLENDER:
global last_commit_hash
global last_commit_date
global last_git_branch
path = Path(__file__).resolve().parent
repo = git.Repo(str(path), search_parent_directories=True)
last_commit_hash = repo.head.object.hexsha
last_commit_date = repo.head.object.committed_datetime.isoformat()
last_git_branch = repo.active_branch.name
except:
pass
+1 -1
View File
@@ -187,7 +187,7 @@ def import_attributes(
info = {a.name(): None for a in attributes}
info["type"] = element
else:
assert (entity := element.wrapped_data.declaration().as_entity())
assert (entity := element.declaration().as_entity())
attributes = entity.all_attributes()
info = element.get_info()
for attribute in attributes:
@@ -101,7 +101,7 @@ class AggregateDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
+20 -1
View File
@@ -73,6 +73,22 @@ def poll_related_object(self: "BIMObjectAggregateProperties", related_obj: bpy.t
return True
def update_relating_object(self, context):
if self.relating_object:
ifc_id = tool.Blender.get_object_bim_props(self.relating_object).ifc_definition_id
if ifc_id:
bpy.ops.bim.aggregate_assign_object(relating_object=ifc_id)
bpy.ops.bim.disable_editing_aggregate()
def update_related_object(self, context):
if self.related_object:
ifc_id = tool.Blender.get_object_bim_props(self.related_object).ifc_definition_id
if ifc_id:
bpy.ops.bim.aggregate_assign_object(related_object=ifc_id)
bpy.ops.bim.disable_editing_aggregate()
def update_aggregate_decorator(self, context):
if self.aggregate_decorator:
AggregateDecorator.install(bpy.context)
@@ -89,12 +105,15 @@ def update_aggregate_mode_decorator(self, context):
class BIMObjectAggregateProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing")
relating_object: PointerProperty(name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object)
relating_object: PointerProperty(
name="Relating Whole", type=bpy.types.Object, poll=poll_relating_object, update=update_relating_object
)
related_object: PointerProperty(
name="Related Part",
description="Related Part, will be used to derive the Relating Object",
type=bpy.types.Object,
poll=poll_related_object,
update=update_related_object,
)
if TYPE_CHECKING:
@@ -295,13 +295,13 @@ class ExplorerShowUIPopup(bpy.types.Operator):
bl_description = "Show Explorer UI to select element as attribute value or edit it."
bl_options = {"REGISTER", "UNDO"}
ifc_class: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
ifc_class: bpy.props.StringProperty()
"""Element IFC class."""
attribute_name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
attribute_name: bpy.props.StringProperty()
"""IFC class attribute name."""
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
data_path: bpy.props.StringProperty()
"""Full data path"""
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
preselect_ifc_id: bpy.props.IntProperty(options={"SKIP_SAVE"})
"""IFC id to preselect in the popup."""
if TYPE_CHECKING:
@@ -41,7 +41,7 @@ class BIMAttributeProperties(PropertyGroup):
class ExplorerEntity(PropertyGroup):
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
ifc_definition_id: bpy.props.IntProperty()
if TYPE_CHECKING:
ifc_definition_id: int
@@ -60,7 +60,7 @@ class BIMExplorerProperties(PropertyGroup):
self.property_unset("editing_entity_id")
self.entity_attributes.clear()
is_loaded: BoolProperty( # pyright: ignore[reportRedeclaration]
is_loaded: BoolProperty(
name="Toggle Explorer UI",
update=update_is_loaded,
)
@@ -76,15 +76,15 @@ class BIMExplorerProperties(PropertyGroup):
def update_ifc_class(self, context: object) -> None:
tool.Attribute.refresh_uilist_entities()
ifc_class: EnumProperty( # pyright: ignore[reportRedeclaration]
ifc_class: EnumProperty(
name="IFC Class To Search",
items=get_ifc_class,
update=update_ifc_class,
)
entities: CollectionProperty(type=ExplorerEntity) # pyright: ignore[reportRedeclaration]
active_entity_index: IntProperty() # pyright: ignore[reportRedeclaration]
editing_entity_id: IntProperty() # pyright: ignore[reportRedeclaration]
entity_attributes: CollectionProperty(type=Attribute) # pyright: ignore[reportRedeclaration]
entities: CollectionProperty(type=ExplorerEntity)
active_entity_index: IntProperty()
editing_entity_id: IntProperty()
entity_attributes: CollectionProperty(type=Attribute)
if TYPE_CHECKING:
is_loaded: bool
+4 -10
View File
@@ -201,16 +201,10 @@ class ExecuteIfcClash(bpy.types.Operator, ExportHelper):
"ALT+click to run a quick clash without selecting a file to save."
)
filter_glob: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
default="*.bcf;*.json", options={"HIDDEN"}
)
format: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name="Format", items=[(i, i, "") for i in ("bcf", "json")]
)
filepath: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
subtype="FILE_PATH", options={"SKIP_SAVE"}
)
quick_clash: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(default="*.bcf;*.json", options={"HIDDEN"})
format: bpy.props.EnumProperty(name="Format", items=[(i, i, "") for i in ("bcf", "json")])
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
quick_clash: bpy.props.BoolProperty(
options={"SKIP_SAVE"},
)
+4 -4
View File
@@ -37,12 +37,12 @@ from bonsai.bim.prop import BIMFilterGroup, StrProperty
class ClashSource(PropertyGroup):
name: StringProperty( # pyright: ignore[reportRedeclaration]
name: StringProperty(
name="File",
description="Absolute filepath to existing .ifc file to use as a clash source.",
)
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups") # pyright: ignore[reportRedeclaration]
mode: EnumProperty( # pyright: ignore[reportRedeclaration]
filter_groups: CollectionProperty(type=BIMFilterGroup, name="Filter Groups")
mode: EnumProperty(
items=[
("a", "All Elements", "All elements will be used for clashing"),
("i", "Include", "Only the selected elements are included for clashing"),
@@ -62,7 +62,7 @@ class Clash(PropertyGroup):
b_global_id: StringProperty(name="B")
a_name: StringProperty(name="A Name")
b_name: StringProperty(name="B Name")
clash_type: EnumProperty( # pyright: ignore[reportRedeclaration]
clash_type: EnumProperty(
name="Clash Type",
items=tuple((i, i, "") for i in CLASH_TYPE_ITEMS),
)
+1 -1
View File
@@ -206,7 +206,7 @@ class CostSchedulesData:
data["UnitSymbol"] = ifcopenshell.util.unit.get_unit_symbol(unit)
if quantity.is_a("IfcPhysicalSimpleQuantity"):
measure_class = (
quantity.wrapped_data.declaration()
quantity.declaration()
.as_entity()
.attribute_by_index(3)
.type_of_attribute()
@@ -87,7 +87,7 @@ class CopyCostSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Copy Cost Schedule"
bl_description = "Create a duplicate of the provided cost schedule."
bl_options = {"REGISTER", "UNDO"}
cost_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
cost_schedule: bpy.props.IntProperty()
if TYPE_CHECKING:
cost_schedule: int
@@ -90,7 +90,7 @@ class PrintIfcFile(bpy.types.Operator):
return tool.Ifc.get()
def execute(self, context):
print(tool.Ifc.get().wrapped_data.to_string())
print(tool.Ifc.get().to_string())
return {"FINISHED"}
@@ -260,14 +260,14 @@ class CreateAllShapes(bpy.types.Operator):
)
bl_options = {"REGISTER"}
geometry_library: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
geometry_library: bpy.props.EnumProperty(
name="Geometry Library",
description="Geometry library to use for testing shape creation.",
items=[(i, i, "") for i in get_args(ifcopenshell.geom.GEOMETRY_LIBRARY)],
# By default use the same library as used for importing ifc project.
default="hybrid-cgal-simple-opencascade",
)
custom_geometry_library: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
custom_geometry_library: bpy.props.StringProperty(
name="Custom Geometry Library",
description="Provide a custom geometry library name, will override the 'geometry library' property.",
)
@@ -781,7 +781,7 @@ class PurgeUnusedObjects(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Purge Unused Objects"
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
object_type: bpy.props.EnumProperty(
name="Object Type",
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
)
@@ -827,7 +827,7 @@ class MergeIdenticalObjects(bpy.types.Operator, tool.Ifc.Operator):
)
bl_options = {"REGISTER", "UNDO"}
object_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
object_type: bpy.props.EnumProperty(
name="Object Type",
items=((s, s.capitalize(), "") for s in get_args(tool.Debug.PurgeMergeObjectType)),
)
@@ -1073,7 +1073,7 @@ class ChangeLogLevel(bpy.types.Operator):
bl_options = {"REGISTER"}
bl_description = "Change general log level across all Python code in Blender"
log_level: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
log_level: bpy.props.EnumProperty(
name="Log Level",
items=[(i, i, "") for i in get_args(LogLevelType)],
default="WARNING",
@@ -246,17 +246,17 @@ class CreateDrawing(bpy.types.Operator):
+ "Add the CTRL modifier to optionally open drawings to view them as\n"
+ "they are created"
)
print_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
print_all: bpy.props.BoolProperty(
name="Print All",
default=False,
options={"SKIP_SAVE"},
)
open_viewer: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
open_viewer: bpy.props.BoolProperty(
name="Open in Viewer",
default=False,
options={"SKIP_SAVE"},
)
sync: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
sync: bpy.props.BoolProperty(
name="Sync Before Creating Drawing",
description="Could save some time if you're sure IFC and current Blender session are already in sync",
default=True,
@@ -2322,14 +2322,14 @@ class ActivateDrawingBase(tool.Ifc.Operator):
+ "SHIFT+CLICK to load a quick preview of the drawing view"
)
drawing: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
should_view_from_camera: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
drawing: bpy.props.IntProperty()
should_view_from_camera: bpy.props.BoolProperty(
name="Should View From Camera",
description="Move view to the activated drawing's camera position.",
default=True,
options={"SKIP_SAVE"},
)
use_quick_preview: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
use_quick_preview: bpy.props.BoolProperty(
name="Use Quick Preview",
description="Just move the camera to the drawing view, without loading anything else.",
default=False,
@@ -3635,14 +3635,12 @@ class ToggleTargetView(bpy.types.Operator):
bl_label = "Toggle Target View"
bl_options = {"REGISTER", "UNDO"}
target_view: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
toggle_all: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
target_view: bpy.props.StringProperty()
toggle_all: bpy.props.BoolProperty(
default=False,
options={"SKIP_SAVE"},
)
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[(i, i, "") for i in get_args(ToggleOption)]
)
option: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(ToggleOption)])
if TYPE_CHECKING:
target_view: str
+2 -2
View File
@@ -860,13 +860,13 @@ class BIMTextProperties(PropertyGroup):
is_editing: BoolProperty(name="Is Editing", default=False)
literals: CollectionProperty(name="Literals", type=LiteralProps)
newline_at: IntProperty(name="Newline At")
symbol: EnumProperty( # pyright: ignore[reportRedeclaration]
symbol: EnumProperty(
name="Symbol",
description="Symbol from symbols.svg to use for this text.",
items=[(s, s, "") for s in ["NO SYMBOL", "CUSTOM SYMBOL"] + tool.Drawing.DEFAULT_SYMBOLS],
default="NO SYMBOL",
)
custom_symbol: StringProperty( # pyright: ignore[reportRedeclaration]
custom_symbol: StringProperty(
name="Custom Symbol",
description="Non-default symbol to use for this text.",
)
@@ -85,7 +85,7 @@ class EditObjectPlacement(bpy.types.Operator, tool.Ifc.Operator):
class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_mesh_separate"
bl_label = "IFC Mesh Separate"
blender_op = bpy.ops.mesh.separate.get_rna_type()
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
bl_description = blender_op.description + ".\nAlso makes sure changes are in sync with IFC."
bl_options = {"REGISTER", "UNDO"}
blender_type_prop = blender_op.properties["type"]
@@ -246,7 +246,7 @@ class OverrideMeshSeparate(bpy.types.Operator, tool.Ifc.Operator):
class OverrideOriginSet(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_origin_set"
blender_op = bpy.ops.object.origin_set.get_rna_type()
blender_op = bpy.ops.object.origin_set.get_rna_type() # ty: ignore[missing-argument]
bl_label = "IFC Origin Set"
bl_description = (
blender_op.description + ".\nAlso makes sure changes are in sync with IFC (operator works only on IFC objects)"
@@ -790,7 +790,7 @@ def lock_error_message(name: str) -> str:
def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context) -> bool:
total_elements = len(tool.Ifc.get().wrapped_data.entity_names())
total_elements = len(tool.Ifc.get().entity_names())
total_polygons = sum([len(o.data.polygons) for o in context.selected_objects if o.type == "MESH"])
# These numbers are a bit arbitrary, but basically batching is only
# really necessary on large models and large geometry removals.
@@ -801,7 +801,7 @@ def calc_delete_is_batch(ifc_file: ifcopenshell.file, context: bpy.types.Context
class OverrideDelete(bpy.types.Operator):
bl_idname = "bim.override_object_delete"
bl_label = "IFC Delete"
blender_op = bpy.ops.object.delete.get_rna_type()
blender_op = bpy.ops.object.delete.get_rna_type() # ty: ignore[missing-argument]
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes in sync with IFC."
@@ -821,7 +821,7 @@ class OverrideDelete(bpy.types.Operator):
def poll(cls, context):
# Match `object.delete` poll for consistency.
# `object.delete` poll just checks for OBJECT mode.
poll = bpy.ops.object.delete.poll()
poll = bpy.ops.object.delete.poll() # ty: ignore[missing-argument]
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
@@ -1045,7 +1045,7 @@ class SelectedIdsData(NamedTuple):
class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_outliner_delete"
bl_label = "IFC Delete"
blender_op = bpy.ops.outliner.delete.get_rna_type()
blender_op = bpy.ops.outliner.delete.get_rna_type() # ty: ignore[missing-argument]
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes in sync with IFC."
@@ -1060,7 +1060,7 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
def poll(cls, context) -> bool:
# Match `outliner.delete` poll for consistency.
# `outliner.delete` just checks `area.type` == `OUTLINER`.
poll = bpy.ops.outliner.delete.poll()
poll = bpy.ops.outliner.delete.poll() # ty: ignore[missing-argument]
if poll:
return True
cls.poll_message_set("Only available from Outliner.")
@@ -1164,7 +1164,7 @@ class OverrideDuplicateMove(bpy.types.Operator):
def poll(cls, context) -> bool:
# Match `object.duplicate_move` poll for consistency.
# `object.duplicate_move` poll checks for OBJECT mode.
poll = bpy.ops.object.duplicate_move.poll()
poll = bpy.ops.object.duplicate_move.poll() # ty: ignore[missing-argument]
if poll:
return True
cls.poll_message_set("Only available in OBJECT mode")
@@ -1908,7 +1908,7 @@ class RefreshLinkedAggregate(bpy.types.Operator, tool.Ifc.Operator):
class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.override_object_join"
bl_label = "IFC Join"
blender_op = bpy.ops.mesh.separate.get_rna_type()
blender_op = bpy.ops.mesh.separate.get_rna_type() # ty: ignore[missing-argument]
bl_description = (
blender_op.description
+ ".\nAlso makes sure changes are in sync with IFC."
@@ -1926,7 +1926,7 @@ class OverrideJoin(bpy.types.Operator, tool.Ifc.Operator):
@classmethod
def poll(cls, context):
if not bpy.ops.object.join.poll():
if not bpy.ops.object.join.poll(): # ty: ignore[missing-argument]
cls.poll_message_set("Active object is not EDITable.")
return False
if not context.selected_editable_objects:
@@ -43,11 +43,11 @@ class ToggleGroup(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Toggle Group"
bl_options = {"REGISTER", "UNDO"}
ifc_definition_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
group_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
ifc_definition_id: bpy.props.IntProperty()
group_type: bpy.props.EnumProperty(
items=[(i, i, "") for i in get_args(tool.Group.GroupType)],
)
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
option: bpy.props.EnumProperty(
items=[(i, i, "") for i in get_args(tool.Group.ToggleOption)],
)
@@ -34,8 +34,10 @@ classes = (
operator.Fetch,
operator.Merge,
operator.ObjectLog,
operator.SelectConflictEntity,
operator.Push,
operator.RefreshGit,
operator.RenameBranch,
operator.SwitchRevision,
operator.InstallGit,
operator.RunGitDiff,
+64 -79
View File
@@ -21,65 +21,71 @@ class IfcGitData:
@classmethod
def load(cls):
repo = None
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
repo = tool.IfcGit.repo_from_path(path_ifc)
cls.data = {
"repo": cls.repo(),
"remotes": cls.remotes(),
"branch_names": cls.branch_names(),
"remote_names": cls.remote_names(),
"remote_urls": cls.remote_urls(),
"repo": repo,
"remotes": repo.remotes if repo else None,
"branch_names": cls.branch_names(repo),
"tag_names": cls.tag_names(repo),
"remote_names": cls.remote_names(repo),
"remote_urls": {r.name: r.url for r in repo.remotes} if repo else {},
"path_ifc": cls.path_ifc(),
"branches_by_hexsha": cls.branches_by_hexsha(),
"tags_by_hexsha": cls.tags_by_hexsha(),
"name_ifc": cls.name_ifc(),
"name_ifc": cls.name_ifc(repo),
"dir_name": cls.dir_name(),
"base_name": cls.base_name(),
"working_dir": cls.working_dir(),
"untracked_files": cls.untracked_files(),
"is_detached": cls.is_detached(),
"active_branch_name": cls.active_branch_name(),
"is_dirty": cls.is_dirty(),
"commit": cls.commit(),
"current_revision": cls.current_revision(),
"working_dir": repo.working_dir if repo else None,
"ifc_is_untracked": cls.ifc_is_untracked(repo),
"is_detached": repo.head.is_detached if repo else None,
"active_branch_name": repo.active_branch.name if repo and not repo.head.is_detached else None,
"is_dirty": cls.is_dirty(repo),
"current_revision": cls.current_revision(repo),
"git_exe": cls.git_exe(),
"ifcmerge_exe": cls.ifcmerge_exe(),
}
cls.is_loaded = True
@classmethod
def repo(cls):
if bool(tool.Ifc.get()):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.IfcGit.repo_from_path(path_ifc)
return None
def branch_names(cls, repo):
if not repo or not repo.heads:
return []
names = sorted([b.name for b in repo.branches])
if "main" in names:
names.remove("main")
names = ["main"] + names
if repo.remotes:
for remote in repo.remotes:
for ref in remote.refs:
names.append(ref.name)
return names
@classmethod
def remotes(cls):
if cls.repo():
return cls.repo().remotes
return None
def tag_names(cls, repo):
if not repo:
return []
return [t.name for t in repo.tags]
@classmethod
def branch_names(cls):
return []
@classmethod
def remote_names(cls):
return []
@classmethod
def remote_urls(cls):
result = {}
if cls.repo():
for remote in cls.repo().remotes:
result[remote.name] = remote.url
return result
def remote_names(cls, repo):
if not repo:
return []
names = sorted([r.name for r in repo.remotes])
if "origin" in names:
names.remove("origin")
names = ["origin"] + names
return names
@classmethod
def path_ifc(cls):
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return tool.Ifc.get_path()
return path_ifc
return None
@classmethod
@@ -88,7 +94,8 @@ class IfcGitData:
if tool.IfcGitRepo.repo.branches:
return tool.IfcGit.branches_by_hexsha(tool.IfcGitRepo.repo)
except AttributeError:
return {}
pass
return {}
@classmethod
def tags_by_hexsha(cls):
@@ -97,12 +104,11 @@ class IfcGitData:
return {}
@classmethod
def name_ifc(cls):
if bool(tool.Ifc.get()):
def name_ifc(cls, repo):
if bool(tool.Ifc.get()) and repo:
path_ifc = tool.Ifc.get_path()
if tool.IfcGitRepo.repo and os.path.isfile(path_ifc):
working_dir = tool.IfcGitRepo.repo.working_dir
return os.path.relpath(path_ifc, working_dir)
if os.path.isfile(path_ifc):
return os.path.relpath(path_ifc, repo.working_dir)
return None
@classmethod
@@ -122,49 +128,28 @@ class IfcGitData:
return None
@classmethod
def working_dir(cls):
if cls.repo():
return cls.repo().working_dir
def ifc_is_untracked(cls, repo):
"""Return True if the IFC file exists in the repo but has not been added to git."""
if not repo:
return False
path_ifc = tool.Ifc.get_path()
if not os.path.isfile(path_ifc):
return False
return not bool(repo.git.ls_files(path_ifc))
@classmethod
def untracked_files(cls):
if cls.repo():
return cls.repo().untracked_files
return []
@classmethod
def is_detached(cls):
if cls.repo():
return cls.repo().head.is_detached
@classmethod
def active_branch_name(cls):
if cls.repo() and not cls.is_detached():
return cls.repo().active_branch.name
@classmethod
def is_dirty(cls):
if cls.repo() and cls.git_exe():
def is_dirty(cls, repo):
if repo and cls.git_exe():
path_ifc = tool.Ifc.get_path()
if os.path.isfile(path_ifc):
return cls.repo().is_dirty(path=path_ifc)
return repo.is_dirty(path=path_ifc)
return False
@classmethod
def commit(cls):
def current_revision(cls, repo):
props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and len(props.ifcgit_commits) > 0:
item = props.ifcgit_commits[props.commit_index]
try:
return cls.repo().commit(rev=item.hexsha)
except ValueError:
return
@classmethod
def current_revision(cls):
props = tool.IfcGit.get_ifcgit_props()
if cls.repo() and cls.repo().head.is_valid() and len(props.ifcgit_commits) > 0:
return tool.IfcGitRepo.repo.commit()
if repo and repo.head.is_valid() and len(props.ifcgit_commits) > 0:
return repo.commit()
@classmethod
def git_exe(cls):
+155 -28
View File
@@ -120,11 +120,11 @@ class CommitChanges(bpy.types.Operator):
if props.commit_message == "":
return False
if repo:
if props.new_branch_name in [branch.name for branch in repo.branches]:
if props.new_branch_name in IfcGitData.data["branch_names"]:
cls.poll_message_set("Branch already exists!")
return False
elif not tool.IfcGit.is_valid_ref_format(props.new_branch_name):
if repo.head.is_detached:
if IfcGitData.data["is_detached"]:
cls.poll_message_set("Branch name is invalid or empty!")
return False
elif props.new_branch_name != "":
@@ -134,10 +134,17 @@ class CommitChanges(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.commit_changes(tool.IfcGit, tool.Ifc, repo)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
props = tool.IfcGit.get_ifcgit_props()
commit_message = props.commit_message
new_branch_name = props.new_branch_name
core.commit_changes(tool.IfcGit, tool.Ifc, commit_message, new_branch_name)
props.new_branch_name = ""
props.commit_message = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
IfcGitData.load()
if new_branch_name:
props.display_branch = new_branch_name
return {"FINISHED"}
@@ -157,7 +164,7 @@ class AddTag(bpy.types.Operator):
repo = IfcGitData.data["repo"]
if repo and (
not tool.IfcGit.is_valid_ref_format(props.new_tag_name)
or props.new_tag_name in [tag.name for tag in repo.tags]
or props.new_tag_name in IfcGitData.data["tag_names"]
):
return False
return True
@@ -165,8 +172,12 @@ class AddTag(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.add_tag(tool.IfcGit, repo)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
props = tool.IfcGit.get_ifcgit_props()
item = props.ifcgit_commits[props.commit_index]
core.add_tag(tool.IfcGit, repo, item.hexsha, props.new_tag_name, props.new_tag_message)
props.new_tag_name = ""
props.new_tag_message = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
@@ -183,7 +194,7 @@ class DeleteTag(bpy.types.Operator):
repo = IfcGitData.data["repo"]
core.delete_tag(tool.IfcGit, repo, self.tag_name)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
@@ -191,7 +202,7 @@ class DeleteTag(bpy.types.Operator):
class RefreshGit(bpy.types.Operator):
"""Refresh revision list"""
bl_label = ""
bl_label = "Refresh"
bl_idname = "ifcgit.refresh"
bl_options = {"REGISTER"}
@@ -205,8 +216,7 @@ class RefreshGit(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
tool.IfcGit.decolourise()
return {"FINISHED"}
@@ -215,7 +225,7 @@ class RefreshGit(bpy.types.Operator):
class DisplayRevision(bpy.types.Operator):
"""Colourise objects by selected revision"""
bl_label = ""
bl_label = "Colourise Revision"
bl_idname = "ifcgit.display_revision"
bl_options = {"REGISTER"}
@@ -250,7 +260,7 @@ class DisplayUncommitted(bpy.types.Operator):
class SwitchRevision(bpy.types.Operator):
"""Switches the repository to the selected revision and reloads the IFC file"""
bl_label = ""
bl_label = "Switch Revision"
bl_idname = "ifcgit.switch_revision"
bl_options = {"REGISTER"}
@@ -268,7 +278,7 @@ class SwitchRevision(bpy.types.Operator):
class Merge(bpy.types.Operator):
"""Merges the selected branch into working branch"""
"""Merges the selected branch into working branch.\nCtrl+click to preview without merging"""
bl_label = "Merge this branch"
bl_idname = "ifcgit.merge"
@@ -282,15 +292,84 @@ class Merge(bpy.types.Operator):
return True
return False
def execute(self, context):
def invoke(self, context, event):
if event.ctrl:
core.dry_run_merge(tool.IfcGit, tool.Ifc, self)
refresh()
return {"FINISHED"}
return self.execute(context)
if core.merge_branch(tool.IfcGit, tool.Ifc, self):
def execute(self, context):
if core.merge_branch(tool.IfcGit, tool.Ifc, self) is not False:
refresh()
return {"FINISHED"}
else:
return {"CANCELLED"}
class SelectConflictEntity(bpy.types.Operator):
"""Select the conflicting entity in the viewport"""
bl_label = "Select Conflict Entity"
bl_idname = "ifcgit.select_conflict_entity"
bl_options = {"REGISTER"}
step_id: bpy.props.IntProperty()
if TYPE_CHECKING:
step_id: int
def execute(self, context):
model = tool.Ifc.get()
if not model:
return {"CANCELLED"}
try:
entity = model.by_id(self.step_id)
except Exception:
self.report({"WARNING"}, f"Entity #{self.step_id} not found (may have been deleted locally)")
return {"CANCELLED"}
obj = tool.Ifc.get_object(entity)
if obj is None:
# Walk inverse references up to 5 hops to find nearest entity with a Blender object
visited = {entity.id()}
queue = [entity]
for _ in range(5):
next_queue = []
for ent in queue:
for inv in model.get_inverse(ent):
if inv.id() in visited:
continue
visited.add(inv.id())
obj = tool.Ifc.get_object(inv)
if obj is not None:
break
next_queue.append(inv)
if obj is not None:
break
if obj is not None:
break
queue = next_queue
if obj is None:
self.report({"INFO"}, f"No viewport representation found for #{self.step_id} ({entity.is_a()})")
return {"CANCELLED"}
bpy.ops.object.select_all(action="DESELECT")
obj.select_set(True)
context.view_layer.objects.active = obj
for area in context.screen.areas:
if area.type == "VIEW_3D":
region = next((r for r in area.regions if r.type == "WINDOW"), None)
if region:
with context.temp_override(area=area, region=region):
bpy.ops.view3d.view_selected()
break
return {"FINISHED"}
class Push(bpy.types.Operator):
"""Pushes the working branch to selected remote"""
@@ -314,9 +393,9 @@ class Fetch(bpy.types.Operator):
def execute(self, context):
props = tool.IfcGit.get_ifcgit_props()
repo = IfcGitData.data["repo"]
remote = repo.remotes[props.select_remote]
remote.fetch()
core.fetch(tool.IfcGit, props.select_remote)
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
@@ -336,7 +415,7 @@ class AddRemote(bpy.types.Operator):
not repo
or not tool.IfcGit.is_valid_ref_format(props.remote_name)
or not props.remote_url
or props.remote_name in [remote.name for remote in repo.remotes]
or props.remote_name in IfcGitData.data["remote_names"]
):
return False
return True
@@ -344,8 +423,11 @@ class AddRemote(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.add_remote(tool.IfcGit, repo)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
props = tool.IfcGit.get_ifcgit_props()
core.add_remote(tool.IfcGit, repo, props.remote_name, props.remote_url)
props.remote_name = ""
props.remote_url = ""
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
@@ -360,8 +442,19 @@ class DeleteRemote(bpy.types.Operator):
def execute(self, context):
repo = IfcGitData.data["repo"]
core.delete_remote(tool.IfcGit, repo)
core.refresh_revision_list(tool.IfcGit, repo, tool.Ifc)
props = tool.IfcGit.get_ifcgit_props()
remote_name = props.select_remote
if props.display_branch.startswith(remote_name + "/"):
active = IfcGitData.data["active_branch_name"]
if active:
props.display_branch = active
else:
local_branches = [b for b in IfcGitData.data["branch_names"] if "/" not in b]
if local_branches:
props.display_branch = local_branches[0]
core.delete_remote(tool.IfcGit, repo, remote_name)
tool.IfcGit.select_first_remote()
core.refresh_revision_list(tool.IfcGit, tool.Ifc)
refresh()
return {"FINISHED"}
@@ -375,8 +468,8 @@ class ObjectLog(bpy.types.Operator):
@classmethod
def poll(cls, context):
if not (obj := context.active_object):
cls.poll_message_set("No Active Object")
if not (obj := context.active_object) or not obj.select_get():
cls.poll_message_set("No selected object")
elif not tool.Blender.get_ifc_definition_id(obj):
cls.poll_message_set("Active Object doesn't have an IFC definition")
else:
@@ -422,7 +515,7 @@ class RunGitDiff(bpy.types.Operator):
)
bl_options = set()
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
save_to_temp: bpy.props.BoolProperty(options={"SKIP_SAVE"})
if TYPE_CHECKING:
save_to_temp: bool
@@ -445,3 +538,37 @@ class RunGitDiff(bpy.types.Operator):
def execute(self, context):
core.run_git_diff(tool.IfcGit, self, self.save_to_temp)
return {"FINISHED"}
class RenameBranch(bpy.types.Operator):
"""Rename the current branch"""
bl_label = "Rename Branch"
bl_idname = "ifcgit.rename_branch"
bl_options = {"REGISTER"}
new_name: bpy.props.StringProperty(name="New name")
if TYPE_CHECKING:
new_name: str
@classmethod
def poll(cls, context):
IfcGitData.make_sure_is_loaded()
if not IfcGitData.data["repo"]:
return False
if IfcGitData.data["is_detached"]:
return False
if IfcGitData.data["is_dirty"]:
return False
return True
def invoke(self, context, event):
self.new_name = IfcGitData.data["active_branch_name"]
return context.window_manager.invoke_props_dialog(self)
def execute(self, context):
repo = IfcGitData.data["repo"]
core.rename_branch(tool.IfcGit, repo, self.new_name)
refresh()
return {"FINISHED"}
+13 -19
View File
@@ -17,28 +17,14 @@ from bonsai.bim.module.ifcgit.data import IfcGitData
def git_branches(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
# NOTE "Python must keep a reference to the strings returned by
# the callback or Blender will misbehave or even crash"
IfcGitData.data["branch_names"] = sorted([branch.name for branch in IfcGitData.data["repo"].heads])
if "main" in IfcGitData.data["branch_names"]:
IfcGitData.data["branch_names"].remove("main")
IfcGitData.data["branch_names"] = ["main"] + IfcGitData.data["branch_names"]
if IfcGitData.data["remotes"]:
for remote in IfcGitData.data["remotes"]:
for remote_branch in remote.refs:
IfcGitData.data["branch_names"].append(remote_branch.name)
return [(myname, myname, myname) for myname in IfcGitData.data["branch_names"]]
# Branch list (local + remote, main first) is computed once in IfcGitData.load()
IfcGitData.make_sure_is_loaded()
return [(name, name, name) for name in IfcGitData.data["branch_names"]]
def git_remotes(self: "IfcGitProperties", context: bpy.types.Context) -> tool.Blender.BLENDER_ENUM_ITEMS:
IfcGitData.data["remote_names"] = sorted([remote.name for remote in IfcGitData.data["remotes"]])
if "origin" in IfcGitData.data["remote_names"]:
IfcGitData.data["remote_names"].remove("origin")
IfcGitData.data["remote_names"] = ["origin"] + IfcGitData.data["remote_names"]
return [(myname, myname, myname) for myname in IfcGitData.data["remote_names"]]
IfcGitData.make_sure_is_loaded()
return [(name, name, name) for name in IfcGitData.data["remote_names"]]
def update_revlist(self: "IfcGitProperties", context: bpy.types.Context) -> None:
@@ -90,6 +76,7 @@ class IfcGitListItem(PropertyGroup):
name="Commit Message",
default="",
)
committed_date: IntProperty(name="Committed Date", default=0)
tags: CollectionProperty(type=IfcGitTag, name="List of revision tags")
if TYPE_CHECKING:
@@ -98,6 +85,7 @@ class IfcGitListItem(PropertyGroup):
author_name: str
author_email: str
message: str
committed_date: int
tags: bpy.types.bpy_prop_collection_idprop[IfcGitTag]
@@ -151,6 +139,11 @@ class IfcGitProperties(PropertyGroup):
],
update=update_revlist,
)
merge_conflicts: StringProperty(
name="Merge Conflicts",
description="JSON report from last failed merge attempt",
default="",
)
if TYPE_CHECKING:
ifcgit_commits: bpy.types.bpy_prop_collection_idprop[IfcGitListItem]
@@ -165,3 +158,4 @@ class IfcGitProperties(PropertyGroup):
display_branch: str
select_remote: str
ifcgit_filter: Literal["all", "tagged", "relevant"]
merge_conflicts: str
+62 -26
View File
@@ -52,7 +52,7 @@ class IFCGIT_PT_panel(bpy.types.Panel):
if IfcGitData.data["repo"] and os.path.exists(IfcGitData.data["repo"].git_dir):
name_ifc = IfcGitData.data["name_ifc"]
row.label(text=IfcGitData.data["working_dir"], icon="SYSTEM")
if name_ifc in IfcGitData.data["untracked_files"]:
if IfcGitData.data["ifc_is_untracked"]:
row.operator(
"ifcgit.addfile",
text="Add '" + name_ifc + "' to repository",
@@ -112,15 +112,13 @@ class IFCGIT_PT_panel(bpy.types.Panel):
row.label(text="Working branch: Detached HEAD")
else:
row.label(text="Working branch: " + IfcGitData.data["active_branch_name"])
row.operator("ifcgit.rename_branch", icon="GREASEPENCIL", text="")
grouped = layout.row()
column = grouped.column()
row = column.row()
row = layout.row()
row.prop(props, "display_branch", text="Browse branch")
row.prop(props, "ifcgit_filter", text="Filter revisions")
row = column.row()
row.template_list(
layout.template_list(
"COMMIT_UL_List",
"The_List",
props,
@@ -128,20 +126,64 @@ class IFCGIT_PT_panel(bpy.types.Panel):
props,
"commit_index",
)
column = grouped.column()
row = column.row()
row = layout.row(align=True)
row.operator("ifcgit.refresh", icon="FILE_REFRESH")
if not is_dirty:
row = column.row()
row.operator("ifcgit.display_revision", icon="SELECT_DIFFERENCE")
row = column.row()
row.operator("ifcgit.switch_revision", icon="CURRENT_FILE")
row.operator("ifcgit.merge", icon="SYSTEM")
row = column.row()
row.operator("ifcgit.merge", icon="EXPERIMENTAL", text="")
conflicts = tool.IfcGit.get_merge_conflicts()
if conflicts is not None:
box = layout.box()
box.alert = True
row = box.row()
row.label(
text=f"Merge failed \u2014 {len(conflicts)} conflict(s)",
icon="ERROR",
)
for conflict in conflicts:
col = box.column(align=True)
conflict_type = conflict.get("type", "")
entity_id = conflict.get("entity_id", "?")
local_id = conflict.get("original_local_id")
if conflict_type == "attribute_conflict":
entity_class = conflict.get("entity_class", "Entity")
attr_idx = conflict.get("attribute_index", "?")
desc = f"#{entity_id} {entity_class}: attribute {attr_idx} conflict"
elif conflict_type == "entity_deleted_and_modified":
entity_class = conflict.get("entity_class", "Entity")
desc = f"#{entity_id} {entity_class}: " + conflict.get("message", "deleted/modified conflict")
elif conflict_type == "class_changed":
desc = (
f"#{entity_id}: class changed "
+ conflict.get("base_class", "?")
+ " \u2192 "
+ conflict.get("modified_class", "?")
)
elif conflict_type == "required_entity_deleted":
desc = f"#{entity_id}: " + conflict.get("message", "required entity deleted")
else:
desc = f"#{entity_id}: {conflict_type}"
row = col.row(align=True)
row.label(text=desc)
if local_id:
op = row.operator(
"ifcgit.select_conflict_entity",
text="",
icon="RESTRICT_SELECT_OFF",
)
op.step_id = local_id
if conflict_type == "attribute_conflict":
sub = col.column(align=True)
sub.scale_y = 0.75
sub.label(text=f" Base: {conflict.get('base_value', '')}")
sub.label(text=f" Local: {conflict.get('local_value', '')}")
sub.label(text=f" Remote: {conflict.get('remote_value', '')}")
if not props.ifcgit_commits:
return
@@ -216,13 +258,7 @@ class COMMIT_UL_List(bpy.types.UIList):
):
current_revision = IfcGitData.data["current_revision"]
# TODO Figure how this "item" can be acesse in "data.py"
# so it's possible to move the ".commit"
try:
commit = IfcGitData.data["repo"].commit(rev=item.hexsha)
except ValueError:
return
current_hexsha = current_revision.hexsha if current_revision else None
lookup = IfcGitData.data["branches_by_hexsha"]
refs = ""
@@ -236,11 +272,11 @@ class COMMIT_UL_List(bpy.types.UIList):
for tag in lookup[item.hexsha]:
refs += "{" + tag.name + "} "
if commit == current_revision:
layout.label(text="[HEAD] " + refs + commit.message.split("\n")[0], icon="DECORATE_KEYFRAME")
if item.hexsha == current_hexsha:
layout.label(text="[HEAD] " + refs + item.message.split("\n")[0], icon="DECORATE_KEYFRAME")
else:
layout.label(text=refs + commit.message.split("\n")[0], icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(commit.committed_date)))
layout.label(text=refs + item.message.split("\n")[0], icon="DECORATE_ANIMATE")
layout.label(text=time.strftime("%c", time.localtime(item.committed_date)))
def draw_filter(self, context, layout):
@@ -272,21 +272,21 @@ class RadianceRender(bpy.types.Operator):
+ '''" map_u map_v
0
1 0.5
# This is a multiplier to colour balance the env map
# In this case, it provides a rough ground luminance from 3k-5k
env_map colorfunc env_colour
4 100 100 100 .
0
0
# .37 .57 1.5 is measured from a HDRI image
# It is multiplied by a factor such that grey(r,g,b) = 1
skyfunc colorfunc sky_colour
4 .64 .99 2.6 .
0
0
void mixpict composite
7 env_colour sky_colour grey "'''
+ hdr_mask_path
@@ -295,22 +295,22 @@ void mixpict composite
+ """" map_u map_v
0
2 0.5 1
composite glow env_map_glow
0
0
4 1 1 1 0
env_map_glow source sky
0
0
4 0 0 1 180
env_colour glow ground_glow
0
0
4 1 1 1 0
ground_glow source ground
0
0
@@ -566,7 +566,7 @@ class LightPickCoordinates(bpy.types.Operator):
)
bl_options = {"REGISTER", "UNDO"}
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
use_current_location: bpy.props.BoolProperty(options={"SKIP_SAVE"})
if TYPE_CHECKING:
use_current_location: bool
@@ -136,7 +136,7 @@ class SplitAlongEdge(bpy.types.Operator, tool.Ifc.Operator):
"Will unassign element from a type if type has a representation."
)
bl_options = {"REGISTER", "UNDO"}
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
mode: bpy.props.EnumProperty(
default="BOOLEAN",
items=tuple((i, i, "") for i in get_args(SplitAlongEdgeMode)),
)
@@ -359,7 +359,7 @@ class ConfirmQuickFavoriteOperator(bpy.types.Operator):
bl_idname = "bim.confirm_quick_favorite_operator"
bl_label = "Confirm Operator"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
index: bpy.props.IntProperty()
if TYPE_CHECKING:
index: int
@@ -452,10 +452,8 @@ class MoveQuickFavoritesItem(bpy.types.Operator):
bl_idname = "bim.move_quick_favorites_item"
bl_label = "Move Quick Favorites Item"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
direction: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[("UP", "Up", ""), ("DOWN", "Down", "")]
)
index: bpy.props.IntProperty()
direction: bpy.props.EnumProperty(items=[("UP", "Up", ""), ("DOWN", "Down", "")])
if TYPE_CHECKING:
index: int
@@ -474,7 +472,7 @@ class RemoveQuickFavoritesItem(bpy.types.Operator):
bl_idname = "bim.remove_quick_favorites_item"
bl_label = "Remove Quick Favorites Item"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
index: bpy.props.IntProperty()
if TYPE_CHECKING:
index: int
+21 -21
View File
@@ -36,9 +36,9 @@ QuickFavoriteValueType = Literal["float_value", "bool_value", "int_value", "stri
class QuickFavoriteEnumItem(PropertyGroup):
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
description: StringProperty(name="Description", default="") # pyright: ignore[reportRedeclaration]
name: StringProperty(name="Name", default="")
display_name: StringProperty(name="Display Name", default="")
description: StringProperty(name="Description", default="")
if TYPE_CHECKING:
name: str
@@ -51,19 +51,19 @@ def get_enum_items(self: "QuickFavoriteProperty", context: bpy.types.Context | N
class QuickFavoriteProperty(PropertyGroup):
name: StringProperty(name="Name", default="") # pyright: ignore[reportRedeclaration]
display_name: StringProperty(name="Display Name", default="") # pyright: ignore[reportRedeclaration]
value_prop: EnumProperty( # pyright: ignore[reportRedeclaration]
name: StringProperty(name="Name", default="")
display_name: StringProperty(name="Display Name", default="")
value_prop: EnumProperty(
name="Value Prop",
items=tuple((v, v, "") for v in get_args(QuickFavoriteValueType)),
)
string_value: StringProperty(name="String Value", default="") # pyright: ignore[reportRedeclaration]
float_value: FloatProperty(name="Float Value", default=0.0) # pyright: ignore[reportRedeclaration]
int_value: IntProperty(name="Int Value", default=0) # pyright: ignore[reportRedeclaration]
bool_value: BoolProperty(name="Bool Value", default=False) # pyright: ignore[reportRedeclaration]
enum_value: EnumProperty(name="Enum Value", items=get_enum_items) # pyright: ignore[reportRedeclaration]
enum_items: CollectionProperty(type=QuickFavoriteEnumItem) # pyright: ignore[reportRedeclaration]
is_active: BoolProperty( # pyright: ignore[reportRedeclaration]
string_value: StringProperty(name="String Value", default="")
float_value: FloatProperty(name="Float Value", default=0.0)
int_value: IntProperty(name="Int Value", default=0)
bool_value: BoolProperty(name="Bool Value", default=False)
enum_value: EnumProperty(name="Enum Value", items=get_enum_items)
enum_items: CollectionProperty(type=QuickFavoriteEnumItem)
is_active: BoolProperty(
name="Is Active",
description="Only active properties will be added to the operator when invoked from Quick Favorites",
default=False,
@@ -100,20 +100,20 @@ def get_operator_suggestions(self: "QuickFavoritesItem", context: bpy.types.Cont
class QuickFavoritesItem(PropertyGroup):
is_expanded: BoolProperty(name="Is Expanded", default=False) # pyright: ignore[reportRedeclaration]
search: StringProperty( # pyright: ignore[reportRedeclaration]
is_expanded: BoolProperty(name="Is Expanded", default=False)
search: StringProperty(
name="Search",
default="",
search=get_operator_suggestions,
# Resetting `search_options`, allowing users only to use suggestions.
search_options=set(),
)
properties: CollectionProperty(type=QuickFavoriteProperty) # pyright: ignore[reportRedeclaration]
operator_id: StringProperty( # pyright: ignore[reportRedeclaration]
properties: CollectionProperty(type=QuickFavoriteProperty)
operator_id: StringProperty(
name="Operator ID",
default="",
)
label: StringProperty( # pyright: ignore[reportRedeclaration]
label: StringProperty(
name="Label",
description="Label that will be used in Quick Favorites for this operator",
default="",
@@ -139,15 +139,15 @@ class QuickFavoritesItem(PropertyGroup):
class BIMMiscProperties(PropertyGroup):
total_storeys: IntProperty( # pyright: ignore[reportRedeclaration]
total_storeys: IntProperty(
name="Total Storeys",
description="Number of storeys above object's storey to take into account for resizing",
default=1,
)
override_colour: FloatVectorProperty( # pyright: ignore[reportRedeclaration]
override_colour: FloatVectorProperty(
name="Override Colour", subtype="COLOR", default=(1, 0, 0, 1), min=0.0, max=1.0, size=4
)
quick_favorites: CollectionProperty(type=QuickFavoritesItem) # pyright: ignore[reportRedeclaration]
quick_favorites: CollectionProperty(type=QuickFavoritesItem)
if TYPE_CHECKING:
total_storeys: int
@@ -545,7 +545,7 @@ class ChangeTypePage(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.change_type_page"
bl_label = "Change Type Page"
bl_options = {"REGISTER"}
page: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
page: bpy.props.IntProperty()
if TYPE_CHECKING:
page: int
@@ -271,7 +271,7 @@ class ExtendProfile(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.extend_profile"
bl_label = "Extend Profile"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
join_type: bpy.props.EnumProperty(
items=[("-", "Unjoin", ""), ("L", "L", ""), ("V", "V", ""), ("T", "T", "")],
default="-",
)
+4 -4
View File
@@ -1729,20 +1729,20 @@ def poll_sverchok_nodes(self: "BIMExternalParametricGeometryProperties", node_tr
class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
is_editing: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
is_editing: bpy.props.BoolProperty(
name="Is Editing Paramteric Geometry",
description="Toggle editing parametric geometry.",
default=False,
update=update_is_editing,
)
geometry_source: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
geometry_source: bpy.props.EnumProperty(
name="Geometry Source",
items=[
("GEONODES", "Geometry Nodes", ""),
("IFCSVERCHOK", "IFC Sverchok", ""),
],
)
geo_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
geo_nodes: bpy.props.PointerProperty(
name="Geometry Nodes",
description="Geometry nodes tree to use as a source for representation.",
type=bpy.types.GeometryNodeTree,
@@ -1750,7 +1750,7 @@ class BIMExternalParametricGeometryProperties(bpy.types.PropertyGroup):
poll=lambda self, node_tree: not node_tree.name.startswith("BBIM_EPG"),
)
sverchok_nodes: bpy.props.PointerProperty( # pyright: ignore[reportRedeclaration]
sverchok_nodes: bpy.props.PointerProperty(
name="Sverchok Nodes",
description="Sverchok node tree to use as a source for representation.",
type=bpy.types.NodeTree,
@@ -101,7 +101,7 @@ class NestDecorator:
cls.is_installed = False
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("FLOAT", "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
+27 -27
View File
@@ -33,7 +33,7 @@ class EnableEditingPerson(bpy.types.Operator):
bl_idname = "bim.enable_editing_person"
bl_label = "Enable Editing Person"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
person: bpy.props.IntProperty()
if TYPE_CHECKING:
person: int
@@ -75,7 +75,7 @@ class RemovePerson(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_person"
bl_label = "Remove Person"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
person: bpy.props.IntProperty()
if TYPE_CHECKING:
person: int
@@ -88,7 +88,7 @@ class AddPersonAttribute(bpy.types.Operator):
bl_idname = "bim.add_person_attribute"
bl_label = "Add Person Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name: bpy.props.EnumProperty(
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
)
@@ -104,10 +104,10 @@ class RemovePersonAttribute(bpy.types.Operator):
bl_idname = "bim.remove_person_attribute"
bl_label = "Remove Person Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name: bpy.props.EnumProperty(
items=tuple((i, i, "") for i in get_args(tool.Owner.PersonAttributeType)),
)
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
id: bpy.props.IntProperty()
if TYPE_CHECKING:
name: tool.Owner.PersonAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
@@ -122,7 +122,7 @@ class EnableEditingRole(bpy.types.Operator):
bl_idname = "bim.enable_editing_role"
bl_label = "Enable Editing Role"
bl_options = {"REGISTER", "UNDO"}
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
role: bpy.props.IntProperty()
if TYPE_CHECKING:
role: int
@@ -146,7 +146,7 @@ class AddRole(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_role"
bl_label = "Add Role"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
parent: bpy.props.IntProperty()
if TYPE_CHECKING:
parent: int
@@ -168,7 +168,7 @@ class RemoveRole(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_role"
bl_label = "Remove Role"
bl_options = {"REGISTER", "UNDO"}
role: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
role: bpy.props.IntProperty()
if TYPE_CHECKING:
role: int
@@ -181,8 +181,8 @@ class AddAddress(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_address"
bl_label = "Add Address"
bl_options = {"REGISTER", "UNDO"}
parent: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
ifc_class: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
parent: bpy.props.IntProperty()
ifc_class: bpy.props.EnumProperty(
items=tuple((i, i, "") for i in get_args(ADDRESS_TYPE)),
)
@@ -198,7 +198,7 @@ class AddAddressAttribute(bpy.types.Operator):
bl_idname = "bim.add_address_attribute"
bl_label = "Add Address Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name: bpy.props.EnumProperty(
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
)
@@ -214,10 +214,10 @@ class RemoveAddressAttribute(bpy.types.Operator):
bl_idname = "bim.remove_address_attribute"
bl_label = "Remove Address Attribute"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
name: bpy.props.EnumProperty(
items=tuple((i, i, "") for i in get_args(tool.Owner.AddressAttributeType)),
)
id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
id: bpy.props.IntProperty()
if TYPE_CHECKING:
name: tool.Owner.AddressAttributeType # pyright: ignore[reportIncompatibleVariableOverride]
@@ -232,7 +232,7 @@ class EnableEditingAddress(bpy.types.Operator):
bl_idname = "bim.enable_editing_address"
bl_label = "Enable Editing Address"
bl_options = {"REGISTER", "UNDO"}
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
address: bpy.props.IntProperty()
if TYPE_CHECKING:
address: int
@@ -265,7 +265,7 @@ class RemoveAddress(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_address"
bl_label = "Remove Address"
bl_options = {"REGISTER", "UNDO"}
address: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
address: bpy.props.IntProperty()
if TYPE_CHECKING:
address: int
@@ -278,7 +278,7 @@ class EnableEditingOrganisation(bpy.types.Operator):
bl_idname = "bim.enable_editing_organisation"
bl_label = "Enable Editing Organisation"
bl_options = {"REGISTER", "UNDO"}
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
organisation: bpy.props.IntProperty()
if TYPE_CHECKING:
organisation: int
@@ -320,7 +320,7 @@ class RemoveOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_organisation"
bl_label = "Remove Organisation"
bl_options = {"REGISTER", "UNDO"}
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
organisation: bpy.props.IntProperty()
if TYPE_CHECKING:
organisation: int
@@ -333,8 +333,8 @@ class AddPersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_person_and_organisation"
bl_label = "Add Person And Organisation"
bl_options = {"REGISTER", "UNDO"}
person: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
person: bpy.props.IntProperty()
organisation: bpy.props.IntProperty()
if TYPE_CHECKING:
person: int
@@ -350,7 +350,7 @@ class RemovePersonAndOrganisation(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_person_and_organisation"
bl_label = "Remove Person And Organisation"
bl_options = {"REGISTER", "UNDO"}
person_and_organisation: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
person_and_organisation: bpy.props.IntProperty()
if TYPE_CHECKING:
person_and_organisation: int
@@ -365,7 +365,7 @@ class SetUser(bpy.types.Operator):
bl_idname = "bim.set_user"
bl_label = "Set User"
bl_options = {"REGISTER", "UNDO"}
user: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
user: bpy.props.IntProperty()
if TYPE_CHECKING:
user: int
@@ -401,7 +401,7 @@ class EnableEditingActor(bpy.types.Operator):
bl_idname = "bim.enable_editing_actor"
bl_label = "Enable Editing Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
actor: bpy.props.IntProperty()
if TYPE_CHECKING:
actor: int
@@ -434,7 +434,7 @@ class RemoveActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_actor"
bl_label = "Remove Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
actor: bpy.props.IntProperty()
if TYPE_CHECKING:
actor: int
@@ -447,7 +447,7 @@ class AssignActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.assign_actor"
bl_label = "Assign Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
actor: bpy.props.IntProperty()
if TYPE_CHECKING:
actor: int
@@ -462,7 +462,7 @@ class UnassignActor(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.unassign_actor"
bl_label = "Unassign Actor"
bl_options = {"REGISTER", "UNDO"}
actor: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
actor: bpy.props.IntProperty()
if TYPE_CHECKING:
actor: int
@@ -481,7 +481,7 @@ class RemoveApplication(bpy.types.Operator, tool.Ifc.Operator):
"Remove provided IfcApplication."
"\n\nFor safety will only work on applications without inverses (they are typically marked as '(unused)'."
)
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
application_id: bpy.props.IntProperty()
if TYPE_CHECKING:
application_id: int
@@ -525,7 +525,7 @@ class EnableEditingApplication(bpy.types.Operator):
bl_idname = "bim.enable_editing_application"
bl_label = "Enable Editing Application"
bl_options = {"REGISTER", "UNDO"}
application_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
application_id: bpy.props.IntProperty()
if TYPE_CHECKING:
application_id: int
@@ -86,9 +86,7 @@ class NewProject(bpy.types.Operator):
bl_label = "New Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Start a new IFC project in a fresh session"
preset: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
items=[(i, i, "") for i in get_args(PresetType)]
)
preset: bpy.props.EnumProperty(items=[(i, i, "") for i in get_args(PresetType)])
if TYPE_CHECKING:
preset: PresetType
@@ -178,13 +176,9 @@ class SelectLibraryFile(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_description = (
"Select an IFC file that can be used as a library.\n\nALT+click to reload the current loaded library file."
)
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
append_all: bpy.props.BoolProperty(default=False) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml", options={"HIDDEN"})
append_all: bpy.props.BoolProperty(default=False)
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
if TYPE_CHECKING:
filter_glob: str
@@ -568,7 +562,7 @@ class AppendLibraryElementByQuery(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.append_library_element_by_query"
bl_label = "Append Library Element By Query"
query: bpy.props.StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty(name="Query")
if TYPE_CHECKING:
query: str
@@ -600,11 +594,9 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
"Append element to the current project.\n\n"
"ALT+CLICK to skip reusing materials, profiles, styles based on their name (may result in duplicates)"
)
definition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
prop_index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
assume_unique_by_name: bpy.props.BoolProperty(
name="Assume Unique By Name", default=True, options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
definition: bpy.props.IntProperty()
prop_index: bpy.props.IntProperty()
assume_unique_by_name: bpy.props.BoolProperty(name="Assume Unique By Name", default=True, options={"SKIP_SAVE"})
if TYPE_CHECKING:
definition: int
@@ -959,28 +951,24 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
bl_label = "Load Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load an existing IFC project"
filepath: bpy.props.StringProperty(
subtype="FILE_PATH", options={"SKIP_SAVE"}
) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(
default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
is_advanced: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
filepath: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE"})
filter_glob: bpy.props.StringProperty(default="*.ifc;*.ifczip;*.ifcxml;*.ifcsqlite", options={"HIDDEN"})
is_advanced: bpy.props.BoolProperty(
name="Enable Advanced Mode",
description="Load IFC file with advanced settings. Checking this option will skip loading IFC file and will open advanced load settings",
default=False,
)
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path",
description="Store the IFC project path relative to the .blend file. Requires .blend file to be saved",
default=False,
)
should_start_fresh_session: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
should_start_fresh_session: bpy.props.BoolProperty(
name="Should Start Fresh Session",
description="Clear current Blender session before loading IFC. Not supported with 'Use Relative Path' option",
default=True,
)
import_without_ifc_data: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
import_without_ifc_data: bpy.props.BoolProperty(
name="Import Without IFC Data",
description=(
"Import IFC objects as Blender objects without any IFC metadata and authoring capabilities."
@@ -988,9 +976,7 @@ class LoadProject(bpy.types.Operator, IFCFileSelector, ImportHelper):
),
default=False,
)
use_detailed_tooltip: bpy.props.BoolProperty(
default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
use_detailed_tooltip: bpy.props.BoolProperty(default=False, options={"HIDDEN"})
filename_ext = ".ifc"
if TYPE_CHECKING:
@@ -1300,7 +1286,7 @@ class ToggleFilterCategories(bpy.types.Operator):
bl_idname = "bim.toggle_filter_categories"
bl_label = "Toggle Filter Categories"
bl_options = {"REGISTER", "UNDO"}
should_select: bpy.props.BoolProperty(name="Should Select", default=True) # pyright: ignore[reportRedeclaration]
should_select: bpy.props.BoolProperty(name="Should Select", default=True)
if TYPE_CHECKING:
should_select: bool
@@ -1327,7 +1313,7 @@ class LinkIfc(bpy.types.Operator, ImportHelper, tool.Ifc.Operator):
default=False,
)
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
query: bpy.props.StringProperty( # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty(
name="Query",
description=(
"Custom selector query to use to load element from a linked model. E.g. 'IfcElement'.\n\n"
@@ -1404,7 +1390,7 @@ class UnlinkIfc(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Remove the selected file from the link list"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
if TYPE_CHECKING:
link_index: int
@@ -1428,7 +1414,7 @@ class UnloadLink(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Unload the selected linked file"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
if TYPE_CHECKING:
link_index: int
@@ -1454,9 +1440,9 @@ class LoadLink(bpy.types.Operator, tool.Ifc.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Load the selected file"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True) # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
use_cache: bpy.props.BoolProperty(name="Use Cache", default=True)
query: bpy.props.StringProperty()
if TYPE_CHECKING:
link_index: int
@@ -1631,7 +1617,7 @@ class ReloadLink(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Reload the selected file"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
if TYPE_CHECKING:
link_index: int
@@ -1647,7 +1633,7 @@ class ToggleLinkSelectability(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle selectability"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
if TYPE_CHECKING:
link_index: int
@@ -1679,8 +1665,8 @@ class ToggleLinkVisibility(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Toggle visibility between SOLID and WIREFRAME"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
mode: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
mode: bpy.props.EnumProperty(
name="Visibility Mode",
items=((i, i, "") for i in ("WIREFRAME", "VISIBLE")),
)
@@ -1821,7 +1807,7 @@ class SelectLinkHandle(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select link empty object handle"
link_index: bpy.props.IntProperty(name="Link Index") # pyright: ignore[reportRedeclaration]
link_index: bpy.props.IntProperty(name="Link Index")
if TYPE_CHECKING:
link_index: int
@@ -1843,7 +1829,7 @@ class SelectLinkedModelElement(bpy.types.Operator):
bl_options = {"REGISTER"}
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration]
guid: bpy.props.StringProperty(name="GlobalId")
if TYPE_CHECKING:
guid: str
@@ -1882,21 +1868,11 @@ class ExportIFC(bpy.types.Operator, ExportHelper):
bl_options = {"REGISTER", "UNDO"}
filename_ext = ".ifc"
supported_filexts = (".ifc", ".ifczip", ".ifcjson")
filter_glob: bpy.props.StringProperty(
default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
json_version: bpy.props.EnumProperty(
items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version"
) # pyright: ignore[reportRedeclaration]
json_compact: bpy.props.BoolProperty(
name="Export Compact IFCJSON", default=False
) # pyright: ignore[reportRedeclaration]
should_save_as: bpy.props.BoolProperty(
name="Should Save As", default=False, options={"HIDDEN"}
) # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path", default=False
) # pyright: ignore[reportRedeclaration]
filter_glob: bpy.props.StringProperty(default=";".join(f"*{ext}" for ext in supported_filexts), options={"HIDDEN"})
json_version: bpy.props.EnumProperty(items=[("4", "4", ""), ("5a", "5a", "")], name="IFC JSON Version")
json_compact: bpy.props.BoolProperty(name="Export Compact IFCJSON", default=False)
should_save_as: bpy.props.BoolProperty(name="Should Save As", default=False, options={"HIDDEN"})
use_relative_path: bpy.props.BoolProperty(name="Use Relative Path", default=False)
if TYPE_CHECKING:
filter_glob: str
@@ -2053,7 +2029,7 @@ class LoadLinkedProject(bpy.types.Operator, ImportHelper):
bl_description = "Operator is used to load a project .cache.blend to then link it to the IFC file."
bl_options = {"REGISTER", "UNDO"}
query: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
query: bpy.props.StringProperty()
"""See ``bim.link_ifc``."""
if TYPE_CHECKING:
@@ -2443,8 +2419,8 @@ class HideQueriedLinkedElement(bpy.types.Operator):
)
bl_options = {"REGISTER", "UNDO"}
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"})
hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"})
if TYPE_CHECKING:
unhide_all: bool
@@ -2918,12 +2894,8 @@ class IFCFileHandlerOperator(bpy.types.Operator):
bl_label = "Import .ifc file"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
directory: bpy.props.StringProperty(
subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
files: bpy.props.CollectionProperty(
type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"}
) # pyright: ignore[reportRedeclaration]
directory: bpy.props.StringProperty(subtype="FILE_PATH", options={"SKIP_SAVE", "HIDDEN"})
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={"SKIP_SAVE", "HIDDEN"})
if TYPE_CHECKING:
directory: str
@@ -2978,7 +2950,7 @@ class MeasureTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Tool"
bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
measure_type: bpy.props.StringProperty()
if TYPE_CHECKING:
measure_type: str
@@ -3077,7 +3049,7 @@ class MeasureFaceAreaTool(bpy.types.Operator, PolylineOperator):
bl_label = "Measure Face Area Tool"
bl_options = {"REGISTER", "UNDO"}
measure_type: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
measure_type: bpy.props.StringProperty()
if TYPE_CHECKING:
measure_type: str
@@ -3379,7 +3351,7 @@ class LoadBlendMetadataAndIFC(bpy.types.Operator):
bl_idname = "bim.load_blend_metadata_and_ifc"
bl_label = "Load Blend Metadata and IFC"
bl_options = {"REGISTER", "UNDO"}
filepath: bpy.props.StringProperty(name="IFC File Path", default="") # pyright: ignore[reportRedeclaration]
filepath: bpy.props.StringProperty(name="IFC File Path", default="")
if TYPE_CHECKING:
filepath: str
+1 -1
View File
@@ -345,7 +345,7 @@ class BIMProjectProperties(PropertyGroup):
),
default=False,
)
should_cache: BoolProperty( # pyright: ignore[reportRedeclaration]
should_cache: BoolProperty(
name="Cache",
description=(
"Cache loaded geometry to .h5 file in your cache directory (see in preferences) "
@@ -19,6 +19,7 @@
from __future__ import annotations
import os
import shutil
from typing import TYPE_CHECKING
import bpy
@@ -384,6 +385,18 @@ class BIM_PT_new_project_wizard(Panel):
row = self.layout.row()
row.operator("bim.create_project")
if shutil.which("git"):
git_props = context.scene.IfcGitProperties
box = self.layout.box()
row = box.row()
row.label(text="Clone a remote Git repository")
row = box.row()
row.prop(git_props, "remote_url")
row = box.row()
row.prop(git_props, "local_folder")
row = box.row()
row.operator("ifcgit.clone_repo", icon="IMPORT")
class BIM_PT_project_library(Panel):
bl_label = "Project Library"
@@ -240,7 +240,7 @@ class CopyPropertyToSelection(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Copy Property To Selection"
bl_options = {"REGISTER", "UNDO"}
name: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
name: bpy.props.StringProperty()
if TYPE_CHECKING:
name: str
@@ -280,10 +280,10 @@ class BIM_OT_add_property_to_edit(bpy.types.Operator):
bl_label = "Add Property to Edit"
bl_idname = "bim.add_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
option: bpy.props.EnumProperty(
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
index: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
index: bpy.props.IntProperty(default=-1)
if TYPE_CHECKING:
option: tool.Pset.BulkOperationType
@@ -307,9 +307,9 @@ class BIM_OT_remove_property_to_edit(bpy.types.Operator):
bl_label = "Remove Property from Editing"
bl_idname = "bim.remove_property_to_edit"
bl_options = {"REGISTER", "UNDO"}
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
index2: bpy.props.IntProperty(default=-1) # pyright: ignore[reportRedeclaration]
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
index: bpy.props.IntProperty()
index2: bpy.props.IntProperty(default=-1)
option: bpy.props.EnumProperty(
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
@@ -336,7 +336,7 @@ class BIM_OT_bulk_edit_clear_list(bpy.types.Operator):
bl_label = "Clear List of Properties"
bl_idname = "bim.pset_bulk_edit_clear_list"
bl_options = {"REGISTER", "UNDO"}
option: bpy.props.EnumProperty( # pyright: ignore[reportRedeclaration]
option: bpy.props.EnumProperty(
items=[(t, t, "") for t in tool.Pset.BULK_OPERATION_TYPES],
)
+3 -3
View File
@@ -368,9 +368,9 @@ class GlobalPsetProperties(PropertyGroup):
qto_filter: StringProperty(name="Qto Filter", options={"TEXTEDIT_UPDATE"})
# Bulk operations.
psets_to_delete: CollectionProperty(type=DeletePsetEntry) # pyright: ignore[reportRedeclaration]
psets_to_rename: CollectionProperty(type=RenamePropertyEntry) # pyright: ignore[reportRedeclaration]
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry) # pyright: ignore[reportRedeclaration]
psets_to_delete: CollectionProperty(type=DeletePsetEntry)
psets_to_rename: CollectionProperty(type=RenamePropertyEntry)
psets_to_add_edit: CollectionProperty(type=AddEditPropertyEntry)
if TYPE_CHECKING:
pset_filter: str
+2 -2
View File
@@ -95,7 +95,7 @@ class ColourByPropertyData:
element = tool.Ifc.get_entity(obj)
if not element:
return default
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
keys = [a.name() for a in element.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element)
for pset, properties in psets.items():
if pset.endswith("Common"):
@@ -126,7 +126,7 @@ class SelectSimilarData:
element = tool.Ifc.get_entity(obj)
if not element:
return []
keys = [a.name() for a in element.wrapped_data.declaration().as_entity().all_attributes()]
keys = [a.name() for a in element.declaration().as_entity().all_attributes()]
psets = ifcopenshell.util.element.get_psets(element, psets_only=True)
for pset, properties in psets.items():
if pset.endswith("Common"):
@@ -799,7 +799,7 @@ class SelectQueryElements(Operator):
bl_description = "Select elements matching an provided selector query"
bl_options = {"REGISTER", "UNDO"}
query: StringProperty(name="Query") # pyright: ignore[reportRedeclaration]
query: StringProperty(name="Query")
if TYPE_CHECKING:
query: str
@@ -829,12 +829,12 @@ class SaveSearch(Operator, tool.Ifc.Operator):
# Extra item so it will be easy to select current text.
return [text] + SaveSearch.name_search_items
name: StringProperty( # pyright: ignore[reportRedeclaration]
name: StringProperty(
name="Name",
search=get_name_search_items,
search_options={"SORT"},
)
module: StringProperty() # pyright: ignore[reportRedeclaration]
module: StringProperty()
def update_use_all_ifcgroups(self, context: object = None) -> None:
ifc_file = tool.Ifc.get()
@@ -845,7 +845,7 @@ class SaveSearch(Operator, tool.Ifc.Operator):
}
self.name_search_items[:] = natsorted(groups)
use_all_ifcgroups: BoolProperty( # pyright: ignore[reportRedeclaration]
use_all_ifcgroups: BoolProperty(
name="Use Any IfcGroup",
description=(
"By default we're targeting only IfcGroups with SEARCH ObjectType "
@@ -106,7 +106,7 @@ class ActivateStatusFilters(bpy.types.Operator):
bl_description = "Filter and display objects based on currently selected IFC statuses"
bl_options = {"REGISTER", "UNDO"}
only_if_enabled: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
only_if_enabled: bpy.props.BoolProperty(
name="Only If Filters are Enabled",
description="Activate status filters only in case if they were enabled from the UI before.",
default=False,
@@ -137,7 +137,7 @@ class SelectStatusFilter(bpy.types.Operator):
bl_description = "Select elements with currently selected status"
bl_options = {"REGISTER", "UNDO"}
status: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
status: bpy.props.StringProperty()
if TYPE_CHECKING:
status: tool.Sequence.ElementStatusUI
@@ -156,7 +156,7 @@ class AssignStatus(bpy.types.Operator, tool.Ifc.Operator):
bl_description = "Assign status to the selected elements.\n\nAlt+CLICK to unassign the status."
bl_options = {"REGISTER", "UNDO"}
should_override_previous_status: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
should_override_previous_status: bpy.props.BoolProperty(
name="Override Previous Status",
description=(
"Whether assigning new status should override previous one.\n\n"
@@ -165,8 +165,8 @@ class AssignStatus(bpy.types.Operator, tool.Ifc.Operator):
),
default=True,
)
status: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
should_unassign_status: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
status: bpy.props.StringProperty()
should_unassign_status: bpy.props.BoolProperty(
options={"SKIP_SAVE"},
)
@@ -415,7 +415,7 @@ class CopyWorkSchedule(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Copy Work Schedule"
bl_description = "Create a duplicate of the provided work schedule."
bl_options = {"REGISTER", "UNDO"}
work_schedule: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
work_schedule: bpy.props.IntProperty()
if TYPE_CHECKING:
work_schedule: int
@@ -412,7 +412,7 @@ WorkPlanEditingType = Literal["-", "ATTRIBUTES", "SCHEDULES", "WORK_SCHEDULE", "
class BIMWorkPlanProperties(PropertyGroup):
work_plan_attributes: CollectionProperty(name="Work Plan Attributes", type=Attribute)
editing_type: EnumProperty( # pyright: ignore[reportRedeclaration]
editing_type: EnumProperty(
items=[(i, i, "") for i in get_args(WorkPlanEditingType)],
)
work_plans: CollectionProperty(name="Work Plans", type=WorkPlan)
@@ -430,8 +430,8 @@ class BIMWorkPlanProperties(PropertyGroup):
class IFCStatus(PropertyGroup):
name: StringProperty() # pyright: ignore[reportRedeclaration]
is_visible: BoolProperty( # pyright: ignore[reportRedeclaration]
name: StringProperty()
is_visible: BoolProperty(
name="Is Visible", default=True, update=lambda x, y: (None, bpy.ops.bim.activate_status_filters())[0]
)
@@ -220,7 +220,7 @@ class CopyToContainer(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Copy to Container"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
container: bpy.props.IntProperty()
if TYPE_CHECKING:
container: int
@@ -167,7 +167,7 @@ class EnableEditingStructuralBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_structural_boundary_condition"
bl_label = "Enable Editing Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
boundary_condition: bpy.props.IntProperty()
if TYPE_CHECKING:
boundary_condition: int
@@ -186,7 +186,7 @@ class EditStructuralBoundaryCondition(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.edit_structural_boundary_condition"
bl_label = "Edit Structural Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
connection: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
connection: bpy.props.IntProperty()
if TYPE_CHECKING:
connection: int
@@ -917,7 +917,7 @@ class EnableEditingBoundaryCondition(bpy.types.Operator):
bl_idname = "bim.enable_editing_boundary_condition"
bl_label = "Enable Editing Boundary Condition"
bl_options = {"REGISTER", "UNDO"}
boundary_condition: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
boundary_condition: bpy.props.IntProperty()
if TYPE_CHECKING:
boundary_condition: int
@@ -83,7 +83,7 @@ class DecorationShader:
PARALLEL DISTRIBUTED FORCE,
DISTRIBUTED MOMENT,
"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty:ignore[too-many-positional-arguments]
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "forces")
vert_out.smooth("VEC3", "co")
@@ -203,7 +203,7 @@ class DecorationShader:
"""param: pattern: type of pattern
SINGLE FORCE,
SINGLE MOMENT"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -253,7 +253,7 @@ class DecorationShader:
def get_planar_shader(self) -> gpu.types.GPUShader:
"""shader for planar loads"""
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface") # ty: ignore[too-many-positional-arguments]
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth("VEC3", "co")
shader_info = gpu.types.GPUShaderCreateInfo()
@@ -54,7 +54,7 @@ class AddSystem(bpy.types.Operator, tool.Ifc.Operator):
bl_label = "Add System"
bl_options = {"REGISTER", "UNDO"}
parent_system_id: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
parent_system_id: bpy.props.IntProperty()
if TYPE_CHECKING:
parent_system_id: int
+3 -3
View File
@@ -26,16 +26,16 @@ from bpy.types import PropertyGroup
class WebProperties(PropertyGroup):
webserver_port: IntProperty( # pyright: ignore[reportRedeclaration]
webserver_port: IntProperty(
name="Webserver Port",
min=0,
max=65535,
)
is_running: BoolProperty( # pyright: ignore[reportRedeclaration]
is_running: BoolProperty(
name="Webserver Running Status",
default=False,
)
is_connected: BoolProperty( # pyright: ignore[reportRedeclaration]
is_connected: BoolProperty(
name="Connection Status",
default=False,
)
+6 -6
View File
@@ -159,9 +159,9 @@ class SelectURIAttribute(bpy.types.Operator, ImportHelper):
bl_label = "Select URI Attribute"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select a local file"
attribute_data_path: bpy.props.StringProperty(name="Data Path") # pyright: ignore[reportRedeclaration]
attribute_data_path: bpy.props.StringProperty(name="Data Path")
"""Full data path to `Attribute`/string property."""
use_relative_path: bpy.props.BoolProperty( # pyright: ignore[reportRedeclaration]
use_relative_path: bpy.props.BoolProperty(
name="Use Relative Path",
default=False,
)
@@ -601,7 +601,7 @@ class CreateMacBonsaiApp(bpy.types.Operator):
"ALT+click to uninstall Bonsai app if it was installed previously."
)
uninstall: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
uninstall: bpy.props.BoolProperty(options={"SKIP_SAVE"})
if TYPE_CHECKING:
uninstall: bool
@@ -1667,7 +1667,7 @@ class BIM_OT_attribute_add_subitem(bpy.types.Operator):
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
data_path: bpy.props.StringProperty()
"""Full data path."""
if TYPE_CHECKING:
@@ -1691,9 +1691,9 @@ class BIM_OT_attribute_remove_subitem(bpy.types.Operator):
bl_description = "Add subitem to the current attribute"
bl_options = {"REGISTER", "UNDO"}
data_path: bpy.props.StringProperty() # pyright: ignore[reportRedeclaration]
data_path: bpy.props.StringProperty()
"""Full data path."""
index: bpy.props.IntProperty() # pyright: ignore[reportRedeclaration]
index: bpy.props.IntProperty()
if TYPE_CHECKING:
data_path: str
+2 -2
View File
@@ -333,7 +333,7 @@ class Attribute(PropertyGroup):
filter_glob: StringProperty()
is_null: BoolProperty(name="Is Null", update=update_is_null)
is_selected: BoolProperty(name="Is Selected", default=False)
subitems_values: CollectionProperty(type=StrProperty) # pyright: ignore[reportRedeclaration]
subitems_values: CollectionProperty(type=StrProperty)
# Attribute parameters.
is_optional: BoolProperty(name="Is Optional")
@@ -342,7 +342,7 @@ class Attribute(PropertyGroup):
value_max: FloatProperty(description="This is used to validate int_value and float_value")
value_max_constraint: BoolProperty(default=False, description="True if the numerical value has an upper bound")
special_type: StringProperty(name="Special Value Type", default="")
use_explorer_ui: BoolProperty() # pyright: ignore[reportRedeclaration]
use_explorer_ui: BoolProperty()
metadata: StringProperty(name="Metadata", description="For storing some additional information about the attribute")
update: StringProperty(name="Update", description="Custom update function to be executed")
+1 -1
View File
@@ -665,7 +665,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
name="Disable Undo When Saving (Faster saves, no undo for you!)", default=False
)
should_stream: BoolProperty(name="Stream Data From IFC-SPF (Only for advanced users)", default=False)
should_always_cache: BoolProperty( # pyright: ignore[reportRedeclaration]
should_always_cache: BoolProperty(
name="Always Cache Geometry",
description="Whether to always cache geometry regardless of 'Cache' setting during Advanced Project Load.",
)
+95 -17
View File
@@ -56,42 +56,50 @@ def discard_uncommitted(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc]) -> None:
ifcgit.load_project(path_ifc)
def commit_changes(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], repo: git.Repo) -> None:
def commit_changes(
ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], commit_message: str, new_branch_name: str = ""
) -> None:
"""Commit and create new branches as required"""
path_ifc = ifc.get_path()
if repo.head.is_detached:
ifcgit.git_commit(path_ifc)
ifcgit.create_new_branch()
if ifcgit.is_head_detached():
ifcgit.git_commit(path_ifc, commit_message)
ifcgit.create_new_branch(new_branch_name)
else:
ifcgit.checkout_new_branch(path_ifc)
ifcgit.git_commit(path_ifc)
if new_branch_name:
ifcgit.checkout_new_branch(path_ifc, new_branch_name)
ifcgit.git_commit(path_ifc, commit_message)
def add_tag(ifcgit: type[tool.IfcGit], repo: git.Repo) -> None:
ifcgit.add_tag(repo)
def add_tag(ifcgit: type[tool.IfcGit], repo: git.Repo, hexsha: str, tag_name: str, tag_message: str = "") -> None:
ifcgit.add_tag(repo, hexsha, tag_name, tag_message)
def delete_tag(ifcgit: type[tool.IfcGit], repo: git.Repo, tag_name: git.TagReference) -> None:
ifcgit.delete_tag(repo, tag_name)
def add_remote(ifcgit: type[tool.IfcGit], repo: git.Repo) -> None:
ifcgit.add_remote(repo)
def add_remote(ifcgit: type[tool.IfcGit], repo: git.Repo, remote_name: str, remote_url: str) -> None:
ifcgit.add_remote(repo, remote_name, remote_url)
def delete_remote(ifcgit: type[tool.IfcGit], repo: git.Repo) -> None:
ifcgit.delete_remote(repo)
def delete_remote(ifcgit: type[tool.IfcGit], repo: git.Repo, remote_name: str) -> None:
ifcgit.delete_remote(repo, remote_name)
def rename_branch(ifcgit: type[tool.IfcGit], repo: git.Repo, new_name: str) -> None:
ifcgit.rename_branch(repo, new_name)
def push(ifcgit: type[tool.IfcGit], repo: git.Repo, remote_name: str, operator: bpy.types.Operator) -> None:
error_message = ifcgit.push(repo, remote_name, repo.active_branch.name)
error_message = ifcgit.push(repo, remote_name, ifcgit.get_active_branch_name())
if error_message:
operator.report({"ERROR"}, error_message)
def refresh_revision_list(ifcgit: type[tool.IfcGit], repo: git.Repo, ifc: type[tool.Ifc]) -> None:
if repo.heads:
def refresh_revision_list(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc]) -> None:
ifcgit.clear_merge_conflicts()
if ifcgit.repo_has_commits():
ifcgit.refresh_revision_list(ifc.get_path())
@@ -125,10 +133,76 @@ def switch_revision(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc]) -> None:
ifcgit.decolourise()
def merge_branch(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], operator: bpy.types.Operator) -> None:
def merge_branch(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], operator: bpy.types.Operator) -> bool | None:
path_ifc = ifc.get_path()
ifcgit.config_ifcmerge()
ifcgit.execute_merge(path_ifc, operator)
branch_name = ifcgit.get_selected_branch()
if branch_name is None:
return
mergetool = ifcgit.get_merge_tool(branch_name)
merge_result = ifcgit.git_merge(branch_name)
if merge_result == "error":
operator.report({"ERROR"}, "Unknown IFC Merge failure")
return False
elif merge_result == "conflict":
conflicts = ifcgit.git_mergetool(mergetool, path_ifc)
if conflicts is not None:
ifcgit.git_merge_abort()
if conflicts:
ifcgit.store_merge_conflicts(conflicts)
operator.report({"WARNING"}, "Merge failed — see the conflict report in the panel below")
else:
operator.report({"ERROR"}, "Merge tool failed — check that ifcmerge is installed correctly")
return False
ifcgit.commit_merge(path_ifc)
ifcgit.clear_merge_conflicts()
ifcgit.set_display_branch()
ifcgit.git_checkout(path_ifc)
ifcgit.load_project(path_ifc)
ifcgit.refresh_revision_list(path_ifc)
ifcgit.decolourise()
def dry_run_merge(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], operator: bpy.types.Operator) -> None:
path_ifc = ifc.get_path()
ifcgit.config_ifcmerge()
branch_name = ifcgit.get_selected_branch()
if branch_name is None:
return
mergetool = ifcgit.get_merge_tool(branch_name)
merge_result = ifcgit.git_merge_no_commit(branch_name)
if merge_result == "error":
try:
ifcgit.git_merge_abort()
except Exception:
pass
operator.report({"ERROR"}, "Unknown IFC Merge failure")
return
if merge_result == "conflict":
conflicts = ifcgit.git_mergetool(mergetool, path_ifc)
ifcgit.git_merge_abort()
if conflicts is not None:
ifcgit.store_merge_conflicts(conflicts)
operator.report({"WARNING"}, "Merge preview: conflicts found — see the panel below")
else:
ifcgit.clear_merge_conflicts()
operator.report({"INFO"}, "Merge preview: no conflicts")
else:
# Clean merge or already up to date — abort the pending merge state if any
try:
ifcgit.git_merge_abort()
except Exception:
pass
ifcgit.clear_merge_conflicts()
operator.report({"INFO"}, "Merge preview: no conflicts")
def entity_log(ifcgit: type[tool.IfcGit], ifc: type[tool.Ifc], step_id: int, operator: bpy.types.Operator) -> None:
@@ -145,5 +219,9 @@ def install_git(ifcgit: type[tool.IfcGit], operator: bpy.types.Operator) -> None
print("install_git() not implemented")
def fetch(ifcgit: type[tool.IfcGit], remote_name: str) -> None:
ifcgit.fetch(remote_name)
def run_git_diff(ifcgit: type[tool.IfcGit], operator: bpy.types.Operator, save_to_temp: bool) -> None:
ifcgit.run_git_diff(operator, save_to_temp)
+54
View File
@@ -535,6 +535,60 @@ class Ifc:
def get_all_element_occurrences(cls, element): pass
@interface
class IfcGit:
def add_file_to_repo(cls, repo, path_file): pass
def add_remote(cls, repo, remote_name, remote_url): pass
def add_tag(cls, repo, hexsha, tag_name, tag_message): pass
def branches_by_hexsha(cls, repo): pass
def checkout_new_branch(cls, path_file, branch_name): pass
def clear_commits_list(cls): pass
def clone_repo(cls, remote_url, local_folder): pass
def colourise(cls, step_ids): pass
def config_ifcmerge(cls): pass
def create_new_branch(cls, branch_name): pass
def decolourise(cls): pass
def delete_remote(cls, repo, remote_name): pass
def delete_tag(cls, repo, tag_name): pass
def dos2unix(cls, path_file): pass
def commit_merge(cls, path_ifc): pass
def entity_log(cls, path_ifc, step_id): pass
def fetch(cls, remote_name): pass
def get_commits_list(cls, path_ifc, lookup): pass
def get_merge_tool(cls, branch_name): pass
def get_selected_branch(cls): pass
def git_merge(cls, branch_name): pass
def git_merge_abort(cls): pass
def git_merge_no_commit(cls, branch_name): pass
def git_mergetool(cls, mergetool, path_ifc): pass
def store_merge_conflicts(cls, conflicts): pass
def clear_merge_conflicts(cls): pass
def get_merge_conflicts(cls): pass
def set_display_branch(cls): pass
def get_active_branch_name(cls): pass
def get_ifcgit_props(cls): pass
def get_modified_step_ids(cls, step_ids): pass
def get_path_dir(cls, path_ifc): pass
def get_revisions_step_ids(cls): pass
def is_head_detached(cls): pass
def repo_has_commits(cls): pass
def git_checkout(cls, path_file): pass
def git_commit(cls, path_file, commit_message): pass
def ifc_diff_ids(cls, repo, hash_a, hash_b, path_ifc): pass
def init_repo(cls, path_dir): pass
def install_git_windows(cls, operator): pass
def is_valid_ref_format(cls, string): pass
def load_anyifc(cls, repo): pass
def load_project(cls, path_ifc): pass
def push(cls, repo, remote_name, branch_name): pass
def refresh_revision_list(cls, path_ifc): pass
def repo_from_path(cls, path): pass
def run_git_diff(cls, operator, save_to_temp): pass
def switch_to_revision_item(cls): pass
def tags_by_hexsha(cls, repo): pass
def update_step_ids(cls, step_ids, modified_step_ids): pass
@interface
class Layer:
pass
+1 -1
View File
@@ -1809,7 +1809,7 @@ class Geometry(bonsai.core.tool.Geometry):
item = tool.Ifc.get().by_id(props.ifc_definition_id)
allowed_attributes = [
a.name()
for a in item.wrapped_data.declaration().as_entity().all_attributes()
for a in item.declaration().as_entity().all_attributes()
if a.type_of_attribute()._is("IfcLengthMeasure")
]
+231 -99
View File
@@ -18,13 +18,14 @@
from __future__ import annotations
import json
import logging
import os
import re
import subprocess
import tempfile
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, Union
from typing import TYPE_CHECKING, Any, Union
import bpy
@@ -128,39 +129,27 @@ class IfcGit:
cls.dos2unix(path_file)
repo.index.add(os.path.normpath(path_file))
repo.index.commit(message="Added " + os.path.relpath(path_file, repo.working_dir))
bpy.ops.ifcgit.refresh()
@classmethod
def git_checkout(cls, path_file: str) -> None:
IfcGitRepo.repo.git.checkout(path_file)
@classmethod
def checkout_new_branch(cls, path_file: str) -> None:
def checkout_new_branch(cls, path_file: str, branch_name: str) -> None:
"""Create a branch and move uncommitted changes to this branch"""
props = cls.get_ifcgit_props()
if props.new_branch_name:
IfcGitRepo.repo.git.checkout(b=props.new_branch_name)
props.display_branch = props.new_branch_name
props.new_branch_name = ""
bpy.ops.ifcgit.refresh()
IfcGitRepo.repo.git.checkout(b=branch_name)
@classmethod
def git_commit(cls, path_file: str) -> None:
props = cls.get_ifcgit_props()
def git_commit(cls, path_file: str, commit_message: str) -> None:
repo = IfcGitRepo.repo
if os.name == "nt":
cls.dos2unix(path_file)
repo.index.add(os.path.normpath(path_file))
repo.index.commit(message=props.commit_message)
props.commit_message = ""
repo.index.commit(message=commit_message)
@classmethod
def add_tag(cls, repo: git.Repo) -> None:
props = cls.get_ifcgit_props()
item = props.ifcgit_commits[props.commit_index]
repo.create_tag(props.new_tag_name, ref=item.hexsha, message=props.new_tag_message)
props.new_tag_name = ""
props.new_tag_message = ""
def add_tag(cls, repo: git.Repo, hexsha: str, tag_name: str, tag_message: str = "") -> None:
repo.create_tag(tag_name, ref=hexsha, message=tag_message)
@classmethod
def delete_tag(cls, repo: git.Repo, tag_name: git.TagReference) -> None:
@@ -168,20 +157,17 @@ class IfcGit:
repo.delete_tag(tag_name)
@classmethod
def add_remote(cls, repo: git.Repo) -> None:
props = cls.get_ifcgit_props()
repo.create_remote(name=props.remote_name, url=props.remote_url)
props.remote_name = ""
props.remote_url = ""
def rename_branch(cls, repo: git.Repo, new_name: str) -> None:
repo.active_branch.rename(new_name)
@classmethod
def delete_remote(cls, repo: git.Repo) -> None:
props = cls.get_ifcgit_props()
remote_name = props.select_remote
def add_remote(cls, repo: git.Repo, remote_name: str, remote_url: str) -> None:
repo.create_remote(name=remote_name, url=remote_url)
@classmethod
def delete_remote(cls, repo: git.Repo, remote_name: str) -> None:
if remote_name in repo.remotes:
repo.delete_remote(remote_name)
if repo.remotes:
props.select_remote = repo.remotes[0].name
@classmethod
def push(cls, repo: git.Repo, remote_name: str, branch_name: str) -> Union[str, None]:
@@ -193,16 +179,25 @@ class IfcGit:
return exc.stderr
@classmethod
def create_new_branch(cls) -> None:
"""Convert a detached HEAD into a branch"""
props = cls.get_ifcgit_props()
repo = IfcGitRepo.repo
new_branch = repo.create_head(props.new_branch_name)
new_branch.checkout()
props.display_branch = props.new_branch_name
props.new_branch_name = ""
def is_head_detached(cls) -> bool:
return bool(IfcGitRepo.repo.head.is_detached)
bpy.ops.ifcgit.refresh()
@classmethod
def repo_has_commits(cls) -> bool:
if IfcGitRepo.repo:
return bool(IfcGitRepo.repo.heads)
return False
@classmethod
def get_active_branch_name(cls) -> str:
return IfcGitRepo.repo.active_branch.name
@classmethod
def create_new_branch(cls, branch_name: str) -> None:
"""Convert a detached HEAD into a branch"""
repo = IfcGitRepo.repo
new_branch = repo.create_head(branch_name)
new_branch.checkout()
@classmethod
def clear_commits_list(cls) -> None:
@@ -222,7 +217,7 @@ class IfcGit:
rev=[props.display_branch],
)
)
commits_relevant = list(
commits_relevant = set(
git.objects.commit.Commit.iter_items(
repo=repo,
rev=[props.display_branch],
@@ -230,11 +225,17 @@ class IfcGit:
)
)
def is_relevant(commit):
if commit in commits_relevant:
return True
# Merge commits are relevant too
return len(commit.parents) > 1 and any(p in commits_relevant for p in commit.parents)
for commit in commits:
if props.ifcgit_filter == "tagged" and commit.hexsha not in lookup:
continue
elif props.ifcgit_filter == "relevant" and commit not in commits_relevant:
elif props.ifcgit_filter == "relevant" and not is_relevant(commit):
continue
props.ifcgit_commits.add()
@@ -243,7 +244,8 @@ class IfcGit:
list_item.message = commit.message
list_item.author_name = commit.author.name
list_item.author_email = commit.author.email
if commit in commits_relevant:
list_item.committed_date = int(commit.committed_date)
if is_relevant(commit):
list_item.relevant = True
if commit.hexsha in lookup:
for tag in lookup[commit.hexsha]:
@@ -284,16 +286,25 @@ class IfcGit:
if re.match("^Ifc", obj.name):
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.orphans_purge(do_recursive=True) # ty:ignore[unknown-argument]
bpy.data.orphans_purge(do_recursive=True)
import bonsai.bim.handler
from bonsai.bim.module.model.data import AuthoringData
from bonsai.bim.module.root.data import IfcClassData
AuthoringData.type_thumbnails = {}
IfcClassData.is_loaded = False
settings = import_ifc.IfcImportSettings.factory(bpy.context, path_ifc, logging.getLogger("ImportIFC"))
settings.should_setup_viewport_camera = False
ifc_importer = import_ifc.IfcImporter(settings)
ifc_importer.execute()
tool.Project.load_project_pset_templates()
tool.Project.load_default_thumbnails()
tool.Project.set_default_context()
tool.Project.set_default_modeling_dimensions()
tool.Root.reload_grid_decorator()
bonsai.bim.handler.refresh_ui_data()
bpy.ops.object.select_all(action="DESELECT")
@classmethod
@@ -393,20 +404,43 @@ class IfcGit:
model = tool.Ifc.get()
modified_step_ids = {"modified": set()}
for step_id in step_ids["modified"] | step_ids["added"]:
try:
entity = model.by_id(step_id)
except:
continue
if entity.is_a("IfcProductDefinitionShape"):
def collect(entity, depth=0):
if depth > 2:
return
if entity.is_a("IfcProduct"):
modified_step_ids["modified"].add(entity.id())
elif entity.is_a("IfcProductDefinitionShape"):
for product in entity.ShapeOfProduct:
modified_step_ids["modified"].add(product.id())
elif entity.is_a("IfcObjectPlacement"):
for product in entity.PlacesObject:
modified_step_ids["modified"].add(product.id())
elif entity.is_a("IfcTypeProduct") and entity.Types:
for related_object in entity.Types[0].RelatedObjects:
modified_step_ids["modified"].add(related_object.id())
elif entity.is_a("IfcTypeProduct"):
for rel in entity.Types:
for obj in rel.RelatedObjects:
modified_step_ids["modified"].add(obj.id())
elif entity.is_a("IfcShapeRepresentation"):
for prod_rep in entity.OfProductRepresentation:
for product in prod_rep.ShapeOfProduct:
modified_step_ids["modified"].add(product.id())
elif entity.is_a("IfcRepresentationItem"):
for referencing in model.get_inverse(entity):
if referencing.is_a("IfcShapeRepresentation"):
collect(referencing, depth + 1)
elif entity.is_a("IfcPropertySet"):
for rel in entity.DefinesOccurrence:
for obj in rel.RelatedObjects:
modified_step_ids["modified"].add(obj.id())
elif entity.is_a("IfcProperty"):
for pset in entity.PartOfPset:
collect(pset, depth + 1)
for step_id in step_ids["modified"] | step_ids["added"]:
try:
entity = model.by_id(step_id)
except:
continue
collect(entity)
return modified_step_ids
@@ -458,38 +492,56 @@ class IfcGit:
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
if isinstance(branch, git.RemoteReference):
# Checking out a remote branch tip goes to detached HEAD.
# Pre-fill the new branch name field with the local equivalent
# so the user isn't blocked from committing without a hint.
local_name = branch.remote_head
props.new_branch_name = cls._unique_branch_name(repo, local_name)
branch.checkout()
return
# NOTE this is calling the git binary in a subprocess
repo.git.checkout(item.hexsha)
@classmethod
def _unique_branch_name(cls, repo: git.Repo, name: str) -> str:
"""Return name if unused, otherwise name-2, name-3, etc."""
existing = {h.name for h in repo.heads}
if name not in existing:
return name
i = 2
while f"{name}-{i}" in existing:
i += 1
return f"{name}-{i}"
@classmethod
def delete_collection(cls, blender_collection: bpy.types.Collection) -> None:
for obj in blender_collection.objects:
bpy.data.objects.remove(obj, do_unlink=True)
bpy.data.collections.remove(blender_collection)
@classmethod
def is_valid_branch_name(cls, new_branch_name: str):
"""Check if a branch name is valid and doesn't conflict with existing branches"""
if not cls.is_valid_ref_format(new_branch_name):
return False
if new_branch_name in [branch.name for branch in IfcGitRepo.repo.branches]:
return False
return True
@classmethod
def config_ifcmerge(cls) -> None:
config_reader = IfcGitRepo.repo.config_reader()
section = 'mergetool "ifcmerge"'
new_cmd = "ifcmerge $BASE $LOCAL $REMOTE $MERGED > $MERGED.ifcmerge"
if not config_reader.has_section(section):
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED")
config_writer.set_value(section, "cmd", new_cmd)
config_writer.set_value(section, "trustExitCode", True)
elif config_reader.get_value(section, "cmd") != new_cmd:
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", new_cmd)
config_writer.set_value(section, "trustExitCode", True)
section = 'mergetool "ifcmerge-forward"'
new_cmd = "ifcmerge --prioritise-local $BASE $LOCAL $REMOTE $MERGED > $MERGED.ifcmerge"
if not config_reader.has_section(section):
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", "ifcmerge $BASE $REMOTE $LOCAL $MERGED")
config_writer.set_value(section, "cmd", new_cmd)
config_writer.set_value(section, "trustExitCode", True)
elif config_reader.get_value(section, "cmd") != new_cmd:
with IfcGitRepo.repo.config_writer() as config_writer:
config_writer.set_value(section, "cmd", new_cmd)
config_writer.set_value(section, "trustExitCode", True)
@classmethod
@@ -519,49 +571,117 @@ class IfcGit:
output.write(line + b"\n")
@classmethod
def execute_merge(cls, path_ifc: str, operator: bpy.types.Operator) -> Union[None, Literal[False]]:
def get_selected_branch(cls) -> Union[str, None]:
"""Return the name of the branch at the selected commit matching display_branch, or None."""
props = cls.get_ifcgit_props()
repo = IfcGitRepo.repo
item = props.ifcgit_commits[props.commit_index]
lookup = cls.branches_by_hexsha(repo)
if item.hexsha in lookup:
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
# this is a branch!
if re.match("^(origin/)?(HEAD|main|master)$", branch.name):
# preserve remote IDs in origin/main or main
mergetool = "ifcmerge"
else:
# rewrite remote IDs
mergetool = "ifcmerge-forward"
try:
# NOTE this is calling the git binary in a subprocess
repo.git.merge(branch)
except git.exc.GitCommandError:
# merge is expected to fail, run ifcmerge
try:
repo.git.mergetool(tool=mergetool)
except git.exc.GitCommandError as exc:
message = re.sub("( stderr: '|')", "", exc.stderr)
# ifcmerge failed, rollback
repo.git.merge(abort=True)
if item.hexsha not in lookup:
return None
for branch in lookup[item.hexsha]:
if branch.name == props.display_branch:
return branch.name
return None
operator.report({"ERROR"}, "IFC Merge failed:" + message)
return False
else:
if os.name == "nt":
cls.dos2unix(path_ifc)
repo.index.add(os.path.normpath(path_ifc))
repo.git.commit("--no-edit")
except git.exc.GitError:
operator.report({"ERROR"}, "Unknown IFC Merge failure")
return False
@classmethod
def get_merge_tool(cls, branch_name: str) -> str:
if re.match("^(origin/)?(HEAD|main|master)$", branch_name):
return "ifcmerge"
return "ifcmerge-forward"
props.display_branch = repo.active_branch.name
@classmethod
def git_merge(cls, branch_name: str) -> Union[str, None]:
"""Attempt a git merge. Returns None on clean merge, 'conflict' on expected
GitCommandError, or 'error' on an unknown GitError."""
repo = IfcGitRepo.repo
branch = repo.refs[branch_name]
try:
repo.git.merge(branch)
return None
except git.exc.GitCommandError:
return "conflict"
except git.exc.GitError:
return "error"
cls.load_project(path_ifc)
cls.refresh_revision_list(path_ifc)
cls.decolourise()
@classmethod
def git_merge_no_commit(cls, branch_name: str) -> Union[str, None]:
"""Attempt a git merge without committing (always leaves a merge state to abort).
Returns None on clean merge, 'conflict' on conflict, or 'error' on unknown failure."""
repo = IfcGitRepo.repo
branch = repo.refs[branch_name]
try:
repo.git.merge(branch, no_commit=True, no_ff=True)
return None
except git.exc.GitCommandError:
return "conflict"
except git.exc.GitError:
return "error"
@classmethod
def git_mergetool(cls, mergetool: str, path_ifc: str) -> Union[list, None]:
"""Run ifcmerge tool. Returns None on success, list of conflict dicts on failure."""
repo = IfcGitRepo.repo
report_path = path_ifc + ".ifcmerge"
try:
repo.git.mergetool(tool=mergetool)
except git.exc.GitCommandError as e:
print(f"ifcgit: mergetool failed: {e}")
conflicts = None
if os.path.exists(report_path):
try:
with open(report_path) as f:
content = f.read().strip()
if content:
data = json.loads(content)
conflicts = data.get("conflicts", [])
except (json.JSONDecodeError, OSError):
pass
try:
os.remove(report_path)
except OSError:
pass
if conflicts is None and repo.index.unmerged_blobs():
conflicts = []
return conflicts
@classmethod
def store_merge_conflicts(cls, conflicts: list) -> None:
cls.get_ifcgit_props().merge_conflicts = json.dumps(conflicts)
@classmethod
def clear_merge_conflicts(cls) -> None:
cls.get_ifcgit_props().merge_conflicts = ""
@classmethod
def get_merge_conflicts(cls) -> Union[list, None]:
raw = cls.get_ifcgit_props().merge_conflicts
if not raw:
return None
try:
return json.loads(raw)
except json.JSONDecodeError:
return None
@classmethod
def git_merge_abort(cls) -> None:
IfcGitRepo.repo.git.merge(abort=True)
@classmethod
def commit_merge(cls, path_ifc: str) -> None:
repo = IfcGitRepo.repo
if os.name == "nt":
cls.dos2unix(path_ifc)
repo.index.add(os.path.normpath(path_ifc))
repo.git.commit("--no-edit")
@classmethod
def set_display_branch(cls) -> None:
props = cls.get_ifcgit_props()
props.display_branch = IfcGitRepo.repo.active_branch.name
@classmethod
def entity_log(cls, path_ifc: str, step_id: int) -> str:
@@ -589,6 +709,18 @@ class IfcGit:
except FileNotFoundError:
operator.report({"ERROR"}, "Winget is not available. Make sure Windows Package Manager is installed.")
@classmethod
def select_first_remote(cls) -> None:
props = cls.get_ifcgit_props()
repo = IfcGitRepo.repo
if repo and repo.remotes:
props.select_remote = repo.remotes[0].name
@classmethod
def fetch(cls, remote_name: str) -> None:
repo = IfcGitRepo.repo
repo.remotes[remote_name].fetch()
@classmethod
def run_git_diff(cls, operator: bpy.types.Operator, save_to_temp: bool) -> None:
path = tool.Ifc.get_path()
+22 -10
View File
@@ -25,11 +25,12 @@ import bmesh
import bpy
import mathutils
import numpy as np
from bpy_extras import view3d_utils
from mathutils import Vector
import bonsai.core.tool
import bonsai.tool as tool
from bpy_extras import view3d_utils
class Raycast(bonsai.core.tool.Raycast):
offset = 10
@@ -200,7 +201,7 @@ class Raycast(bonsai.core.tool.Raycast):
if inter_world is None:
print("No intersection with viewport near plane found for the segment.")
return
return None, None
init_2d = view3d_utils.location_3d_to_region_2d(region, rv3d, inter_world)
@@ -215,7 +216,7 @@ class Raycast(bonsai.core.tool.Raycast):
if found_world is None:
if init_2d is None:
print("Initial projection invalid and iterative search failed.")
return
return None, None
# fallback: clamp projected point to border via manual mapping
final_2d = clamp_to_region_border(init_2d, region)
final_world = None
@@ -432,11 +433,8 @@ class Raycast(bonsai.core.tool.Raycast):
seg_len_sq = sx * sx + sy * sy
if seg_len_sq == 0.0:
# degenerate segment: return distance to p0
dx = px - p0x
dy = py - p0y
dist = math.hypot(dx, dy)
return dist, (p0x, p0y), 0.0
# degenerate segment: skip it
continue
# project (p - p0) onto seg: t = dot(p-p0, seg) / |seg|^2
apx = px - p0x
@@ -726,7 +724,8 @@ class Raycast(bonsai.core.tool.Raycast):
if tool.Raycast.intersect_mouse_2d_bounding_box(mouse_pos, bbox_2d):
if tool.Raycast.object_is_visible_in_clipping_plane(obj):
snap_obj = cls.create_snap_obj(obj)
objs_to_raycast.append(snap_obj)
if snap_obj is not None:
objs_to_raycast.append(snap_obj)
return objs_to_raycast
@@ -887,8 +886,21 @@ class Raycast(bonsai.core.tool.Raycast):
@classmethod
def create_snap_obj(cls, obj):
for snap_obj in cls.snap_objs:
if obj.data is None or not isinstance(obj.data, bpy.types.Mesh):
return None
for i, snap_obj in enumerate(cls.snap_objs):
if obj.name == snap_obj.obj.name:
# Handle objects modified while a modal operator is active.
# Example: adding a door or window alters the wall geometry.
if len(obj.data.vertices) != len(snap_obj.verts_3d):
cls.snap_objs.pop(i)
snap_obj = SnapObj(obj)
cls.snap_objs.append(snap_obj)
for v1, v2 in zip(obj.data.vertices, snap_obj.verts_3d):
if (obj.matrix_world @ v1.co) != v2:
cls.snap_objs.pop(i)
snap_obj = SnapObj(obj)
cls.snap_objs.append(snap_obj)
return snap_obj
snap_obj = SnapObj(obj)
cls.snap_objs.append(snap_obj)
@@ -7,7 +7,7 @@ Python code formatters
For Python code formatting, we use `Black code formatter <https://pypi.org/project/black/>`__,
black settings are stored in the repository's pyproject.toml.
We have GitHub workflow `ci-black-formatting` to maintain black formatting across the repository.
We have GitHub workflow `ci-lint` to maintain black formatting across the repository.
``black`` can be installed using ``pip install black`` and files can be formatted with the following example command:
@@ -13,7 +13,7 @@ When adding or removing a supported Python version, update the following:
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
* - ``.github/workflows/ci-lint.yaml``
- ``MIN_IOS_PY_VERSION``
* - ``.github/workflows/ci-ifcopenshell-python-pypi.yml``
- ``pyver`` matrix
@@ -44,6 +44,8 @@ When a new Blender version is released and supported:
* - File
- What to update
* - ``.github/workflows/ci-bonsai.yml``
- ``pyver`` matrix
* - ``.github/workflows/ci-bonsai-daily.yml``
- Blender download URL
@@ -57,9 +59,47 @@ When Blender ships with a new Python version:
* - File
- What to update
* - ``.github/workflows/ci-black-formatting.yaml``
* - ``.github/workflows/ci-lint.yaml``
- ``MIN_BLENDER_PY_VERSION``
* - ``src/bonsai/Makefile``
- ``SUPPORTED_PYVERSIONS``
* - ``src/bonsai/scripts/dev_environment.py``
- ``PYTHON_VERSION`` mapping (Blender version, bundled Python version)
Release
-------
Notes:
- Typically all packages are released at once using the same version schema
- The ``README.md`` badges can serve as a visual reference for what versions have been released
Things to update:
- ``.github/workflows/ci-bcf-pypi.yml`` - release `bcf-client <https://pypi.org/project/bcf-client/>`_ to PyPI
- ``.github/workflows/ci-bonsai.yml`` - release bonsai in GitHub releases
- ``.github/workflows/ci-bsdd-pypi.yaml`` - release `bsdd <https://pypi.org/project/bsdd/>`_ to PyPI
- ``.github/workflows/ci-ifc4d-pypi.yaml`` - release `ifc4d <https://pypi.org/project/ifc4d/>`_ to PyPI
- ``.github/workflows/ci-ifc5d-pypi.yaml`` - release `ifc5d <https://pypi.org/project/ifc5d/>`_ to PyPI
- ``.github/workflows/ci-ifcclash-pypi.yaml`` - release `ifcclash <https://pypi.org/project/ifcclash/>`_ to PyPI
- ``.github/workflows/ci-ifcconvert.yml`` - release ifcconvert binaries in GitHub releases
- ``.github/workflows/ci-ifccsv-pypi.yaml`` - release `ifccsv <https://pypi.org/project/ifccsv/>`_ to PyPI
- ``.github/workflows/ci-ifcdiff-pypi.yaml`` - release `ifcdiff <https://pypi.org/project/ifcdiff/>`_ to PyPI
- ``.github/workflows/ci-ifcedit-pypi.yaml`` - release `ifcedit <https://pypi.org/project/ifcedit/>`_ to PyPI
- ``.github/workflows/ci-ifcfm-pypi.yaml`` - release `ifcfm <https://pypi.org/project/ifcfm/>`_ to PyPI
- ``.github/workflows/ci-ifccityjson-pypi.yaml`` - release `ifccityjson <https://pypi.org/project/ifccityjson/>`_ to PyPI
- ``.github/workflows/ci-ifcmcp-pypi.yaml`` - release `ifcopenshell-mcp <https://pypi.org/project/ifcopenshell-mcp/>`_ to PyPI
- ``.github/workflows/ci-ifcopenshell-python.yml`` - release ifcopenshell-python binaries in GitHub releases
- ``.github/workflows/ci-ifcopenshell-python-pypi.yml`` - release `ifcopenshell <https://pypi.org/project/ifcopenshell/>`_ wheels to PyPI
- ``.github/workflows/ci-ifcpatch-pypi.yaml`` - release `ifcpatch <https://pypi.org/project/ifcpatch/>`_ to PyPI
- ``.github/workflows/ci-ifcquery-pypi.yaml`` - release `ifcquery <https://pypi.org/project/ifcquery/>`_ to PyPI
- ``.github/workflows/ci-ifcsverchok.yml`` - release ifcsverchok Blender add-on in GitHub releases
- ``.github/workflows/ci-ifctester-pypi.yml`` - release `ifctester <https://pypi.org/project/ifctester/>`_ to PyPI
- ``.github/workflows/ci-pyodide-wasm-release.yml`` - release pyodide wasm wheel to `wasm-wheels <https://github.com/IfcOpenShell/wasm-wheels>`_
- Release Bonsai Blender extension - zip files from ci-bonsai.yml releases should be uploaded manually to `Blender extensions platform <https://extensions.blender.org/add-ons/bonsai/>`_
- Publishing documentation and websites (see `website <https://github.com/IfcOpenShell/website>`_ repository):
- `ifcopenshell-docs.yml` - builds and publishes IfcOpenShell documentation to `docs.ifcopenshell.org <https://docs.ifcopenshell.org>`_ (`ifcopenshell_org_docs <https://github.com/IfcOpenShell/ifcopenshell_org_docs>`_ repo)
- `bonsai-docs.yml` - builds and publishes Bonsai documentation to `docs.bonsaibim.org <https://docs.bonsaibim.org>`_ (`bonsaibim_org_docs <https://github.com/IfcOpenShell/bonsaibim_org_docs>`_ repo)
- `main.yml` - publishes `bonsaibim.org <https://bonsaibim.org>`_ (`bonsaibim_org_static_html <https://github.com/IfcOpenShell/bonsaibim_org_static_html>`_ repo) and `ifcopenshell.org <https://ifcopenshell.org>`_ (`ifcopenshell_org_static_html <https://github.com/IfcOpenShell/ifcopenshell_org_static_html>`_ repo)
- ``VERSION`` to the release version - **UPDATE THIS LAST** as all workflows above typically depend on it to set the version correctly
+7
View File
@@ -102,6 +102,13 @@ def geometry():
prophet.verify()
@pytest.fixture
def ifcgit():
prophet = Prophecy(bonsai.core.tool.IfcGit)
yield prophet
prophet.verify()
@pytest.fixture
def georeference():
prophet = Prophecy(bonsai.core.tool.Georeference)
+329
View File
@@ -0,0 +1,329 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2025 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/>.
# This file was generated with the assistance of an AI coding tool.
import pytest
import bonsai.core.ifcgit as subject
from test.core.bootstrap import ifc, ifcgit
class MockOperator:
def __init__(self):
self.reports = []
def report(self, level, message):
self.reports.append((level, message))
class TestCreateRepo:
def test_run(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.get_path_dir("path/to/model.ifc").should_be_called().will_return("path/to")
ifcgit.init_repo("path/to").should_be_called()
subject.create_repo(ifcgit, ifc)
class TestAddFile:
def test_run(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.repo_from_path("path/to/model.ifc").should_be_called().will_return("repo")
ifcgit.add_file_to_repo("repo", "path/to/model.ifc").should_be_called()
subject.add_file(ifcgit, ifc)
class TestCloneRepo:
def test_successful_clone(self, ifcgit):
ifcgit.clone_repo("http://example.com/repo.git", "/local/folder").should_be_called().will_return("repo")
ifcgit.load_anyifc("repo").should_be_called()
op = MockOperator()
subject.clone_repo(ifcgit, "http://example.com/repo.git", "/local/folder", operator=op)
assert op.reports == [({"INFO"}, "Repository cloned")]
def test_failed_clone_reports_error(self, ifcgit):
ifcgit.clone_repo("http://example.com/repo.git", "/local/folder").should_be_called().will_return(None)
op = MockOperator()
subject.clone_repo(ifcgit, "http://example.com/repo.git", "/local/folder", operator=op)
assert op.reports == [({"ERROR"}, "Clone failed")]
class TestDiscardUncommitted:
def test_run(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.git_checkout("path/to/model.ifc").should_be_called()
ifcgit.load_project("path/to/model.ifc").should_be_called()
subject.discard_uncommitted(ifcgit, ifc)
class TestCommitChanges:
def test_commit_on_branch_without_new_branch(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.is_head_detached().should_be_called().will_return(False)
ifcgit.git_commit("path/to/model.ifc", "my message").should_be_called()
subject.commit_changes(ifcgit, ifc, "my message", "")
def test_commit_on_branch_with_new_branch(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.is_head_detached().should_be_called().will_return(False)
ifcgit.checkout_new_branch("path/to/model.ifc", "feature").should_be_called()
ifcgit.git_commit("path/to/model.ifc", "my message").should_be_called()
subject.commit_changes(ifcgit, ifc, "my message", "feature")
def test_commit_on_detached_head(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.is_head_detached().should_be_called().will_return(True)
ifcgit.git_commit("path/to/model.ifc", "my message").should_be_called()
ifcgit.create_new_branch("feature").should_be_called()
subject.commit_changes(ifcgit, ifc, "my message", "feature")
class TestAddTag:
def test_run(self, ifcgit):
ifcgit.add_tag("repo", "abc123", "v1.0", "Release notes").should_be_called()
subject.add_tag(ifcgit, "repo", "abc123", "v1.0", "Release notes")
class TestDeleteTag:
def test_run(self, ifcgit):
ifcgit.delete_tag("repo", "v1.0").should_be_called()
subject.delete_tag(ifcgit, "repo", "v1.0")
class TestAddRemote:
def test_run(self, ifcgit):
ifcgit.add_remote("repo", "origin", "http://example.com").should_be_called()
subject.add_remote(ifcgit, "repo", "origin", "http://example.com")
class TestDeleteRemote:
def test_run(self, ifcgit):
ifcgit.delete_remote("repo", "origin").should_be_called()
subject.delete_remote(ifcgit, "repo", "origin")
class TestPush:
def test_push_succeeds_silently(self, ifcgit):
ifcgit.get_active_branch_name().should_be_called().will_return("main")
ifcgit.push("repo", "origin", "main").should_be_called().will_return(None)
subject.push(ifcgit, "repo", "origin", operator=None)
def test_push_failure_reports_error(self, ifcgit):
ifcgit.get_active_branch_name().should_be_called().will_return("main")
ifcgit.push("repo", "origin", "main").should_be_called().will_return("stderr: rejected")
op = MockOperator()
subject.push(ifcgit, "repo", "origin", operator=op)
assert op.reports == [({"ERROR"}, "stderr: rejected")]
class TestRefreshRevisionList:
def test_refreshes_when_repo_has_heads(self, ifcgit, ifc):
ifcgit.clear_merge_conflicts().should_be_called()
ifcgit.repo_has_commits().should_be_called().will_return(True)
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.refresh_revision_list("path/to/model.ifc").should_be_called()
subject.refresh_revision_list(ifcgit, ifc)
def test_skips_when_repo_has_no_heads(self, ifcgit, ifc):
ifcgit.clear_merge_conflicts().should_be_called()
ifcgit.repo_has_commits().should_be_called().will_return(False)
subject.refresh_revision_list(ifcgit, ifc)
# nothing else should be called — Prophecy will verify
class TestColouriseRevision:
def test_skips_when_no_step_ids(self, ifcgit):
ifcgit.get_revisions_step_ids().should_be_called().will_return(None)
subject.colourise_revision(ifcgit)
def test_colourises_with_step_ids(self, ifcgit):
ifcgit.get_revisions_step_ids().should_be_called().will_return("step_ids")
ifcgit.get_modified_step_ids("step_ids").should_be_called().will_return("modified_step_ids")
ifcgit.update_step_ids("step_ids", "modified_step_ids").should_be_called().will_return("final_step_ids")
ifcgit.colourise("final_step_ids").should_be_called()
subject.colourise_revision(ifcgit)
class TestColouriseUncommitted:
def test_skips_when_no_step_ids(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.ifc_diff_ids("repo", None, "HEAD", "path/to/model.ifc").should_be_called().will_return(None)
subject.colourise_uncommitted(ifcgit, ifc, "repo")
def test_colourises_with_step_ids(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.ifc_diff_ids("repo", None, "HEAD", "path/to/model.ifc").should_be_called().will_return("step_ids")
ifcgit.get_modified_step_ids("step_ids").should_be_called().will_return("modified_step_ids")
ifcgit.update_step_ids("step_ids", "modified_step_ids").should_be_called().will_return("final_step_ids")
ifcgit.colourise("final_step_ids").should_be_called()
subject.colourise_uncommitted(ifcgit, ifc, "repo")
class TestSwitchRevision:
def test_run(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.switch_to_revision_item().should_be_called()
ifcgit.load_project("path/to/model.ifc").should_be_called()
ifcgit.refresh_revision_list("path/to/model.ifc").should_be_called()
ifcgit.decolourise().should_be_called()
subject.switch_revision(ifcgit, ifc)
class TestMergeBranch:
def test_no_branch_at_selected_commit(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return(None)
subject.merge_branch(ifcgit, ifc, operator=None)
def test_clean_merge(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge("feature").should_be_called().will_return(None)
ifcgit.clear_merge_conflicts().should_be_called()
ifcgit.set_display_branch().should_be_called()
ifcgit.git_checkout("path/to/model.ifc").should_be_called()
ifcgit.load_project("path/to/model.ifc").should_be_called()
ifcgit.refresh_revision_list("path/to/model.ifc").should_be_called()
ifcgit.decolourise().should_be_called()
subject.merge_branch(ifcgit, ifc, operator=None)
def test_conflict_mergetool_success(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge("feature").should_be_called().will_return("conflict")
ifcgit.git_mergetool("ifcmerge-forward", "path/to/model.ifc").should_be_called().will_return(None)
ifcgit.commit_merge("path/to/model.ifc").should_be_called()
ifcgit.clear_merge_conflicts().should_be_called()
ifcgit.set_display_branch().should_be_called()
ifcgit.git_checkout("path/to/model.ifc").should_be_called()
ifcgit.load_project("path/to/model.ifc").should_be_called()
ifcgit.refresh_revision_list("path/to/model.ifc").should_be_called()
ifcgit.decolourise().should_be_called()
subject.merge_branch(ifcgit, ifc, operator=None)
def test_conflict_mergetool_failure(self, ifcgit, ifc):
conflicts = [{"type": "attribute_conflict", "entity_id": 42}]
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge("feature").should_be_called().will_return("conflict")
ifcgit.git_mergetool("ifcmerge-forward", "path/to/model.ifc").should_be_called().will_return(conflicts)
ifcgit.git_merge_abort().should_be_called()
ifcgit.store_merge_conflicts(conflicts).should_be_called()
op = MockOperator()
subject.merge_branch(ifcgit, ifc, op)
assert op.reports == [({"WARNING"}, "Merge failed — see the conflict report in the panel below")]
def test_unknown_merge_error(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge("feature").should_be_called().will_return("error")
op = MockOperator()
subject.merge_branch(ifcgit, ifc, op)
assert op.reports == [({"ERROR"}, "Unknown IFC Merge failure")]
class TestDryRunMerge:
def test_no_branch_at_selected_commit(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return(None)
subject.dry_run_merge(ifcgit, ifc, operator=None)
def test_clean_merge_preview(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge_no_commit("feature").should_be_called().will_return(None)
ifcgit.git_merge_abort().should_be_called()
ifcgit.clear_merge_conflicts().should_be_called()
op = MockOperator()
subject.dry_run_merge(ifcgit, ifc, op)
assert op.reports == [({"INFO"}, "Merge preview: no conflicts")]
def test_conflict_preview_shows_report(self, ifcgit, ifc):
conflicts = [{"type": "attribute_conflict", "entity_id": 42}]
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge_no_commit("feature").should_be_called().will_return("conflict")
ifcgit.git_mergetool("ifcmerge-forward", "path/to/model.ifc").should_be_called().will_return(conflicts)
ifcgit.git_merge_abort().should_be_called()
ifcgit.store_merge_conflicts(conflicts).should_be_called()
op = MockOperator()
subject.dry_run_merge(ifcgit, ifc, op)
assert op.reports == [({"WARNING"}, "Merge preview: conflicts found — see the panel below")]
def test_conflict_preview_mergetool_succeeds(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.config_ifcmerge().should_be_called()
ifcgit.get_selected_branch().should_be_called().will_return("feature")
ifcgit.get_merge_tool("feature").should_be_called().will_return("ifcmerge-forward")
ifcgit.git_merge_no_commit("feature").should_be_called().will_return("conflict")
ifcgit.git_mergetool("ifcmerge-forward", "path/to/model.ifc").should_be_called().will_return(None)
ifcgit.git_merge_abort().should_be_called()
ifcgit.clear_merge_conflicts().should_be_called()
op = MockOperator()
subject.dry_run_merge(ifcgit, ifc, op)
assert op.reports == [({"INFO"}, "Merge preview: no conflicts")]
class TestEntityLog:
def test_run(self, ifcgit, ifc):
ifc.get_path().should_be_called().will_return("path/to/model.ifc")
ifcgit.entity_log("path/to/model.ifc", 42).should_be_called().will_return("log text")
op = MockOperator()
subject.entity_log(ifcgit, ifc, 42, op)
assert op.reports == [({"ERROR"}, "log text")]
class TestInstallGit:
def test_windows(self, ifcgit):
import unittest.mock as mock
with mock.patch("platform.system", return_value="Windows"):
ifcgit.install_git_windows(operator="op").should_be_called()
subject.install_git(ifcgit, "op")
def test_non_windows_does_nothing(self, ifcgit):
import unittest.mock as mock
with mock.patch("platform.system", return_value="Linux"):
subject.install_git(ifcgit, "op")
# no tool method should be called — Prophecy will verify
class TestFetch:
def test_run(self, ifcgit):
ifcgit.fetch("origin").should_be_called()
subject.fetch(ifcgit, "origin")
class TestRunGitDiff:
def test_run(self, ifcgit):
ifcgit.run_git_diff("operator", False).should_be_called()
subject.run_git_diff(ifcgit, "operator", False)
+581
View File
@@ -0,0 +1,581 @@
# Bonsai - OpenBIM Blender Add-on
# Copyright (C) 2025 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/>.
# This file was generated with the assistance of an AI coding tool.
import os
import tempfile
import bonsai.core.tool
from bonsai.tool.ifcgit import IfcGit, IfcGitRepo
from test.bim.bootstrap import NewFile
try:
import git
import git.exc
HAS_GIT = True
except ImportError:
HAS_GIT = False
import pytest
requires_git = pytest.mark.skipif(not HAS_GIT, reason="GitPython not available")
def _make_repo(tmpdir: str) -> "git.Repo":
"""Initialise a git repo with user config (needed for commits)."""
repo = git.Repo.init(tmpdir)
with repo.config_writer() as cfg:
cfg.set_value("user", "name", "Test User")
cfg.set_value("user", "email", "test@example.com")
return repo
def _commit_ifc(repo: "git.Repo", tmpdir: str, content: str, message: str) -> str:
"""Write content to model.ifc, stage and commit it. Returns commit hexsha."""
ifc_path = os.path.join(tmpdir, "model.ifc")
with open(ifc_path, "w") as f:
f.write(content)
repo.index.add([os.path.normpath(ifc_path)])
commit = repo.index.commit(message)
return commit.hexsha
# ---------------------------------------------------------------------------
# Interface conformance
# ---------------------------------------------------------------------------
class TestImplementsTool(NewFile):
def test_run(self):
assert isinstance(IfcGit(), bonsai.core.tool.IfcGit)
# ---------------------------------------------------------------------------
# Pure-logic (no git required)
# ---------------------------------------------------------------------------
class TestIsValidRefFormat(NewFile):
def test_simple_name(self):
assert IfcGit.is_valid_ref_format("main")
def test_name_with_slash(self):
assert IfcGit.is_valid_ref_format("feature/my-feature")
def test_name_with_numbers(self):
assert IfcGit.is_valid_ref_format("release-2024")
def test_rejects_leading_dot(self):
assert not IfcGit.is_valid_ref_format(".hidden")
def test_rejects_leading_dash(self):
assert not IfcGit.is_valid_ref_format("-branch")
def test_rejects_space(self):
assert not IfcGit.is_valid_ref_format("my branch")
def test_rejects_double_dot(self):
assert not IfcGit.is_valid_ref_format("my..branch")
def test_rejects_tilde(self):
assert not IfcGit.is_valid_ref_format("my~branch")
def test_rejects_caret(self):
assert not IfcGit.is_valid_ref_format("my^branch")
def test_rejects_trailing_dot(self):
assert not IfcGit.is_valid_ref_format("branch.")
def test_rejects_trailing_slash(self):
assert not IfcGit.is_valid_ref_format("branch/")
def test_rejects_dot_lock_suffix(self):
assert not IfcGit.is_valid_ref_format("branch.lock")
def test_rejects_empty_string(self):
assert not IfcGit.is_valid_ref_format("")
def test_rejects_at_brace(self):
assert not IfcGit.is_valid_ref_format("branch@{upstream}")
class TestGetPathDir(NewFile):
def test_returns_parent_directory(self):
assert IfcGit.get_path_dir("/some/path/model.ifc") == "/some/path"
def test_handles_nested_path(self):
assert IfcGit.get_path_dir("/a/b/c/d.ifc") == "/a/b/c"
def test_returns_absolute_path(self):
result = IfcGit.get_path_dir("/a/b/model.ifc")
assert os.path.isabs(result)
# ---------------------------------------------------------------------------
# File I/O
# ---------------------------------------------------------------------------
class TestDos2Unix(NewFile):
def test_converts_crlf_to_lf(self):
with tempfile.NamedTemporaryFile(suffix=".ifc", delete=False, mode="wb") as f:
f.write(b"line1\r\nline2\r\nline3\r\n")
path = f.name
try:
IfcGit.dos2unix(path)
with open(path, "rb") as f:
assert f.read() == b"line1\nline2\nline3\n"
finally:
os.unlink(path)
def test_lf_only_file_is_unchanged(self):
with tempfile.NamedTemporaryFile(suffix=".ifc", delete=False, mode="wb") as f:
f.write(b"line1\nline2\nline3\n")
path = f.name
try:
IfcGit.dos2unix(path)
with open(path, "rb") as f:
assert f.read() == b"line1\nline2\nline3\n"
finally:
os.unlink(path)
def test_empty_file_unchanged(self):
with tempfile.NamedTemporaryFile(suffix=".ifc", delete=False, mode="wb") as f:
f.write(b"")
path = f.name
try:
IfcGit.dos2unix(path)
with open(path, "rb") as f:
assert f.read() == b""
finally:
os.unlink(path)
# ---------------------------------------------------------------------------
# Git repo initialisation
# ---------------------------------------------------------------------------
class TestInitRepo(NewFile):
@requires_git
def test_creates_git_repo(self):
with tempfile.TemporaryDirectory() as tmpdir:
IfcGitRepo.repo = None
IfcGit.init_repo(tmpdir)
assert IfcGitRepo.repo is not None
assert os.path.isdir(IfcGitRepo.repo.git_dir)
IfcGitRepo.repo = None
@requires_git
def test_creates_info_attributes_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
IfcGitRepo.repo = None
IfcGit.init_repo(tmpdir)
attrs_path = os.path.join(IfcGitRepo.repo.git_dir, "info", "attributes")
assert os.path.isfile(attrs_path)
with open(attrs_path) as f:
assert "*.ifc text" in f.read()
IfcGitRepo.repo = None
class TestRepoFromPath(NewFile):
@requires_git
def test_finds_repo_from_file_in_root(self):
with tempfile.TemporaryDirectory() as tmpdir:
IfcGitRepo.repo = None
repo = _make_repo(tmpdir)
ifc_path = os.path.join(tmpdir, "model.ifc")
open(ifc_path, "w").close()
result = IfcGit.repo_from_path(ifc_path)
assert result is not None
assert os.path.abspath(result.working_dir) == os.path.abspath(tmpdir)
IfcGitRepo.repo = None
@requires_git
def test_finds_repo_from_subdirectory(self):
with tempfile.TemporaryDirectory() as tmpdir:
IfcGitRepo.repo = None
_make_repo(tmpdir)
subdir = os.path.join(tmpdir, "sub", "dir")
os.makedirs(subdir)
result = IfcGit.repo_from_path(subdir)
assert result is not None
IfcGitRepo.repo = None
@requires_git
def test_returns_none_for_path_outside_any_repo(self):
with tempfile.TemporaryDirectory() as tmpdir:
IfcGitRepo.repo = None
# a plain directory with no .git anywhere above it (using /tmp directly
# is safe since /tmp is not a git repo on this system)
result = IfcGit.repo_from_path("/nonexistent/path/that/does/not/exist")
assert result is None
IfcGitRepo.repo = None
# ---------------------------------------------------------------------------
# Git repo configuration
# ---------------------------------------------------------------------------
class TestConfigInfoAttributes(NewFile):
@requires_git
def test_creates_attributes_file(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
IfcGit.config_info_attributes(repo)
attrs_path = os.path.join(repo.git_dir, "info", "attributes")
assert os.path.isfile(attrs_path)
with open(attrs_path) as f:
assert "*.ifc text" in f.read()
@requires_git
def test_does_not_overwrite_existing_attributes(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
attrs_path = os.path.join(repo.git_dir, "info", "attributes")
os.makedirs(os.path.dirname(attrs_path), exist_ok=True)
with open(attrs_path, "w") as f:
f.write("*.png binary\n")
IfcGit.config_info_attributes(repo)
with open(attrs_path) as f:
content = f.read()
assert "*.png binary" in content # original content preserved
class TestConfigPush(NewFile):
@requires_git
def test_sets_push_defaults(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
IfcGit.config_push(repo)
reader = repo.config_reader()
assert reader.get_value("push", "default") == "current"
assert reader.get_value("push", "autoSetupRemote") is True
@requires_git
def test_does_not_overwrite_existing_push_section(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
with repo.config_writer() as w:
w.set_value("push", "default", "simple")
IfcGit.config_push(repo)
reader = repo.config_reader()
assert reader.get_value("push", "default") == "simple"
# ---------------------------------------------------------------------------
# Branch / tag lookups
# ---------------------------------------------------------------------------
class TestBranchesByHexsha(NewFile):
@requires_git
def test_maps_head_commit_to_active_branch(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
result = IfcGit.branches_by_hexsha(repo)
head_sha = repo.head.commit.hexsha
assert head_sha in result
names = [b.name for b in result[head_sha]]
assert repo.active_branch.name in names
@requires_git
def test_returns_empty_dict_when_no_commits(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
result = IfcGit.branches_by_hexsha(repo)
assert result == {}
@requires_git
def test_includes_both_branches_when_pointing_to_same_commit(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
repo.create_head("feature")
result = IfcGit.branches_by_hexsha(repo)
head_sha = repo.head.commit.hexsha
names = [b.name for b in result[head_sha]]
assert len(names) == 2
class TestTagsByHexsha(NewFile):
@requires_git
def test_returns_empty_dict_when_no_tags(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
assert IfcGit.tags_by_hexsha(repo) == {}
@requires_git
def test_maps_commit_to_annotated_tag(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
repo.create_tag("v1.0", message="Release 1.0")
result = IfcGit.tags_by_hexsha(repo)
head_sha = repo.head.commit.hexsha
assert head_sha in result
assert result[head_sha][0].name == "v1.0"
@requires_git
def test_maps_commit_to_lightweight_tag(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
repo.create_tag("v0.1")
result = IfcGit.tags_by_hexsha(repo)
head_sha = repo.head.commit.hexsha
assert head_sha in result
class TestDeleteTag(NewFile):
@requires_git
def test_removes_existing_tag(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
repo.create_tag("v1.0")
IfcGit.delete_tag(repo, "v1.0")
assert "v1.0" not in [t.name for t in repo.tags]
@requires_git
def test_does_not_raise_for_nonexistent_tag(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
IfcGit.delete_tag(repo, "does-not-exist") # must not raise
# ---------------------------------------------------------------------------
# IFC diff parsing
# ---------------------------------------------------------------------------
class TestIfcDiffIds(NewFile):
@requires_git
def test_detects_modified_entity(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
sha_a = _commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
ifc_path = os.path.join(tmpdir, "model.ifc")
sha_b = _commit_ifc(repo, tmpdir, "#1=IFCPROJECT('xyz',$,$,$,$,$,$,$,$);\n", "update")
result = IfcGit.ifc_diff_ids(repo, sha_a, sha_b, ifc_path)
assert 1 in result["modified"]
assert result["added"] == set()
assert result["removed"] == set()
@requires_git
def test_detects_added_entity(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
sha_a = _commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
ifc_path = os.path.join(tmpdir, "model.ifc")
sha_b = _commit_ifc(
repo,
tmpdir,
"#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n#2=IFCSITE('new',$,$,$,$,$,$,$,$,$,$,$,$);\n",
"add site",
)
result = IfcGit.ifc_diff_ids(repo, sha_a, sha_b, ifc_path)
assert 2 in result["added"]
assert 1 not in result["modified"]
assert result["removed"] == set()
@requires_git
def test_detects_removed_entity(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
sha_a = _commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
ifc_path = os.path.join(tmpdir, "model.ifc")
sha_b = _commit_ifc(repo, tmpdir, "", "clear")
result = IfcGit.ifc_diff_ids(repo, sha_a, sha_b, ifc_path)
assert 1 in result["removed"]
assert result["added"] == set()
assert result["modified"] == set()
@requires_git
def test_no_changes_between_identical_commits(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
sha = _commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
ifc_path = os.path.join(tmpdir, "model.ifc")
result = IfcGit.ifc_diff_ids(repo, sha, sha, ifc_path)
assert result["modified"] == set()
assert result["added"] == set()
assert result["removed"] == set()
@requires_git
def test_diff_against_working_tree_with_none_hash_a(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
_commit_ifc(repo, tmpdir, "#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n", "init")
ifc_path = os.path.join(tmpdir, "model.ifc")
# Make an uncommitted change in working tree
with open(ifc_path, "w") as f:
f.write("#1=IFCPROJECT('xyz',$,$,$,$,$,$,$,$);\n")
result = IfcGit.ifc_diff_ids(repo, None, "HEAD", ifc_path)
assert 1 in result["modified"]
@requires_git
def test_handles_multiple_changed_entities(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
sha_a = _commit_ifc(
repo,
tmpdir,
"#1=IFCPROJECT('abc',$,$,$,$,$,$,$,$);\n#2=IFCSITE('s',$,$,$,$,$,$,$,$,$,$,$,$);\n",
"init",
)
ifc_path = os.path.join(tmpdir, "model.ifc")
sha_b = _commit_ifc(
repo,
tmpdir,
"#1=IFCPROJECT('xyz',$,$,$,$,$,$,$,$);\n#2=IFCSITE('t',$,$,$,$,$,$,$,$,$,$,$,$);\n",
"update both",
)
result = IfcGit.ifc_diff_ids(repo, sha_a, sha_b, ifc_path)
assert 1 in result["modified"]
assert 2 in result["modified"]
# ---------------------------------------------------------------------------
# Merge conflict report — store / clear / get
# ---------------------------------------------------------------------------
class TestStoreClearGetMergeConflicts(NewFile):
def test_round_trip(self):
conflicts = [{"type": "attribute_conflict", "entity_id": 42}]
IfcGit.store_merge_conflicts(conflicts)
result = IfcGit.get_merge_conflicts()
assert result == conflicts
def test_get_returns_none_when_empty(self):
IfcGit.clear_merge_conflicts()
assert IfcGit.get_merge_conflicts() is None
def test_clear_removes_stored_conflicts(self):
IfcGit.store_merge_conflicts([{"type": "class_changed"}])
IfcGit.clear_merge_conflicts()
assert IfcGit.get_merge_conflicts() is None
def test_get_returns_none_on_corrupt_json(self):
import bpy
bpy.context.scene.IfcGitProperties.merge_conflicts = "not valid json {"
assert IfcGit.get_merge_conflicts() is None
# ---------------------------------------------------------------------------
# git_mergetool — report file reading
# ---------------------------------------------------------------------------
class TestGitMergetool:
@requires_git
def test_returns_none_when_report_file_absent(self):
import unittest.mock as mock
with tempfile.TemporaryDirectory() as tmpdir:
ifc_path = os.path.join(tmpdir, "model.ifc")
mock_repo = mock.MagicMock()
IfcGitRepo.repo = mock_repo
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
assert result is None
IfcGitRepo.repo = None
@requires_git
def test_returns_none_when_report_file_empty(self):
import unittest.mock as mock
with tempfile.TemporaryDirectory() as tmpdir:
ifc_path = os.path.join(tmpdir, "model.ifc")
report_path = ifc_path + ".ifcmerge"
open(report_path, "w").close()
mock_repo = mock.MagicMock()
IfcGitRepo.repo = mock_repo
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
assert result is None
assert not os.path.exists(report_path)
IfcGitRepo.repo = None
@requires_git
def test_parses_conflict_report_and_deletes_file(self):
import json
import unittest.mock as mock
with tempfile.TemporaryDirectory() as tmpdir:
ifc_path = os.path.join(tmpdir, "model.ifc")
report_path = ifc_path + ".ifcmerge"
conflicts = [{"type": "attribute_conflict", "entity_id": 5}]
with open(report_path, "w") as f:
json.dump({"status": "failed", "conflicts": conflicts}, f)
mock_repo = mock.MagicMock()
mock_repo.git.mergetool.side_effect = git.exc.GitCommandError("mergetool", 1)
IfcGitRepo.repo = mock_repo
result = IfcGit.git_mergetool("ifcmerge", ifc_path)
assert result == conflicts
assert not os.path.exists(report_path)
IfcGitRepo.repo = None
# ---------------------------------------------------------------------------
# config_ifcmerge — cmd format and update
# ---------------------------------------------------------------------------
class TestConfigIfcmerge:
@requires_git
def test_writes_redirect_cmd_on_first_call(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
IfcGitRepo.repo = repo
IfcGit.config_ifcmerge()
reader = repo.config_reader()
cmd = reader.get_value('mergetool "ifcmerge"', "cmd")
assert "> $MERGED.ifcmerge" in cmd
IfcGitRepo.repo = None
@requires_git
def test_updates_cmd_missing_redirect(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
IfcGitRepo.repo = repo
with repo.config_writer() as w:
w.set_value('mergetool "ifcmerge"', "cmd", "ifcmerge $BASE $LOCAL $REMOTE $MERGED")
w.set_value('mergetool "ifcmerge"', "trustExitCode", True)
IfcGit.config_ifcmerge()
reader = repo.config_reader()
cmd = reader.get_value('mergetool "ifcmerge"', "cmd")
assert "> $MERGED.ifcmerge" in cmd
IfcGitRepo.repo = None
@requires_git
def test_forward_tool_writes_redirect_cmd(self):
with tempfile.TemporaryDirectory() as tmpdir:
repo = _make_repo(tmpdir)
IfcGitRepo.repo = repo
IfcGit.config_ifcmerge()
reader = repo.config_reader()
cmd = reader.get_value('mergetool "ifcmerge-forward"', "cmd")
assert "--prioritise-local" in cmd
assert "> $MERGED.ifcmerge" in cmd
IfcGitRepo.repo = None
+21 -22
View File
@@ -39,12 +39,11 @@
#include <Standard_Version.hxx>
#define IfcSchema Ifc2x3
#include "ifcparse/macros.h"
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcBaseClass.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/macros.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/hierarchy_helper.h"
#include "ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/Serialization/Serialization.h"
#if USE_VLD
#include <vld.h>
@@ -57,18 +56,18 @@ void createGroundShape(TopoDS_Shape& shape);
int main() {
// The IfcHierarchyHelper is a subclass of the regular IfcFile that provides several
// The hierarchy_helper is a subclass of the regular file that provides several
// convenience functions for working with geometry in IFC files.
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname("IfcAdvancedHouse.ifc");
hierarchy_helper<IfcSchema> file;
file.header().file_name().setname("IfcAdvancedHouse.ifc");
IfcSchema::IfcBuilding* building = file.addBuilding();
auto building = file.addBuilding();
// By adding a building, a hierarchy has been automatically created that consists of the following
// structure: IfcProject > IfcSite > IfcBuilding
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
// project, which has been created automatically.
file.getSingle<IfcSchema::IfcProject>()->setName("IfcAdvancedHouse"s);
file.getSingle<IfcSchema::IfcProject>().setName("IfcAdvancedHouse"s);
// To demonstrate the ability to serialize arbitrary opencascade solids a building envelope is
// constructed by applying boolean operations. Naturally, in IFC, building elements should be
@@ -91,13 +90,13 @@ int main() {
// IfcFacetedBRep. If it would not be a polyhedron, serialise() can only be successful when linked
// to the IFC4 model and with `advanced` set to `true` which introduces IfcAdvancedFace. It would
// return `0` otherwise.
IfcSchema::IfcProductDefinitionShape* building_shape = IfcGeom::serialise(STRINGIFY(IfcSchema), building_shell, false)->as<IfcSchema::IfcProductDefinitionShape>();
auto building_shape = IfcGeom::serialise(file, building_shell, false).as<IfcSchema::IfcProductDefinitionShape>();
file.addEntity(building_shape);
IfcSchema::IfcRepresentation* rep = *building_shape->Representations()->begin();
file.add_entity(building_shape);
auto rep = building_shape.Representations().begin();
rep->setContextOfItems(file.getRepresentationContext("model"));
building->setRepresentation(building_shape);
building.setRepresentation(building_shape);
// A pale white colour is assigned to the building.
setSurfaceColour(file, building_shape, 0.75, 0.73, 0.68);
@@ -107,18 +106,18 @@ int main() {
TopoDS_Shape shape;
createGroundShape(shape);
auto ground_representation = IfcGeom::serialise(STRINGIFY(IfcSchema), shape, true);
auto ground_representation = IfcGeom::serialise(file, shape, true);
if (!ground_representation) {
ground_representation = IfcGeom::tesselate(STRINGIFY(IfcSchema), shape, 100.);
ground_representation = IfcGeom::tesselate(file, shape, 100.);
}
file.getSingle<IfcSchema::IfcSite>()->setRepresentation(ground_representation->as<IfcSchema::IfcProductDefinitionShape>());
file.getSingle<IfcSchema::IfcSite>().setRepresentation(ground_representation.as<IfcSchema::IfcProductDefinitionShape>());
IfcSchema::IfcRepresentation::list::ptr ground_reps = file.getSingle<IfcSchema::IfcSite>()->Representation()->Representations();
for (IfcSchema::IfcRepresentation::list::it it = ground_reps->begin(); it != ground_reps->end(); ++it) {
(*it)->setContextOfItems(file.getRepresentationContext("Model"));
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
for (auto& rep : ground_reps) {
rep.setContextOfItems(file.getRepresentationContext("Model"));
}
file.addEntity(ground_representation);
setSurfaceColour(file, ground_representation->as<IfcSchema::IfcProductDefinitionShape>(), 0.15, 0.25, 0.05);
file.add_entity(ground_representation);
setSurfaceColour(file, ground_representation.as<IfcSchema::IfcProductDefinitionShape>(), 0.15, 0.25, 0.05);
/*
// Note that IFC lacks elementary surfaces that STEP does have, such as spherical_surface.
+299 -210
View File
@@ -30,8 +30,8 @@
// Disable warnings coming from IfcOpenShell
#pragma warning(disable : 4018 4267 4250 4984 4985)
#include "ifcparse/Ifc4x3_add2.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/Ifc4x3_add2.h"
#include "../ifcparse/hierarchy_helper.h"
#include <boost/math/constants/constants.hpp>
#include <fstream>
@@ -43,37 +43,49 @@ double to_radian(double deg) { return PI * deg / 180; }
// performs basic project setup including created the IfcProject object
// and initializing the project units to FEET
Schema::IfcProject* setup_project(IfcHierarchyHelper<Schema>& file) {
Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
std::vector<std::string> file_description;
file_description.push_back("ViewDefinition[Alignment-basedReferenceView]");
file.header().file_description()->setdescription(file_description);
file.header().file_description().setdescription(file_description);
auto project = file.addProject();
project->setName(std::string("FHWA Bridge Geometry Manual Example Alignment"));
project->setDescription(std::string("C++ Example"));
project.setName("FHWA Bridge Geometry Manual Example Alignment");
project.setDescription("C++ Example");
// set up project units for feet
// the call to file.addProject() sets up length units as millimeter.
auto units_in_context = project->UnitsInContext();
auto units = units_in_context->Units();
auto begin = units->begin();
auto units_in_context = project.UnitsInContext();
auto units = units_in_context.Units();
auto begin = units.begin();
auto iter = begin;
auto end = units->end();
auto end = units.end();
for (; iter != end; iter++) {
auto unit = *iter;
if (unit->as<Schema::IfcSIUnit>() && unit->as<Schema::IfcSIUnit>()->UnitType() == Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT) {
auto dimensions = new Schema::IfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0);
file.addEntity(dimensions);
auto& unit = *iter;
if (unit.as<Schema::IfcSIUnit>() && unit.as<Schema::IfcSIUnit>().UnitType() == Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT) {
auto dimensions = file.create<Schema::IfcDimensionalExponents>();
dimensions.setLengthExponent(1);
dimensions.setMassExponent(0);
dimensions.setTimeExponent(0);
dimensions.setElectricCurrentExponent(0);
dimensions.setThermodynamicTemperatureExponent(0);
dimensions.setAmountOfSubstanceExponent(0);
dimensions.setLuminousIntensityExponent(0);
auto conversion_factor = file.create<Schema::IfcMeasureWithUnit>();
auto length = file.create<Schema::IfcLengthMeasure>();
length.set_attribute_value(0, 304.80);
conversion_factor.setValueComponent(length);
conversion_factor.setUnitComponent(unit);
auto conversion_factor = new Schema::IfcMeasureWithUnit(new Schema::IfcLengthMeasure(304.80), unit->as<Schema::IfcSIUnit>());
file.addEntity(conversion_factor);
auto conversion_based_unit = new Schema::IfcConversionBasedUnit(dimensions, Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT, "FEET", conversion_factor);
file.addEntity(conversion_based_unit);
units->remove(unit); // remove the millimeter unit
units->push(conversion_based_unit); // add the feet unit
units_in_context->setUnits(units); // update the UnitsInContext
auto conversion_based_unit = file.create<Schema::IfcConversionBasedUnit>();
conversion_based_unit.setDimensions(dimensions);
conversion_based_unit.setUnitType(Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT);
conversion_based_unit.setName("FEET");
conversion_based_unit.setConversionFactor(conversion_factor);
units.erase(std::remove(units.begin(), units.end(), unit)); // remove the millimeter unit
units.push_back(conversion_based_unit); // add the feet unit
units_in_context.setUnits(units); // update the UnitsInContext
break; // Done!, the length unit was found, so break out of the loop
}
@@ -83,135 +95,198 @@ Schema::IfcProject* setup_project(IfcHierarchyHelper<Schema>& file) {
}
// creates geometry and business logic segments for horizontal alignment tangent runs
std::pair<typename Schema::IfcCurveSegment*, typename Schema::IfcAlignmentSegment*> create_tangent(typename Schema::IfcCartesianPoint* p, double dir, double length) {
std::pair<typename Schema::IfcCurveSegment, typename Schema::IfcAlignmentSegment> create_tangent(hierarchy_helper<Schema>& file, const typename Schema::IfcCartesianPoint& p, double dir, double length) {
// geometry
auto parent_curve = new Schema::IfcLine(
new Schema::IfcCartesianPoint(std::vector<double>({0, 0})),
new Schema::IfcVector(new Schema::IfcDirection(std::vector<double>{1.0, 0.0}), 1.0));
auto parent_curve = file.create<Schema::IfcLine>();
parent_curve.setPnt(file.addDoublet<Schema::IfcCartesianPoint>(0.0, 0.0));
auto vec = file.create<Schema::IfcVector>();
vec.setOrientation(file.addDoublet<Schema::IfcDirection>(1.0, 0.0));
vec.setMagnitude(1.0);
parent_curve.setDir(vec);
auto curve_segment = new Schema::IfcCurveSegment(
Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Schema::IfcAxis2Placement2D(p, new Schema::IfcDirection(std::vector<double>{cos(dir), sin(dir)})),
new Schema::IfcLengthMeasure(0.0), // start
new Schema::IfcLengthMeasure(length),
parent_curve);
auto place = file.create<Schema::IfcAxis2Placement2D>();
place.setLocation(p);
place.setRefDirection(file.addDoublet<Schema::IfcDirection>(cos(dir), sin(dir)));
auto curve_segment = file.create<Schema::IfcCurveSegment>();
curve_segment.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT);
curve_segment.setPlacement(place);
curve_segment.setSegmentStart(file.addValue<Schema::IfcLengthMeasure>(0.0));
curve_segment.setSegmentLength(file.addValue<Schema::IfcLengthMeasure>(length));
curve_segment.setParentCurve(parent_curve);
// business logic
auto design_parameters = new Schema::IfcAlignmentHorizontalSegment(
boost::none, boost::none, p, dir, 0.0, 0.0, length, boost::none, Schema::IfcAlignmentHorizontalSegmentTypeEnum::IfcAlignmentHorizontalSegmentType_LINE);
auto design_parameters = file.create<Schema::IfcAlignmentHorizontalSegment>();
design_parameters.setStartPoint(p);
design_parameters.setStartDirection(dir);
design_parameters.setStartRadiusOfCurvature(0.0);
design_parameters.setEndRadiusOfCurvature(0.0);
design_parameters.setSegmentLength(length);
design_parameters.setPredefinedType(Schema::IfcAlignmentHorizontalSegmentTypeEnum::IfcAlignmentHorizontalSegmentType_LINE);
auto alignment_segment = new Schema::IfcAlignmentSegment(
IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
auto alignment_segment = file.create<Schema::IfcAlignmentSegment>();
alignment_segment.setGlobalId(ifcopenshell::global_id());
alignment_segment.setDesignParameters(design_parameters);
return {curve_segment, alignment_segment};
}
// creates geometry and business logic segments for horizontal alignment horizonal curves
std::pair<typename Schema::IfcCurveSegment*, typename Schema::IfcAlignmentSegment*> create_hcurve(typename Schema::IfcCartesianPoint* pc, double dir, double radius, double lc) {
std::pair<typename Schema::IfcCurveSegment, typename Schema::IfcAlignmentSegment> create_hcurve(hierarchy_helper<Schema>& file, const typename Schema::IfcCartesianPoint& pc, double dir, double radius, double lc) {
// geometry
double sign = radius / fabs(radius);
auto parent_curve = new Schema::IfcCircle(
new Schema::IfcAxis2Placement2D(new Schema::IfcCartesianPoint(std::vector<double>({0, 0})), new Schema::IfcDirection(std::vector<double>{1, 0})),
fabs(radius));
auto place = file.create<Schema::IfcAxis2Placement2D>();
place.setLocation(file.addDoublet<Schema::IfcCartesianPoint>(0.0, 0.0));
place.setRefDirection(file.addDoublet<Schema::IfcDirection>(1.0, 0.0));
auto parent_curve = file.create<Schema::IfcCircle>();
parent_curve.setPosition(place);
parent_curve.setRadius(fabs(radius));
auto curve_segment = new Schema::IfcCurveSegment(
Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Schema::IfcAxis2Placement2D(pc, new Schema::IfcDirection(std::vector<double>{cos(dir), sin(dir)})),
new Schema::IfcLengthMeasure(0.0),
new Schema::IfcLengthMeasure(sign * lc),
parent_curve);
auto place2 = file.create<Schema::IfcAxis2Placement2D>();
place2.setLocation(pc);
place2.setRefDirection(file.addDoublet<Schema::IfcDirection>(cos(dir), sin(dir)));
auto curve_segment = file.create<Schema::IfcCurveSegment>();
curve_segment.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT);
curve_segment.setPlacement(place2);
curve_segment.setSegmentStart(file.addValue<Schema::IfcLengthMeasure>(0.0));
curve_segment.setSegmentLength(file.addValue<Schema::IfcLengthMeasure>(sign * lc));
curve_segment.setParentCurve(parent_curve);
// business logic
auto design_parameters = new Schema::IfcAlignmentHorizontalSegment(boost::none, boost::none, pc, dir, radius, radius, lc, boost::none, Schema::IfcAlignmentHorizontalSegmentTypeEnum::IfcAlignmentHorizontalSegmentType_CIRCULARARC);
auto alignment_segment = new Schema::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
auto design_parameters = file.create<Schema::IfcAlignmentHorizontalSegment>();
design_parameters.setStartPoint(pc);
design_parameters.setStartDirection(dir);
design_parameters.setStartRadiusOfCurvature(radius);
design_parameters.setEndRadiusOfCurvature(radius);
design_parameters.setSegmentLength(lc);
design_parameters.setPredefinedType(Schema::IfcAlignmentHorizontalSegmentTypeEnum::IfcAlignmentHorizontalSegmentType_CIRCULARARC);
auto alignment_segment = file.create<Schema::IfcAlignmentSegment>();
alignment_segment.setGlobalId(ifcopenshell::global_id());
alignment_segment.setDesignParameters(design_parameters);
return {curve_segment, alignment_segment};
}
// creates geometry and business logic segments for vertical profile gradient runs
std::pair<typename Schema::IfcCurveSegment*, typename Schema::IfcAlignmentSegment*> create_gradient(typename Schema::IfcCartesianPoint* p, double slope, double length) {
std::pair<typename Schema::IfcCurveSegment, typename Schema::IfcAlignmentSegment> create_gradient(hierarchy_helper<Schema>& file, const typename Schema::IfcCartesianPoint& p, double slope, double length) {
// geometry
auto parent_curve = new Schema::IfcLine(
new Schema::IfcCartesianPoint(std::vector<double>({0, 0})),
new Schema::IfcVector(new Schema::IfcDirection(std::vector<double>{1, 0}), 1.0));
auto parent_curve = file.create<Schema::IfcLine>();
parent_curve.setPnt(file.addDoublet<Schema::IfcCartesianPoint>(0.0, 0.0));
auto vec = file.create<Schema::IfcVector>();
vec.setOrientation(file.addDoublet<Schema::IfcDirection>(1.0, 0.0));
vec.setMagnitude(1.0);
parent_curve.setDir(vec);
auto curve_segment = new Schema::IfcCurveSegment(
Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Schema::IfcAxis2Placement2D(p, new Schema::IfcDirection(std::vector<double>{sqrt(1 - slope * slope), slope})),
new Schema::IfcLengthMeasure(0.0), // start
new Schema::IfcLengthMeasure(length),
parent_curve);
auto place2 = file.create<Schema::IfcAxis2Placement2D>();
place2.setLocation(p);
place2.setRefDirection(file.addDoublet<Schema::IfcDirection>(sqrt(1 - slope * slope), slope));
auto curve_segment = file.create<Schema::IfcCurveSegment>();
curve_segment.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT);
curve_segment.setPlacement(place2);
curve_segment.setSegmentStart(file.addValue<Schema::IfcLengthMeasure>(0.0));
curve_segment.setSegmentLength(file.addValue<Schema::IfcLengthMeasure>(length));
curve_segment.setParentCurve(parent_curve);
// business logic
auto design_parameters = new Schema::IfcAlignmentVerticalSegment(boost::none, boost::none, p->Coordinates()[0], length, p->Coordinates()[1], slope, slope, boost::none, Schema::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT);
auto alignment_segment = new Schema::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
auto design_parameters = file.create<Schema::IfcAlignmentVerticalSegment>();
design_parameters.setStartDistAlong(p.Coordinates()[0]);
design_parameters.setHorizontalLength(length);
design_parameters.setStartHeight(p.Coordinates()[1]);
design_parameters.setStartGradient(slope);
design_parameters.setEndGradient(slope);
design_parameters.setPredefinedType(Schema::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_CONSTANTGRADIENT);
auto alignment_segment = file.create<Schema::IfcAlignmentSegment>();
alignment_segment.setGlobalId(ifcopenshell::global_id());
alignment_segment.setDesignParameters(design_parameters);
return {curve_segment, alignment_segment};
}
// creates geometry and business logic segments for vertical profile parabolic vertical curves
std::pair<typename Schema::IfcCurveSegment*, typename Schema::IfcAlignmentSegment*> create_vcurve(typename Schema::IfcCartesianPoint* p, double start_slope, double end_slope, double length) {
std::pair<typename Schema::IfcCurveSegment, typename Schema::IfcAlignmentSegment> create_vcurve(hierarchy_helper<Schema>& file, const typename Schema::IfcCartesianPoint& p, double start_slope, double end_slope, double length) {
// geometry
double A = p->Coordinates()[1];
double A = p.Coordinates()[1];
double B = start_slope;
double C = (end_slope - start_slope) / (2 * length);
auto parent_curve = new Schema::IfcPolynomialCurve(
new Schema::IfcAxis2Placement2D(new Schema::IfcCartesianPoint(std::vector<double>{0.0, 0.0}), new Schema::IfcDirection(std::vector<double>{1.0, 0.0})),
std::vector<double>{0.0, 1.0},
std::vector<double>{A, B, C},
boost::none);
auto place = file.create<Schema::IfcAxis2Placement2D>();
place.setLocation(file.addDoublet<Schema::IfcCartesianPoint>(0.0, 0.0));
place.setRefDirection(file.addDoublet<Schema::IfcDirection>(1.0, 0.0));
auto curve_segment = new Schema::IfcCurveSegment(
Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT,
new Schema::IfcAxis2Placement2D(p, new Schema::IfcDirection(std::vector<double>{sqrt(1 - start_slope * start_slope), start_slope})),
new Schema::IfcLengthMeasure(0.0),
new Schema::IfcLengthMeasure(length),
parent_curve);
auto parent_curve = file.create<Schema::IfcPolynomialCurve>();
parent_curve.setPosition(place);
parent_curve.setCoefficientsX(std::vector<double>{0.0, 1.0});
parent_curve.setCoefficientsY(std::vector<double>{A, B, C});
auto place2 = file.create<Schema::IfcAxis2Placement2D>();
place2.setLocation(p);
place2.setRefDirection(file.addDoublet<Schema::IfcDirection>(sqrt(1 - start_slope * start_slope), start_slope));
auto curve_segment = file.create<Schema::IfcCurveSegment>();
curve_segment.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT);
curve_segment.setPlacement(place2);
curve_segment.setSegmentStart(file.addValue<Schema::IfcLengthMeasure>(0.0));
curve_segment.setSegmentLength(file.addValue<Schema::IfcLengthMeasure>(length));
curve_segment.setParentCurve(parent_curve);
// business logic
double k = (end_slope - start_slope) / length;
auto design_parameters = new Schema::IfcAlignmentVerticalSegment(boost::none, boost::none, p->Coordinates()[0], length, p->Coordinates()[1], start_slope, end_slope, 1 / k, Schema::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_PARABOLICARC);
auto alignment_segment = new Schema::IfcAlignmentSegment(IfcParse::IfcGlobalId(), nullptr, boost::none, boost::none, boost::none, nullptr, nullptr, design_parameters);
auto design_parameters = file.create<Schema::IfcAlignmentVerticalSegment>();
design_parameters.setStartDistAlong(p.Coordinates()[0]);
design_parameters.setHorizontalLength(length);
design_parameters.setStartHeight(p.Coordinates()[1]);
design_parameters.setStartGradient(start_slope);
design_parameters.setEndGradient(end_slope);
design_parameters.setRadiusOfCurvature(1 / k);
design_parameters.setPredefinedType(Schema::IfcAlignmentVerticalSegmentTypeEnum::IfcAlignmentVerticalSegmentType_PARABOLICARC);
auto alignment_segment = file.create<Schema::IfcAlignmentSegment>();
alignment_segment.setGlobalId(ifcopenshell::global_id());
alignment_segment.setDesignParameters(design_parameters);
return {curve_segment, alignment_segment};
}
// creates representations for each IfcAlignmentSegment per CT 4.1.7.1.1.4
// https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/concepts/Product_Shape/Product_Geometric_Representation/Alignment_Geometry/Alignment_Geometry_-_Segments/content.html
void create_segment_representations(IfcHierarchyHelper<Schema>& file, Schema::IfcLocalPlacement* global_placement, Schema::IfcGeometricRepresentationSubContext* segment_axis_subcontext, typename aggregate_of<typename Schema::IfcSegment>::ptr curve_segments, typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr segments) {
auto cs_iter = curve_segments->begin();
auto s_iter = segments->begin();
for (; cs_iter != curve_segments->end(); cs_iter++, s_iter++) {
auto curve_segment = *cs_iter;
auto alignment_segment = (*s_iter)->as<Schema::IfcAlignmentSegment>();
void create_segment_representations(hierarchy_helper<Schema>& file, const Schema::IfcLocalPlacement& global_placement, const Schema::IfcGeometricRepresentationSubContext& segment_axis_subcontext, std::vector<Schema::IfcSegment>& curve_segments, std::vector<Schema::IfcObjectDefinition>& segments) {
auto cs_iter = curve_segments.begin();
auto s_iter = segments.begin();
for (; cs_iter != curve_segments.end(); cs_iter++, s_iter++) {
auto& curve_segment = *cs_iter;
auto alignment_segment = (*s_iter).as<Schema::IfcAlignmentSegment>();
typename aggregate_of<typename Schema::IfcRepresentationItem>::ptr representation_items(new aggregate_of<typename Schema::IfcRepresentationItem>());
representation_items->push(curve_segment);
auto axis_representation = file.create<Schema::IfcShapeRepresentation>();
axis_representation.setContextOfItems(segment_axis_subcontext);
axis_representation.setRepresentationIdentifier("Axis");
axis_representation.setRepresentationType("Segment");
axis_representation.setItems({curve_segment});
auto axis_representation = new Schema::IfcShapeRepresentation(segment_axis_subcontext, std::string("Axis"), std::string("Segment"), representation_items);
file.addEntity(axis_representation);
auto product = file.create<Schema::IfcProductDefinitionShape>();
product.setRepresentations({axis_representation});
typename aggregate_of<typename Schema::IfcRepresentation>::ptr representations(new aggregate_of<typename Schema::IfcRepresentation>());
representations->push(axis_representation);
auto product = new Schema::IfcProductDefinitionShape(boost::none, boost::none, representations);
file.addEntity(product);
alignment_segment->setObjectPlacement(global_placement);
alignment_segment->setRepresentation(product);
alignment_segment.setObjectPlacement(global_placement);
alignment_segment.setRepresentation(product);
}
}
int main() {
IfcHierarchyHelper<Schema> file;
hierarchy_helper<Schema> file;
auto project = setup_project(file);
auto geometric_representation_context = file.getRepresentationContext(std::string("Model")); // creates the representation context if it doesn't already exist
auto axis_model_representation_subcontext = new Schema::IfcGeometricRepresentationSubContext(std::string("Axis"), std::string("Model"), geometric_representation_context, boost::none, Schema::IfcGeometricProjectionEnum::IfcGeometricProjection_MODEL_VIEW, boost::none);
file.addEntity(axis_model_representation_subcontext);
auto axis_model_representation_subcontext = file.create<Schema::IfcGeometricRepresentationSubContext>();
axis_model_representation_subcontext.setContextIdentifier("Axis");
axis_model_representation_subcontext.setContextType("Model");
axis_model_representation_subcontext.setParentContext(geometric_representation_context);
axis_model_representation_subcontext.setTargetView(Schema::IfcGeometricProjectionEnum::IfcGeometricProjection_MODEL_VIEW);
auto global_placement = file.addLocalPlacement();
@@ -251,86 +326,91 @@ int main() {
double angle_4 = to_radian(289.0395);
// create containers to store the curve segments
typename aggregate_of<typename Schema::IfcSegment>::ptr horizontal_curve_segments(new aggregate_of<typename Schema::IfcSegment>()); // geometry
typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr horizontal_segments(new aggregate_of<typename Schema::IfcObjectDefinition>()); // business logic
std::vector<Schema::IfcSegment> horizontal_curve_segments; // geometry
std::vector<Schema::IfcObjectDefinition> horizontal_segments; // business logic
//
// Build the horizontal alignment segments
//
// POB to PC1
auto curve_segment_1 = create_tangent(pob, angle_1, run_1);
horizontal_curve_segments->push(curve_segment_1.first);
horizontal_segments->push(curve_segment_1.second);
auto curve_segment_1 = create_tangent(file, pob, angle_1, run_1);
horizontal_curve_segments.push_back(curve_segment_1.first);
horizontal_segments.push_back(curve_segment_1.second);
// Curve 1
auto curve_segment_2 = create_hcurve(pc1, angle_1, rc_1, lc_1);
horizontal_curve_segments->push(curve_segment_2.first);
horizontal_segments->push(curve_segment_2.second);
auto curve_segment_2 = create_hcurve(file, pc1, angle_1, rc_1, lc_1);
horizontal_curve_segments.push_back(curve_segment_2.first);
horizontal_segments.push_back(curve_segment_2.second);
// PT1 to PC2
auto curve_segment_3 = create_tangent(pt1, angle_2, run_2);
horizontal_curve_segments->push(curve_segment_3.first);
horizontal_segments->push(curve_segment_3.second);
auto curve_segment_3 = create_tangent(file, pt1, angle_2, run_2);
horizontal_curve_segments.push_back(curve_segment_3.first);
horizontal_segments.push_back(curve_segment_3.second);
// Curve 2
auto curve_segment_4 = create_hcurve(pc2, angle_2, rc_2, lc_2);
horizontal_curve_segments->push(curve_segment_4.first);
horizontal_segments->push(curve_segment_4.second);
auto curve_segment_4 = create_hcurve(file, pc2, angle_2, rc_2, lc_2);
horizontal_curve_segments.push_back(curve_segment_4.first);
horizontal_segments.push_back(curve_segment_4.second);
// PT2 to PC3
auto curve_segment_5 = create_tangent(pt2, angle_3, run_3);
horizontal_curve_segments->push(curve_segment_5.first);
horizontal_segments->push(curve_segment_5.second);
auto curve_segment_5 = create_tangent(file, pt2, angle_3, run_3);
horizontal_curve_segments.push_back(curve_segment_5.first);
horizontal_segments.push_back(curve_segment_5.second);
// Curve 3
auto curve_segment_6 = create_hcurve(pc3, angle_3, rc_3, lc_3);
horizontal_curve_segments->push(curve_segment_6.first);
horizontal_segments->push(curve_segment_6.second);
auto curve_segment_6 = create_hcurve(file, pc3, angle_3, rc_3, lc_3);
horizontal_curve_segments.push_back(curve_segment_6.first);
horizontal_segments.push_back(curve_segment_6.second);
// PT3 to POE
auto curve_segment_7 = create_tangent(pt3, angle_4, run_4);
horizontal_curve_segments->push(curve_segment_7.first);
horizontal_segments->push(curve_segment_7.second);
auto curve_segment_7 = create_tangent(file, pt3, angle_4, run_4);
horizontal_curve_segments.push_back(curve_segment_7.first);
horizontal_segments.push_back(curve_segment_7.second);
// Zero-length terminator segment
auto terminator_segment = create_tangent(poe, angle_4, 0.0);
terminator_segment.first->setTransition(Schema::IfcTransitionCode::IfcTransitionCode_DISCONTINUOUS);
horizontal_curve_segments->push(terminator_segment.first);
horizontal_segments->push(terminator_segment.second);
auto terminator_segment = create_tangent(file, poe, angle_4, 0.0);
terminator_segment.first.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_DISCONTINUOUS);
horizontal_curve_segments.push_back(terminator_segment.first);
horizontal_segments.push_back(terminator_segment.second);
//
// Create the horizontal alignment (IfcAlignmentHorizontal) and nest alignment segments
//
auto horizontal_alignment = new Schema::IfcAlignmentHorizontal(IfcParse::IfcGlobalId(), nullptr, std::string("Horizontal Alignment"), boost::none, boost::none, nullptr, nullptr);
file.addEntity(horizontal_alignment);
auto nests_horizontal_segments = new Schema::IfcRelNests(IfcParse::IfcGlobalId(), nullptr, boost::none, std::string("Nests horizontal alignment segments with horizontal alignment"), horizontal_alignment, horizontal_segments);
file.addEntity(nests_horizontal_segments);
auto horizontal_alignment = file.create<Schema::IfcAlignmentHorizontal>();
horizontal_alignment.setGlobalId(ifcopenshell::global_id());
horizontal_alignment.setName("Example Alignment");
auto nests_horizontal_segments = file.create<Schema::IfcRelNests>();
nests_horizontal_segments.setGlobalId(ifcopenshell::global_id());
nests_horizontal_segments.setName("Nests horizontal alignment segments with horizontal alignment");
nests_horizontal_segments.setRelatingObject(horizontal_alignment);
nests_horizontal_segments.setRelatedObjects(horizontal_segments);
//
// Create plan view footprint model representation for the horizontal alignment
//
// start by defining a composite curve composed of the horizonal curve segments
auto composite_curve = new Schema::IfcCompositeCurve(horizontal_curve_segments, false /*not self-intersecting*/);
file.addEntity(composite_curve);
// the composite curve is a representation item
typename aggregate_of<typename Schema::IfcRepresentationItem>::ptr alignment_representation_items(new aggregate_of<typename Schema::IfcRepresentationItem>());
alignment_representation_items->push(composite_curve);
auto composite_curve = file.create<Schema::IfcCompositeCurve>();
composite_curve.setSegments(horizontal_curve_segments);
composite_curve.setSelfIntersect(false);
// create the footprint representation
auto footprint_shape_representation = new Schema::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("FootPrint"), std::string("Curve2D"), alignment_representation_items);
file.addEntity(footprint_shape_representation);
auto footprint_shape_representation = file.create<Schema::IfcShapeRepresentation>();
footprint_shape_representation.setContextOfItems(axis_model_representation_subcontext);
footprint_shape_representation.setRepresentationIdentifier("Footprint");
footprint_shape_representation.setRepresentationType("Curve2D");
// the composite curve is a representation item
footprint_shape_representation.setItems({composite_curve});
//
// Define vertical profile segments
//
// create containers to store the curve segments
typename aggregate_of<typename Schema::IfcSegment>::ptr vertical_curve_segments(new aggregate_of<typename Schema::IfcSegment>()); // geometry
typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr vertical_segments(new aggregate_of<typename Schema::IfcObjectDefinition>()); // business logic
std::vector<Schema::IfcSegment> vertical_curve_segments; // geometry
std::vector<Schema::IfcObjectDefinition> vertical_segments; // business logic
// define key profile points
auto vpob = file.addDoublet<Schema::IfcCartesianPoint>(0.0, 100.0); // beginning
@@ -349,80 +429,86 @@ int main() {
//
// Grade start to VPC1
auto vertical_profile_segment_1 = create_gradient(vpob, 1.75 / 100, 1200);
vertical_curve_segments->push(vertical_profile_segment_1.first);
vertical_segments->push(vertical_profile_segment_1.second);
auto vertical_profile_segment_1 = create_gradient(file, vpob, 1.75 / 100, 1200);
vertical_curve_segments.push_back(vertical_profile_segment_1.first);
vertical_segments.push_back(vertical_profile_segment_1.second);
// Vertical Curve 1
auto vertical_profile_segment_2 = create_vcurve(vpc1, 1.75 / 100, -1.0 / 100, 1600);
vertical_curve_segments->push(vertical_profile_segment_2.first);
vertical_segments->push(vertical_profile_segment_2.second);
auto vertical_profile_segment_2 = create_vcurve(file, vpc1, 1.75 / 100, -1.0 / 100, 1600);
vertical_curve_segments.push_back(vertical_profile_segment_2.first);
vertical_segments.push_back(vertical_profile_segment_2.second);
// Grade VPT1 to VPC2
auto vertical_profile_segment_3 = create_gradient(vpt1, -1.0 / 100, 1600);
vertical_curve_segments->push(vertical_profile_segment_3.first);
vertical_segments->push(vertical_profile_segment_3.second);
auto vertical_profile_segment_3 = create_gradient(file, vpt1, -1.0 / 100, 1600);
vertical_curve_segments.push_back(vertical_profile_segment_3.first);
vertical_segments.push_back(vertical_profile_segment_3.second);
// Vertical Curve 2
auto vertical_profile_segment_4 = create_vcurve(vpc2, -1.0 / 100, 2.0 / 100, 1200);
vertical_curve_segments->push(vertical_profile_segment_4.first);
vertical_segments->push(vertical_profile_segment_4.second);
auto vertical_profile_segment_4 = create_vcurve(file, vpc2, -1.0 / 100, 2.0 / 100, 1200);
vertical_curve_segments.push_back(vertical_profile_segment_4.first);
vertical_segments.push_back(vertical_profile_segment_4.second);
// Grade PVT2 to VPC3
auto vertical_profile_segment_5 = create_gradient(vpt2, 2.0 / 100, 800);
vertical_curve_segments->push(vertical_profile_segment_5.first);
vertical_segments->push(vertical_profile_segment_5.second);
auto vertical_profile_segment_5 = create_gradient(file, vpt2, 2.0 / 100, 800);
vertical_curve_segments.push_back(vertical_profile_segment_5.first);
vertical_segments.push_back(vertical_profile_segment_5.second);
// Vertical Curve 3
auto vertical_profile_segment_6 = create_vcurve(vpc3, 2.0 / 100, -2.0 / 100, 2000);
vertical_curve_segments->push(vertical_profile_segment_6.first);
vertical_segments->push(vertical_profile_segment_6.second);
auto vertical_profile_segment_6 = create_vcurve(file, vpc3, 2.0 / 100, -2.0 / 100, 2000);
vertical_curve_segments.push_back(vertical_profile_segment_6.first);
vertical_segments.push_back(vertical_profile_segment_6.second);
// Grade PVT3 to VPC4
auto vertical_profile_segment_7 = create_gradient(vpt3, -2.0 / 100, 1000);
vertical_curve_segments->push(vertical_profile_segment_7.first);
vertical_segments->push(vertical_profile_segment_7.second);
auto vertical_profile_segment_7 = create_gradient(file, vpt3, -2.0 / 100, 1000);
vertical_curve_segments.push_back(vertical_profile_segment_7.first);
vertical_segments.push_back(vertical_profile_segment_7.second);
// Vertical Curve 4
auto vertical_profile_segment_8 = create_vcurve(vpc4, -2.0 / 100, -0.5 / 100, 800);
vertical_curve_segments->push(vertical_profile_segment_8.first);
vertical_segments->push(vertical_profile_segment_8.second);
auto vertical_profile_segment_8 = create_vcurve(file, vpc4, -2.0 / 100, -0.5 / 100, 800);
vertical_curve_segments.push_back(vertical_profile_segment_8.first);
vertical_segments.push_back(vertical_profile_segment_8.second);
// Grade VPT4 to End
auto vertical_profile_segment_9 = create_gradient(vpt4, -0.5 / 100, 2600);
vertical_curve_segments->push(vertical_profile_segment_9.first);
vertical_segments->push(vertical_profile_segment_9.second);
auto vertical_profile_segment_9 = create_gradient(file, vpt4, -0.5 / 100, 2600);
vertical_curve_segments.push_back(vertical_profile_segment_9.first);
vertical_segments.push_back(vertical_profile_segment_9.second);
// Zero-length terminator
auto vertical_terminator_segment = create_gradient(vpoe, -0.5 / 100, 0.0);
vertical_terminator_segment.first->setTransition(Schema::IfcTransitionCode::IfcTransitionCode_DISCONTINUOUS);
vertical_curve_segments->push(vertical_terminator_segment.first);
vertical_segments->push(vertical_terminator_segment.second);
auto vertical_terminator_segment = create_gradient(file, vpoe, -0.5 / 100, 0.0);
vertical_terminator_segment.first.setTransition(Schema::IfcTransitionCode::IfcTransitionCode_DISCONTINUOUS);
vertical_curve_segments.push_back(vertical_terminator_segment.first);
vertical_segments.push_back(vertical_terminator_segment.second);
//
// Create the vertical alignment (IfcAlignmentVertical) and nest alignment segments
//
auto vertical_profile = new Schema::IfcAlignmentVertical(IfcParse::IfcGlobalId(), nullptr, std::string("Vertical Alignment"), boost::none, boost::none, nullptr, nullptr);
file.addEntity(vertical_profile);
auto vertical_profile = file.create<Schema::IfcAlignmentVertical>();
vertical_profile.setGlobalId(ifcopenshell::global_id());
vertical_profile.setName("Example Vertical Profile");
auto nests_vertical_segments = new Schema::IfcRelNests(IfcParse::IfcGlobalId(), nullptr, boost::none, std::string("Nests vertical alignment segments with vertical alignment"), vertical_profile, vertical_segments);
file.addEntity(nests_vertical_segments);
auto nests_vertical_segments = file.create<Schema::IfcRelNests>();
nests_vertical_segments.setGlobalId(ifcopenshell::global_id());
nests_vertical_segments.setName("Nests vertical alignment segments with vertical profile");
nests_vertical_segments.setRelatingObject(vertical_profile);
nests_vertical_segments.setRelatedObjects(vertical_segments);
//
// Create profile view axis model representation for the vertical profile
//
// start by defining a gradient curve composed of the vertical curve segments and associated with the horizontal composite curve
auto gradient_curve = new Schema::IfcGradientCurve(vertical_curve_segments, false, composite_curve, nullptr);
file.addEntity(gradient_curve);
// the gradient curve is a representation item
typename aggregate_of<typename Schema::IfcRepresentationItem>::ptr profile_representation_items(new aggregate_of<typename Schema::IfcRepresentationItem>());
profile_representation_items->push(gradient_curve);
auto gradient_curve = file.create<Schema::IfcGradientCurve>();
gradient_curve.setSegments(vertical_curve_segments);
gradient_curve.setSelfIntersect(false);
gradient_curve.setBaseCurve(composite_curve);
// create the axis representation
auto axis3d_shape_representation = new Schema::IfcShapeRepresentation(axis_model_representation_subcontext, std::string("Axis"), std::string("Curve3D"), profile_representation_items);
file.addEntity(axis3d_shape_representation);
auto axis3d_shape_representation = file.create<Schema::IfcShapeRepresentation>();
axis3d_shape_representation.setContextOfItems(axis_model_representation_subcontext);
axis3d_shape_representation.setRepresentationIdentifier("Axis");
axis3d_shape_representation.setRepresentationType("Curve3D");
// the gradient curve is a representation item
axis3d_shape_representation.setItems({gradient_curve});
// create axis representations for each segment
create_segment_representations(file, global_placement, axis_model_representation_subcontext, horizontal_curve_segments, horizontal_segments);
@@ -432,38 +518,39 @@ int main() {
// Create the IfcAlignment
//
// the alignment has two representations, a plan view footprint and a 3d curve
typename aggregate_of<typename Schema::IfcRepresentation>::ptr alignment_representations(new aggregate_of<typename Schema::IfcRepresentation>());
alignment_representations->push(footprint_shape_representation); // 2D footprint
alignment_representations->push(axis3d_shape_representation); // 3D curve
// create the alignment product definition
auto alignment_product = new Schema::IfcProductDefinitionShape(std::string("Alignment Product Definition Shape"), boost::none, alignment_representations);
auto alignment_product = file.create<Schema::IfcProductDefinitionShape>();
alignment_product.setName("Alignment Product Definition Shape");
// the alignment has two representations, a plan view footprint and a 3d curve
alignment_product.setRepresentations({footprint_shape_representation, axis3d_shape_representation});
// create the alignment
auto alignment = new Schema::IfcAlignment(IfcParse::IfcGlobalId(), nullptr, std::string("Example Alignment"), boost::none, boost::none, global_placement, alignment_product, boost::none);
file.addEntity(alignment);
auto alignment = file.create<Schema::IfcAlignment>();
alignment.setGlobalId(ifcopenshell::global_id());
alignment.setName("Example Alignment");
alignment.setObjectPlacement(global_placement);
alignment.setRepresentation(alignment_product);
// Nest the IfcAlignmentHorizontal and IfcAlignmentVertical with the IfcAlignment to complete the business logic
// 4.1.4.4.1 Alignments nest horizontal and vertical layouts
// https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/concepts/Object_Composition/Nesting/Alignment_Layouts/content.html
typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr alignment_layout_list(new aggregate_of<typename Schema::IfcObjectDefinition>());
alignment_layout_list->push(horizontal_alignment);
alignment_layout_list->push(vertical_profile);
auto nests_alignment_layouts = new Schema::IfcRelNests(IfcParse::IfcGlobalId(), nullptr, std::string("Nest horizontal and vertical alignment layouts with the alignment"), boost::none, alignment, alignment_layout_list);
file.addEntity(nests_alignment_layouts);
auto nests_alignment_layouts = file.create<Schema::IfcRelNests>();
nests_alignment_layouts.setGlobalId(ifcopenshell::global_id());
nests_alignment_layouts.setName("Nest horizontal and vertical alignment layouts with the alignment");
nests_alignment_layouts.setRelatingObject(alignment);
nests_alignment_layouts.setRelatedObjects({horizontal_alignment, vertical_profile});
// Define the relationship with the project
// IFC 4.1.4.1.1 "Every IfcAlignment must be related to IfcProject using the IfcRelAggregates relationship"
// https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/concepts/Object_Composition/Aggregation/Alignment_Aggregation_To_Project/content.html
// IfcProject <-> IfcRelAggregates <-> IfcAlignment
typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr list_of_alignments_in_project(new aggregate_of<typename Schema::IfcObjectDefinition>());
list_of_alignments_in_project->push(alignment);
auto aggregate_alignments_with_project = new Schema::IfcRelAggregates(IfcParse::IfcGlobalId(), nullptr, std::string("Alignments in project"), boost::none, project, list_of_alignments_in_project);
file.addEntity(aggregate_alignments_with_project);
auto aggregate_alignments_with_project = file.create<Schema::IfcRelAggregates>();
aggregate_alignments_with_project.setGlobalId(ifcopenshell::global_id());
aggregate_alignments_with_project.setName("Alignments in project");
aggregate_alignments_with_project.setRelatingObject(project);
aggregate_alignments_with_project.setRelatedObjects({alignment});
// Define the spatial structure of the alignment with respect to the site
// IFC 4.1.5.1 alignment is referenced in spatial structure of an IfcSpatialElement. In this case IfcSite is the highest level IfcSpatialElement
@@ -471,24 +558,26 @@ int main() {
// IfcSite <-> IfcRelReferencedInSpatialStructure <-> IfcAlignment
// This means IfcAlignment is not part of the IfcSite (it is not an aggregate component) but instead IfcAlignment is used within
// the IfcSite by reference. This implies an IfcAlignment can traverse many IfcSite instances within an IfcProject
typename Schema::IfcSpatialReferenceSelect::list::ptr list_alignments_referenced_in_site(new Schema::IfcSpatialReferenceSelect::list);
list_alignments_referenced_in_site->push(alignment);
std::vector<Schema::IfcSpatialReferenceSelect> list_alignments_referenced_in_site{alignment};
// this alignment traverse 3 bridge sites.
for (int i = 1; i <= 3; i++) {
std::ostringstream os;
os << "Site of Bridge " << i;
auto site = file.addSite(project, nullptr);
site->setName(os.str());
auto site = file.addSite(project);
site.setName(os.str());
std::ostringstream description;
description << "Alignments referenced into the spatial structure of Bridge Site " << i;
auto rel_referenced_in_spatial_structure = new Schema::IfcRelReferencedInSpatialStructure(IfcParse::IfcGlobalId(), nullptr, boost::none, description.str(), list_alignments_referenced_in_site, site);
file.addEntity(rel_referenced_in_spatial_structure);
auto rel_referenced_in_spatial_structure = file.create<Schema::IfcRelReferencedInSpatialStructure>();
rel_referenced_in_spatial_structure.setGlobalId(ifcopenshell::global_id());
rel_referenced_in_spatial_structure.setDescription(description.str());
rel_referenced_in_spatial_structure.setRelatedElements(list_alignments_referenced_in_site);
rel_referenced_in_spatial_structure.setRelatingStructure(site);
}
// That's it - save the model to a file
std::ofstream ofs("IfcAlignment.ifc");
std::ofstream ofs("FHWA_Bridge_Geometry_Alignment_Example.ifc");
ofs << file;
}
+374 -324
View File
@@ -36,12 +36,11 @@
#include <Precision.hxx>
#define IfcSchema Ifc2x3
#include "ifcparse/macros.h"
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcBaseClass.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/macros.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/hierarchy_helper.h"
#include "ifcgeom/Serialization/Serialization.h"
#include "../ifcgeom/Serialization/Serialization.h"
#if USE_VLD
#include <vld.h>
@@ -50,7 +49,7 @@
using namespace std::string_literals;
// Some convenience typedefs and definitions.
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
typedef std::pair<double, double> XY;
boost::none_t const null = boost::none;
@@ -59,235 +58,276 @@ void createGroundShape(TopoDS_Shape& shape);
int main() {
// The IfcHierarchyHelper is a subclass of the regular IfcFile that provides several
// convenience functions for working with geometry in IFC files.
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname("IfcOpenHouse.ifc");
// The hierarchy_helper is a subclass of the regular file that provides several
// convenience functions for working with geometry in IFC files.
hierarchy_helper<IfcSchema> file;
file.header().file_name().setname("IfcOpenHouse.ifc");
// Start by adding a wall to the file, initially leaving most attributes blank.
IfcSchema::IfcWallStandardCase* south_wall = new IfcSchema::IfcWallStandardCase(
guid(), // GlobalId
0, // OwnerHistory
"South wall"s, // Name
null, // Description
null, // ObjectType
0, // ObjectPlacement
0, // Representation
null // Tag
// Start by adding a wall to the file, initially leaving most attributes blank.
auto south_wall = file.create<IfcSchema::IfcWallStandardCase>();
south_wall.setGlobalId(guid());
south_wall.setName("South wall");
#ifdef USE_IFC4
, IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD
south_wall.setPredefinedType(IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD);
#endif
);
file.addBuildingProduct(south_wall);
file.addBuildingProduct(south_wall);
// By adding a wall, a hierarchy has been automatically created that consists of the following
// structure: IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey > IfcWall
// By adding a wall, a hierarchy has been automatically created that consists of the following
// structure: IfcProject > IfcSite > IfcBuilding > IfcBuildingStorey > IfcWall
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
// project, which has been created automatically.
file.getSingle<IfcSchema::IfcProject>()->setName("IfcOpenHouse"s);
// Lateron changing the name of the IfcProject can be done by obtaining a reference to the
// project, which has been created automatically.
file.getSingle<IfcSchema::IfcProject>().setName("IfcOpenHouse"s);
// An IfcOwnerHistory has been initialized as well, which should be assigned to the wall.
south_wall->setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
// An IfcOwnerHistory has been initialized as well, which should be assigned to the wall.
south_wall.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
// The wall will be shaped as a box, with the dimensions specified in millimeters. The resulting
// product definition will consist of both a body representation as well as an axis representation
// that runs over the centerline of the box in the X-axis.
IfcSchema::IfcProductDefinitionShape* south_wall_shape = file.addAxisBox(10000, 360, 3000);
// The wall will be shaped as a box, with the dimensions specified in millimeters. The resulting
// product definition will consist of both a body representation as well as an axis representation
// that runs over the centerline of the box in the X-axis.
auto south_wall_shape = file.addAxisBox(10000, 360, 3000);
// Obtain a reference to the placement of the IfcBuildingStorey in order to create a hierarchy
// of placements for the products
IfcSchema::IfcObjectPlacement* storey_placement = file.getSingle<IfcSchema::IfcBuildingStorey>()->ObjectPlacement();
// Obtain a reference to the placement of the IfcBuildingStorey in order to create a hierarchy
// of placements for the products
auto storey_placement = file.getSingle<IfcSchema::IfcBuildingStorey>().ObjectPlacement();
// The shape has to be assigned to the representation of the wall and is placed at the origin
// of the coordinate system.
south_wall->setRepresentation(south_wall_shape);
south_wall->setObjectPlacement(file.addLocalPlacement(storey_placement));
// The shape has to be assigned to the representation of the wall and is placed at the origin
// of the coordinate system.
south_wall.setRepresentation(south_wall_shape);
south_wall.setObjectPlacement(file.addLocalPlacement(storey_placement));
// A pale white colour is assigned to the wall.
IfcSchema::IfcPresentationStyleAssignment* wall_colour = setSurfaceColour(file, south_wall_shape, 0.75, 0.73, 0.68);
// A pale white colour is assigned to the wall.
auto wall_colour = setSurfaceColour(file, south_wall_shape, 0.75, 0.73, 0.68);
// Now create a footing for the wall to rest on.
IfcSchema::IfcFooting* footing = new IfcSchema::IfcFooting(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
"Footing"s, null, null, 0, 0, null, IfcSchema::IfcFootingTypeEnum::IfcFootingType_STRIP_FOOTING);
// Now create a footing for the wall to rest on.
auto footing = file.create<IfcSchema::IfcFooting>();
footing.setGlobalId(guid());
footing.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
footing.setName("Footing");
footing.setPredefinedType(IfcSchema::IfcFootingTypeEnum::IfcFootingType_STRIP_FOOTING);
file.addBuildingProduct(footing);
file.addBuildingProduct(footing);
// The footing will span the entire floor plan of our building. The IfcRepresentationContext is
// something that has been created automatically as well, but representations could have been
// assigned to a specific context, for example to add a two dimensional plan representation as well.
footing->setRepresentation(file.addBox(10100, 5460, 2000));
footing->setObjectPlacement(file.addLocalPlacement(storey_placement, 0, 2500, -2000));
// The footing will have a dark gray colour
IfcSchema::IfcPresentationStyleAssignment* footing_colour = setSurfaceColour(file,footing->Representation(), 0.26, 0.22, 0.18);
// The footing will span the entire floor plan of our building. The IfcRepresentationContext is
// something that has been created automatically as well, but representations could have been
// assigned to a specific context, for example to add a two dimensional plan representation as well.
footing.setRepresentation(file.addBox(10100, 5460, 2000));
footing.setObjectPlacement(file.addLocalPlacement(storey_placement, 0, 2500, -2000));
// The footing will have a dark gray colour
auto footing_colour = setSurfaceColour(file, footing.Representation(), 0.26, 0.22, 0.18);
// IFC has two ways to apply boolean operations to geometry. IfcBooleanResults are commonly used
// to clip geometry to a surface, for example to a slanted roof. For openings that are filled
// with another element, for example a door or a window, an IfcOpeningElement is used instead.
// IFC has two ways to apply boolean operations to geometry. IfcBooleanResults are commonly used
// to clip geometry to a surface, for example to a slanted roof. For openings that are filled
// with another element, for example a door or a window, an IfcOpeningElement is used instead.
// An opening element is created with rectangular geometry:
IfcSchema::IfcOpeningElement* west_opening = new IfcSchema::IfcOpeningElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, file.addLocalPlacement(south_wall->ObjectPlacement(), -2500, 0, 400),
file.addBox(6000, 3630, 1600), null
// An opening element is created with rectangular geometry:
auto west_opening = file.create<IfcSchema::IfcOpeningElement>();
west_opening.setGlobalId(guid());
west_opening.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
west_opening.setObjectPlacement(file.addLocalPlacement(south_wall.ObjectPlacement(), -2500, 0, 400));
west_opening.setRepresentation(file.addBox(6000, 3630, 1600));
#ifdef USE_IFC4
, IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING
west_opening.setPredefinedType(IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING);
#endif
);
file.addEntity(west_opening);
// Relate the opening element to the wall.
IfcSchema::IfcRelVoidsElement* void_element = new IfcSchema::IfcRelVoidsElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, south_wall, west_opening);
file.addEntity(void_element);
// Relate the opening element to the wall.
auto void_element = file.create<IfcSchema::IfcRelVoidsElement>();
void_element.setGlobalId(guid());
void_element.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
void_element.setRelatingBuildingElement(south_wall);
void_element.setRelatedOpeningElement(west_opening);
// Now create an additional opening
IfcSchema::IfcOpeningElement* south_opening = new IfcSchema::IfcOpeningElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, file.addLocalPlacement(storey_placement, 3000, 0, 400),
file.addBox(1860, 3000, 1600), null
// Now create an additional opening
auto south_opening = file.create<IfcSchema::IfcOpeningElement>();
south_opening.setGlobalId(guid());
south_opening.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
south_opening.setObjectPlacement(file.addLocalPlacement(storey_placement, 3000, 0, 400));
south_opening.setRepresentation(file.addBox(1860, 3000, 1600));
#ifdef USE_IFC4
, IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING
south_opening.setPredefinedType(IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING);
#endif
);
file.addEntity(south_opening);
file.addEntity(new IfcSchema::IfcRelVoidsElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, south_wall, south_opening));
// Create a roof element that will consist of two slabs:
IfcSchema::IfcRoof* roof = new IfcSchema::IfcRoof(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "Roof"s, null, null,
file.addLocalPlacement(storey_placement), 0, null, IfcSchema::IfcRoofTypeEnum::IfcRoofType_GABLE_ROOF);
// The roof geometry is slanted 45 degrees by specifying a direction for the box extrusion
IfcSchema::IfcShapeRepresentation* roof_rep = file.addEmptyRepresentation();
file.addBox(roof_rep, 10200, 360, sqrt(2.0*2900*2900), 0, file.addPlacement3d(0, 0, 0, 0, 1, 0),
file.addTriplet<IfcSchema::IfcDirection>(0, -sqrt(0.5), sqrt(0.5)));
// Relate the opening element to the wall.
auto void_element2 = file.create<IfcSchema::IfcRelVoidsElement>();
void_element2.setGlobalId(guid());
void_element2.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
void_element2.setRelatingBuildingElement(south_wall);
void_element2.setRelatedOpeningElement(south_opening);
// CV-2x3-144: Roofs are aggregates and shall have at least one contained element and no own geometry
IfcSchema::IfcSlab* south_roof_part = new IfcSchema::IfcSlab(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "South roof"s,
null, null, 0, 0, null, IfcSchema::IfcSlabTypeEnum::IfcSlabType_ROOF);
// The geometry is instantiated by using IfcMappedItems. This way geometry definitions can
// be reused while maintaining the cardinality constraint that the ShapeOfProduct relation
// imposes on the IfcProductDefinitionShape. Note that this constrained is lifted in IFC4.
south_roof_part->setRepresentation(file.addMappedItem(roof_rep));
south_roof_part->setObjectPlacement(file.addLocalPlacement(roof->ObjectPlacement(), 0, -400, 2700));
// The same roof geometry is re-used on the north side of the roof, by inverting the X-axis of
// the local placement the roof is rotated 180 degrees around the Z-axis
IfcSchema::IfcSlab* north_roof_part = new IfcSchema::IfcSlab(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "North roof"s,
null, null, 0, 0, null, IfcSchema::IfcSlabTypeEnum::IfcSlabType_ROOF);
north_roof_part->setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
north_roof_part->setRepresentation(file.addMappedItem(roof_rep));
north_roof_part->setObjectPlacement(file.addLocalPlacement(roof->ObjectPlacement(), 0, 5400, 2700, 0, 0, 1, -1, 0, 0));
IfcSchema::IfcObjectDefinition::list::ptr roof_parts(new IfcSchema::IfcObjectDefinition::list);
roof_parts->push(south_roof_part);
roof_parts->push(north_roof_part);
IfcSchema::IfcRelDecomposes* roof_decomposition = new IfcSchema::IfcRelAggregates(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, roof, roof_parts);
file.addEntity(roof_decomposition);
file.addBuildingProduct(south_roof_part);
file.addBuildingProduct(north_roof_part);
file.addBuildingProduct(roof);
setSurfaceColour(file, roof_rep, 0.24, 0.08, 0.04);
// Copy the south wall to the north
IfcSchema::IfcWallStandardCase* north_wall = new IfcSchema::IfcWallStandardCase(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "North wall"s,
null, null, file.addLocalPlacement(storey_placement, 0, 5000, 0), file.addAxisBox(10000, 360, 3000), null
// Create a roof element that will consist of two slabs:
auto roof = file.create<IfcSchema::IfcRoof>();
roof.setGlobalId(guid());
roof.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
roof.setName("Roof");
roof.setObjectPlacement(file.addLocalPlacement(storey_placement));
#ifdef USE_IFC4
, IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD
#endif
);
file.addBuildingProduct(north_wall);
setSurfaceColour(file,north_wall->Representation(), wall_colour);
roof.setPredefinedType(IfcSchema::IfcRoofTypeEnum::IfcRoofType_GABLE_ROOF);
#else
roof.setShapeType(IfcSchema::IfcRoofTypeEnum::IfcRoofType_GABLE_ROOF);
#endif
// Two identical representations are created for the two remaining walls. Mapped items
// are not used, because it is not allowed by the standard for wall body representations.
// MappedItems are not allowed for Axis representations as per CV-2x3-161
IfcSchema::IfcProductDefinitionShape* clipped_wall_body_reps[2];
for (int i = 0; i < 2; ++i) {
IfcSchema::IfcShapeRepresentation* body = file.addEmptyRepresentation();
file.addBox(body, 5000, 360, 6000);
// The wall geometry is clipped using two IfcHalfSpaceSolids, created from an
// 'axis 3d placement' that specifies the plane against which the geometry is clipped.
file.clipRepresentation(body, file.addPlacement3d(-2500, 0, 3000, -1, 0, 1), false);
file.clipRepresentation(body, file.addPlacement3d(2500, 0, 3000, 1, 0, 1), false);
setSurfaceColour(file, body, wall_colour);
// The roof geometry is slanted 45 degrees by specifying a direction for the box extrusion
auto roof_rep = file.addEmptyRepresentation();
file.addBox(roof_rep, 10200, 360, sqrt(2.0 * 2900 * 2900), IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d(0, 0, 0, 0, 1, 0), file.addTriplet<IfcSchema::IfcDirection>(0, -sqrt(0.5), sqrt(0.5)));
IfcSchema::IfcShapeRepresentation* axis = file.addEmptyRepresentation("Axis", "Curve2D");
file.addAxis(axis, 5000);
// CV-2x3-144: Roofs are aggregates and shall have at least one contained element and no own geometry
auto south_roof_part = file.create<IfcSchema::IfcSlab>();
south_roof_part.setGlobalId(guid());
south_roof_part.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
south_roof_part.setName("South roof");
south_roof_part.setPredefinedType(IfcSchema::IfcSlabTypeEnum::IfcSlabType_ROOF);
IfcSchema::IfcRepresentation::list::ptr reps(new IfcSchema::IfcRepresentation::list);
reps->push(body);
reps->push(axis);
clipped_wall_body_reps[i] = new IfcSchema::IfcProductDefinitionShape(null, null, reps);
}
// The geometry is instantiated by using IfcMappedItems. This way geometry definitions can
// be reused while maintaining the cardinality constraint that the ShapeOfProduct relation
// imposes on the IfcProductDefinitionShape. Note that this constrained is lifted in IFC4.
south_roof_part.setRepresentation(file.addMappedItem(roof_rep));
south_roof_part.setObjectPlacement(file.addLocalPlacement(roof.ObjectPlacement(), 0, -400, 2700));
// Now create a wall on the east of the building, again starting with just a box shape
IfcSchema::IfcWallStandardCase* east_wall = new IfcSchema::IfcWallStandardCase(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
"East wall"s, null, null, file.addLocalPlacement(storey_placement, 4820, 2500, 0, 0, 0, 1, 0, 1, 0), clipped_wall_body_reps[0], null
// The same roof geometry is re-used on the north side of the roof, by inverting the X-axis of
// the local placement the roof is rotated 180 degrees around the Z-axis
auto north_roof_part = file.create<IfcSchema::IfcSlab>();
north_roof_part.setGlobalId(guid());
north_roof_part.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
north_roof_part.setName("North roof");
north_roof_part.setPredefinedType(IfcSchema::IfcSlabTypeEnum::IfcSlabType_ROOF);
north_roof_part.setRepresentation(file.addMappedItem(roof_rep));
north_roof_part.setObjectPlacement(file.addLocalPlacement(roof.ObjectPlacement(), 0, 5400, 2700, 0, 0, 1, -1, 0, 0));
{
auto rel = file.create<IfcSchema::IfcRelAggregates>();
rel.setGlobalId(guid());
rel.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
rel.setRelatingObject(roof);
rel.setRelatedObjects({south_roof_part, north_roof_part});
}
file.addBuildingProduct(south_roof_part);
file.addBuildingProduct(north_roof_part);
file.addBuildingProduct(roof);
setSurfaceColour(file, roof_rep, 0.24, 0.08, 0.04);
// Copy the south wall to the north
auto north_wall = file.create<IfcSchema::IfcWallStandardCase>();
north_wall.setGlobalId(guid());
north_wall.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
north_wall.setName("North wall");
north_wall.setObjectPlacement(file.addLocalPlacement(storey_placement, 0, 5000, 0));
north_wall.setRepresentation(file.addAxisBox(10000, 360, 3000));
#ifdef USE_IFC4
, IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD
#endif
);
file.addBuildingProduct(east_wall);
north_wall.setPredefinedType(IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD);
#endif
// The east wall is copied to the west location of the house
IfcSchema::IfcWallStandardCase* west_wall = new IfcSchema::IfcWallStandardCase(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
"West wall"s, null, null, file.addLocalPlacement(storey_placement, -4820, 2500, 0, 0, 0, 1, 0, -1, 0), clipped_wall_body_reps[1], null
file.addBuildingProduct(north_wall);
setSurfaceColour(file, north_wall.Representation(), wall_colour);
// Two identical representations are created for the two remaining walls. Mapped items
// are not used, because it is not allowed by the standard for wall body representations.
// MappedItems are not allowed for Axis representations as per CV-2x3-161
IfcSchema::IfcProductDefinitionShape clipped_wall_body_reps[2];
for (int i = 0; i < 2; ++i) {
auto body = file.addEmptyRepresentation();
file.addBox(body, 5000, 360, 6000);
// The wall geometry is clipped using two IfcHalfSpaceSolids, created from an
// 'axis 3d placement' that specifies the plane against which the geometry is clipped.
file.clipRepresentation(body, file.addPlacement3d(-2500, 0, 3000, -1, 0, 1), false);
file.clipRepresentation(body, file.addPlacement3d(2500, 0, 3000, 1, 0, 1), false);
setSurfaceColour(file, body, wall_colour);
auto axis = file.addEmptyRepresentation("Axis", "Curve2D");
file.addAxis(axis, 5000);
clipped_wall_body_reps[i] = file.create<IfcSchema::IfcProductDefinitionShape>();
clipped_wall_body_reps[i].setRepresentations({body, axis});
}
// Now create a wall on the east of the building, again starting with just a box shape
auto east_wall = file.create<IfcSchema::IfcWallStandardCase>();
east_wall.setGlobalId(guid());
east_wall.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
east_wall.setName("East wall");
east_wall.setObjectPlacement(file.addLocalPlacement(storey_placement, 4820, 2500, 0, 0, 0, 1, 0, 1, 0));
east_wall.setRepresentation(clipped_wall_body_reps[0]);
#ifdef USE_IFC4
, IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD
#endif
);
file.addBuildingProduct(west_wall);
east_wall.setPredefinedType(IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD);
#endif
// The west wall is assigned an opening element we created for the south wall, opening elements are
// not shared across building elements, even if they share the same representation. Hence, the east
// wall will not feature this opening.
// NB: an Opening Element can only be used to create a single void within a single Element, as per:
// http://www.buildingsmart-tech.org/ifc/IFC2x3/TC1/html/ifcproductextension/lexical/ifcfeatureelementsubtraction.htm
file.addBuildingProduct(east_wall);
// Not all viewers support opening elements with mapped representations, hence an exact copy of the
// same subtraction box is instantiated for the otherwise identical opening element.
IfcSchema::IfcOpeningElement* west_opening_copy = new IfcSchema::IfcOpeningElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, file.addLocalPlacement(west_wall->ObjectPlacement(), 2500, -2500+4820, 400, 0, 0, 1, 0, 1, 0),
file.addBox(6000, 3630, 1600), null
// The east wall is copied to the west location of the house
auto west_wall = file.create<IfcSchema::IfcWallStandardCase>();
west_wall.setGlobalId(guid());
west_wall.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
west_wall.setName("West wall");
west_wall.setObjectPlacement(file.addLocalPlacement(storey_placement, -4820, 2500, 0, 0, 0, 1, 0, -1, 0));
west_wall.setRepresentation(clipped_wall_body_reps[1]);
#ifdef USE_IFC4
, IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING
#endif
);
file.addEntity(west_opening_copy);
file.addEntity(new IfcSchema::IfcRelVoidsElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, west_wall, west_opening_copy));
west_wall.setPredefinedType(IfcSchema::IfcWallTypeEnum::IfcWallType_STANDARD);
#endif
file.addBuildingProduct(west_wall);
// The west wall is assigned an opening element we created for the south wall, opening elements are
// not shared across building elements, even if they share the same representation. Hence, the east
// wall will not feature this opening.
// NB: an Opening Element can only be used to create a single void within a single Element, as per:
// http://www.buildingsmart-tech.org/ifc/IFC2x3/TC1/html/ifcproductextension/lexical/ifcfeatureelementsubtraction.htm
// Not all viewers support opening elements with mapped representations, hence an exact copy of the
// same subtraction box is instantiated for the otherwise identical opening element.
auto west_opening_copy = file.create<IfcSchema::IfcOpeningElement>();
west_opening_copy.setGlobalId(guid());
west_opening_copy.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
west_opening_copy.setObjectPlacement(file.addLocalPlacement(west_wall.ObjectPlacement(), 2500, -2500 + 4820, 400, 0, 0, 1, 0, 1, 0));
west_opening_copy.setRepresentation(file.addBox(6000, 3630, 1600));
#ifdef USE_IFC4
west_opening_copy.setPredefinedType(IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING);
#endif
{
auto rel = file.create<IfcSchema::IfcRelVoidsElement>();
rel.setGlobalId(guid());
rel.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
rel.setRelatingBuildingElement(west_wall);
rel.setRelatedOpeningElement(west_opening_copy);
}
// Up until now we have only used simple extrusions for the creation of the geometry. For the
// ground mesh of the IfcSite we will use a Nurbs surface created in Open Cascade. The surface
// will be tessellated using the deflection specified.
TopoDS_Shape shape;
createGroundShape(shape);
IfcSchema::IfcProductDefinitionShape* ground_representation = IfcGeom::tesselate(STRINGIFY(IfcSchema), shape, 100.)->as<IfcSchema::IfcProductDefinitionShape>();
file.getSingle<IfcSchema::IfcSite>()->setRepresentation(ground_representation);
auto ground_representation = IfcGeom::tesselate(file, shape, 100.).as<IfcSchema::IfcProductDefinitionShape>();
file.getSingle<IfcSchema::IfcSite>().setRepresentation(ground_representation);
GProp_GProps prop;
BRepGProp::SurfaceProperties(shape, prop);
const double site_area = prop.Mass() / 1000 / 1000;
IfcSchema::IfcProperty::list::ptr properties(new IfcSchema::IfcProperty::list);
properties->push(new IfcSchema::IfcPropertySingleValue("TotalArea", null, new IfcSchema::IfcAreaMeasure(site_area), 0));
IfcSchema::IfcPropertySet* pset = new IfcSchema::IfcPropertySet(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "Pset_SiteCommon"s, null, properties);
#ifdef USE_IFC4
IfcSchema::IfcObjectDefinition::list::ptr related_objs(new IfcSchema::IfcObjectDefinition::list);
#else
IfcSchema::IfcObject::list::ptr related_objs(new IfcSchema::IfcObject::list);
#endif
related_objs->push(file.getSingle<IfcSchema::IfcSite>());
IfcSchema::IfcRelDefinesByProperties* site_prop = new IfcSchema::IfcRelDefinesByProperties(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, related_objs, pset);
file.addEntity(site_prop);
auto total_area = file.create<IfcSchema::IfcPropertySingleValue>();
total_area.setName("TotalArea");
auto area = file.create<IfcSchema::IfcAreaMeasure>();
area.set_attribute_value(0, site_area);
total_area.setNominalValue(area);
IfcSchema::IfcRepresentation::list::ptr ground_reps = file.getSingle<IfcSchema::IfcSite>()->Representation()->Representations();
for (IfcSchema::IfcRepresentation::list::it it = ground_reps->begin(); it != ground_reps->end(); ++it) {
(*it)->setContextOfItems(file.getRepresentationContext("Model"));
auto pset = file.create<IfcSchema::IfcPropertySet>();
pset.setGlobalId(guid());
pset.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
pset.setName("Pset_SiteCommon");
pset.setHasProperties({total_area});
auto site_prop = file.create<IfcSchema::IfcRelDefinesByProperties>();
site_prop.setGlobalId(guid());
site_prop.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
site_prop.setRelatedObjects({file.getSingle<IfcSchema::IfcSite>()});
site_prop.setRelatingPropertyDefinition(pset);
auto ground_reps = file.getSingle<IfcSchema::IfcSite>().Representation().Representations();
for (auto& rep : ground_reps) {
rep.setContextOfItems(file.getRepresentationContext("Model"));
}
file.addEntity(ground_representation);
file.add_entity(ground_representation);
setSurfaceColour(file,ground_representation, 0.15, 0.25, 0.05);
// According to the Ifc2x3 schema an IfcWallStandardCase needs to have an IfcMaterialLayerSet
@@ -297,58 +337,28 @@ int main() {
// Some BIM authoring applications, such as Autodesk Revit, ignore the geometrical representation
// by and large and construct native walls using the layer thickness and reference line offset
// provided here.
#ifdef USE_IFC4
IfcSchema::IfcMaterial* material = new IfcSchema::IfcMaterial("Brick", null, null);
#else
IfcSchema::IfcMaterial* material = new IfcSchema::IfcMaterial("Brick");
#endif
IfcSchema::IfcMaterialLayer* layer = new IfcSchema::IfcMaterialLayer(
material,
360,
null
#ifdef USE_IFC4
, null
, null
, null
, null
#endif
);
IfcSchema::IfcMaterialLayer::list::ptr layers (new aggregate_of<IfcSchema::IfcMaterialLayer>());
layers->push(layer);
IfcSchema::IfcMaterialLayerSet* layer_set = new IfcSchema::IfcMaterialLayerSet(
layers,
"Wall"s
#ifdef USE_IFC4
, null
#endif
);
IfcSchema::IfcMaterialLayerSetUsage* layer_usage = new IfcSchema::IfcMaterialLayerSetUsage(
layer_set,
IfcSchema::IfcLayerSetDirectionEnum::IfcLayerSetDirection_AXIS2,
IfcSchema::IfcDirectionSenseEnum::IfcDirectionSense_POSITIVE,
-180
#ifdef USE_IFC4
, null
#endif
);
auto material = file.create<IfcSchema::IfcMaterial>();
material.setName("Brick");
IfcSchema::IfcRelAssociatesMaterial* associates_material = new IfcSchema::IfcRelAssociatesMaterial(
guid(),
file.getSingle<IfcSchema::IfcOwnerHistory>(),
null,
null,
#ifdef USE_IFC4
file.instances_by_type<IfcSchema::IfcWallStandardCase>()->as<IfcSchema::IfcDefinitionSelect>(),
#else
file.instances_by_type<IfcSchema::IfcWallStandardCase>()->as<IfcSchema::IfcRoot>(),
#endif
layer_usage);
auto layer = file.create<IfcSchema::IfcMaterialLayer>();
layer.setMaterial(material);
layer.setLayerThickness(360);
file.addEntity(material);
file.addEntity(layer);
file.addEntity(layer_set);
file.addEntity(layer_usage);
file.addEntity(associates_material);
auto layer_set = file.create<IfcSchema::IfcMaterialLayerSet>();
layer_set.setMaterialLayers({layer});
layer_set.setLayerSetName("Wall");
auto layer_usage = file.create<IfcSchema::IfcMaterialLayerSetUsage>();
layer_usage.setForLayerSet(layer_set);
layer_usage.setDirectionSense(IfcSchema::IfcDirectionSenseEnum::IfcDirectionSense_POSITIVE);
layer_usage.setLayerSetDirection(IfcSchema::IfcLayerSetDirectionEnum::IfcLayerSetDirection_AXIS1);
layer_usage.setOffsetFromReferenceLine(-180);
auto associates_material = file.create<IfcSchema::IfcRelAssociatesMaterial>();
associates_material.setGlobalId(guid());
associates_material.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
associates_material.setRelatedObjects({south_wall, north_wall, east_wall, west_wall});
associates_material.setRelatingMaterial(layer_usage);
// In addition, another common way to represent geometry in IFC files is to use extrusions of
// planar areas bounded by a polygon.
@@ -359,55 +369,88 @@ int main() {
stair_points.push_back(XY(500, 200));
stair_points.push_back(XY(500, 400));
stair_points.push_back(XY( 0, 400));
IfcSchema::IfcStairFlight* stair = new IfcSchema::IfcStairFlight(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, file.addLocalPlacement(storey_placement, 5050, 1000, 0, 0, 1, 0, 1, 0, 0),
file.addExtrudedPolyline(stair_points, 1200), null, 2, 2, 0.2, 0.25
auto stair = file.create<IfcSchema::IfcStairFlight>();
stair.setGlobalId(guid());
stair.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
stair.setObjectPlacement(file.addLocalPlacement(storey_placement, 5050, 1000, 0, 0, 1, 0, 1, 0, 0));
stair.setRepresentation(file.addExtrudedPolyline(stair_points, 1200));
#ifdef USE_IFC4
, IfcSchema::IfcStairFlightTypeEnum::IfcStairFlightType_STRAIGHT
stair.setNumberOfRisers(2);
#else
stair.setNumberOfRiser(2);
#endif
stair.setNumberOfTreads(2);
stair.setRiserHeight(0.2);
stair.setTreadLength(0.25);
#ifdef USE_IFC4
stair.setPredefinedType(IfcSchema::IfcStairFlightTypeEnum::IfcStairFlightType_STRAIGHT);
#endif
);
file.addBuildingProduct(stair);
setSurfaceColour(file, stair->Representation(), footing_colour);
setSurfaceColour(file, stair.Representation(), footing_colour);
IfcSchema::IfcOpeningElement* door_opening = new IfcSchema::IfcOpeningElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, file.addLocalPlacement(storey_placement, 5000-180, 2500-900, 0), file.addBox(1000, 1000, 2200), null
auto door_opening = file.create<IfcSchema::IfcOpeningElement>();
door_opening.setGlobalId(guid());
door_opening.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
door_opening.setObjectPlacement(file.addLocalPlacement(storey_placement, 5000 - 180, 2500 - 900, 0));
door_opening.setRepresentation(file.addBox(1000, 1000, 2200));
#ifdef USE_IFC4
, IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING
#endif
);
file.addEntity(door_opening);
file.addEntity(new IfcSchema::IfcRelVoidsElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, east_wall, door_opening));
door_opening.setPredefinedType(IfcSchema::IfcOpeningElementTypeEnum::IfcOpeningElementType_OPENING);
#endif
auto rel3 = file.create<IfcSchema::IfcRelVoidsElement>();
rel3.setGlobalId(guid());
rel3.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
rel3.setRelatingBuildingElement(east_wall);
rel3.setRelatedOpeningElement(door_opening);
// A single shape representation can contain multiple representiation items. This way a product
// can be a composition of multiple solids. The following door will be composed of four boxes
// which constitute the door and its frame.
IfcSchema::IfcDoor* door = new IfcSchema::IfcDoor(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, null,
file.addLocalPlacement(storey_placement, 4800, 1600, 0, 0, 0, 1, 0, 1, 0), 0, null, 2200, 1000
auto door = file.create<IfcSchema::IfcDoor>();
door.setGlobalId(guid());
door.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
door.setObjectPlacement(file.addLocalPlacement(storey_placement, 4800, 1600, 0, 0, 0, 1, 0, 1, 0));
door.setOverallWidth(1000);
door.setOverallHeight(2200);
#ifdef USE_IFC4
, IfcSchema::IfcDoorTypeEnum::IfcDoorType_DOOR
, IfcSchema::IfcDoorTypeOperationEnum::IfcDoorTypeOperation_SINGLE_SWING_LEFT
, null
door.setPredefinedType(IfcSchema::IfcDoorTypeEnum::IfcDoorType_DOOR);
door.setOperationType(IfcSchema::IfcDoorTypeOperationEnum::IfcDoorTypeOperation_SINGLE_SWING_LEFT);
#endif
);
door->setRepresentation(file.addBox(80, 80, 2120, 0, file.addPlacement3d(460, 0, 0)));
IfcSchema::IfcRepresentation::list::ptr door_representations = door->Representation()->Representations();
IfcSchema::IfcShapeRepresentation* door_body = 0;
for (IfcSchema::IfcRepresentation::list::it i = door_representations->begin(); i != door_representations->end(); ++i) {
IfcSchema::IfcRepresentation* rep = *i;
if (rep->declaration().is(IfcSchema::IfcShapeRepresentation::Class()) && rep->RepresentationIdentifier().get_value_or("") == "Body") {
door_body = (IfcSchema::IfcShapeRepresentation*) rep;
door.setRepresentation(file.addBox(80, 80, 2120, IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d(460, 0, 0)));
auto door_representations = door.Representation().Representations();
IfcSchema::IfcShapeRepresentation door_body;
for (auto& rep : door_representations) {
if (rep.declaration().is(IfcSchema::IfcShapeRepresentation::Class()) && rep.RepresentationIdentifier().value_or("") == "Body") {
door_body = rep.as<IfcSchema::IfcShapeRepresentation>();
}
}
file.addBox(door_body, 80, 80, 2120, 0, file.addPlacement3d(-460, 0, 0));
file.addBox(door_body, 1000, 80, 80, 0, file.addPlacement3d( 0, 0, 2120));
file.addBox(door_body, 80, 80, 2120, IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d(-460, 0, 0));
file.addBox(door_body, 1000, 80, 80, IfcSchema::IfcAxis2Placement2D{}, file.addPlacement3d( 0, 0, 2120));
file.addBox(door_body, 860, 30, 2120);
file.addBuildingProduct(door);
setSurfaceColour(file, door->Representation(), 0.9, 0.9, 0.9);
file.addEntity(new IfcSchema::IfcRelFillsElement(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null, null, door_opening, door));
IfcSchema::IfcDoorStyle* door_style = new IfcSchema::IfcDoorStyle(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), "Door type"s, null, null, null, null, null,
IfcSchema::IfcDoorStyleOperationEnum::IfcDoorStyleOperation_SINGLE_SWING_LEFT, IfcSchema::IfcDoorStyleConstructionEnum::IfcDoorStyleConstruction_WOOD, false, false);
setSurfaceColour(file, door.Representation(), 0.9, 0.9, 0.9);
{
auto rel = file.create<IfcSchema::IfcRelFillsElement>();
rel.setGlobalId(guid());
rel.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
rel.setRelatingOpeningElement(door_opening);
rel.setRelatedBuildingElement(door);
}
auto door_style = file.create<IfcSchema::IfcDoorStyle>();
door_style.setGlobalId(guid());
door_style.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
door_style.setName("Door type");
door_style.setOperationType(IfcSchema::IfcDoorStyleOperationEnum::IfcDoorStyleOperation_SINGLE_SWING_LEFT);
door_style.setConstructionType(IfcSchema::IfcDoorStyleConstructionEnum::IfcDoorStyleConstruction_WOOD);
door_style.setParameterTakesPrecedence(false);
door_style.setSizeable(false);
// NOTE: typing by IfcDoorStyle will cause validation errors in IFC4+ but it's allowed for backwards compatibility
// better to use IfcDoorType in the actual use case
file.addRelatedObject<IfcSchema::IfcRelDefinesByType>(door_style, door);
@@ -423,23 +466,23 @@ int main() {
// Therefore the OverallWidth and OverallHeight of the window attributes will need to
// match the bounding box of the representation. Furthermore, the window placement needs
// to align with the lowerleft corner of the constituent parts.
IfcSchema::IfcShapeRepresentation::list::ptr frame_representations(new IfcSchema::IfcShapeRepresentation::list);
std::vector<IfcSchema::IfcShapeRepresentation> frame_representations;
IfcSchema::IfcShapeRepresentation* horizontal_bar = file.addEmptyRepresentation();
IfcSchema::IfcShapeRepresentation* vertical_bar = file.addEmptyRepresentation();
auto horizontal_bar = file.addEmptyRepresentation();
auto vertical_bar = file.addEmptyRepresentation();
file.addBox(horizontal_bar, 1860, 90, 90);
file.addBox(vertical_bar, 90, 90, 1420);
frame_representations->push(horizontal_bar);
frame_representations->push(horizontal_bar); // Add another reference to the horizontal bar created above
frame_representations->push(vertical_bar);
frame_representations->push(vertical_bar); // Add another reference to the vertical bar created above
frame_representations.push_back(horizontal_bar);
frame_representations.push_back(horizontal_bar); // Add another reference to the horizontal bar created above
frame_representations.push_back(vertical_bar);
frame_representations.push_back(vertical_bar); // Add another reference to the vertical bar created above
// The beams all have the same surface style assigned
IfcSchema::IfcPresentationStyleAssignment* frame_style = 0;
for (IfcSchema::IfcShapeRepresentation::list::it i = frame_representations->begin(); i != frame_representations->end(); i += 2) {
IfcSchema::IfcPresentationStyleAssignment frame_style;
for (auto i = frame_representations.begin(); i != frame_representations.end(); i += 2) {
if (frame_style) {
setSurfaceColour(file,*i, frame_style);
setSurfaceColour(file, *i, frame_style);
} else {
frame_style = setSurfaceColour(file,*i, 0.5, 0.4, 0.3);
}
@@ -448,73 +491,80 @@ int main() {
// This window will be placed at five locations within the building. A list of placements is
// created and is iterated over to create all window instances.
IfcSchema::IfcLocalPlacement::list::ptr window_placements (new IfcSchema::IfcLocalPlacement::list);
window_placements->push(file.addLocalPlacement(storey_placement, 2*-1770-430-930, -45, 400));
window_placements->push(file.addLocalPlacement(storey_placement, -1770-430-930, -45, 400));
window_placements->push(file.addLocalPlacement(storey_placement, -430-930, -45, 400));
window_placements->push(file.addLocalPlacement(storey_placement, 3000-930, -45, 400));
window_placements->push(file.addLocalPlacement(storey_placement, -4855+45, 885-930, 400, 0, 0, 1, 0, 1, 0));
std::vector<IfcSchema::IfcLocalPlacement> window_placements;
window_placements.push_back(file.addLocalPlacement(storey_placement, 2*-1770-430-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, -1770-430-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, -430-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, 3000-930, -45, 400));
window_placements.push_back(file.addLocalPlacement(storey_placement, -4855+45, 885-930, 400, 0, 0, 1, 0, 1, 0));
for (IfcSchema::IfcLocalPlacement::list::it it = window_placements->begin(); it != window_placements->end(); ++it) {
// Create the window at the current location
IfcSchema::IfcLocalPlacement* place = *it;
IfcSchema::IfcWindow* window = new IfcSchema::IfcWindow(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, place, 0, null, 1600, 1860
for (auto& place : window_placements) {
auto window = file.create<IfcSchema::IfcWindow>();
window.setGlobalId(guid());
window.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
window.setObjectPlacement(place);
window.setOverallWidth(1860);
window.setOverallHeight(1600);
#ifdef USE_IFC4
, IfcSchema::IfcWindowTypeEnum::IfcWindowType_WINDOW
, IfcSchema::IfcWindowTypePartitioningEnum::IfcWindowTypePartitioning_SINGLE_PANEL
, null
window.setPredefinedType(IfcSchema::IfcWindowTypeEnum::IfcWindowType_WINDOW);
window.setPartitioningType(IfcSchema::IfcWindowTypePartitioningEnum::IfcWindowTypePartitioning_SINGLE_PANEL);
#endif
);
file.addBuildingProduct(window);
file.addBuildingProduct(window);
// Initialize a list of parts for the window to be composed of
IfcSchema::IfcObjectDefinition::list::ptr window_parts(new aggregate_of<IfcSchema::IfcObjectDefinition>());
std::vector<IfcSchema::IfcObjectDefinition> window_parts;
// The placements for the beams are not shared across the different windows because every
// beam is placed relative to its parent window entity.
IfcSchema::IfcLocalPlacement::list::ptr frame_placements (new aggregate_of<IfcSchema::IfcLocalPlacement>());
frame_placements->push(file.addLocalPlacement(storey_placement, 930,45));
frame_placements->push(file.addLocalPlacement(storey_placement, 930, 45, 1510));
frame_placements->push(file.addLocalPlacement(storey_placement, -885+930, 45, 90));
frame_placements->push(file.addLocalPlacement(storey_placement, 885+930, 45, 90));
std::vector<IfcSchema::IfcLocalPlacement> frame_placements;
frame_placements.push_back(file.addLocalPlacement(storey_placement, 930,45));
frame_placements.push_back(file.addLocalPlacement(storey_placement, 930, 45, 1510));
frame_placements.push_back(file.addLocalPlacement(storey_placement, -885+930, 45, 90));
frame_placements.push_back(file.addLocalPlacement(storey_placement, 885+930, 45, 90));
// Now iterate over the placements and representations of the beam and add them to list of parts
IfcSchema::IfcLocalPlacement::list::it frame_placement;
IfcSchema::IfcShapeRepresentation::list::it frame_representation;
for (frame_placement = frame_placements->begin(), frame_representation = frame_representations->begin();
frame_placement != frame_placements->end() && frame_representation != frame_representations->end();
std::vector<IfcSchema::IfcLocalPlacement>::const_iterator frame_placement;
std::vector<IfcSchema::IfcShapeRepresentation>::const_iterator frame_representation;
for (frame_placement = frame_placements.begin(), frame_representation = frame_representations.begin();
frame_placement != frame_placements.end() && frame_representation != frame_representations.end();
++frame_placement, ++frame_representation)
{
IfcSchema::IfcMember* frame_part = new IfcSchema::IfcMember(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, null, *frame_placement, file.addMappedItem(*frame_representation), null
auto frame_part = file.create<IfcSchema::IfcMember>();
frame_part.setGlobalId(guid());
frame_part.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
frame_part.setObjectPlacement(*frame_placement);
frame_part.setRepresentation(file.addMappedItem(*frame_representation));
#ifdef USE_IFC4
, IfcSchema::IfcMemberTypeEnum::IfcMemberType_MULLION
frame_part.setPredefinedType(IfcSchema::IfcMemberTypeEnum::IfcMemberType_MULLION);
#endif
);
file.addEntity(frame_part);
window_parts->push(frame_part);
window_parts.push_back(frame_part);
file.relatePlacements(window, frame_part);
}
// Add the glass plate to the list of parts
IfcSchema::IfcPlate* glass_part = new IfcSchema::IfcPlate(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(), null,
null, null, file.addLocalPlacement(storey_placement, 930, 45, 90), file.addBox(1680, 10, 1420), null
auto glass_part = file.create<IfcSchema::IfcPlate>();
glass_part.setGlobalId(guid());
glass_part.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
glass_part.setObjectPlacement(file.addLocalPlacement(storey_placement, 930, 45, 90));
glass_part.setRepresentation(file.addBox(1860, 10, 1420));
#ifdef USE_IFC4
, IfcSchema::IfcPlateTypeEnum::IfcPlateType_SHEET
glass_part.setPredefinedType(IfcSchema::IfcPlateTypeEnum::IfcPlateType_SHEET);
#endif
);
file.addEntity(glass_part);
window_parts->push(glass_part);
window_parts.push_back(glass_part);
file.relatePlacements(window, glass_part);
setSurfaceColour(file,glass_part->Representation(), 0.6, 0.7, 0.75, 0.1);
setSurfaceColour(file, glass_part.Representation(), 0.6, 0.7, 0.75, 0.1);
// Now create a decomposition relation between the window and the parts. Most viewers and authoring
// tools will consider the window a single entity that can be selected as a whole.
IfcSchema::IfcRelDecomposes* decomposition = new IfcSchema::IfcRelAggregates(guid(), file.getSingle<IfcSchema::IfcOwnerHistory>(),
null, null, window, window_parts);
file.addEntity(decomposition);
{
auto rel = file.create<IfcSchema::IfcRelAggregates>();
rel.setGlobalId(guid());
rel.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
rel.setRelatingObject(window);
rel.setRelatedObjects(window_parts);
}
}
// Finally create a file stream for our output and write the IFC file to it.
+60 -69
View File
@@ -1,4 +1,4 @@
/********************************************************************************
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
@@ -20,9 +20,9 @@
// TODO: Multiple schemas
#define IfcSchema Ifc2x3
#include "ifcparse/IfcFile.h"
#include "ifcparse/IfcLogger.h"
#include "ifcparse/Ifc2x3.h"
#include "../ifcparse/file.h"
#include "../ifcparse/logger.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include <boost/preprocessor/stringize.hpp>
#include <boost/preprocessor/seq/for_each.hpp>
@@ -47,7 +47,7 @@ static_assert(false, "A boost preprocessor sequence of schema identifiers is nee
// for and then overflow into an existing empty include file.
#define INCLUDE_SCHEMA(n) \
BOOST_PP_IIF(BOOST_PP_GREATER(BOOST_PP_SEQ_SIZE(SCHEMA_SEQ), n), BOOST_PP_STRINGIZE(../ifcparse/BOOST_PP_CAT(Ifc,BOOST_PP_SEQ_ELEM(BOOST_PP_MIN(n, BOOST_PP_SEQ_SIZE(BOOST_PP_SEQ_POP_BACK(SCHEMA_SEQ))),SCHEMA_SEQ)).h), "../ifcgeom/empty.h")
BOOST_PP_IIF(BOOST_PP_GREATER(BOOST_PP_SEQ_SIZE(SCHEMA_SEQ), n), BOOST_PP_STRINGIZE(../ifcparse/schemas/BOOST_PP_CAT(Ifc,BOOST_PP_SEQ_ELEM(BOOST_PP_MIN(n, BOOST_PP_SEQ_SIZE(BOOST_PP_SEQ_POP_BACK(SCHEMA_SEQ))),SCHEMA_SEQ)).h), "../ifcgeom/empty.h")
#include INCLUDE_SCHEMA(0)
#include INCLUDE_SCHEMA(1)
@@ -80,7 +80,7 @@ struct is_ifc4_or_higher<T, std::void_t<decltype(T::IfcMaterialDefinition)>> : s
typedef std::map<std::string, std::map<std::string, std::string>> element_properties;
std::string format_string(const AttributeValue& argument) {
std::string format_string(const attribute_value& argument) {
// Argument is a runtime tagged variant for the various data types in a IFC model,
// in this particular case we only care about flattening it to a string.
// @todo mostly duplicated from XmlSerializer.cpp
@@ -89,21 +89,21 @@ std::string format_string(const AttributeValue& argument) {
}
auto argument_type = argument.type();
switch (argument_type) {
case IfcUtil::Argument_BOOL: {
case ifcopenshell::Argument_BOOL: {
const bool b = argument;
return b ? "true" : "false";
}
case IfcUtil::Argument_DOUBLE: {
case ifcopenshell::Argument_DOUBLE: {
const double d = argument;
std::stringstream stream;
stream << std::setprecision(std::numeric_limits< double >::max_digits10) << d;
return stream.str();
break; }
case IfcUtil::Argument_STRING:
case IfcUtil::Argument_ENUMERATION: {
case ifcopenshell::Argument_STRING:
case ifcopenshell::Argument_ENUMERATION: {
return static_cast<std::string>(argument);
break; }
case IfcUtil::Argument_INT: {
case ifcopenshell::Argument_INT: {
const int v = argument;
std::stringstream stream;
stream << v;
@@ -114,44 +114,42 @@ std::string format_string(const AttributeValue& argument) {
}
template <typename Schema, typename T>
void process_pset(element_properties& props, const T* inst) {
void process_pset(element_properties& props, const T& inst) {
// Process an individual Property or Quantity set.
if (auto pset = inst->template as<typename Schema::IfcPropertySet>()) {
if (!pset->Name()) {
if (auto pset = inst.template as<typename Schema::IfcPropertySet>()) {
if (!pset.Name()) {
return;
}
auto ps = pset->HasProperties();
for (auto it = ps->begin(); it != ps->end(); ++it) {
auto& p = *it;
if (auto singleval = p->template as<typename Schema::IfcPropertySingleValue>()) {
auto ps = pset.HasProperties();
for (auto& p : ps) {
if (auto singleval = p.template as<typename Schema::IfcPropertySingleValue>()) {
std::string propname, propvalue;
if constexpr (is_ifc4_or_higher<Schema>::value) {
if (!singleval->Name()) {
if (!singleval.Name()) {
continue;
}
propname = *singleval->Name();
propname = *singleval.Name();
}
if constexpr (!is_ifc4_or_higher<Schema>::value) {
propname = singleval->Name();
propname = singleval.Name();
}
if (!singleval->NominalValue()) {
if (!singleval.NominalValue()) {
propvalue = "-";
} else {
props[*pset->Name()][propname] = format_string(singleval->NominalValue()->template as<IfcUtil::IfcBaseClass>()->get_attribute_value(0));
props[*pset.Name()][propname] = format_string(singleval.NominalValue().concrete().get_attribute_value(0));
}
}
}
}
if (auto qset = inst->template as<typename Schema::IfcElementQuantity>()) {
if (!qset->Name()) {
if (auto qset = inst.template as<typename Schema::IfcElementQuantity>()) {
if (!qset.Name()) {
return;
}
auto qs = qset->Quantities();
for (auto it = qs->begin(); it != qs->end(); ++it) {
auto& q = *it;
if (q->template as<typename Schema::IfcPhysicalSimpleQuantity>() && q->get_attribute_value(3).type() == IfcUtil::Argument_DOUBLE) {
double v = q->get_attribute_value(3);
props[*qset->Name()][q->Name()] = std::to_string(v);
auto qs = qset.Quantities();
for (auto& q : qs) {
if (q.template as<typename Schema::IfcPhysicalSimpleQuantity>() && q.get_attribute_value(3).type() == ifcopenshell::Argument_DOUBLE) {
double v = q.get_attribute_value(3);
props[*qset.Name()][q.Name()] = std::to_string(v);
}
}
}
@@ -163,49 +161,44 @@ void process_pset(element_properties& props, const T* inst) {
}
template <typename Schema>
void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefinition* inst) {
void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefinition& inst) {
// Extracts the property definitions for an IFC instance.
if (auto tyob = inst->template as<typename Schema::IfcTypeObject>()) {
if (tyob->HasPropertySets()) {
auto defs = *tyob->HasPropertySets();
for (auto it = defs->begin(); it != defs->end(); ++it) {
auto& def = *it;
if (auto tyob = inst.template as<typename Schema::IfcTypeObject>()) {
if (tyob.HasPropertySets()) {
auto defs = *tyob.HasPropertySets();
for (auto& def : defs) {
process_pset<Schema>(props, def);
}
}
}
if constexpr (is_ifc4_or_higher<Schema>::value) {
if (auto mdef = inst->template as<typename Schema::IfcMaterialDefinition>()) {
auto defs = mdef->HasProperties();
for (auto it = defs->begin(); it != defs->end(); ++it) {
auto& def = *it;
if (auto mdef = inst.template as<typename Schema::IfcMaterialDefinition>()) {
auto defs = mdef.HasProperties();
for (auto& def : defs) {
process_pset<Schema>(props, def);
}
}
if (auto pdef = inst->template as<typename Schema::IfcProfileDef>()) {
if (auto pdef = inst.template as<typename Schema::IfcProfileDef>()) {
auto defs = pdef->HasProperties();
for (auto it = defs->begin(); it != defs->end(); ++it) {
auto& def = *it;
for (auto& def : defs) {
process_pset<Schema>(props, def);
}
}
}
if (auto ob = inst->template as<typename Schema::IfcObject>()) {
if (auto ob = inst.template as<typename Schema::IfcObject>()) {
if constexpr (is_ifc4_or_higher<Schema>::value) {
auto rels = ob->IsTypedBy();
for (auto it = rels->begin(); it != rels->end(); ++it) {
auto& rel = *it;
auto rels = ob.IsTypedBy();
for (auto& rel : rels) {
get_psets_s<Schema>(props, rel->RelatingType());
}
}
{
auto rels = ob->IsDefinedBy();
for (auto it = rels->begin(); it != rels->end(); ++it) {
auto& rel = *it;
if (auto bytype = rel->template as<typename Schema::IfcRelDefinesByType>()) {
get_psets_s<Schema>(props, bytype->RelatingType());
} else if (auto byprops = rel->template as<typename Schema::IfcRelDefinesByProperties>()) {
process_pset<Schema>(props, byprops->RelatingPropertyDefinition());
auto rels = ob.IsDefinedBy();
for (auto& rel : rels) {
if (auto bytype = rel.template as<typename Schema::IfcRelDefinesByType>()) {
get_psets_s<Schema>(props, bytype.RelatingType());
} else if (auto byprops = rel.template as<typename Schema::IfcRelDefinesByProperties>()) {
process_pset<Schema>(props, byprops.RelatingPropertyDefinition());
}
}
}
@@ -220,10 +213,10 @@ void get_psets_s(element_properties& props, const typename Schema::IfcObjectDefi
#define GENERATE_LITERAL_STRING(elem) "Ifc" # elem
#define TEST_AND_DISPATCH(r, data, elem) \
if (strcasecmp(schema_name, GENERATE_LITERAL_STRING(elem)) == 0) { get_psets_s<EXPAND_AND_CONCATENATE(elem)>(props, inst->as<EXPAND_AND_CONCATENATE(elem)::IfcObjectDefinition>()); }
if (strcasecmp(schema_name, GENERATE_LITERAL_STRING(elem)) == 0) { get_psets_s<EXPAND_AND_CONCATENATE(elem)>(props, inst.as<EXPAND_AND_CONCATENATE(elem)::IfcObjectDefinition>()); }
void get_psets(element_properties& props, const IfcUtil::IfcBaseClass* inst) {
auto schema_name = inst->declaration().schema()->name().c_str();
void get_psets(element_properties& props, const express::Base& inst) {
auto schema_name = inst.declaration().schema()->name().c_str();
BOOST_PP_SEQ_FOR_EACH(TEST_AND_DISPATCH, , SCHEMA_SEQ)
}
@@ -234,10 +227,10 @@ int main(int argc, char** argv) {
}
// Redirect the output (both progress and log) to stdout
Logger::SetOutput(&std::cout, &std::cout);
logger::set_output(&std::cout, &std::cout);
// Parse the IFC file provided in argv[1]
IfcParse::IfcFile file(argv[1]);
ifcopenshell::file file(argv[1]);
if (!file.good()) {
std::cout << "Unable to parse .ifc file" << std::endl;
return 1;
@@ -259,19 +252,17 @@ int main(int argc, char** argv) {
// we need to cast them to IfcWindows. Since these properties
// are optional we need to make sure the properties are
// defined for the window in question before accessing them.
IfcSchema::IfcBuildingElement::list::ptr elements = file.instances_by_type<IfcSchema::IfcBuildingElement>();
auto elements = file.instances_by_type<IfcSchema::IfcBuildingElement>();
std::cout << "Found " << elements->size() << " elements in " << argv[1] << ":" << std::endl;
std::cout << "Found " << elements.size() << " elements in " << argv[1] << ":" << std::endl;
for (auto it = elements->begin(); it != elements->end(); ++it) {
const auto* element = *it;
element->toString(std::cout);
for (auto& element : elements) {
element.to_string(std::cout);
std::cout << std::endl;
const IfcSchema::IfcWindow* window;
if ((window = element->as<IfcSchema::IfcWindow>()) != 0) {
if (window->OverallWidth() && window->OverallHeight()) {
const double area = *window->OverallWidth() * *window->OverallHeight();
if (auto window = element.as<IfcSchema::IfcWindow>()) {
if (window.OverallWidth() && window.OverallHeight()) {
const double area = *window.OverallWidth() * *window.OverallHeight();
std::cout << "The area of this window is " << area << std::endl;
}
}
+46 -33
View File
@@ -30,8 +30,8 @@
// Disable warnings coming from IfcOpenShell
#pragma warning(disable : 4018 4267 4250 4984 4985)
#include "ifcparse/Ifc4x3_add2.h"
#include "ifcparse/IfcAlignmentHelper.h"
#include "../ifcparse/Ifc4x3_add2.h"
#include "../ifcparse/alignment_helper.h"
#include <fstream>
@@ -39,37 +39,49 @@
// performs basic project setup including created the IfcProject object
// and initializing the project units to FEET
Schema::IfcProject* setup_project(IfcHierarchyHelper<Schema>& file) {
Schema::IfcProject setup_project(hierarchy_helper<Schema>& file) {
std::vector<std::string> file_description;
file_description.push_back("ViewDefinition[Alignment-basedReferenceView]");
file.header().file_description()->setdescription(file_description);
file.header().file_description().setdescription(file_description);
auto project = file.addProject();
project->setName(std::string("FHWA Bridge Geometry Manual Example Alignment"));
project->setDescription(std::string("C++ Example - Simplified"));
project.setName(std::string("FHWA Bridge Geometry Manual Example Alignment"));
project.setDescription(std::string("C++ Example - Simplified"));
// set up project units for feet
// the call to file.addProject() sets up length units as millimeter.
auto units_in_context = project->UnitsInContext();
auto units = units_in_context->Units();
auto begin = units->begin();
auto units_in_context = project.UnitsInContext();
auto units = units_in_context.Units();
auto begin = units.begin();
auto iter = begin;
auto end = units->end();
auto end = units.end();
for (; iter != end; iter++) {
auto unit = *iter;
if (unit->as<Schema::IfcSIUnit>() && unit->as<Schema::IfcSIUnit>()->UnitType() == Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT) {
auto dimensions = new Schema::IfcDimensionalExponents(1, 0, 0, 0, 0, 0, 0);
file.addEntity(dimensions);
if (unit.as<Schema::IfcSIUnit>() && unit.as<Schema::IfcSIUnit>().UnitType() == Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT) {
auto dimensions = file.create<Schema::IfcDimensionalExponents>();
dimensions.setLengthExponent(1);
dimensions.setMassExponent(0);
dimensions.setTimeExponent(0);
dimensions.setElectricCurrentExponent(0);
dimensions.setThermodynamicTemperatureExponent(0);
dimensions.setAmountOfSubstanceExponent(0);
dimensions.setLuminousIntensityExponent(0);
auto conversion_factor = new Schema::IfcMeasureWithUnit(new Schema::IfcLengthMeasure(304.80), unit->as<Schema::IfcSIUnit>());
file.addEntity(conversion_factor);
auto conversion_factor = file.create<Schema::IfcMeasureWithUnit>();
auto length = file.create<Schema::IfcLengthMeasure>();
length.set_attribute_value(0, 304.80);
conversion_factor.setValueComponent(length);
conversion_factor.setUnitComponent(unit);
auto conversion_based_unit = new Schema::IfcConversionBasedUnit(dimensions, Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT, "FEET", conversion_factor);
file.addEntity(conversion_based_unit);
auto conversion_based_unit = file.create<Schema::IfcConversionBasedUnit>();
conversion_based_unit.setDimensions(dimensions);
conversion_based_unit.setUnitType(Schema::IfcUnitEnum::IfcUnit_LENGTHUNIT);
conversion_based_unit.setName("FEET");
conversion_based_unit.setConversionFactor(conversion_factor);
units->remove(unit); // remove the millimeter unit
units->push(conversion_based_unit); // add the feet unit
units_in_context->setUnits(units); // update the UnitsInContext
units.erase(std::remove(units.begin(), units.end(), unit)); // remove the millimeter unit
units.push_back(conversion_based_unit); // add the feet unit
units_in_context.setUnits(units); // update the UnitsInContext
break; // Done!, the length unit was found, so break out of the loop
}
@@ -79,7 +91,7 @@ Schema::IfcProject* setup_project(IfcHierarchyHelper<Schema>& file) {
}
int main() {
IfcHierarchyHelper<Schema> file;
hierarchy_helper<Schema> file;
auto project = setup_project(file);
@@ -129,10 +141,11 @@ int main() {
// IFC 4.1.4.1.1 "Every IfcAlignment must be related to IfcProject using the IfcRelAggregates relationship"
// https://standards.buildingsmart.org/IFC/RELEASE/IFC4_3/HTML/concepts/Object_Composition/Aggregation/Alignment_Aggregation_To_Project/content.html
// IfcProject <-> IfcRelAggregates <-> IfcAlignment
typename aggregate_of<typename Schema::IfcObjectDefinition>::ptr list_of_alignments_in_project(new aggregate_of<typename Schema::IfcObjectDefinition>());
list_of_alignments_in_project->push(alignment);
auto aggregate_alignments_with_project = new Schema::IfcRelAggregates(IfcParse::IfcGlobalId(), nullptr, std::string("Alignments in project"), boost::none, project, list_of_alignments_in_project);
file.addEntity(aggregate_alignments_with_project);
auto aggregate_alignments_with_project = file.create<Schema::IfcRelAggregates>();
aggregate_alignments_with_project.setGlobalId(ifcopenshell::global_id());
aggregate_alignments_with_project.setName("Alignments in project");
aggregate_alignments_with_project.setRelatingObject(project);
aggregate_alignments_with_project.setRelatedObjects({alignment});
// Define the spatial structure of the alignment with respect to the site
@@ -141,24 +154,24 @@ int main() {
// IfcSite <-> IfcRelReferencedInSpatialStructure <-> IfcAlignment
// This means IfcAlignment is not part of the IfcSite (it is not an aggregate component) but instead IfcAlignment is used within
// the IfcSite by reference. This implies an IfcAlignment can traverse many IfcSite instances within an IfcProject
typename Schema::IfcSpatialReferenceSelect::list::ptr list_alignments_referenced_in_site(new Schema::IfcSpatialReferenceSelect::list);
list_alignments_referenced_in_site->push(alignment);
// Complete the model by creating sites for the 3 bridges
for (int i = 1; i <= 3; i++) {
std::ostringstream os;
os << "Site of Bridge " << i;
auto site = file.addSite(project, nullptr);
site->setName(os.str());
auto site = file.addSite(project);
site.setName(os.str());
std::ostringstream description;
description << "Alignments referenced into the spatial structure of Bridge Site " << i;
auto rel_referenced_in_spatial_structure = new Schema::IfcRelReferencedInSpatialStructure(IfcParse::IfcGlobalId(), nullptr, boost::none, description.str(), list_alignments_referenced_in_site, site);
file.addEntity(rel_referenced_in_spatial_structure);
auto rel_referenced_in_spatial_structure = file.create<Schema::IfcRelReferencedInSpatialStructure>();
rel_referenced_in_spatial_structure.setGlobalId(ifcopenshell::global_id());
rel_referenced_in_spatial_structure.setDescription(description.str());
rel_referenced_in_spatial_structure.setRelatedElements(std::vector<Schema::IfcSpatialReferenceSelect>{alignment});
rel_referenced_in_spatial_structure.setRelatingStructure(site);
}
// That's it - save the model to a file
std::ofstream ofs("IfcSimplifiedAlignment.ifc");
std::ofstream ofs("FHWA_Bridge_Geometry_Alignment_Example_Simplified.ifc");
ofs << file;
}
+31 -33
View File
@@ -28,16 +28,15 @@
#include <iostream>
#include <fstream>
#define IfcSchema Ifc4
#include "ifcparse/Ifc4.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
boost::none_t const null = boost::none;
static int i = 0;
void create_product_from_item(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcRepresentationItem* item, const std::string& s) {
void create_product_from_item(hierarchy_helper& file, IfcSchema::IfcRepresentationItem* item, const std::string& s) {
IfcSchema::IfcBuildingElementProxy* product = new IfcSchema::IfcBuildingElementProxy(
guid(), 0, S("product"), null, null, 0, 0, null, null);
file.addBuildingProduct(product);
@@ -50,8 +49,8 @@ void create_product_from_item(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::If
items->push(item);
if (s == "GeometricSet") {
IfcSchema::IfcGeometricSet* set = new IfcSchema::IfcGeometricSet(items->as<IfcSchema::IfcGeometricSetSelect>());
file.addEntity(set);
IfcSchema::IfcGeometricSet* set = new IfcSchema::IfcGeometricSet(items->generalize());
file.add_entity(set);
items = IfcSchema::IfcRepresentationItem::list::ptr(new IfcSchema::IfcRepresentationItem::list());
items->push(set);
}
@@ -61,55 +60,55 @@ void create_product_from_item(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::If
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);
file.addEntity(rep);
file.addEntity(shape);
file.add_entity(rep);
file.add_entity(shape);
product->setRepresentation(shape);
}
void create_surfaces_from_profile(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcProfileDef* profile) {
void create_surfaces_from_profile(hierarchy_helper& file, IfcSchema::IfcProfileDef* profile) {
IfcSchema::IfcSurfaceOfLinearExtrusion* extrusion = new IfcSchema::IfcSurfaceOfLinearExtrusion(profile, file.addPlacement3d(), file.addTriplet<IfcSchema::IfcDirection>(0, 0, 1), 100.);
file.addEntity(extrusion);
file.add_entity(extrusion);
IfcSchema::IfcAxis1Placement* ax1 = new IfcSchema::IfcAxis1Placement(file.addTriplet<IfcSchema::IfcCartesianPoint>(0,100,0), file.addTriplet<IfcSchema::IfcDirection>(1,0,0));
IfcSchema::IfcSurfaceOfRevolution* revolution = new IfcSchema::IfcSurfaceOfRevolution(profile, file.addPlacement3d(), ax1);
file.addEntity(ax1);
file.addEntity(revolution);
file.add_entity(ax1);
file.add_entity(revolution);
create_product_from_item(file, extrusion, "GeometricSet");
create_product_from_item(file, revolution, "GeometricSet");
}
void create_solids_from_profile(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcProfileDef* profile) {
void create_solids_from_profile(hierarchy_helper& file, IfcSchema::IfcProfileDef* profile) {
IfcSchema::IfcExtrudedAreaSolid* extrusion = new IfcSchema::IfcExtrudedAreaSolid(profile, file.addPlacement3d(), file.addTriplet<IfcSchema::IfcDirection>(0, 0, 1), 100.);
file.addEntity(extrusion);
file.add_entity(extrusion);
IfcSchema::IfcAxis1Placement* ax1 = new IfcSchema::IfcAxis1Placement(file.addTriplet<IfcSchema::IfcCartesianPoint>(0,100,0), file.addTriplet<IfcSchema::IfcDirection>(1,0,0));
IfcSchema::IfcRevolvedAreaSolid* revolution1 = new IfcSchema::IfcRevolvedAreaSolid(profile, file.addPlacement3d(), ax1, 360.);
IfcSchema::IfcRevolvedAreaSolid* revolution2 = new IfcSchema::IfcRevolvedAreaSolid(profile, file.addPlacement3d(), ax1, 90.);
file.addEntity(ax1);
file.addEntity(revolution1);
file.addEntity(revolution2);
file.add_entity(ax1);
file.add_entity(revolution1);
file.add_entity(revolution2);
create_product_from_item(file, extrusion, "SweptSolid");
create_product_from_item(file, revolution1, "SweptSolid");
create_product_from_item(file, revolution2, "SweptSolid");
}
void create_products_from_curve(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcBoundedCurve* curve) {
void create_products_from_curve(hierarchy_helper& file, IfcSchema::IfcBoundedCurve* curve) {
IfcSchema::IfcArbitraryOpenProfileDef* open = new IfcSchema::IfcArbitraryOpenProfileDef(IfcSchema::IfcProfileTypeEnum::IfcProfileType_CURVE, null, curve);
IfcSchema::IfcCenterLineProfileDef* center_line = new IfcSchema::IfcCenterLineProfileDef(IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA, null, curve, 10.);
file.addEntity(open);
file.addEntity(center_line);
file.add_entity(open);
file.add_entity(center_line);
create_surfaces_from_profile(file, open);
create_solids_from_profile(file, center_line);
}
int main(int argc, char** argv) {
const char filename[] = "arbitrary_open_profile_def.ifc";
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname(filename);
const char filename[] = "IfcArbitraryOpenProfileDef.ifc";
hierarchy_helper file;
file.header().file_name().name(filename);
double coords1[] = {-50.0, 0.0};
double coords2[] = { 50.0, 0.0};
@@ -118,24 +117,23 @@ int main(int argc, char** argv) {
points->push(new IfcSchema::IfcCartesianPoint(std::vector<double>(coords2, coords2+2)));
file.addEntities(points->generalize());
IfcSchema::IfcPolyline* poly = new IfcSchema::IfcPolyline(points);
file.addEntity(poly);
file.add_entity(poly);
create_products_from_curve(file, poly);
IfcSchema::IfcEllipse* ellipse = new IfcSchema::IfcEllipse(file.addPlacement2d(), 50., 25.);
file.addEntity(ellipse);
aggregate_of_instance::ptr trim1(new aggregate_of_instance);
aggregate_of_instance::ptr trim2(new aggregate_of_instance);
file.add_entity(ellipse);
IfcEntityList::ptr trim1(new IfcEntityList);
IfcEntityList::ptr trim2(new IfcEntityList);
trim1->push(new IfcSchema::IfcParameterValue( 0.));
trim2->push(new IfcSchema::IfcParameterValue(180.));
IfcSchema::IfcTrimmedCurve* trim = new IfcSchema::IfcTrimmedCurve(ellipse, trim1->as<IfcSchema::IfcTrimmingSelect>(), trim2->as<IfcSchema::IfcTrimmingSelect>(), true, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
file.addEntity(trim);
IfcSchema::IfcTrimmedCurve* trim = new IfcSchema::IfcTrimmedCurve(ellipse, trim1, trim2, true, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
file.add_entity(trim);
create_products_from_curve(file, trim);
using namespace std::string_literals;
file.getSingle<IfcSchema::IfcProject>()->setName("arbitrary_open_profile_def"s);
file.getSingle<Ifc2x3::IfcProject>()->setName("IfcArbitraryOpenProfileDef");
std::ofstream f(filename);
f << file;
}
}
+16 -17
View File
@@ -27,18 +27,18 @@
#include <iostream>
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/IfcUtil.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
boost::none_t const null = boost::none;
int main(int argc, char** argv) {
const char filename[] = "composite_profile_def.ifc";
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname(filename);
const char filename[] = "IfcCompositeProfileDef.ifc";
hierarchy_helper file;
file.header().file_name().name(filename);
double coords1[] = {100.0, 0.0};
double coords2[] = {200.0, 0.0};
@@ -65,11 +65,11 @@ int main(int argc, char** argv) {
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, file.addPlacement2d(80.), 50.0, 25.0, 5.0, 10.0, 2.0, null);
file.addEntity(p2);
file.addEntity(p3);
file.add_entity(p2);
file.add_entity(p3);
file.addEntity(transform1);
file.addEntity(transform2);
file.add_entity(transform1);
file.add_entity(transform2);
IfcSchema::IfcDerivedProfileDef* p5 = new IfcSchema::IfcDerivedProfileDef(IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA, null, p2, transform1, null);
IfcSchema::IfcDerivedProfileDef* p6 = new IfcSchema::IfcDerivedProfileDef(IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA, null, p3, transform2, null);
@@ -95,8 +95,8 @@ int main(int argc, char** argv) {
IfcSchema::IfcExtrudedAreaSolid* solid = new IfcSchema::IfcExtrudedAreaSolid(composite,
file.addPlacement3d(), file.addTriplet<IfcSchema::IfcDirection>(0, 0, 1), 20.0);
file.addEntity(composite);
file.addEntity(solid);
file.add_entity(composite);
file.add_entity(solid);
IfcSchema::IfcRepresentation::list::ptr reps (new IfcSchema::IfcRepresentation::list());
IfcSchema::IfcRepresentationItem::list::ptr items (new IfcSchema::IfcRepresentationItem::list());
@@ -107,13 +107,12 @@ int main(int argc, char** argv) {
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);
file.addEntity(rep);
file.addEntity(shape);
file.add_entity(rep);
file.add_entity(shape);
product->setRepresentation(shape);
using namespace std::string_literals;
file.getSingle<IfcSchema::IfcProject>()->setName("composite_profile_def"s);
file.getSingle<IfcSchema::IfcProject>()->setName("IfcCompositeProfileDef");
std::ofstream f(filename);
f << file;
+14 -15
View File
@@ -27,12 +27,12 @@
#include <iostream>
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/IfcUtil.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
boost::none_t const null = boost::none;
class Node {
@@ -101,7 +101,7 @@ public:
return operate(OP_INTERSECT, p);
}
IfcSchema::IfcRepresentationItem* serialize(IfcHierarchyHelper<IfcSchema>& file) const {
IfcSchema::IfcRepresentationItem* serialize(hierarchy_helper& file) const {
IfcSchema::IfcRepresentationItem* my;
if (op == OP_TERMINAL) {
IfcSchema::IfcAxis2Placement3D* place = file.addPlacement3d(x,y,z,zx,zy,zz,xx,xy,xz);
@@ -117,7 +117,7 @@ public:
my = new IfcSchema::IfcRightCircularCone(place, b, a);
}
} else {
IfcSchema::IfcBooleanOperator o = IfcSchema::IfcBooleanOperator::IfcBooleanOperator_UNION;
IfcSchema::IfcBooleanOperator::IfcBooleanOperator o;
if (op == OP_ADD) {
o = IfcSchema::IfcBooleanOperator::IfcBooleanOperator_UNION;
} else if (op == OP_SUBTRACT) {
@@ -125,17 +125,17 @@ public:
} else if (op == OP_INTERSECT) {
o = IfcSchema::IfcBooleanOperator::IfcBooleanOperator_INTERSECTION;
}
my = new IfcSchema::IfcBooleanResult(o, left->serialize(file)->as<IfcSchema::IfcBooleanOperand>(), right->serialize(file)->as<IfcSchema::IfcBooleanOperand>());
my = new IfcSchema::IfcBooleanResult(o, left->serialize(file), right->serialize(file));
}
file.addEntity(my);
file.add_entity(my);
return my;
}
};
int main(int argc, char** argv) {
const char filename[] = "csg_primitive.ifc";
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname(filename);
const char filename[] = "IfcCsgPrimitive.ifc";
hierarchy_helper file;
file.header().file_name().name(filename);
IfcSchema::IfcRepresentationItem* csg1 = Node::Box(8000.,6000.,3000.).subtract(
Node::Box(7600.,5600.,2800.).move(200.,200.,200.)
@@ -181,13 +181,12 @@ int main(int argc, char** argv) {
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(null, null, reps);
file.addEntity(rep);
file.addEntity(shape);
file.add_entity(rep);
file.add_entity(shape);
product->setRepresentation(shape);
using namespace std::string_literals;
file.getSingle<IfcSchema::IfcProject>()->setName("csg_primitive"s);
file.getSingle<IfcSchema::IfcProject>()->setName("IfcCompositeProfileDef");
std::ofstream f(filename);
f << file;
+30 -30
View File
@@ -27,12 +27,12 @@
#include <iostream>
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/IfcUtil.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
boost::none_t const null = boost::none;
typedef struct {
@@ -44,7 +44,7 @@ typedef struct {
static int i = 0;
void create_testcase_for(IfcHierarchyHelper<IfcSchema>& file, const EllipsePie& pie, IfcSchema::IfcTrimmingPreference pref) {
void create_testcase_for(hierarchy_helper& file, const EllipsePie& pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference pref) {
const double deg = 1. / 180. * 3.141592653;
double flt1[] = {0. , 0. };
double flt2[] = {pie.r1 * cos(pie.t1*deg), pie.r2 * sin(pie.t1*deg)};
@@ -66,24 +66,24 @@ void create_testcase_for(IfcHierarchyHelper<IfcSchema>& file, const EllipsePie&
Ifc2x3::IfcEllipse* ellipse = new Ifc2x3::IfcEllipse(file.addPlacement2d(), pie.r1, pie.r2);
file.addEntity(ellipse);
aggregate_of_instance::ptr trim1(new aggregate_of_instance);
aggregate_of_instance::ptr trim2(new aggregate_of_instance);
if (pref == IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER) {
file.add_entity(ellipse);
IfcEntityList::ptr trim1(new IfcEntityList);
IfcEntityList::ptr trim2(new IfcEntityList);
if (pref == Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER) {
trim1->push(new Ifc2x3::IfcParameterValue(pie.t1));
trim2->push(new Ifc2x3::IfcParameterValue(pie.t2));
} else {
trim1->push(p2);
trim2->push(p3);
}
Ifc2x3::IfcTrimmedCurve* trim = new Ifc2x3::IfcTrimmedCurve(ellipse, trim1->as<IfcSchema::IfcTrimmingSelect>(), trim2->as<IfcSchema::IfcTrimmingSelect>(), true, pref);
file.addEntity(trim);
Ifc2x3::IfcTrimmedCurve* trim = new Ifc2x3::IfcTrimmedCurve(ellipse, trim1, trim2, true, pref);
file.add_entity(trim);
Ifc2x3::IfcCompositeCurveSegment::list::ptr segments(new Ifc2x3::IfcCompositeCurveSegment::list());
Ifc2x3::IfcCompositeCurveSegment* s2 = new Ifc2x3::IfcCompositeCurveSegment(Ifc2x3::IfcTransitionCode::IfcTransitionCode_CONTINUOUS, true, trim);
Ifc2x3::IfcPolyline* poly = new Ifc2x3::IfcPolyline(points);
file.addEntity(poly);
file.add_entity(poly);
Ifc2x3::IfcCompositeCurveSegment* s1 = new Ifc2x3::IfcCompositeCurveSegment(Ifc2x3::IfcTransitionCode::IfcTransitionCode_CONTINUOUS, true, poly);
segments->push(s1);
@@ -92,8 +92,8 @@ void create_testcase_for(IfcHierarchyHelper<IfcSchema>& file, const EllipsePie&
Ifc2x3::IfcCompositeCurve* ccurve = new Ifc2x3::IfcCompositeCurve(segments, false);
Ifc2x3::IfcArbitraryClosedProfileDef* profile = new Ifc2x3::IfcArbitraryClosedProfileDef(Ifc2x3::IfcProfileTypeEnum::IfcProfileType_AREA, null, ccurve);
file.addEntity(ccurve);
file.addEntity(profile);
file.add_entity(ccurve);
file.add_entity(profile);
IfcSchema::IfcBuildingElementProxy* product = new IfcSchema::IfcBuildingElementProxy(
guid(), 0, S("profile"), null, null, 0, 0, null, null);
@@ -105,7 +105,7 @@ void create_testcase_for(IfcHierarchyHelper<IfcSchema>& file, const EllipsePie&
IfcSchema::IfcExtrudedAreaSolid* solid = new IfcSchema::IfcExtrudedAreaSolid(profile,
file.addPlacement3d(), file.addTriplet<IfcSchema::IfcDirection>(0, 0, 1), 20.0);
file.addEntity(solid);
file.add_entity(solid);
IfcSchema::IfcRepresentation::list::ptr reps (new IfcSchema::IfcRepresentation::list());
IfcSchema::IfcRepresentationItem::list::ptr items (new IfcSchema::IfcRepresentationItem::list());
@@ -116,33 +116,33 @@ void create_testcase_for(IfcHierarchyHelper<IfcSchema>& file, const EllipsePie&
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);
file.addEntity(rep);
file.addEntity(shape);
file.add_entity(rep);
file.add_entity(shape);
product->setRepresentation(shape);
}
int main(int argc, char** argv) {
const std::string filename = "ellipse_pies.ifc";
IfcHierarchyHelper<IfcSchema> file;
hierarchy_helper file;
{ EllipsePie pie = {80., 50., 0., 150.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
{ EllipsePie pie = {80, 50., 30., 300.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
{ EllipsePie pie = {80, 50., 300., 30.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
{ EllipsePie pie = {50., 80., 0., 150.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
{ EllipsePie pie = {50, 80., 30., 300.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
{ EllipsePie pie = {50, 80., 300., 30.};
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
create_testcase_for(file, pie, Ifc2x3::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);}
std::ofstream f(filename.c_str());
f << file;
}
+14 -15
View File
@@ -22,19 +22,18 @@
* Example that generates various forms of IfcFace *
* *
********************************************************************************/
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/IfcUtil.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
boost::none_t const null = boost::none;
typedef ifcopenshell::global_id guid;
boost::none_t const null = (static_cast<boost::none_t>(0));
static int x = 0;
void create_testcase(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcFace* face, const std::string& name) {
void create_testcase(hierarchy_helper& file, IfcSchema::IfcFace* face, const std::string& name) {
IfcSchema::IfcFace::list::ptr faces(new IfcSchema::IfcFace::list);
faces->push(face);
IfcSchema::IfcOpenShell* shell = new IfcSchema::IfcOpenShell(faces);
@@ -58,14 +57,14 @@ void create_testcase(IfcHierarchyHelper<IfcSchema>& file, IfcSchema::IfcFace* fa
file.getRepresentationContext("Model"), S("Body"), S("SurfaceModel"), items);
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);
file.addEntity(shape);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(0, 0, reps);
file.add_entity(shape);
product->setRepresentation(shape);
}
int main(int argc, char** argv) {
IfcHierarchyHelper<IfcSchema> file;
hierarchy_helper file;
{
IfcSchema::IfcCartesianPoint::list::ptr points (new IfcSchema::IfcCartesianPoint::list);
points->push(file.addTriplet<IfcSchema::IfcCartesianPoint>(-400, -400, 0));
@@ -193,7 +192,7 @@ int main(int argc, char** argv) {
IfcSchema::IfcCartesianPoint::list::ptr trim2(new IfcSchema::IfcCartesianPoint::list);
trim1->push(point1);
trim2->push(point2);
IfcSchema::IfcTrimmedCurve* trimmed_curve = new IfcSchema::IfcTrimmedCurve(circle, trim1->generalize()->as<IfcSchema::IfcTrimmingSelect>(), trim2->generalize()->as<IfcSchema::IfcTrimmingSelect>(), true, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);
IfcSchema::IfcTrimmedCurve* trimmed_curve = new IfcSchema::IfcTrimmedCurve(circle, trim1->generalize(), trim2->generalize(), true, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_CARTESIAN);
IfcSchema::IfcArbitraryOpenProfileDef* profile = new IfcSchema::IfcArbitraryOpenProfileDef(IfcSchema::IfcProfileTypeEnum::IfcProfileType_CURVE, boost::none, trimmed_curve);
IfcSchema::IfcAxis1Placement* place = new IfcSchema::IfcAxis1Placement(file.addTriplet<IfcSchema::IfcCartesianPoint>(0., 0., 0.), file.addTriplet<IfcSchema::IfcDirection>(1., 0., 0.));
IfcSchema::IfcSurfaceOfRevolution* surface = new IfcSchema::IfcSurfaceOfRevolution(profile, file.addPlacement3d(), place);
@@ -201,8 +200,8 @@ int main(int argc, char** argv) {
IfcSchema::IfcFace* face = new IfcSchema::IfcFaceSurface(bounds, surface, true);
create_testcase(file, face, "face surface");
}
const char filename[] = "faces.ifc";
file.header().file_name()->setname(filename);
std::ofstream f(filename);
const std::string filename = "faces.ifc";
file.header().file_name().name(filename);
std::ofstream f(filename.c_str());
f << file;
}
}
+22 -25
View File
@@ -27,24 +27,22 @@
#include <string>
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include <boost/math/constants/constants.hpp>
const static double PI = boost::math::constants::pi<double>();
#include "ifcparse\Ifc2x3.h"
#include "ifcparse\IfcUtil.h"
#include "ifcparse\hierarchy_helper.h"
#include "ifcgeom\IfcGeom.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
typedef ifcopenshell::global_id guid;
boost::none_t const null = boost::none;
void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
void create_curve_rebar(hierarchy_helper& file)
{
int dia = 24;
int R = 3 * dia;
int length = 12 * dia;
double crossSectionarea = PI * dia * dia / 4;
double crossSectionarea = M_PI * (dia / 2) * 2;
IfcSchema::IfcReinforcingBar* rebar = new IfcSchema::IfcReinforcingBar(
guid(), 0, S("test"), null,
null, 0, 0,
@@ -73,32 +71,31 @@ void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
points1->push(p2);
file.addEntities(points1->generalize());
IfcSchema::IfcPolyline* poly1 = new IfcSchema::IfcPolyline(points1);
file.addEntity(poly1);
file.add_entity(poly1);
IfcSchema::IfcCompositeCurveSegment* segment1 = new IfcSchema::IfcCompositeCurveSegment(IfcSchema::IfcTransitionCode::IfcTransitionCode_CONTINUOUS, true, poly1);
file.addEntity(segment1);
file.add_entity(segment1);
segments->push(segment1);
/*second segment - arc */
IfcSchema::IfcAxis2Placement3D* axis1 = new IfcSchema::IfcAxis2Placement3D(p3, file.addTriplet<IfcSchema::IfcDirection>(1, 0, 0), file.addTriplet<IfcSchema::IfcDirection>(0, 1, 0));
file.addEntity(axis1);
file.add_entity(axis1);
IfcSchema::IfcCircle* circle = new IfcSchema::IfcCircle(axis1, R);
file.addEntity(circle);
file.add_entity(circle);
IfcEntityList::ptr trim1(new IfcEntityList);
IfcEntityList::ptr trim2(new IfcEntityList);
IfcSchema::IfcTrimmingSelect::list::ptr trim1(new IfcSchema::IfcTrimmingSelect::list);
IfcSchema::IfcTrimmingSelect::list::ptr trim2(new IfcSchema::IfcTrimmingSelect::list);
trim1->push(new IfcSchema::IfcParameterValue(180));
trim1->push(p2);
trim2->push(new IfcSchema::IfcParameterValue(270));
trim2->push(p4);
IfcSchema::IfcTrimmedCurve* trimmed_curve = new IfcSchema::IfcTrimmedCurve(circle, trim1, trim2, false, IfcSchema::IfcTrimmingPreference::IfcTrimmingPreference_PARAMETER);
file.addEntity(trimmed_curve);
file.add_entity(trimmed_curve);
IfcSchema::IfcCompositeCurveSegment* segment2 = new IfcSchema::IfcCompositeCurveSegment(IfcSchema::IfcTransitionCode::IfcTransitionCode_CONTSAMEGRADIENT, false, trimmed_curve);
file.addEntity(segment2);
file.add_entity(segment2);
segments->push(segment2);
/*third segment - line */
@@ -107,14 +104,14 @@ void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
points2->push(p5);
file.addEntities(points2->generalize());
IfcSchema::IfcPolyline* poly2 = new IfcSchema::IfcPolyline(points2);
file.addEntity(poly2);
file.add_entity(poly2);
IfcSchema::IfcCompositeCurveSegment* segment3 = new IfcSchema::IfcCompositeCurveSegment(IfcSchema::IfcTransitionCode::IfcTransitionCode_CONTINUOUS, true, poly2);
file.addEntity(segment3);
file.add_entity(segment3);
segments->push(segment3);
IfcSchema::IfcCompositeCurve* curve = new IfcSchema::IfcCompositeCurve(segments, false);
file.addEntity(curve);
file.add_entity(curve);
IfcSchema::IfcSweptDiskSolid* solid = new IfcSchema::IfcSweptDiskSolid(curve, dia / 2, null, 0, 1);
@@ -126,7 +123,7 @@ void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(null, null, reps);
file.addEntity(shape);
file.add_entity(shape);
rebar->setRepresentation(shape);
@@ -136,8 +133,8 @@ void create_curve_rebar(IfcHierarchyHelper<IfcSchema>& file)
int main()
{
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname("ifc_curve_rebar.ifc");
hierarchy_helper file;
file.header().file_name().name("ifc_curve_rebar.ifc");
create_curve_rebar(file);
std::ofstream f("ifc_curve_rebar.ifc");
f << file;
+44 -45
View File
@@ -27,21 +27,21 @@
#include <iostream>
#include <fstream>
#define IfcSchema Ifc2x3
#include "ifcparse/Ifc2x3.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc2x3.h"
#include "../ifcparse/IfcUtil.h"
#include "../ifcparse/hierarchy_helper.h"
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
boost::none_t const null = boost::none;
typedef IfcWrite::IfcGuidHelper guid;
boost::none_t const null = (static_cast<boost::none_t>(0));
void create_testcase_for(IfcSchema::IfcProfileDef::list::ptr profiles) {
IfcSchema::IfcProfileDef* profile = *profiles->begin();
const std::string& profile_type = profile->declaration().name();
const std::string profile_type = IfcSchema::Type::ToString(profile->type());
const std::string filename = profile_type + ".ifc";
IfcHierarchyHelper<IfcSchema> file;
file.header().file_name()->setname(filename);
hierarchy_helper file;
file.filename(filename);
int i = 0;
for (IfcSchema::IfcProfileDef::list::it it = profiles->begin(); it != profiles->end(); ++it, ++i) {
@@ -54,15 +54,15 @@ void create_testcase_for(IfcSchema::IfcProfileDef::list::ptr profiles) {
product->setObjectPlacement(file.addLocalPlacement(0, 100. * i));
if (profile->declaration().is(IfcSchema::IfcParameterizedProfileDef::Class())) {
if (profile->is(IfcSchema::Type::IfcParameterizedProfileDef)) {
((IfcSchema::IfcParameterizedProfileDef*) profile)->setPosition(file.addPlacement2d());
}
IfcSchema::IfcExtrudedAreaSolid* solid = new IfcSchema::IfcExtrudedAreaSolid(profile,
file.addPlacement3d(), file.addTriplet<IfcSchema::IfcDirection>(0, 0, 1), 20.0);
file.addEntity(profile);
file.addEntity(solid);
file.add_entity(profile);
file.add_entity(solid);
IfcSchema::IfcRepresentation::list::ptr reps (new IfcSchema::IfcRepresentation::list);
IfcSchema::IfcRepresentationItem::list::ptr items (new IfcSchema::IfcRepresentationItem::list);
@@ -72,9 +72,9 @@ void create_testcase_for(IfcSchema::IfcProfileDef::list::ptr profiles) {
file.getSingle<IfcSchema::IfcRepresentationContext>(), S("Body"), S("SweptSolid"), items);
reps->push(rep);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(null, null, reps);
file.addEntity(rep);
file.addEntity(shape);
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(0, 0, reps);
file.add_entity(rep);
file.add_entity(shape);
product->setRepresentation(shape);
}
@@ -84,123 +84,122 @@ void create_testcase_for(IfcSchema::IfcProfileDef::list::ptr profiles) {
}
int main(int argc, char** argv) {
IfcSchema::get_schema(); // Ensure schema is initialized
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcUShapeProfileDef(
profiles->push(new Ifc2x3::IfcUShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, null, null, null, null));
profiles->push(new IfcSchema::IfcUShapeProfileDef(
profiles->push(new Ifc2x3::IfcUShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, 2.0, 2.0, null, null));
profiles->push(new IfcSchema::IfcUShapeProfileDef(
profiles->push(new Ifc2x3::IfcUShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, null, null, 4.0, null));
profiles->push(new IfcSchema::IfcUShapeProfileDef(
profiles->push(new Ifc2x3::IfcUShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, 1.0, 3.0, 6.0, null));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcTShapeProfileDef(
profiles->push(new Ifc2x3::IfcTShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, null, null, null, null, null, null));
profiles->push(new IfcSchema::IfcTShapeProfileDef(
profiles->push(new Ifc2x3::IfcTShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, 2.0, 2.0, 2.0, null, null, null));
profiles->push(new IfcSchema::IfcTShapeProfileDef(
profiles->push(new Ifc2x3::IfcTShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, null, null, null, 2.0, 2.0, null));
profiles->push(new IfcSchema::IfcTShapeProfileDef(
profiles->push(new Ifc2x3::IfcTShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, 3.0, 2.0, 1.0, 2.0, 2.0, null));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcZShapeProfileDef(
profiles->push(new Ifc2x3::IfcZShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, null, null));
profiles->push(new IfcSchema::IfcZShapeProfileDef(
profiles->push(new Ifc2x3::IfcZShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 5.0, 2.0, 2.0));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcEllipseProfileDef(
profiles->push(new Ifc2x3::IfcEllipseProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0, 15.0));
profiles->push(new IfcSchema::IfcEllipseProfileDef(
profiles->push(new Ifc2x3::IfcEllipseProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 15.0, 25.0));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcIShapeProfileDef(
profiles->push(new Ifc2x3::IfcIShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0, 50.0, 5.0, 5.0, null));
profiles->push(new IfcSchema::IfcIShapeProfileDef(
profiles->push(new Ifc2x3::IfcIShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0, 50.0, 5.0, 5.0, 2.0));
profiles->push(new IfcSchema::IfcAsymmetricIShapeProfileDef(
profiles->push(new Ifc2x3::IfcAsymmetricIShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0, 50.0, 5.0, 5.0, 2.0, 20.0, 10.0, 5.0, null));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcLShapeProfileDef(
profiles->push(new Ifc2x3::IfcLShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, null, null, null, null, null));
profiles->push(new IfcSchema::IfcLShapeProfileDef(
profiles->push(new Ifc2x3::IfcLShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 2.0, 2.0, null, null, null));
profiles->push(new IfcSchema::IfcLShapeProfileDef(
profiles->push(new Ifc2x3::IfcLShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, null, null, 2.0, null, null));
profiles->push(new IfcSchema::IfcLShapeProfileDef(
profiles->push(new Ifc2x3::IfcLShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 1.0, 2.0, 2.0, null, null));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcCShapeProfileDef(
profiles->push(new Ifc2x3::IfcCShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 10.0, null, null));
profiles->push(new IfcSchema::IfcCShapeProfileDef(
profiles->push(new Ifc2x3::IfcCShapeProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 10.0, 2.0, null));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcCircleProfileDef(
profiles->push(new Ifc2x3::IfcCircleProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0));
profiles->push(new IfcSchema::IfcCircleHollowProfileDef(
profiles->push(new Ifc2x3::IfcCircleHollowProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 25.0, 5.0));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcRectangleProfileDef(
profiles->push(new Ifc2x3::IfcRectangleProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0));
profiles->push(new IfcSchema::IfcRoundedRectangleProfileDef(
profiles->push(new Ifc2x3::IfcRoundedRectangleProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0));
profiles->push(new IfcSchema::IfcRectangleHollowProfileDef(
profiles->push(new Ifc2x3::IfcRectangleHollowProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, null, null));
profiles->push(new IfcSchema::IfcRectangleHollowProfileDef(
profiles->push(new Ifc2x3::IfcRectangleHollowProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 25.0, 5.0, 2.0, 4.0));
create_testcase_for(profiles); }
{ IfcSchema::IfcProfileDef::list::ptr profiles (new IfcSchema::IfcProfileDef::list);
profiles->push(new IfcSchema::IfcTrapeziumProfileDef(
profiles->push(new Ifc2x3::IfcTrapeziumProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 30.0, 25.0, 0.0));
profiles->push(new IfcSchema::IfcTrapeziumProfileDef(
profiles->push(new Ifc2x3::IfcTrapeziumProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 60.0, 25.0, -20.0));
profiles->push(new IfcSchema::IfcTrapeziumProfileDef(
profiles->push(new Ifc2x3::IfcTrapeziumProfileDef(
IfcSchema::IfcProfileTypeEnum::IfcProfileType_AREA,
null, 0, 50.0, 10.0, 25.0, 30.0));
create_testcase_for(profiles); }
+20 -29
View File
@@ -23,19 +23,15 @@
* *
********************************************************************************/
#include <iostream>
#include <fstream>
#include <optional>
#define IfcSchema Ifc4
#include "ifcparse/Ifc4.h"
#include "ifcparse/IfcHierarchyHelper.h"
#include "../ifcparse/schemas/Ifc4.h"
#include "../ifcparse/hierarchy_helper.h"
#include "suzanne_geometry.h"
#include <fstream>
typedef std::string S;
typedef IfcParse::IfcGlobalId guid;
boost::none_t const null = boost::none;
typedef ifcopenshell::global_id guid;
template <typename T>
std::vector< std::vector<T> > create_vector_from_array(const T* arr, unsigned size) {
@@ -54,36 +50,31 @@ std::vector< std::vector<T> > create_vector_from_array(const T* arr, unsigned si
}
int main(int argc, char** argv) {
IfcHierarchyHelper<IfcSchema> file;
hierarchy_helper<Ifc4> file;
IfcSchema::IfcBuildingElementProxy* product = new IfcSchema::IfcBuildingElementProxy(
guid(), 0, S("Blender's Suzanne"), null, null, 0, 0, null, null);
Ifc4::IfcBuildingElementProxy product = file.create<Ifc4::IfcBuildingElementProxy>().initialize(
guid(), std::nullopt, S("Blender's Suzanne"), std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt);
file.addBuildingProduct(product);
product->setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
product.setOwnerHistory(file.getSingle<Ifc4::IfcOwnerHistory>());
product->setObjectPlacement(file.addLocalPlacement());
IfcSchema::IfcRepresentation::list::ptr reps (new IfcSchema::IfcRepresentation::list);
IfcSchema::IfcRepresentationItem::list::ptr items (new IfcSchema::IfcRepresentationItem::list);
product.setObjectPlacement(file.addLocalPlacement());
std::vector< std::vector< double > > vertices_vector = create_vector_from_array(vertices, sizeof(vertices) / sizeof(vertices[0]));
std::vector< std::vector< int > > indices_vector = create_vector_from_array(indices, sizeof(indices) / sizeof(indices[0]));
IfcSchema::IfcCartesianPointList3D* coordinates = new IfcSchema::IfcCartesianPointList3D(vertices_vector);
IfcSchema::IfcTriangulatedFaceSet* faceset = new IfcSchema::IfcTriangulatedFaceSet(coordinates, null, null, indices_vector, null);
Ifc4::IfcCartesianPointList3D coordinates = file.create<Ifc4::IfcCartesianPointList3D>().initialize(vertices_vector);
Ifc4::IfcTriangulatedFaceSet faceset = file.create<Ifc4::IfcTriangulatedFaceSet>().initialize(coordinates, std::nullopt, std::nullopt, indices_vector, std::nullopt);
items->push(faceset);
IfcSchema::IfcShapeRepresentation* rep = new IfcSchema::IfcShapeRepresentation(
file.getRepresentationContext("Model"), S("Body"), S("SurfaceModel"), items);
reps->push(rep);
Ifc4::IfcShapeRepresentation rep = file.create<Ifc4::IfcShapeRepresentation>().initialize(
file.getRepresentationContext("Model"), "Body", "SurfaceModel", {faceset});
IfcSchema::IfcProductDefinitionShape* shape = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);
file.addEntity(shape);
Ifc4::IfcProductDefinitionShape shape = file.create<Ifc4::IfcProductDefinitionShape>().initialize(std::nullopt, std::nullopt, {rep});
file.add_entity(shape);
product->setRepresentation(shape);
product.setRepresentation(shape);
const std::string filename = "triangulated_faceset.ifc";
file.header().file_name()->setname(filename);
std::ofstream f(filename);
const std::string filename = "tesselated_faceset.ifc";
file.header().file_name().setname(filename);
std::ofstream f(filename.c_str());
f << file;
}

Some files were not shown because too many files have changed in this diff Show More