SUNDIAL EDITING a Developing Today project

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.

ws.ts
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.

move.ts
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.

rules.ts
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.

floodway.ts
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.

repair.ts
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.

construct.ts
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.

inspect.ts
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.

roundtrip.ts
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.

EditWorkspace
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
Violation
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
Options
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