thi-ng/umbrella

View on GitHub
packages/geom-axidraw/src/as-geometry.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import type { DrawCommand } from "@thi.ng/axidraw";
import type { Attribs } from "@thi.ng/geom-api";
import { group } from "@thi.ng/geom/group";
import { points } from "@thi.ng/geom/points";
import { polyline } from "@thi.ng/geom/polyline";
import type { ReadonlyVec } from "@thi.ng/vectors";
import { add2 } from "@thi.ng/vectors/add";
import { copy } from "@thi.ng/vectors/copy";

export interface AsGeometryOpts {
    /**
     * If true (default), records all movements whilst pen is up (otherwise only
     * whilst pen is down)
     */
    rapids: boolean;
    /**
     * If true (default), records all pen up/down positions.
     */
    pen: boolean;
    /**
     * Attributes for result groups/pointclouds.
     */
    attribs: Partial<{
        paths: Attribs;
        rapids: Attribs;
        ups: Attribs;
        downs: Attribs;
    }>;
}

const DEFAULT_ATTRIBS = {
    paths: { stroke: "#000" },
    rapids: { stroke: "#0ff" },
    ups: { fill: "#0f0", stroke: "none" },
    downs: { fill: "#f00", stroke: "none" },
};

/**
 * Converts a sequence of
 * [DrawCommands](https://docs.thi.ng/umbrella/axidraw/types/DrawCommand.html)
 * into thi.ng/geom geometry. Returns an object of shapes. The conversion can be
 * controlled via given options.
 *
 * @remarks
 * The returned object contains groups & shapes which are being color coded by
 * default:
 *
 * - `paths`: a group of polylines for which pen is down (#000)
 * - `rapids`: a group of polylines for which pen is up (#0ff)
 * - `ups`: a point cloud of positions where pen is being lifted (#0f0)
 * - `downs`: a point cloud of positions where pen is being placed down (#f00)
 *
 * @param src
 * @param opts
 */
export const asGeometry = (
    src: Iterable<DrawCommand>,
    opts: Partial<AsGeometryOpts> = {}
) => {
    opts = {
        rapids: true,
        pen: true,
        ...opts,
        attribs: { ...DEFAULT_ATTRIBS, ...opts.attribs },
    };
    const rapids = [];
    const paths = [];
    const downs = [];
    const ups = [];
    let penDown = false;
    let pts: ReadonlyVec[] | null = null;
    let currPos: ReadonlyVec = [0, 0];
    const $move = (newPos: ReadonlyVec) => {
        if (penDown || opts.rapids) {
            if (!pts) pts = [copy(currPos), newPos];
            else pts.push(newPos);
        }
        currPos = newPos;
    };
    for (let cmd of src) {
        switch (cmd[0]) {
            // absolute
            case "M":
                $move(copy(cmd[1]));
                break;
            // relative
            case "m":
                $move(add2([], currPos, cmd[1]));
                break;
            case "u":
                if (pts) {
                    if (penDown) paths.push(pts);
                    else if (opts.rapids) rapids.push(pts);
                    pts = null;
                }
                if (opts.pen) ups.push(copy(currPos));
                penDown = false;
                break;
            case "d":
                if (pts) {
                    if (!penDown) {
                        if (opts.rapids) rapids.push(pts);
                    } else paths.push(pts);
                    pts = null;
                }
                if (opts.pen) downs.push(copy(currPos));
                penDown = true;
                break;
            case "home":
                currPos = [0, 0];
                if (!pts) pts = [currPos];
                else pts.push(currPos);
                break;
            default:
                console.log("skipping command", cmd);
        }
    }
    if (pts) {
        if (penDown) paths.push(pts);
        else rapids.push(pts);
    }
    return {
        paths: group(
            opts.attribs!.paths,
            paths.map((pts) => polyline(pts))
        ),
        rapids: group(
            opts.attribs!.rapids,
            rapids.map((pts) => polyline(pts))
        ),
        ups: points(ups, opts.attribs!.ups),
        downs: points(downs, opts.attribs!.downs),
    };
};