Rules between layers
Layers stay topologically independent (see Layers (concept)). A rule adds a check on top: buildings must lie within parcels, or must not overlap a protected area. A rule only checks. It never moves or changes anything; the editor refuses an edit that would make things worse (see Counted, not blocked).
A rule belongs to the constrained layer (X) and names a target layer (Y). The free editor brings
two kinds of rule, 'innerhalb' and 'ueberlappungsfrei', and an extension interface to add more
(@vectorstream/editor/erweiterung, experimental). Ten further kinds, attribute conditions, rules in meters, the red preview, snapping onto the
target layers and the list of violations come with vectorstream Pro.
Rules are on by default; new Editor(…, adapter, { geofencing: false }) leaves them unchecked and
regelVerstoesse() empty.
import type { Editor, Regelverstoss } from '@vectorstream/editor';
import { BUILDINGS, PARCELS } from './layers';
/**
* Two rules between the layers: every building lies within one parcel, and no parcel reaches into a
* building. Adding a rule is never refused because of data that is already there; what violates it
* is returned as the legacy.
*/
export function addBuildingRules(editor: Editor): { within: string; apart: string; legacy: readonly Regelverstoss[] } {
const within = editor.regelHinzufuegen(BUILDINGS, { art: 'innerhalb', ziel: PARCELS });
const apart = editor.regelHinzufuegen(PARCELS, { art: 'ueberlappungsfrei', ziel: BUILDINGS });
if (within === undefined || apart === undefined) throw new Error('a rule was rejected');
return { within, apart, legacy: editor.regelVerstoesse() };
}
/**
* Runs `work` with a rule switched off and switches it on again afterwards, even if `work` throws.
* The way to edit against a rule that would refuse a step on the way to a state that satisfies it.
* Each of the three steps is its own undo step.
*/
export function withRuleOff<T>(editor: Editor, ruleId: string, work: () => T): T {
editor.regelAendern(ruleId, { aktiv: false });
try {
return work();
} finally {
editor.regelAendern(ruleId, { aktiv: true });
}
}
Its test uses the sample parcels and the sample house, which lies across the boundary of two parcels:
the house is in no single parcel and reaches into both, so the rules report it as legacy, and
withRuleOff leaves the rule on afterwards, also when the work throws. A free editor refuses a rule with a
condition. Adding a rule is undoable.
Kinds of rule#
An element of the constrained layer is every bounded area, every line chain (a maximal run of line edges) and every free point. An enclosed area such as a courtyard is an element of its own. Every bounded area of the target layer is a target area, labeled or not, a courtyard included. The boundary of the target layer is formed by the edges that border one of its areas; line edges of the target do not belong to it.
| Group | Kind (dialog text) | art |
An element violates when … | Checked elements | Measure |
|---|---|---|---|---|---|
| Position to areas | must lie within | 'innerhalb' |
it does not lie within exactly one target area. | areas, lines, points of X | — |
| must not overlap | 'ueberlappungsfrei' |
it reaches into a target area. | areas, lines, points of X | — |
The other ten kinds (touching, overlapping, covering, filling without gaps, lying on a line or on a boundary,
distance, minimum area and width) are part of vectorstream Pro. Two rules on the
same layer are duplicates when kind and target are equal; the measure does not count (ruleDuplicate,
ruleDuplicateNoTarget). Rules apply to hidden and locked layers as well, and an inactive rule (aktiv: false)
checks nothing.
- "Line in area" (a pipe within its corridor) is not a kind of its own: it is
'innerhalb'applied to a line.
Touching and tolerance#
Coordinates lie on a grid (see Faces and edges). The tolerance τ of the position rules is one grid unit, the coarser one of both layers: 2−26 by default, about 1.5 · 10−8 degrees, around 1.7 mm. A part of an element at most τ from the boundary of the target counts as touching it. This is what lets a snapped point "be on" an oblique edge, which a grid could never hit exactly.
What this guarantees, in numbers:
- Clearly below τ touches, from 2τ on it does not. Gaps and overlaps of up to about one grid unit are not seen: an uncovered strip at most τ wide counts as touching, an element that reaches τ beyond a boundary counts as not reaching beyond.
- Exactly τ depends on floating-point rounding. On axis-parallel edges between grid points it holds
in the cases measured (the test tables check 0, 2τ and 4τ and the contact rows at τ), but a known
counterexample exists where an edge is split at a corner, for
'abdeckung','lueckenlos'and'innerhalb'alike. Do not build on exactly τ. - Oblique edges: the decision is reliable except for a margin of ±0.1 τ around τ (776 cases measured).
Importing rounds onto the grid, so there is no distance between 0 and τ along axis-parallel grid edges: half a unit becomes zero or one unit. The rules in meters of Pro have a tolerance in meters of their own.
Legacy is counted, not blocked#
Adding, enabling or changing a rule is never refused because of the data: whatever violates it then is
legacy and only listed by editor.regelVerstoesse(). Afterwards an edit is refused when it
raises the number of violating elements of a rule. An element that violated before may keep
violating, move, or gain a vertex.
- A violation is counted per rule and per violating element, not per place. An edit that fixes one violation and creates another elsewhere in the same step goes through; this is known and accepted.
- Both directions count: an edit of the target layer that makes elements of X violate is refused just like an edit of X itself.
- Undo and redo are checked the same way.
- The editor's rejection carries
rule: 'geofencing'and a code per kind (see API); the sentence names the layers and, where it applies, the measure, never an element. The element ids are inparams.
Showing a violation#
editor.zeigeElement(layer, element) moves the map to a violating element: it passes the box of a
face's outer ring, of a line chain or of a point to Adapter.bereichZeigen. The view is centered on
the box and zooms out only, just far enough that the box fits with a margin of 40 pixels; it never
zooms in, so a small element keeps the user's scale, and an element of size zero only pans. Use
Regelverstoss.elementEbene as layer and Regelverstoss.element as element. The call selects
nothing and activates no layer, so a locked layer can be shown but not selected. It returns false
and moves nothing if the layer or the element no longer exists.
Snapping#
The free editor snaps, while drawing and editing, onto the active layer only. vectorstream Pro adds snapping onto the target layers of the active rules. See Snapping.
Removing a layer#
editor.ebeneEntfernen refuses a layer while a rule belongs to it or points to it (also an unreadable
rule value on it). Remove the rules first: editor.regelEntfernen takes one id or a list and removes
them in one undo step. The application of this repository does that for the user.
API#
| Call | What it does |
|---|---|
editor.regeln(layer?) |
The rules of all layers or of one: { id, ebene, art, ziel, aktiv, mass?, bedingung? }. ziel is null for a kind without a target; mass and bedingung are present only when set. A rule the editor keeps but cannot check has status: 'benoetigtErweiterung' and roh (see Compatibility). |
editor.regelarten() |
The kinds this editor knows, in the order a dialog offers them: { art, eigenschaften: { ziel, mass, einrasten } }. The two free kinds, then those of the installed extensions. |
editor.regelHinzufuegen(layer, { art, ziel?, mass?, bedingung? }) |
Adds a rule and returns its id (label 'add-rule'), or undefined after a rejected event. |
editor.regelAendern(id, { aktiv?, art?, ziel?, mass?, bedingung? }) |
Changes it ('change-rule'); null removes ziel, mass or bedingung. |
editor.regelEntfernen(id | ids) |
Removes one or several in one step ('remove-rule'). |
editor.regelVerstoesse() |
One entry per violating element: { regel, ebene, elementEbene, element, zielflaechen }. The element ids belong to elementEbene. |
editor.zeigeElement(layer, element) |
Moves the map to an element (see Showing a violation); false if it is gone. |
laengenmassAus |
The meters of a metric. |
A kind with a target needs ziel (ruleNeedsTarget), a kind without one must not get it
(ruleNoTarget). A kind with a measure needs mass, a finite number above zero; a kind without one
must not get it (ruleMeasureInvalid). The library sets no default values. Further refusals: an
unknown layer, target or kind, a rule on its own layer, an incomplete condition, a duplicate, a locked
layer. Each call ends in exactly one change, rejected or unchanged event
(see Events); a change that leaves the stored rule as it was reports unchanged.
A rejection by a rule has the rule 'geofencing', and one of the codes editor.ruleViolationInside and
editor.ruleViolationOverlap; the kinds of Pro have theirs (see the Pro page). Its params are name, target, ebene, ziel, regel,
elementEbene, element and zielflaechen.
Storage#
A rule is one JSON value under the attribute key vs:regel:<id> on the outer face of the
constrained layer, so documents, autosave and the GeoJSON export of the layer (its geometry: null
feature) carry it. The keys come in a fixed order, optional ones only when set:
{"art":"innerhalb","ziel":"parcels","aktiv":true}
{"art":"mindestabstand","ziel":"water","aktiv":true,"mass":5}
{"art":"mindestflaeche","aktiv":true,"mass":20,"bedingung":{"schluessel":"use","op":"gleich","wert":"house"}}
A rule written before the new kinds existed reads and writes byte for byte as it did, so older
documents load unchanged and an export, import, export round trip yields the same file. A field that
does not fit the kind (a ziel on a kind without target, a mass on one without a measure), a missing
required field or an invalid condition makes the value unreadable: such a value is invisible in
regeln(), blocks removing the layer, and is dropped on import with a notice. A value of a kind the
editor does not know stays as it is, see Compatibility.
Compatibility#
There is no version field. A rule is read in two stages: first its envelope (a text art, a boolean
aktiv, a ziel that is absent or a non-empty text), then the fields of the kind, if the editor knows
the kind. This gives three states per stored value:
| State | When | What the editor does |
|---|---|---|
| a rule | the kind is registered, its fields read, and a condition can be evaluated | listed by regeln(), checked |
kept (status: 'benoetigtErweiterung') |
the envelope reads, but the kind is not registered with this editor (a Pro kind in the free editor, a customer's extension in another one), or the rule has a condition and no extension evaluates conditions | listed by regeln() with art as text; never checked, snapped onto or marked; the stored value stays byte for byte |
| unreadable | not even the envelope reads, or a registered kind with unreadable fields | invisible in regeln(), kept as an attribute, blocks removing the layer |
A rule that is kept is never lost: not by editing, undo and redo, saving, autosave, a second editor on
the same document, nor by a GeoJSON export and import. An editor without the kind can only remove it
(regelEntfernen, one undo step); regelAendern refuses it with ruleNeedsExtension, even switching
aktiv, because changing it would mean rewriting what the editor cannot read. Removing a layer stays
refused while such a rule belongs to the layer or points to it. Opened in an editor that knows the kind,
the rule is checked again; violations that arose meanwhile are legacy and only marked (see
Legacy is counted, not blocked). A rule with a condition counts as
kept in an editor that cannot evaluate conditions even if its kind is known: checking it without the
condition would be stricter than the rule says.
An editor that cannot evaluate conditions does not create one: regelHinzufuegen and regelAendern with
a bedingung are refused (ruleConditionNeedsExtension). Taking a condition away stays possible, and
it is the only change regelAendern accepts on a rule that is kept for its condition alone.
editor.regeln() returns both kinds of entry; tell them apart by status (absent for a checkable
rule). A kept rule carries the stored text as roh. PRO_ARTEN is the bare list of the ten kinds
beyond 'innerhalb' and 'ueberlappungsfrei', for an interface that wants to say "needs vectorstream
Pro". huelleRoh(value) reads the envelope alone, and ersetzeZiel(value, id) replaces the target of
a stored value and leaves every other byte alone (the application's import uses it when a layer id
changes).
The import of the application keeps every rule whose envelope reads. It drops, with a notice, a rule whose envelope is unreadable, a rule of a known kind whose fields do not read, and a rule whose target is not in the file or is the layer itself. In the rules dialog a kept rule is greyed out with the badge "Needs vectorstream Pro" (or "Needs an extension" with the kind), only "Remove" works, and opening a document with such rules shows a toast; their violations are never computed. An older build of the library (before this behaviour) reads a rule of one of the ten newer kinds as unreadable and drops it on import.
Performance#
Every active rule is checked after each transaction that touches its layer or its target layer. The numbers are
the ratio of the time of the check of a rule to checkGeometry of the same layer, with 2025 areas per layer,
rotated squares, an index in place, median of 3 to 7 runs on an Apple M5 Pro (Node 26); measurements scatter by up
to about ±45 % between runs.
| Kind | Buildings each in one parcel | Buildings across boundaries |
|---|---|---|
'innerhalb', 'ueberlappungsfrei' |
0.59 – 0.62 | 0.64 – 0.66 |
Identical cells in X and Y (every building ring equals a parcel ring) are expensive: 'innerhalb' 12.5×,
'ueberlappungsfrei' 12.8 – 12.9× at 2025 areas. If a rule makes editing feel slow, switch the rule off
(aktiv: false). The kinds of Pro, with an incremental check that keeps the verdict of every element an edit
cannot have changed, are described in their performance section.
Not included#
- Repairing violations. Violating elements are drawn in the
invalidstate;regelVerstoesse()lists them andzeigeElementshows them. - Relations beyond the two kinds above, rules in meters and attribute conditions: see vectorstream Pro.