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