Validity and rejections
The editor never stores an invalid state. Every edit runs as a transaction: all of its operations are applied, the result is checked, and if a check fails, everything is rolled back. There is no half-applied edit.
What is checked#
After every transaction — and after every undo and redo — two checks run:
- Structure (
checkInvariants): half-edge pairs, closed boundary cycles, a consistent outer face, Euler's formula, coordinates on the grid, and more. A failure here means a programming error, never a user mistake. - Geometry (
checkGeometry): no crossing edges, no two nodes at the same place, no node lying on a foreign edge, every boundary and node inside the face it belongs to. This check always runs with tolerance zero, independent of the snap radius.
Before that, the operation itself may refuse its input: three points on one straight line enclose no area, a pin cannot sit exactly on a boundary, the difference tool needs a selection.
How a rejection reaches you#
A rejected edit does not throw. The editor emits a rejected event, and the partition is exactly
as before:
editor.on('rejected', ({ label, violations }) => {
// label: what was attempted, e.g. 'draw-polygon', or 'undo' / 'redo'
// violations: one or more findings
});
Each finding (Violation) has:
| Field | Content |
|---|---|
rule |
Which rule failed, e.g. 'edge-crossing', 'node-on-edge'; 'intent' when an operation refused its input. |
detail |
A sentence for the user, in the language current when it was created. |
code |
A stable, namespaced code such as 'core.violationNodeOnEdge' or 'editor.differenceNoSelection' — compare this, not the text. Present wherever the finding comes from a message table. |
params |
The values of the sentence's placeholders, plus IDs of the elements concerned. |
nodes, edges, faces |
The elements concerned, where the rule names them. |
formatMessage(code, params) renders the sentence again, for example in another language; see
Events and Language.
Methods called directly behave differently from gestures: layer methods such as
editor.setzeSichtbar or editor.ebeneEntfernen throw an InputRejected (with code and
params) when they refuse, and editor.umzugVorschau throws the same rejection the move would
produce. Methods that run a transaction — editor.umziehen, editor.benenneElement, editor.undo
— report through rejected like a gesture and return undefined or false.
Errors that are thrown from a gesture are bugs, not rejections: a broken structure
(StructuralError), access to an element that does not exist (TopologyError), or a failed
rollback (RollbackError). The editor does not catch them.
Rules between layers#
An active rule between layers adds a third check with the rule 'geofencing'. It rejects
an edit only when the edit raises the number of violating elements of a rule; data that violated a rule
before stays legacy and is merely listed by editor.regelVerstoesse().
Undo can be rejected too#
Undo and redo are checked like any edit. If an edit had removed a pre-existing geometry problem,
undoing it brings the problem back. That is allowed: undo and redo are checked against the state
the history recorded for that point. If a check still fails, the editor emits rejected with the
label 'undo' or 'redo', and undo() returns false.
Pre-existing problems#
Imported data can carry geometry problems. Such a partition stays editable: the editor rejects only violations that are new, not those that were already there.
By default importGeoJSON runs checkGeometry on its result and throws an ImportError
('core.importInvalidGeometry', the findings in violations) for crossing edges, overlapping areas or
a vertex lying on a foreign edge. To open such a legacy file anyway, import it with
geometriePruefen: false and inspect the findings yourself:
import { SpatialIndex, checkGeometry, importGeoJSON, type FeatureCollection, type Violation } from '@vectorstream/core';
/**
* By default `importGeoJSON` rejects data with geometry problems (crossing edges, overlapping areas,
* a vertex lying on a foreign edge) with an `ImportError`. To open such a legacy file anyway, import
* it with `geometriePruefen: false` and look at the problems yourself.
*/
export function importChecked(data: FeatureCollection): { problems: Violation[]; partition: ReturnType<typeof importGeoJSON> } {
const partition = importGeoJSON(data, { geometriePruefen: false });
const problems = checkGeometry(partition, new SpatialIndex(partition), 0);
return { problems, partition };
}
Its test imports the sample parcels plus a line whose end lies in the middle of a parcel edge: the
default import rejects it, the opt-out import succeeds, checkGeometry reports node-on-edge, and
drawing elsewhere is still accepted.
API: Violation, InputRejected,
checkGeometry.