Files
IfcOpenShell/src/ifcviewer-web/WebFederation.h
T
Dion Moult b252cd25f8 Give the web viewer a federation: false origin and per-model transforms
Models now resolve to global coordinates, which alone would make things worse:
composed per-instance transforms are float32, and around six million metres
that quantises at roughly half a metre. So the first model to load also sets a
false origin, derived from where its geometry actually sits, unless a host has
set one itself.

WebFederation owns the concepts an .ifcfed carries — a federation unit, a false
origin, a per-model transform and display name — without the file format. The
desktop Federation class is a document model whose sources are local filesystem
paths, which mean nothing in a browser; a host page that wants .ifcfed can parse
the JSON and drive these calls.

Models are keyed by the JS source id rather than the session model id. The
source id exists the moment a File or URL is registered, whereas the session id
is minted inside the async range-read chain, so keying on it lets a transform be
set before the model has streamed and applied when it arrives — the model never
visibly jumps. loadSidecarMetadataWeb gained a completion callback to carry that
id back out, and addFile/addUrl now return the source id and fire onModelLoaded,
where before they were fire-and-forget with no handle and no completion signal.

The embedded sample bypasses the source registry, so it is bound separately;
otherwise the guess never runs for a page that only ever shows the sample.

georef-a and georef-b are the regression fixture: two boxes whose different map
conversions resolve to the same real-world point, so a viewer that applies them
draws one box's worth of scene and one that ignores them spans 707 m. They carry
two meshes each because reorderSidecarByMorton bails out below two and then
writes no chunk table, and a sidecar without one cannot stream over byte ranges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 17:06:37 +10:00

119 lines
5.9 KiB
C++

/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#ifndef WEBFEDERATION_H
#define WEBFEDERATION_H
#include "FederationMath.h"
#include <cstdint>
#include <string>
#include <unordered_map>
class ViewportCore;
// Federation state for the web viewer: the concepts a host page needs to place
// several models in one scene — a federation unit, a false origin, and a
// per-model transform and display name.
//
// Deliberately NOT an .ifcfed reader. The desktop Federation class is a
// document model (QObject, JSON persistence, groups, cloud manifests) and its
// model sources are local filesystem paths, which mean nothing in a browser.
// What the web needs is the underlying concepts, exposed so JS can drive them;
// a host page that wants .ifcfed can parse the JSON itself and call these.
//
// Models are keyed by the JS-side source id, NOT the session model id. The
// source id exists synchronously the moment a File or URL is registered,
// whereas the session model id is minted deep inside the async range-read
// chain. Keying on the source id is what lets a caller set a transform BEFORE
// the load finishes, so the model appears already in place instead of jumping
// once its state is applied afterwards.
class WebFederation {
public:
explicit WebFederation(ViewportCore& core) : core_(core) {}
// ---- Federation-wide -----------------------------------------------
void setConfig(const FederationConfig& cfg);
const FederationConfig& config() const { return config_; }
// Marks the origin as explicitly authored, which suppresses the automatic
// guess in onModelLoaded. A host that sets an origin means it.
void setFalseOrigin(const FederatedFalseOrigin& origin);
const FederatedFalseOrigin& falseOrigin() const { return false_origin_; }
bool falseOriginIsExplicit() const { return false_origin_explicit_; }
// ---- Per-model -------------------------------------------------------
void setModelTransformation(int source_id, const ModelTransformation& xf);
void clearModelTransformation(int source_id);
void setModelName(int source_id, std::string name);
std::string modelName(int source_id) const;
// Session model id for a source, or 0 when that source has not finished
// loading. Session ids start at 1, so 0 is unambiguous.
std::uint32_t sessionModelId(int source_id) const;
// ---- Lifecycle -------------------------------------------------------
// Call when a sidecar load completes. Binds the source to its session model
// id, applies whatever state was staged against the source id, and — for
// the first model only, and only when no origin was set explicitly — runs
// the false-origin guess.
void onModelLoaded(int source_id, std::uint32_t session_model_id);
// A model that did not come from a JS source — the embedded sample, read
// synchronously from MEMFS. It has no source id and so cannot carry a
// per-model transform (a host that wants to place a model adds it as a
// source), but it must still take part in the false-origin guess.
// Otherwise a page showing the sample renders it unshifted, which for a
// georeferenced model means out at its surveyor coordinates.
void onModelLoadedWithoutSource(std::uint32_t session_model_id);
// Drop all state. Pairs with ViewportCore::resetScene so a fresh
// federation does not inherit the previous one's origin or transforms.
void clear();
private:
// Push the composed false-origin matrix; every model recomposes against it.
void applyFalseOrigin();
// Push one model's composed transform. No-op until the model has loaded,
// since composing needs its georef.
void applyModelTransformation(int source_id);
// Position + grid-north heading that puts the first model near the origin
// instead of out at its surveyor coordinates.
void guessFalseOriginFrom(std::uint32_t session_model_id);
struct ModelState {
std::uint32_t session_model_id = 0; // 0 until loaded
std::string name;
ModelTransformation transformation;
bool has_transformation = false;
};
ViewportCore& core_;
FederationConfig config_;
FederatedFalseOrigin false_origin_;
bool false_origin_explicit_ = false;
bool guessed_false_origin_ = false;
std::unordered_map<int, ModelState> models_;
};
#endif // WEBFEDERATION_H