Build Linearity Curve

KINDAback to the verdict

0%0 of 16 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A vector editor whose hard problems are one path representation that every operation can trust, boolean operations that stay undoable, and an export that reports what it had to throw away.

estimated effort multi-daythe files for this build are in the project pack

RuntimeA desktop application in TypeScript, document model kept separate from the rendererGeometryCubic Beziers only, with arcs, quadratics and polylines converted at importBoolean opsA maintained path boolean library, not a sweep line of your ownRenderingCanvas 2D with dirty rectangle redraw, hit testing against the modelDocumentVersioned JSON as the save format, with SVG and PDF as export targetsUndoA log of operations and their inverses, not snapshots of the scene

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the editor and its test suite.

    Get it nodejs.org, or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Boolean operations on Bezier paths are the part of this project most likely to produce quietly wrong geometry if written from scratch.

    Get it Choose a library with a test suite covering tangential and self-intersecting cases, and read its issue tracker before committing.

    Verify A fixture of two tangentially touching circles returns a result rather than hanging

  3. have readyfree

    Why Import correctness is only demonstrable against files that use the commands you would rather not implement.

    Get it Assemble files using elliptical arcs, smooth shorthands, nested transforms, even-odd fills and text, from your own work or from public domain artwork.

    Verify Each fixture imports and exports with a written report

  4. decidefree

    Why The target format cannot express it, so either you decide now or the exporter decides for you and nobody finds out until the artwork is printed.

    Get it Read the current SVG Strokes draft, confirm stroke-alignment is still not normative in SVG 2, and write down your rule. open ↗

    Verify The rule is in the README and asserted in a test

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- documents(id, name, format_version, width, height, unit, created_at, updated_at)
- nodes(id, document_id, parent_id, ordinal, kind, transform, visible, locked, name) · kind is group, path, text, image or boolean
- paths(node_id, subpaths_json, fill_rule, closed) · cubic segments only, everything normalised into this shape at import
- styles(node_id, fill_json, strokes_json, opacity, blend_mode) · a stroke carries width, alignment, caps, joins and dash pattern
- boolean_ops(node_id, op, operand_order_json) · operands stay as hidden children so the operation is still undoable a year later
- ops(id, document_id, ordinal, kind, payload_json, inverse_json) · undo replays inverses, and a drag is one coalesced entry
- export_reports(id, document_id, target, at, dropped_json) · what the exporter could not represent, written every time you export
- Stroke alignment is stored because the editor supports it and the export format does not. Export is where that gets resolved, loudly.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DOCUMENT_DIRrequired./documentsWhere saved documents live. This is the data you cannot regenerate.
ARC_TOLERANCErequired0.01Maximum deviation in document units when converting arcs to cubics. Recorded in each document.
HIT_TOLERANCE_UNITSrequired3Pick radius in document units before scaling by zoom.
HISTORY_MAX_BYTESrequired64000000Ceiling for the undo log. Exceeding it trims the oldest entries and says so.
EXPORT_REPORT_DIRrequired./exports/reportsWhere export reports are written. One per export, including the clean ones.

The build, in order

  1. One path representation and nothing else

    Every curve in the document is a cubic Bezier, whatever the file it came from used.

    1. One representation is the decision the rest of the editor rests on.

      Files src/geometry/normalise.tssrc/io/svg-import.ts

    2. An unrecorded tolerance makes a round trip failure unarguable.

      Files src/geometry/arc-to-cubic.tstests/arc-tolerance.test.ts

    3. Nested transforms are where coordinates quietly stop matching.

      Files src/geometry/transform.ts

    done when · tick each as it passes
    watch out
    • Keeping five segment types because the file format has them means boolean operations, offsetting, hit testing and simplification each need five cases, and the arc case is wrong in all four of them. Convert once at the door, keep cubics only, and treat the importer as the only code in the project that has ever heard of an elliptical arc.
  2. Direct manipulation that stays smooth

    Selecting, dragging and editing nodes runs against the document model at interactive speed.

    1. Opacity and device pixel ratio both break the pixel approach.

      Files src/interaction/hit-test.tstests/hit-test.test.ts

    2. A hairline has to stay clickable when you are zoomed out.

      Files src/interaction/hit-test.ts

    3. A full repaint per pointer move is what makes an editor feel cheap.

      Files src/render/dirty.ts

    done when · tick each as it passes
    watch out
    • Hit testing by reading the pixel under the cursor picks the wrong node for anything with opacity, misses a shape whose stroke is wider than its fill, and returns nothing at all once the canvas is scaled for a high density display. Test the point against the geometry, with the tolerance scaled by zoom so a hairline stays clickable at every magnification.
  3. Boolean operations that stay undoable

    Union, subtract, intersect and exclude produce a node that still remembers its operands.

    1. Writing your own is how you meet tangential intersections.

      Files src/geometry/boolean.tspackage.json

    2. The operation stays editable for as long as the document lives.

      Files src/model/boolean-node.ts

    3. Destroying the operands should take a deliberate click.

      Files src/commands/flatten.tstests/boolean.test.ts

    done when · tick each as it passes
    watch out
    • Flattening a union into one path the moment the user clicks the button means fixing a mistake made ten operations ago requires redrawing both shapes from scratch, which is the single most common complaint about cheap vector editors. Keep the operands, recompute the result when either changes, and make flattening an action with a name.
  4. Undo as a log of inverses

    Every change is an operation with an inverse, and a drag is one entry rather than four hundred.

    1. Not a snapshot. The difference is the whole memory budget.

      Files src/history/ops.ts

    2. One drag is one thing the user did.

      Files src/history/coalesce.ts

    3. Undo bugs hide in the combination, not the single case.

      Files tests/history.property.test.ts

    done when · tick each as it passes
    watch out
    • Pushing a copy of the whole document onto a stack per change is fine for twenty shapes and unusable for two thousand, and the memory cost appears exactly when the document finally matters. Store the inverse of each operation instead, and treat a pointer drag as one operation recorded when the pointer is released.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more design.