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.
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).
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.
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
The solver is triggered on every drag event with a one-tick defer (setTimeout(..., 0)), matching fSpy's original middleware behaviour exactly.
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.
A single effect() watches these signals and calls Solver.solve2VP() when any of them changes:
imageStatecalibrationSettingsBasecalibrationSettings2VPcontrolPointsStateBasecontrolPointsState2VP
globalSettings and resultDisplaySettings changes do not trigger a re-solve — they are display-only.
- Selector:
fspy-calibrator - Provided:
CalibrationStateService(component-level, isolated per instance) - Owns the three-panel layout (left / centre / right).
- On
imageUrlinput change: loads the image viaHTMLImageElement, readsnaturalWidth/naturalHeight, writesimageStateinto the service. - Subscribes to
solverResult; on each change derivesThreeCameraConfigand emits bothcalibrationChangeandthreeCameraChange.
- Hosts a Konva.js
Stage+Layersized 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
Linenodes, computed fromsolverResultvia verbatimOverlay3DPanelprojection logic. - Magnifying glass: HTML element overlaid on canvas during Shift + drag (matches fSpy behaviour).
- All drag events call
CalibrationStateServicemutators; solver re-runs on the next tick.
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.
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.
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.
After each solve2VP() call that returns a valid result, CalibratorComponent derives a ThreeCameraConfig:
- Vertical FOV converted from radians to degrees.
- Aspect ratio read from
imageStatedimensions. - Camera-to-world matrix converted to Three.js coordinate conventions (Y-up, right-handed, column-major order).
- Near/far defaults:
0.1/1000.
The exported applyThreeCameraConfig(camera, config) helper applies all fields to a THREE.PerspectiveCamera and calls updateProjectionMatrix().
| 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 |
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:
- Extract
SavedStateJSON (control points + settings + expectedcameraParameters). - Call
Solver.solve2VP()with the parsed inputs. - Assert every numeric field in the result matches
cameraParameterswithin ≤ 1e-4.
The .fspy binary format: [4 bytes "fspy"] [uint32 version] [uint32 stateSize] [uint32 imageSize] [stateSize bytes JSON] [imageSize bytes image data].