THE EDIT LAB
The topology SDK, run against a live county fabric. Each section pairs the code you would write with a button that runs it here; the readouts are the actual return values, and the log below the sheet records every call.
- Fabric
- loading · 869 deeds, Pennington County, arc-true ≤ 2 cm
- Rules
- gap · overlap · self-intersection · floodway (this page's own)
- Entry
- @devtomorrow/sundial/edit · outside the 29 KB core payload
Build a workspace
Features in, shared-boundary graph out: coincident boundaries weld to single edges, T-nodes heal, condo stacks and partial overlaps are classified honestly. Labels ride along so every diagnostic names the parcel, not an index. This ran when the page loaded; the readout is its result.
import {
EditWorkspace, curveFeaturesToInputs,
} from '@devtomorrow/sundial/edit';
const ws = new EditWorkspace(
curveFeaturesToInputs(
deeds, // Esri curveRings, arcs intact
(f) => `parcel ${f.properties.id}`,
),
44.08, // reference latitude
{ rules: [floodway] }, // your rules (below)
);
ws.invariants(); // DCEL health, signed-area zero
Move structure, not shapes
Snapping returns references into the graph. Moving a shared corner deforms every deed riding it (area flows between neighbors while the total holds), and the move's dirty neighborhood validates on commit.
const hit = ws.snap(lngLat, 6); // node | edge | null
if (hit?.kind === 'node') {
const violations = ws.moveVertex(hit.node, to);
if (violations.length) ws.undo(); // exact, to
} // the last ulp
Rules on every edit
validate() audits the whole fabric; every moveVertex validates its neighborhood. Each finding names its features (labels first), locates itself, and states the recovery.
for (const v of ws.validate()) {
v.rule; // 'gap' | 'overlap' |
// 'self-intersection' | yours
v.detail; // "“parcel 3715…”: boundary …"
v.at; // [lng, lat] for the map badge
v.hint; // the recovery, as an action
}
Your rules, your layers
The violet square on the sheet is a floodway easement (this page's layer, not the library's). The rule below runs beside the built-ins on every edit, receives the same dirty set, and reports through the same Violation shape.
import {
WORLD, DEAD, type CustomRule,
} from '@devtomorrow/sundial/edit';
const floodway: CustomRule = {
id: 'floodway',
check(t, faces) {
const out = [];
for (let f = 0; f < t.faceCount; f++) {
if (faces && !faces.has(f)) continue;
if (t.faceFeature[f] === WORLD) continue;
if (t.heFace[t.faceEdge[f]] === DEAD) continue;
for (const n of ringNodes(t, f)) {
if (inMyLayer(t.nodeX[n], t.nodeY[n])) {
out.push({
rule: 'floodway',
detail:
`${t.featureName(t.faceFeature[f])}: ` +
'boundary enters the floodway',
faces: [f],
at: t.toLngLat(t.nodeX[n], t.nodeY[n]),
hint: 'press U to undo the move',
});
break;
}
}
}
return out;
},
};
Findings drive repairs
A gap finding carries the feature pair; sealGap conforms one boundary onto the other's recorded line, arcs included. mergeFeatures dissolves a shared line, and refuses ambiguous shapes with the reason.
for (const v of ws.validate()) {
if (v.rule === 'gap' && v.features) {
ws.sealGap(v.features[0], v.features[1]);
}
}
const r = ws.mergeFeatures(keepId, goneId);
if (!r.ok) console.log(r.reason);
// "features 3 and 7 share no boundary line,
// merge needs adjacency"
Construct on the fabric
Draw a new deed and it joins the live topology: snapping, dragging, and every rule apply to it immediately. Buffers are survey constructions: a point becomes one true circle in four quarter arcs, a sketched line becomes a corridor with arc caps and joins, recorded as curves, tessellated only for the screen.
import { bufferPoint, bufferPolyline }
from '@devtomorrow/sundial/edit';
// One true circle, four quarter arcs.
const circle = bufferPoint(clickLngLat, 25, frame);
// Offset sides, 180° end caps, round joins.
const corridor = bufferPolyline(sketch, 8, frame);
// Both export as curveRings, never densified.
Attributes ride the record
Arm inspection and click any deed: the id-buffer pick resolves it, the panel opens its county record (PIN and friends), and the geometry facts beside it are computed arc-true on the spot. Edit a value, add a field, apply.
map.on('click', (ev) => {
if (!ev.feature) return;
// The id-buffer resolves the deed under
// the cursor in < 2 ms, no geometry scan.
openPanel(ev.feature.properties);
});
The record round-trips
Untouched geometry exports byte-identical; recorded arcs re-emit as arcs; labels ride along. The button undoes every command on the stack and compares the export's SHA-256 against the load-time baseline.
const features = ws.export();
// → [{ id, label?, rings: [{ pts, arcs, hole }] }]
// A sealed fabric rebuilds into TRUE shared
// structure, coincident coordinates weld.
const next = new EditWorkspace(features, 44.08);
The contract
The shapes the code above consumes: dense, complete, current.
| member | returns | notes |
|---|---|---|
| snap(p, tolM) | SnapResult | node beats edge, then distance; refs into the graph |
| moveVertex(node, to) | Violation[] | undoable; every incident feature follows |
| insertVertex(he, at) | number | structural; splits arcs into true sub-arcs |
| filletNode(node, rM) | { ok, reason? } | tangent arc on a two-leg corner; both sides follow |
| reshape(path) | ReshapeResult | splices the span between two boundary crossings |
| sealGap(auth, mover, bandM?) | SealReport | conforms mover onto auth's recorded line (0.25 m band) |
| mergeFeatures(keep, gone) | MergeResult | dissolves the shared line; holes transfer; refusals explain |
| validate(opts?) | Violation[] | whole fabric; workspace rules always included |
| undo() / redo() | boolean | command stack; coordinates restore to the last ulp |
| export(faces?) | ExportedFeature[] | byte-identical where untouched; arcs stay arcs |
| invariants() | InvariantReport | twin involution, cycle partition, Σ signed area = 0 |
| field | type | notes |
|---|---|---|
| rule | string | built-in id, or your CustomRule's id |
| detail | string | one sentence; names features by label, id fallback |
| features? | [id, id] | gap pairs, the direct hand-off to sealGap |
| at? / outline? | LngLat / LngLat[] | badge anchor and overlay ring |
| hint? | string | the recovery, stated as an action |
| field | default | notes |
|---|---|---|
| toleranceM | 0.001 | build snap-rounding; identity only, never geometry |
| gridCellM | 25 | spatial hash cell for snap and proximity queries |
| rules | [] | CustomRule[], run on every validation |
| gapSliverM2 | 5 | world cycles smaller than this flag as slivers |
| gapProximityM | 0.25 | disconnected near-boundary band; 0 disables |