Skip to content

Latest commit

 

History

History
171 lines (118 loc) · 8.04 KB

File metadata and controls

171 lines (118 loc) · 8.04 KB

Architecture

This document describes the internal structure of the fspy-calibrator Angular library for contributors and integrators who need to understand how the code is organized.

For usage, inputs/outputs, and integration examples, see API.md.
For the original fSpy concepts (vanishing points, calibration workflow, camera math), see the fSpy documentation.


Repository Layout

The Angular port lives in the angular/ subfolder of the fSpy repository as a two-project workspace:

fSpy/
└── angular/
    ├── projects/
    │   ├── fspy-calibrator/     # Publishable Angular library
    │   └── demo/                # Standalone demo application
    ├── angular.json
    ├── tsconfig.json
    └── package.json

The solver source files (solver/, types/, defaults/, style/, strings/) are copied verbatim from the fSpy root into the library. No refactoring, no renaming — this guarantees numerical parity with the original desktop app (≤ 1e-4 tolerance).


Component Tree

ParentApp
  └── <fspy-calibrator>          CalibratorComponent   (public entry point)
        ├── SettingsPanelComponent                      (left panel)
        ├── ControlPointsCanvasComponent                (centre canvas)
        └── ResultsPanelComponent                       (right panel)

All three child components share a single CalibrationStateService instance, provided at the CalibratorComponent level — so each <fspy-calibrator> element on the page gets its own isolated state.


Data Flow

sequenceDiagram
    participant Konva as Konva Drag Event
    participant Service as CalibrationStateService
    participant Effect as Angular effect()
    participant Solver as Solver.solve2VP()
    participant Output as @Output

    Konva->>Service: mutator call (e.g. setFirstVanishingPointControlPoint)
    Service->>Effect: signal change detected (next tick)
    Effect->>Solver: solve2VP(settingsBase, settings2VP, controlPointsBase, controlPoints2VP, imageState)
    Solver-->>Effect: SolverResult
    Effect->>Service: write solverResult signal
    Effect->>Output: emit calibrationChange + threeCameraChange
Loading

The solver is triggered on every drag event with a one-tick defer (setTimeout(..., 0)), matching fSpy's original middleware behaviour exactly.


CalibrationStateService

The service is the central nervous system of the component. It replaces fSpy's Redux store with Angular Signals — one WritableSignal per Redux slice, all initialised from the verbatim fSpy defaults/ values.

Signal Type fSpy Redux equivalent
globalSettings WritableSignal<GlobalSettings> globalSettings reducer
calibrationSettingsBase WritableSignal<CalibrationSettingsBase> calibrationSettingsBase reducer
calibrationSettings2VP WritableSignal<CalibrationSettings2VP> calibrationSettings2VP reducer
controlPointsStateBase WritableSignal<ControlPointsStateBase> controlPointsStateBase reducer
controlPointsState2VP WritableSignal<ControlPointsState2VP> controlPointsState2VP reducer
imageState WritableSignal<ImageState> image reducer
solverResult WritableSignal<SolverResult> solverResult reducer
resultDisplaySettings WritableSignal<ResultDisplaySettings> resultDisplaySettings reducer

The service exposes typed mutator methods that mirror fSpy's action creators (e.g. setFirstVanishingPointControlPoint, setRectangleModeEnabled, setOrigin, setPrincipalPointMode). Child components never write to signals directly.

Solver trigger

A single effect() watches these signals and calls Solver.solve2VP() when any of them changes:

  • imageState
  • calibrationSettingsBase
  • calibrationSettings2VP
  • controlPointsStateBase
  • controlPointsState2VP

globalSettings and resultDisplaySettings changes do not trigger a re-solve — they are display-only.


Components

CalibratorComponent

  • Selector: fspy-calibrator
  • Provided: CalibrationStateService (component-level, isolated per instance)
  • Owns the three-panel layout (left / centre / right).
  • On imageUrl input change: loads the image via HTMLImageElement, reads naturalWidth/naturalHeight, writes imageState into the service.
  • Subscribes to solverResult; on each change derives ThreeCameraConfig and emits both calibrationChange and threeCameraChange.

ControlPointsCanvasComponent

  • Hosts a Konva.js Stage + Layer sized to the container element.
  • Renders background image (aspect-ratio-preserving) and all interactive control points via an Angular effect() that imperatively updates Konva nodes.
  • 2VP mode: 8 draggable circles (4 per vanishing point) + dashed extension lines to computed VPs.
  • Rectangle mode: 4 draggable corners; VP2 is derived automatically.
  • Origin: always-visible draggable marker; updates camera translation in real time.
  • 3D overlay: axes / box / grid rendered as Konva Line nodes, computed from solverResult via verbatim Overlay3DPanel projection logic.
  • Magnifying glass: HTML element overlaid on canvas during Shift + drag (matches fSpy behaviour).
  • All drag events call CalibrationStateService mutators; solver re-runs on the next tick.

SettingsPanelComponent

Left panel. Thin wrappers over CalibrationStateService mutators — no local state.

Controls: VP count selector (visible but locked to 2), VP axis dropdowns, reference distance form, principal point mode dropdown, rectangle mode checkbox, 3D guide dropdown, dim image checkbox.

ResultsPanelComponent

Right panel. Reads solverResult and resultDisplaySettings signals (read-only — no writes).

Displays: errors/warnings, field of view (with unit toggle), camera position, camera orientation (axis-angle or quaternion), principal point (absolute or relative). Each numeric value has a Copy button.


Solver Pipeline

The solver (src/solver/) is copied verbatim from fSpy's TypeScript source — zero modifications. This is a deliberate constraint: any divergence in types, defaults, or math would break numerical parity with .fspy project files.

CalibrationMode is hardcoded to TwoVanishingPoints. The 1VP code path exists in the source copy but is never invoked.

Three.js output derivation

After each solve2VP() call that returns a valid result, CalibratorComponent derives a ThreeCameraConfig:

  1. Vertical FOV converted from radians to degrees.
  2. Aspect ratio read from imageState dimensions.
  3. Camera-to-world matrix converted to Three.js coordinate conventions (Y-up, right-handed, column-major order).
  4. Near/far defaults: 0.1 / 1000.

The exported applyThreeCameraConfig(camera, config) helper applies all fields to a THREE.PerspectiveCamera and calls updateProjectionMatrix().


Key Technology Decisions

Area Choice Reason
State management Angular Signals Direct 1:1 mapping to fSpy Redux slices; no extra dependencies
Canvas Konva.js (direct) Same library as fSpy; no React wrapper needed
Solver Verbatim copy Numerical parity guarantee; zero risk of math divergence
Component style Standalone components (Angular 17+) No NgModule boilerplate
Service scope Component-level providers Each <fspy-calibrator> instance is fully isolated
Solver trigger One-tick defer via setTimeout Matches fSpy's setTimeout(..., 0) pattern exactly

Parity Tests

Tests parse real .fspy binary project files from fSpy's own test_data/ corpus using a small FspyFileParser utility (not shipped in the library bundle). For each file:

  1. Extract SavedState JSON (control points + settings + expected cameraParameters).
  2. Call Solver.solve2VP() with the parsed inputs.
  3. Assert every numeric field in the result matches cameraParameters within ≤ 1e-4.

The .fspy binary format: [4 bytes "fspy"] [uint32 version] [uint32 stateSize] [uint32 imageSize] [stateSize bytes JSON] [imageSize bytes image data].