maker.js icon indicating copy to clipboard operation
maker.js copied to clipboard

[request] Simple (alternative) public api or static "make" function on objects

Open vespakoen opened this issue 7 years ago • 10 comments

Hi,

I think it would be nice to have a simpler public API for scripting / hacking in the spirit of openscad, basically throwing all functions together in a single object, an example of what I mean would looks like this:

import { make } from 'makerjs'

const ball = make.ellipse(18, 18)
const hanger = make.move(make.ellipse(4, 4), [0, 18 + 2])
const ballInside = make.offset(ball, -2)
const hangerInside = make.offset(hanger, -2)
const ballAndHanger = make.union(ball, hanger)
const christmasBall = make.move({
    models: { ballAndHanger, ballInside, hangerInside }
}, [20, 20])

I have got the above somewhat working using my hack below:

interface Make {
    union: typeof maker.model.combineUnion,
    difference: typeof maker.model.combineSubtraction,
    intersection: typeof maker.model.combineIntersection,
}

const make: Make = {
    ...(Object.keys(maker.models).reduce((memo: any, modelName: string) => {
        const lcFirstModelName = modelName.charAt(0).toLowerCase() + modelName.slice(1);
        memo[lcFirstModelName] = (...args: any[]) => new maker.models[modelName](...args)
        return memo
    }, {})),
    ...(Object.keys(maker.model).reduce((memo: any, modelName: string) => {
        memo[modelName] = (...args: any[]) => new maker.model[modelName](...args)
        return memo
    }, {})),
    union: maker.model.combineUnion,
    difference: maker.model.combineSubtraction,
    intersection: maker.model.combineIntersection,
    offset: (modelToOutline: maker.IModel, offset: number, joints: number = 0) => {
        return maker.model.outline(modelToOutline, Math.abs(offset), joints, offset < 0)
    },
    // ... etc

However, the types of the model constructors are not working (as expected) so I would like to turn the above into something like:

const make = {
    belt: maker.models.Belt.make,
    bezierCurve: maker.models.BezierCurve.make,
    // ...etc
}

Most of the models constructors have overloads, which means I would have to re-implement them & keep them up to date (unless there is another way to do it that I am not aware of?) So I am wondering if it's possible to get this into the core (not necessarily the simple api, but at least the static "make" functions that construct a new instance of those model classes)

Hope this makes sense,

  • Koen

vespakoen avatar Dec 12 '18 10:12 vespakoen

Hi @vespakoen , are you using TypeScript? One thing you may want to look at, I am also doing some code generation. I save a copy of the AST here. This script looks at the AST and produces this file. At runtime, I use pretty much the same technique as you describe, here and here.

danmarshall avatar Dec 12 '18 16:12 danmarshall

Hey dan, yes I am using TypeScript indeed, I am trying to shorten all the method calls from makerjs, but I want to keep the type information intact (for auto completion / documentation when scripting) makerjs.$ seems to preserve it correctly indeed, just not sure if it will work with constructing models (which is the main thing I want to simplify).

eg something like:

make.ellipse(some, params)
// versus
new makerjs.models.Ellipse(some, params)

I can do something like:

export function belt(leftRadius: number, distance: number, rightRadius: number) {
  return new makerjs.models.Belt(leftRadius, distance, rightRadius)
}

however, it get's more tricky when there are overloads used in the constructor:

export function bezierCurve(points: IPoint[], accuracy?: number);
export function bezierCurve(seed: IPathBezierSeed, accuracy?: number);
export function bezierCurve(origin: IPoint, control: IPoint, end: IPoint, accuracy?: number);
export function bezierCurve(origin: IPoint, controls: IPoint[], end: IPoint, accuracy?: number);
export function bezierCurve(origin: IPoint, control1: IPoint, control2: IPoint, end: IPoint, accuracy?: number);
export function bezierCurve(...args: any[]) {
    var isArrayArg0 = Array.isArray(args[0]);
    switch (args.length) {
        case 2:
            if (isArrayArg0) {
                return new makerjs.model.BezierCurve(args[0] as IPoint[], args[1] as number);
            } else {
                return new makerjs.model.BezierCurve(args[0] as IPathBezierSeed, args[1] as number);
            }
        case 1:
            // etc
}

Which works fine but means I have to re-implement the constructor logic for all those types. My question is if we can put this kind of logic on the models themselves, for example

class BezierCurve implements IModel {
    // stuff

    static make(points: IPoint[], accuracy?: number);
    static make(seed: IPathBezierSeed, accuracy?: number);
    static make(origin: IPoint, control: IPoint, end: IPoint, accuracy?: number);
    static make(origin: IPoint, controls: IPoint[], end: IPoint, accuracy?: number);
    static make(origin: IPoint, control1: IPoint, control2: IPoint, end: IPoint, accuracy?: number);
    static make(...args: any[]) {
        var isArrayArg0 = Array.isArray(args[0]);
        switch (args.length) {
            case 2:
                if (isArrayArg0) {
                    return new BezierCurve(args[0] as IPoint[], args[1] as number);
                } else {
                    return new BezierCurve(args[0] as IPathBezierSeed, args[1] as number);
                }
            case 1:
                // etc
    }

    // other stuff
}

I am hoping that when it's a static method it's easier for me to remap it to something else, e.g.:

export function bezierCurve = makerjs.models.BezierCurve.make

Hope that clears things up

vespakoen avatar Dec 12 '18 17:12 vespakoen

Hi @vespakoen , I see what you mean about the static methods for constructors. I have actually been wanting to do something like this, but for performance reasons. I want to avoid the switch statement that decides how to handle the construction. So, what I think I'd like is a different name for each static function, based of its parameters. Example:

class BezierCurve implements IModel {
    // stuff

    static bezierCurveFromPointArray(points: IPoint[], accuracy?: number);
    static bezierCurveFromSeed(seed: IPathBezierSeed, accuracy?: number);
    static bezierCurveQuadratic(origin: IPoint, control: IPoint, end: IPoint, accuracy?: number);
    static bezierCurveCubic(origin: IPoint, controls: IPoint[], end: IPoint, accuracy?: number);
    static bezierCurveCubicExplicit(origin: IPoint, control1: IPoint, control2: IPoint, end: IPoint, accuracy?: number);

    // other stuff
}

Do you think this idea is compatible with your make proposal?

danmarshall avatar Dec 12 '18 20:12 danmarshall

Hi Dan,

That's great and would totally work for my purposes as well! Perhaps use the make prefix?

class BezierCurve implements IModel {
    // stuff
    static makeFromPointArray(points: IPoint[], accuracy?: number);
    static makeFromSeed(seed: IPathBezierSeed, accuracy?: number);
    static makeQuadratic(origin: IPoint, control: IPoint, end: IPoint, accuracy?: number);
    static makeCubic(origin: IPoint, controls: IPoint[], end: IPoint, accuracy?: number);
    static makeCubicExplicit(origin: IPoint, control1: IPoint, control2: IPoint, end: IPoint, accuracy?: number);
    // other stuff
}

vespakoen avatar Dec 12 '18 21:12 vespakoen

Perhaps you still want to have bezierCurve in the name, when you use it from the make namespace:

var b = make.bezierCurveQuadratic(...)

danmarshall avatar Dec 12 '18 21:12 danmarshall

another thought, is you actually don't need to re-implement each overload. Just use the apply keyword. This is already provided with makerjs.kit:

var make = {
  bezierCurve: function (args) {
    return makerjs.kit.construct(makerjs.models.BezierCurve, args);
  },
 ...
}

danmarshall avatar Dec 12 '18 21:12 danmarshall

Ah yeah that makes sense, then it would indeed be cool to have the "bezierCurve" prefix.

About the kit example, that wouldn't work as I want it to because the type information / documentation is lost.

vespakoen avatar Dec 12 '18 23:12 vespakoen

So, to get the type information, you can write a script that looks at the signatures in the AST. Here are the signatures for BezierCurve for example. Then you can generate a TypeScript source file witha Node.js script.

danmarshall avatar Dec 13 '18 01:12 danmarshall

That seems like a good solution, but if you are planning to add the static methods i'll wait for that or possibly make a PR for that someday =)

By the way, today I milled my first thing using my openjscam project together with makerjs, I also created another package (called makercam) that takes models from makerjs and turns them into CNC operations (right now I have contour, pocket and trace implemented) will push that stuff online soon and post a link.

Here is the result =)

screenshot 2018-12-13 at 18 28 20

img_0018

I used jsClipper for now to make the offsets, beware, messy code ahead:


const EndTypes = {
    etOpenSquare: 0,
    etOpenRound: 1,
    etOpenButt: 2,
    etClosedPolygon: 3,
    etClosedLine: 4
};

const JoinTypes = [
    ClipperLib.JoinType.jtSquare,
    ClipperLib.JoinType.jtRound,
    ClipperLib.JoinType.jtMiter
]

function clipperOffset(modelToOutline: maker.IModel, offset: number, joints: number = 0) {
    const scale = 100
    const chains = maker.model.findChains(modelToOutline) as maker.IChain[]
    const models = chains.reduce((memo, chain, i) => {
        const minimumSpacing = 0.1;
        const divisions = Math.floor(chain.pathLength / minimumSpacing);
        const spacing = chain.pathLength / divisions;
        const keyPoints = maker.chain.toPoints(chain, spacing);
        keyPoints.push(keyPoints[0])
        let paths = [
            keyPoints.map((point: any) =>
                ({
                    X: Math.round(point[0] * scale),
                    Y: Math.round(point[1] * scale)
                })
            )
        ]
        paths = ClipperLib.Clipper.SimplifyPolygons(paths, ClipperLib.PolyFillType.pftNonZero);
        // const cleanDelta = 0.001
        // paths = ClipperLib.JS.Clean(paths, cleanDelta * scale);
        // const endType = EndTypes.etClosedPolygon
        const co = new ClipperLib.ClipperOffset()
        const offsetted = new ClipperLib.Paths()
        co.Clear()
        co.AddPaths(paths, JoinTypes[joints], EndTypes.etClosedLine)
        co.MiterLimit = 0
        co.ArcTolerance = 0.25;
        co.Execute(offsetted, offset * scale);
        let result: maker.IPoint[] = []
        offsetted.forEach((points: any) => {
            points.forEach((point: any) => {
                result.push([point.X / scale, point.Y / scale])
            })
        })
        // @ts-ignore
        memo[i] = new maker.models.ConnectTheDots(true, result)
        return memo
    }, {})
    return {
        models
    }
}

And also found this module that is a WebAssembly version of ClipperLib: https://www.npmjs.com/package/js-angusj-clipper

It might be worth considering these for offsets / booleans etc?

vespakoen avatar Dec 13 '18 15:12 vespakoen

I just uploaded the christmas ball script here:

https://github.com/makercam/makercam-example-christmas/blob/master/index.ts

And published all packages under the makercam namespace https://github.com/makercam

Note: it's a bit of a mess at the moment =)

vespakoen avatar Dec 13 '18 16:12 vespakoen