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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>