export function findChains()

in packages/maker.js/src/core/chain.ts [137:291]


    export function findChains(modelContext: IModel, ...args: any[]): IChain[] | IChainsMap {

        var options: IFindChainsOptions;
        var callback: IChainCallback;

        switch (args.length) {
            case 1:
                if (typeof args[0] === 'function') {
                    callback = args[0];
                } else {
                    options = args[0];
                }
                break;

            case 2:
                callback = args[0];
                options = args[1];
                break;
        }

        var opts: IFindChainsOptions = {
            pointMatchingDistance: .005
        };
        extendObject(opts, options);

        const pointGraphsByLayer: { [layer: string]: PointGraph<IChainLink>; } = {};
        var chainsByLayer: IChainsMap = {};
        var ignored: { [layer: string]: IWalkPath[]; } = {};

        var walkOptions: IWalkOptions = {
            onPath: function (walkedPath: IWalkPath) {

                var layer = opts.byLayers ? walkedPath.layer : '';
                if (!pointGraphsByLayer[layer]) {
                    pointGraphsByLayer[layer] = new PointGraph<IChainLink>();
                }

                const pointGraph = pointGraphsByLayer[layer];
                var pathLength = measure.pathLength(walkedPath.pathContext);

                //circles are loops by nature
                if (
                    walkedPath.pathContext.type === pathType.Circle ||
                    (walkedPath.pathContext.type === pathType.Arc && round(angle.ofArcSpan(walkedPath.pathContext as IPathArc) - 360) === 0) ||
                    (walkedPath.pathContext.type === pathType.BezierSeed && measure.isPointEqual(walkedPath.pathContext.origin, (walkedPath.pathContext as IPathBezierSeed).end, opts.pointMatchingDistance))
                ) {

                    var chain: IChain = {
                        links: [{
                            walkedPath: walkedPath,
                            reversed: null,
                            endPoints: null,
                            pathLength: pathLength
                        }],
                        endless: true,
                        pathLength: pathLength
                    };

                    //store circles so that layers fire grouped
                    if (!chainsByLayer[layer]) {
                        chainsByLayer[layer] = [];
                    }
                    chainsByLayer[layer].push(chain);

                } else {

                    //don't add lines which are 5x shorter than the tolerance
                    if (pathLength < opts.pointMatchingDistance / 5) {

                        if (!ignored[layer]) {
                            ignored[layer] = [];
                        }
                        ignored[layer].push(walkedPath);

                        return;
                    }

                    //gather both endpoints from all non-circle segments
                    const endPoints = point.fromPathEnds(walkedPath.pathContext, walkedPath.offset);

                    for (var i = 0; i < 2; i++) {
                        var link: IChainLink = {
                            walkedPath: walkedPath,
                            endPoints: endPoints,
                            reversed: i != 0,
                            pathLength: pathLength
                        };
                        let valueId = pointGraph.insertValue(link);
                        pointGraph.insertValueIdAtPoint(valueId, endPoints[i]);
                    }
                }
            }
        };

        if (opts.shallow) {
            walkOptions.beforeChildWalk = function () { return false; };
        }

        var beziers: IWalkModel[];
        if (opts.unifyBeziers) {
            beziers = getBezierModels(modelContext);
            swapBezierPathsWithSeeds(beziers, true);
        }

        walk(modelContext, walkOptions);

        for (let layer in pointGraphsByLayer) {
            let pointGraph = pointGraphsByLayer[layer];

            pointGraph.mergeNearestSinglePoints(opts.pointMatchingDistance);

            var loose: IWalkPath[] = [];

            if (!chainsByLayer[layer]) {
                chainsByLayer[layer] = [];
            }

            //follow paths to find endless chains
            followLinks(
                pointGraph,
                function (chain: IChain, checkEndless: boolean) {
                    if (checkEndless) {
                        chain.endless = measure.isPointEqual(linkEndpoint(chain.links[0], true), linkEndpoint(chain.links[chain.links.length - 1], false), opts.pointMatchingDistance);
                    } else {
                        chain.endless = !!chain.endless;
                    }
                    chainsByLayer[layer].push(chain);
                },
                function (walkedPath: IWalkPath) {
                    loose.push(walkedPath);
                }
            );

            //sort to return largest chains first
            chainsByLayer[layer].sort((a: IChain, b: IChain) => { return b.pathLength - a.pathLength });

            if (opts.contain) {
                var containChainsOptions: IContainChainsOptions = isObject(opts.contain) ? opts.contain as IContainChainsOptions : { alternateDirection: false };
                var containedChains = getContainment(chainsByLayer[layer], containChainsOptions);
                chainsByLayer[layer] = containedChains;
            }

            if (callback) callback(chainsByLayer[layer], loose, layer, ignored[layer]);
        }

        if (beziers) {
            swapBezierPathsWithSeeds(beziers, false);
        }

        if (opts.byLayers) {
            return chainsByLayer;
        } else {
            return chainsByLayer[''];
        }
    }