const gridSize = 6; //min=1 max=10 step=1
const includePoissonDisc = 0; //min=0 max=1 step=1 (No, Yes) PoissonDisc is slow compared to the others
const start = 0; //min=0 max=23 step=1 (Regular HerringBone, Chevron, Circular, Triple Herringbone, Spiral, Brick, Tread Plate, Sine, Polka Dot, Honey Comb, Triangle, QBert, ZigZag, Triple Weave, Radial, Sine Radial, Weave, Segments using Weave, Toggle Block Weave, Line, ZigZag Radial, Block Radial (web), PoissonDisc on point, PoissonDisc connected) Pick a hatch instance to start with. Useful with gridSize of 1. Mind disabling PoissonDisc as it, when included, is initialized even if not used
const background = 1; //min=0 max=1 step=1 (No, Yes)

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

// Global code will be evaluated once.
loadHatcheryNamespace();
const turtle = new Turtle;
const polygons = new Polygons;
const grid = new SquareGrid(gridSize).iterator();

const hatchings = [
    new HerringboneHatching(1, [[7,0], [0,3.5]]),
    new ChevronHatching(0, [[9,0],[2,3]]),
    new CircularHatching(2, [30, -30]),
    new HerringboneHatching(1, [[7,0], [2,3.5]], 3),
    new SpiralHatching(3, [40,-75], 1, 1, -1.4),
    new BrickHatching(.2, 5, 3),
    new TreadPlateHatching(7, .5, .7, 3),
    new SineHatching(10, 2, 1, 4),
    new PolkaDotHatching(Math.PI / 5, 10, 2.5),
    new HoneyCombHatching(1.2, 10),
    new TriangleHatching(1.5, 4),
    new QBertHatching(1.2, 8),
    new ZigZagHatching(7, 3, 2.55, 4),
    new WeaveHatching(8, .5, 1, .3, 3),
    new RadialHatching(100),
    new SineRadialHatching(25),
    new WeaveHatching(7, .7, 1, .5),
    new WeaveHatching(6, .4, .4, .4, 2, 0, false),
    new WeaveHatching(6, .8, .3, 1.4),
    new LineHatching(1 + Math.PI / 2, 2),
    new ZigZagRadialHatching(25),
    new BlockRadialHatching(25),
    includePoissonDisc? new PoissonDiscHatching(1, false): null,
    includePoissonDisc? new PoissonDiscHatching(1, true): null,
].filter(e => e !== null);

// The walk function will be called until it returns false.
function walk(i) {
    const iteration = grid.next();
    const cell = iteration.value;
    
    const cp = (r, edges) => Array.from({length: edges}).map((v, i, a, f = i * 2*Math.PI / a.length) => [Math.sin(f)*r, Math.cos(f)*r]);
    
    const edges = [4 + Math.random()*7].map(v => v <= 10? v: cell.size * Math.PI).pop() | 0;

    function add2(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
    function scale2(a, s) { return [a[0]*s,a[1]*s]; }
    function rot2(a) { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
    function trans2(m, a) { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; } //Matrix(2x1) x Matrix(2x2)
    
    const rot = rot2(Math.random()*2*Math.PI/edges);

    for(let j = 0; j <= .1; j+=.1) {
        const p = polygons.create();
        p.addPoints(...cp(.5,edges).map(v => add2(cell.center, trans2(rot, scale2(v, Math.min(cell.size + 5, cell.size * (1 + j)))))));
        if(j == 0) {
            p.addOutline();
            p.addHatching(hatchings[(i+start)%hatchings.length]);
        }
        polygons.draw(turtle, p);
    }
    
    if(iteration.done && background) {
        const p = polygons.create();
        p.addPoints([-99, -99], [99, -99], [99, 99], [-99, 99]);
        p.addOutline();
        p.addHatching(hatchings[(i+1+start)%hatchings.length]);
        polygons.draw(turtle, p);
    }
    return !iteration.done;
}

////////////////////////////////////////////////////////////////
// Square Grid utility code - Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/f9fc3f2348
////////////////////////////////////////////////////////////////
function SquareGrid(grid, margin = 5, padding=2, canvasSize = 200) {
    class SquareGrid {
        constructor(grid, margin = 5, padding = 2, canvasSize = 200) { this.gridSize = grid;this.padding = padding; this.margin = margin; this.canvasSize = canvasSize; this.resetCellSize(); }
        resetCellSize() { this.cellSize = ((this.canvasSize - this.margin - this.margin - ((this.gridSize - 1) * this.padding)) / this.gridSize); }
        getColumnRow(i) { return [i % this.gridSize, i / this.gridSize | 0]; }
        getCellCenter(col, row) { return [col, row].map(v => this.margin - 100 + (v * this.padding) + ((v + .5) * this.cellSize)); }
        getCell(col, row) { return { center: this.getCellCenter(col, row), column: col, row: row, size: this.cellSize }}
        get length() { return this.gridSize**2; }
        *iterator() { let ptr = 0; while(ptr < this.length - 1) { yield this.getCell(...this.getColumnRow(ptr++)); } return this.getCell(...this.getColumnRow(ptr++)); }
    }
    return new SquareGrid(grid, margin, padding, canvasSize);
}

///////////////////////////////////////////////////////////////////
// Polygon Hatching utility code - Created by Jurgen Westerhof 2024
// https://turtletoy.net/turtle/d068ad6040
// ////////////////////////////////////////////////////////////////
// // To be used with modified Polygon Clipping utility code
// //            Orginal: https://turtletoy.net/turtle/a5befa1f8d
// //    Polygon binning: https://turtletoy.net/turtle/95f33bd383
// // Delegated Hatching: https://turtletoy.net/turtle/d068ad6040
///////////////////////////////////////////////////////////////////
function loadHatcheryNamespace() {
    //for convenience on Turtletoy you can click the arrow to the right of the line number to collapse a class
    //////////////////////////////////////////////////// root
    class PolygonHatching {
        constructor() {
            if (this.constructor === PolygonHatching) {
                throw new Error("PolygonHatching is an abstract class and can't be instantiated.");
            }
            
            this.minX = -100;
            this.minY = -100;
            this.maxX = 100;
            this.maxY = 100;
            this.width = 200;
            this.height = 200;
            
            this.segments = [];
            
            this.init();
        }
        hatch(polygonsClass, thePolygonToHatch) {
            const e = new polygonsClass;
            e.cp.push(...thePolygonToHatch.aabb);//[-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
            
            this.addHatchSegments(e.dp);
            
            e.boolean(thePolygonToHatch,!1);
            thePolygonToHatch.dp=[...thePolygonToHatch.dp,...e.dp]
        }
        addHatchSegments(segments) {
            this.getSegments().forEach(e => segments.push(e));
        }
        getSegments() { return this.segments; }
        init() {
            ///////////////////////////////////////////////////////
            // Vector functions - Created by Jurgen Westerhof 2024
            // https://turtletoy.net/turtle/d068ad6040
            ///////////////////////////////////////////////////////
            class Vector {
                static add  (a,b) { return a.map((v,i)=>v+b[i]); }
                static sub  (a,b) { return a.map((v,i)=>v-b[i]); }
                static mul  (a,b) { return a.map((v,i)=>v*b[i]); }
                static div  (a,b) { return a.map((v,i)=>v/b[i]); }
                static scale(a,s) { return a.map(v=>v*s); }
            
                static det(m)                { return m.length == 1? m[0][0]: m.length == 2 ? m[0][0]*m[1][1]-m[0][1]*m[1][0]: m[0].reduce((r,e,i) => r+(-1)**(i+2)*e*this.det(m.slice(1).map(c => c.filter((_,j) => i != j))),0); }
                static angle(a)              { return Math.PI - Math.atan2(a[1], -a[0]); } //compatible with turtletoy heading
                static rot2d(angle)          { return [[Math.cos(angle), -Math.sin(angle)], [Math.sin(angle), Math.cos(angle)]]; }
                static rot3d(yaw,pitch,roll) { return [[Math.cos(yaw)*Math.cos(pitch), Math.cos(yaw)*Math.sin(pitch)*Math.sin(roll)-Math.sin(yaw)*Math.cos(roll), Math.cos(yaw)*Math.sin(pitch)*Math.cos(roll)+Math.sin(yaw)*Math.sin(roll)],[Math.sin(yaw)*Math.cos(pitch), Math.sin(yaw)*Math.sin(pitch)*Math.sin(roll)+Math.cos(yaw)*Math.cos(roll), Math.sin(yaw)*Math.sin(pitch)*Math.cos(roll)-Math.cos(yaw)*Math.sin(roll)],[-Math.sin(pitch), Math.cos(pitch)*Math.sin(roll), Math.cos(pitch)*Math.cos(roll)]]; }
                static trans(matrix,a)       { return a.map((v,i) => a.reduce((acc, cur, ci) => acc + cur * matrix[ci][i], 0)); }
                //Mirror vector a in a ray through [0,0] with direction mirror
                static mirror2d(a,mirror)    { return [Math.atan2(...mirror)].map(angle => this.trans(this.rot2d(angle), this.mul([-1,1], this.trans(this.rot2d(-angle), a)))).pop(); }
        
                static approx(a,b,p) { return this.len(this.sub(a,b)) < (p === undefined? .001: p); }
                static norm  (a)     { return this.scale(a,1/this.len(a)); }
                static len   (a)     { return Math.hypot(...a); }
                static lenSq (a)     { return a.reduce((a,c)=>a+c**2,0); }
                static lerp  (a,b,t) { return a.map((v, i) => v*(1-t) + b[i]*t); }
                static dist  (a,b)   { return Math.hypot(...this.sub(a,b)); }
                
                static dot  (a,b)   { return a.reduce((a,c,i) => a+c*b[i], 0); }
                static cross(...ab) { return ab[0].map((e, i) => ab.map(v => v.filter((ee, ii) => ii != i))).map((m,i) => (i%2==0?-1:1)*this.det(m)); }
            }
            this.V = Vector;
            
            class Intersection2D {
                //a-start, a-direction, b-start, b-direction
                //returns false on no intersection or [[intersection:x,y], scalar a-direction, scalar b-direction
                static info(as, ad, bs, bd) {
                    const d = V.sub(bs, as), det = -V.det([bd, ad]);
                    if(det === 0) return false;
                    const res = [V.det([d, bd]) / det, V.det([d, ad]) / det];
                    return [V.add(as, V.scale(ad, res[0])), ...res];
                }
                static ray(a, b, c, d) {
                    return this.info(a, b, c, d);
                }
                static segment(a,b,c,d, inclusiveStart = true, inclusiveEnd = true) {
                    const i = this.info(a, V.sub(b, a), c, V.sub(d, c));
                    return i === false? false: (
                        (inclusiveStart? 0<=i[1] && 0<=i[2]: 0<i[1] && 0<i[2])
                     && (inclusiveEnd?   i[1]<=1 && i[2]<=1: i[1]<1 && i[2]<1)
                    )?i[0]:false;
                }
            }
            this.Intersection = Intersection2D;
        }
    }
    this.PolygonHatching = PolygonHatching;
    //////////////////////////////////////////////////// first gen
    // extending PolygonHatching
    class CircularHatching extends PolygonHatching {
        constructor(distance = 360, center = [0,0], precision = 1) {
            super();
            const dist = typeof distance == 'function'? distance: (c) => distance;
            
            this.segments = [];
            for(let j = 0, r = dist(j)/2; r < 201; r+=dist(j++)) {
                for(let i = 0, max = Math.max(12, 2*Math.PI*r/precision | 0); i < max; i++) {
                    this.segments.push(
                        [center[0]+r*Math.sin(i*2*Math.PI/max),center[1]+r*-Math.cos(i*2*Math.PI/max)],
                        [center[0]+r*Math.sin((i+1)*2*Math.PI/max),center[1]+r*-Math.cos((i+1)*2*Math.PI/max)]
                    );
                }
            }
        }
    }
    this.CircularHatching = CircularHatching;
    class GridHatching extends PolygonHatching {
        constructor(cellSize, angle = 0) {
            super();
    
            if (this.constructor === GridHatching) {
                throw new Error("GridHatching is an abstract class and can't be instantiated.");
            }
            
            this.cellSize = cellSize;
            this.rotation = this.V.rot2d(angle);
            
            this.grid = ((this.width * 1.5 / cellSize) | 0) + 1;
    
            this.cells = this.grid**2;
            
            this.segments = [];
        }
        getCell(n) {
            if(n > this.cells) return false;
            const col = n % this.grid;
            const row = n / this.grid | 0;
            return [
                (col + .5) - (this.grid / 2), 
                (row + .5) - (this.grid / 2), 
            ];
        }
        oddness(cellN, n = 2) {
            return ((cellN % this.grid) // column
                    + (cellN / this.grid | 0)) // row
                % n;
        }
        getSegments() {
            if(this.segments.length == 0) {
                const segmentsSet = this.getSegmentsSet();
                for(let i = 0; i < this.cells; i++) {
                    segmentsSet[this.oddness(i, segmentsSet.length)].forEach(segment => {
                        const cellLocation = this.getCell(i);
                        this.segments.push(
                            this.V.trans(this.rotation, this.V.scale(this.V.add(cellLocation, segment[0]), this.cellSize)),
                            this.V.trans(this.rotation, this.V.scale(this.V.add(cellLocation, segment[1]), this.cellSize))
                        );
                    });
                }
            }
            return this.segments;
        }
        getSegmentsSet() {
            return [this.getTileSegments()].flatMap(ts => [ts, ts.map(segment => segment.map(pt => [...pt].reverse()))]);
        }
    }
    this.GridHatching = GridHatching;
    class LaticeHatching extends PolygonHatching {
        constructor(angle = 0, xUnit = [10, 0], yUnit = [0, 5]) {
            super();
            this.tile = [[...xUnit], [...yUnit]];
            this.rotation = this.V.rot2d(angle);
        }
        setLaticeSegments(info) {
            for(let i = 0, max = info.columns * info.rows - 1; i < max; i++) {
                const col = i % info.columns;
                const row = i / info.columns | 0;
                
                const position = this.V.add(this.V.scale(info.rowIncrement, row - info.rows / 2 + .5), this.V.scale(info.columnIncrement, col - info.columns / 2));
                info.lines.forEach(e => { this.segments.push(
                    this.V.trans(this.rotation, this.V.add(e[0], position)),
                    this.V.trans(this.rotation, this.V.add(e[1], position))
                )});
            }
        }
    }
    this.LaticeHatching = LaticeHatching;
    class LineHatching extends PolygonHatching {
        constructor(angle, distance, inp = 0) {
            super();
            
            const h=Math.sin(angle)*distance,o=Math.cos(angle)*distance,a=200*Math.sin(angle),i=200*Math.cos(angle);
            this.segments = Array.from({length: 150/distance}).flatMap((x,y,z,t=.5+y) => [
                [h*t+i+inp*(Math.random()-.5),o*t-a+inp*(Math.random()-.5)],[h*t-i+inp*(Math.random()-.5),o*t+a+inp*(Math.random()-.5)], [-h*t+i+inp*(Math.random()-.5),-o*t-a+inp*(Math.random()-.5)],[-h*t-i+inp*(Math.random()-.5),-o*t+a+inp*(Math.random()-.5)]
            ])
        }
    }
    this.LineHatching = LineHatching;
    class PoissonDiscHatching extends PolygonHatching {
        constructor(radius = 1.75, connect = false, randomGrowOrder = .1, loosePacking = .5, startPoint = [0,0]) {
            super();
            
            ////////////////////////////////////////////////////////////////
            // Poisson-Disc utility code. Created by Reinder Nijhoff 2019
            // https://turtletoy.net/turtle/b5510898dc
            ////////////////////////////////////////////////////////////////
            function PoissonDisc(startPoints, radius) {
                class PoissonDiscGrid {
                    constructor(sp, radius) {
                        this.cellSize = 1/Math.sqrt(2)/radius;
                        this.radius2 = radius*radius;
                        this.cells = [];
                        sp.forEach( p => this.insert(p) );
                    }
                    insert(p) {
                        const x = p[0]*this.cellSize|0, y=p[1]*this.cellSize|0;
                        for (let xi = x-1; xi<=x+1; xi++) {
                            for (let yi = y-1; yi<=y+1; yi++) {
                                const ps = this.cell(xi,yi);
                                for (let i=0; i<ps.length; i++) {
                                    if ((ps[i][0]-p[0])**2 + (ps[i][1]-p[1])**2 < this.radius2) {
                                        return false;
                                    }
                                }
                            }       
                        }
                        this.cell(x, y).push(p);
                        return true;
                    }
                    cell(x,y) {
                        const c = this.cells;
                        return (c[x]?c[x]:c[x]=[])[y]?c[x][y]:c[x][y]=[];
                    }
                }
                class PoissonDisc {
                    constructor(sp, radius) {
                        this.result = [...sp];
                        this.active = [...sp];
                        this.grid = new PoissonDiscGrid(sp, radius);
                    }
                    addPoints(count, maxTries=16, loosePacking=0, randomGrowOrder=0) {
                    	mainLoop: while (this.active.length > 0 && count > 0) {
                    		const index = (Math.random() * this.active.length * randomGrowOrder) | 0;
                    		const point = this.active[index];
                    		for (let i=0; i < maxTries; i++) {
                    			const a = Math.random() * 2 * Math.PI;
                    			const d = (Math.random()*loosePacking + 1) * radius;
                    			const p = [point[0] + Math.cos(a)*d, point[1] + Math.sin(a)*d, point];
                    			if (this.grid.insert(p)) {
                        			this.result.push(p);
                        			this.active.push(p);
                        			count--;
                        			continue mainLoop;
                    			}
                    		}
                		    this.active.splice(index, 1);
                    	}
                    	return this.result;
                    }
                }
                return new PoissonDisc(startPoints, radius);
            }
    
            const disc = new PoissonDisc([startPoint], radius);
            let index = 0;
            
            const maxR = Math.max(
                this.V.lenSq(this.V.sub([this.minX, this.minY], startPoint)),
                this.V.lenSq(this.V.sub([this.maxX, this.minY], startPoint)),
                this.V.lenSq(this.V.sub([this.minX, this.maxY], startPoint)),
                this.V.lenSq(this.V.sub([this.maxX, this.maxY], startPoint))
            );
    
            while(disc.active.some(pt => this.V.lenSq([pt[0], pt[1]]) < maxR)) {
                const points = disc.addPoints(1, 32, loosePacking, randomGrowOrder);
            
                while (index<points.length) {
                    if (points[index][2]) {
                        this.segments.push(
                            points[index],
                            connect? points[index][2]: this.V.add(points[index], [0,.25])
                        )
                    }
                    index++;
                }
            }
        }
    }
    this.PoissonDiscHatching = PoissonDiscHatching;
    class RadialHatching extends PolygonHatching {
        constructor(n = 30, origin = [0,0], phaseExp = 1, phaseMod = 1, ampMod = 2, angle = 0) {
            super();

            function redistributeLinearPath(path, length = .5) {path = path.map(pt => [...pt]); const sub  = (a,b)   => a.map((v,i)=>v-b[i]);const len  = (a)     => Math.hypot(...a);const lerp = (a,b,t) => a.map((v, i) => v*(1-t) + b[i]*t);const result = [path[0]];const vectors = Array.from({length: path.length - 1}).map((e, i) => [path[i], len(sub(path[i+1], path[i]))]);done: {for(let i = 0; i < vectors.length; i++) {let l = length;while(l > 0 && i < vectors.length) {l-=vectors[i][1];i++;if(i == vectors.length) break done;}i--;[lerp(path[i], path[i+1], -l/vectors[i][1])].forEach(v => {result.push(v);path[i] = v;vectors[i] = [v, len(sub(path[i+1], v))];});}}result.push(path[path.length - 1]);return result;}
            
            const rotation = this.V.rot2d(angle);
            const diameter = (1.5 + 2*this.V.len(origin)/this.width) * this.width;

            const path = redistributeLinearPath(Array.from({length: ((diameter / 2) ** 2| 0) + 1}).map((e, i) => this.V.trans(this.V.rot2d(this.getX(i**.5, phaseExp, phaseMod) * Math.PI * ampMod / n), [i**.5, 0])), this.getRedistributionPrecision());

            for(let i = 0; i < n; i++) {
                const currentPath = path.map(e => this.V.trans(rotation, this.V.add(origin, this.V.trans(this.V.rot2d(i * 2 * Math.PI / n), e))));
                for(let i = 0; i < currentPath.length - 1; i++) {
                    this.segments.push(currentPath[i], currentPath[i+1]);
                }
            }
        }
        getX(i, phaseExp, phaseMod) {
            return 0;
        }
        getRedistributionPrecision() {
            return .25;
        }
    }
    this.RadialHatching = RadialHatching;
    class SineHatching extends PolygonHatching {
        constructor(waveLength = 5, amplitude = 1, angle = 0, distance = 1, samplesPerWavelength = waveLength * amplitude) {
            super();
            const rotation = this.V.rot2d(angle);
    
            const oneWaveLengthPath = Array.from({length: samplesPerWavelength}).map((v, i, a, f = i/a.length) => [f*waveLength, Math.sin(f*2*Math.PI) * amplitude]);
            const halfNWaveLengths = (this.maxX * 1.5 / waveLength | 0) + 1;
            const fullWave = Array.from({length: halfNWaveLengths * 2})
                                  .map((v, i) => [(i - halfNWaveLengths) * waveLength, 0])
                                  .flatMap(v => oneWaveLengthPath.map(pt => this.V.add(v, pt)))
                
                                  .map((v, i, a) => [v, a[(i+1)%a.length]]);
            fullWave.pop();
            
            for(let y = ((this.minY * 1.5) / distance | 0) - 1; y < ((this.maxY * 1.5) / distance | 0) + 1; y++) {
                fullWave.forEach(segment => {
                    this.segments.push(
                        this.V.trans(rotation, this.V.add([0, y * distance], segment[0])),
                        this.V.trans(rotation, this.V.add([0, y * distance], segment[1]))
                    );
                });
            }
        }
    }
    this.SineHatching = SineHatching;
    //////////////////////////////////////////////////// second gen
    // extending GridHatching
    class TreadPlateHatching extends GridHatching {
        constructor(size = 7, width = .4, length = .8, angle = 0, treads = 2, skew = 0) {
            super(size, angle);
            this.width = width;
            this.length = length;
            this.treads = treads;
            this.skew = skew;
        }
        getTileSegments() {
            const l = (2-this.width) * this.length;
            const w = this.width/(2*this.treads -1);
            const r = (w**2 + l**2) / (4*w);
    
            const skewAngle = (this.skew/10)*2*Math.PI;
            const rotationSkewMatrix = this.V.rot2d(skewAngle);
            
            const alpha = Math.asin(l/(2*r));
            const steps = (this.cellSize * 2) | 0;
            
            const arc = [];
            for(let i = 0; i < steps; i++) {
                arc.push([
                    r * Math.cos(Math.PI/2 - alpha + (i * 2 * alpha / steps)),
                    r * (Math.sin(Math.PI/2 - alpha + (i * 2 * alpha / steps)) - Math.sin(Math.PI/2 - alpha))
                ])
            }
            arc.forEach(pt => arc.push([-pt[0], -pt[1]]));
            const path = arc.map(pt => [this.V.trans(rotationSkewMatrix, pt)].map(ppt => [pt[0], ppt[1]]).pop())
            
            const paths = [];
            for(let i = 0; i < this.treads; i++) {
                paths.push(path.map(pt => [pt[0], pt[1] - ((this.treads-1) * w) + (2*i*w)]));
            }
            
            return paths.flatMap(path => path.map((pt, i, a) => [pt, a[(i+1)%a.length]]));
        }
    }
    this.TreadPlateHatching = TreadPlateHatching;
    class WeaveHatching extends GridHatching {
        constructor(size = 7, width = .7, length = 1, angle = 0, threads = 1, skew = 0, closeSegments = true) {
            super(size, angle);
            this.width = width;
            this.length = length;
            this.threads = threads;
            this.skew = skew;
            this.closeSegments = closeSegments;
        }
        getTileSegments() {
            const skewAngle = (this.skew/10)*2*Math.PI;
            const rotationSkewMatrix = this.V.rot2d(skewAngle);
    
            const path = [
                [(-.5 - (1-this.width)/2) * this.length, -this.width/2],
                                [(.5 + (1-this.width)/2) * this.length, -this.width/2],
                                [(.5 + (1-this.width)/2) * this.length,  this.width/2],
                [(-.5 - (1-this.width)/2) * this.length, this.width/2]
            ].map((pt, i) => this.V.add(pt, [(i < 2? .5:-.5)*Math.sin(skewAngle), 0]))
            .map(pt => this.V.trans(rotationSkewMatrix, pt));
            
            const segments = path.map((pt, i, a) => [pt, a[(i+1)%a.length]])
                                 .map((segment, i, a) => i < a.length/2? segment: [...segment.reverse()]);
            
            const hSegments = segments.filter((v, i) => i%2 == 0);
            const vSegments = segments.filter((v, i) => i%2 == 1 && (this.length < 1 || this.skew != 0));
    
            return Array.from({length: this.threads+1}).map((e, i) => [this.V.lerp(hSegments[0][0], hSegments[1][0], i/this.threads), this.V.lerp(hSegments[0][1], hSegments[1][1], i/this.threads)]).concat(this.closeSegments? vSegments: []);
        }
    }
    this.WeaveHatching = WeaveHatching;
    // extending LaticeHatching
    class ChevronHatching extends LaticeHatching {
        constructor(angle, tile, offset = 0, margin = 0) {
            super(angle, ...tile);
            
            this.setLaticeSegments(this.getInfo(offset, margin));
        }
        getInfo(offset, margin) {
            const mirror = this.V.mirror2d(...this.tile);
            const vOffset = this.V.scale(this.tile[1], offset);
            const vMargin = this.tile.map(v => this.V.scale(v, margin/this.V.len(v)/2));
            
            const lines = [...this.tile.map((v, i) => [[0,0], this.V.sub(v, this.V.scale(vMargin[i], 2))])];
            if(margin == 0) {
                lines.push([[0,0], mirror].map(e => this.V.add(vOffset, e)));
                lines.push(lines[1].map(pt => this.V.add(pt, lines[2][1])));
            } else {
                lines.push(lines[0].map(v => this.V.add(lines[1][1], v)));
                lines.push(lines[1].map(v => this.V.add(lines[0][1], v)));
        
                [this.V.add(...vMargin)].forEach(os => lines.forEach((e, i) => lines[i] = lines[i].map(pt => this.V.add(pt, os))) );
                lines.forEach(l => lines.push(l.map(pt => this.V.add(vOffset, this.V.mirror2d(pt, this.tile[1])))));
            }
            
            return {
                lines: lines,
                columns: ((this.width * 1.5 + this.V.len(this.V.add(this.V.add(vOffset, mirror), this.tile[0]))) / this.V.len(this.tile[1]) | 0) + 2,
                rows: (this.width * 1.5 / this.V.len(this.V.sub(this.tile[0], mirror)) | 0) + 2,
                columnIncrement: [...this.tile[1]],
                rowIncrement: this.V.add(this.V.scale(this.tile[0], -1), mirror),
            }
        }
    }
    this.ChevronHatching = ChevronHatching;
    class HerringboneHatching extends LaticeHatching {
        constructor(angle, tile, poly = 1) {
            super(angle, ...tile);
            
            this.setLaticeSegments(this.getInfo(poly));
        }
        getInfo(poly) {
            const otherTile = this.tile.map((e, i, a) => this.V.scale(e, this.V.len(a[(i+1)%a.length])/this.V.len(e)))
            
            const colInc = this.V.add(this.tile[1], otherTile[0]);
            const rowInc = this.V.sub(otherTile[1], this.tile[0]);
            
            const lines = [[this.V.scale(otherTile[0], -1), this.tile[0]], [[0,0], this.V.add(this.tile[1], otherTile[1])]];
            
            const colWidth = this.V.len(colInc);
            const colStartOffset = colWidth / 2 + this.V.len(this.V.sub(...lines[0]));
            const rowHeight = this.V.len(rowInc);
            
            for(let i = 1; i < poly; i++) {
                lines.push([this.V.scale(this.tile[1]     , i /  poly)].map(v => [v, this.V.add(v, this.tile[0]     )]).pop());
                lines.push([this.V.scale(otherTile[0], i / -poly)].map(v => [v, this.V.add(v, otherTile[1])]).pop());
            }
            
            return {
                lines: lines,
                columns: ((this.width * 1.5 + colStartOffset) / colWidth | 0) + 2,
                rows: (this.width * 1.5 / rowHeight | 0) + 2,
                columnIncrement: colInc,
                rowIncrement: rowInc,
            }
        }
    }
    this.HerringboneHatching = HerringboneHatching;
    class HexGridHatching extends LaticeHatching {
        constructor(angle = 0, size = 10, cellSize = 1) {
            const vSize = size/2;
            const hSize = vSize*3**.5/2;
    
            super( angle, ...[[hSize, 1.5*vSize], [2*hSize, 0]] );
            
            this.vSize = vSize;
            this.hSize = hSize;
            
            this.setLaticeSegments(this.getInfo(cellSize));
        }
        getInfo(cellSize) {
            return {
                lines: this.getPath()
                           .map(pt => this.V.scale(pt, cellSize))
                           .map((e,i,a) => [e, a[(i+1)%a.length]])
                           .filter((e,i) => cellSize < 1 || i < 3),
                columns: this.width * 1.5 / this.V.len(this.tile[0]) | 0,
                rows: this.height * 1.5 / this.V.len(this.tile[1]) | 0,
                columnIncrement: [...this.tile[1]],
                rowIncrement: [...this.tile[0]],
            }
        }
        getPath() {
            return [
                [-this.hSize,  this.vSize/2],
                [-this.hSize, -this.vSize/2],
                [          0, -this.vSize  ],
                [ this.hSize, -this.vSize/2],
                [ this.hSize,  this.vSize/2],
                [          0,  this.vSize  ]
            ];
        }
    }
    this.HexGridHatching = HexGridHatching;
    // extending RadialHatching
    class BlockRadialHatching extends RadialHatching {
        constructor(n = 30, origin = [0,0], phaseExp = 1, phaseMod = 1, ampMod = 1, angle = 0) {
            super(n, origin, phaseExp, phaseMod, ampMod, angle);
        }
        getX(i, phaseExp, phaseMod) {
            return Math.sign(Math.sin(i**phaseExp*phaseMod));
        }
        getRedistributionPrecision() {
            return .1;
        }
    }
    this.BlockRadialHatching = BlockRadialHatching;
    class SineRadialHatching extends RadialHatching {
        getX(i, phaseExp, phaseMod) {
            return Math.sin(i**phaseExp*phaseMod);
        }
    }
    this.SineRadialHatching = SineRadialHatching;
    class SpiralHatching extends RadialHatching {
        getX(i, phaseExp, phaseMod) {
            return i**phaseExp*phaseMod;
        }
    }
    this.SpiralHatching = SpiralHatching;
    class ZigZagRadialHatching extends RadialHatching {
        getX(i, phaseExp, phaseMod) {
            return 2 * Math.asin(Math.sin(i**phaseExp*phaseMod)) / Math.PI;
        }
    }
    this.ZigZagRadialHatching = ZigZagRadialHatching;
    // extending SineHatching
    class ZigZagHatching extends SineHatching {
        constructor(waveLength = 5, amplitude = 2, angle = 0, distance = 1) {
            super(waveLength, amplitude, angle, distance, 4);
        }
    }
    this.ZigZagHatching = ZigZagHatching;
    //////////////////////////////////////////////////// third gen
    // extending ChevronHatchting
    class BrickHatching extends ChevronHatching {
        constructor(angle = 0, width = 5, height = 2.5, margin = 0) {
            super(angle, [[0,height], [width,0]], .5, margin);
        }
    }
    this.BrickHatching = BrickHatching;
    // extending HexGridHatching
    class HoneyCombHatching extends HexGridHatching {
        constructor(angle, hexSize, drawSize = .8) {
            super(angle, hexSize, drawSize);
        }
    }
    this.HoneyCombHatching = HoneyCombHatching;
    class QBertHatching extends HexGridHatching {
        getInfo() {
            return {
                lines: this.getPath().map(e => [[0, 0], e]),
                columns: this.width * 1.5 / Math.max(this.tile[0][0], this.tile[1][0]) | 0,
                rows: this.height * 1.5 / Math.max(this.tile[0][1], this.tile[1][1]) | 0,
                columnIncrement: [...this.tile[1]],
                rowIncrement: [...this.tile[0]],
            }
        }
    }
    this.QBertHatching = QBertHatching;
    class TriangleHatching extends HexGridHatching {
        getPath() {
            return super.getPath().filter((e,i) => i%2 == 1);
        }
        getInfo(cellSize) {
            const info = super.getInfo(cellSize);
            info.columns *= 2;
            info.rows *= 2;
            return info;
        }
    }
    this.TriangleHatching = TriangleHatching;
    // extending TreadPlateHatching
    class PolkaDotHatching extends TreadPlateHatching {
        constructor(angle = 0, distance = 5, radius = 1) {
            super(distance, 2 * (radius+(radius == distance? .00001: 0))/distance, radius/(distance-radius+(radius == distance? .00001: 0)), angle, 1);
        }
    }
    this.PolkaDotHatching = PolkaDotHatching;
}

////////////////////////////////////////////////////////////////
// Polygon Clipping utility code - Created by Reinder Nijhoff 2019
// (Polygon binning by Lionel Lemarie 2021) https://turtletoy.net/turtle/95f33bd383
// (Delegated Hatching by Jurgen Westerhof 2024) https://turtletoy.net/turtle/d068ad6040
// https://turtletoy.net/turtle/a5befa1f8d
//
// const polygons = new Polygons();
// const p = polygons.create();
// polygons.draw(turtle, p);
// polygons.list();
//
// p.addPoints(...[[x,y],]);
// p.addSegments(...[[x,y],]);
// p.addOutline();
// p.addHatching(angle, distance); OR p.addHatching(HatchObject); where HatchObject has a method 'hatch(PolygonClass, thisPolygonInstance)'
// p.inside([x,y]);
// p.boolean(polygon, diff = true);
// p.segment_intersect([x,y], [x,y], [x,y], [x,y]);
////////////////////////////////////////////////////////////////
function Polygons(){
    const t=[],s=25,e=Array.from({length:s**2},t=>[]),n=class{
        constructor(){
            this.cp=[],this.dp=[],this.aabb=[]
        }
        addPoints(...t){let s=1e5,e=-1e5,n=1e5,h=-1e5;(this.cp=[...this.cp,...t]).forEach(t=>{s=Math.min(s,t[0]),e=Math.max(e,t[0]),n=Math.min(n,t[1]),h=Math.max(h,t[1])}),this.aabb=[s,n,e,h]}
        addSegments(...t){t.forEach(t=>this.dp.push(t))}
        addOutline(){for(let t=0,s=this.cp.length;t<s;t++)this.dp.push(this.cp[t],this.cp[(t+1)%s])}draw(t){for(let s=0,e=this.dp.length;s<e;s+=2)t.jump(this.dp[s]),t.goto(this.dp[s+1])}
        addHatching(t, s) {
            if(typeof t == 'object') return t.hatch(n, this);
            
            const e=new n;
            e.cp.push([-1e5,-1e5],[1e5,-1e5],[1e5,1e5],[-1e5,1e5]);
            const h=Math.sin(t)*s,o=Math.cos(t)*s,a=200*Math.sin(t),i=200*Math.cos(t);
            for(let t=.5;t<150/s;t++) {
                e.dp.push([h*t+i,o*t-a],[h*t-i,o*t+a]);
                e.dp.push([-h*t+i,-o*t-a],[-h*t-i,-o*t+a]);
            }
            e.boolean(this,!1);
            this.dp=[...this.dp,...e.dp]
        }
        inside(t){let s=0;for(let e=0,n=this.cp.length;e<n;e++)this.segment_intersect(t,[.1,-1e3],this.cp[e],this.cp[(e+1)%n])&&s++;return 1&s}
        boolean(t,s=!0){const e=[];for(let n=0,h=this.dp.length;n<h;n+=2){const h=this.dp[n],o=this.dp[n+1],a=[];for(let s=0,e=t.cp.length;s<e;s++){const n=this.segment_intersect(h,o,t.cp[s],t.cp[(s+1)%e]);!1!==n&&a.push(n)}if(0===a.length)s===!t.inside(h)&&e.push(h,o);else{a.push(h,o);const n=o[0]-h[0],i=o[1]-h[1];a.sort((t,s)=>(t[0]-h[0])*n+(t[1]-h[1])*i-(s[0]-h[0])*n-(s[1]-h[1])*i);for(let n=0;n<a.length-1;n++)(a[n][0]-a[n+1][0])**2+(a[n][1]-a[n+1][1])**2>=.001&&s===!t.inside([(a[n][0]+a[n+1][0])/2,(a[n][1]+a[n+1][1])/2])&&e.push(a[n],a[n+1])}}return(this.dp=e).length>0}
        segment_intersect(t,s,e,n){const h=(n[1]-e[1])*(s[0]-t[0])-(n[0]-e[0])*(s[1]-t[1]);if(0===h)return!1;const o=((n[0]-e[0])*(t[1]-e[1])-(n[1]-e[1])*(t[0]-e[0]))/h,a=((s[0]-t[0])*(t[1]-e[1])-(s[1]-t[1])*(t[0]-e[0]))/h;return o>=0&&o<=1&&a>=0&&a<=1&&[t[0]+o*(s[0]-t[0]),t[1]+o*(s[1]-t[1])]}
    };
    return{
        list:()=>t,
        create:()=>new n,
        draw:(n,h,o=!0)=>{reducedPolygonList=function(n){const h={},o=200/s;for(var a=0;a<s;a++){const c=a*o-100,r=[0,c,200,c+o];if(!(n[3]<r[1]||n[1]>r[3]))for(var i=0;i<s;i++){const c=i*o-100;r[0]=c,r[2]=c+o,n[0]>r[2]||n[2]<r[0]||e[i+a*s].forEach(s=>{const e=t[s];n[3]<e.aabb[1]||n[1]>e.aabb[3]||n[0]>e.aabb[2]||n[2]<e.aabb[0]||(h[s]=1)})}}return Array.from(Object.keys(h),s=>t[s])}(h.aabb);for(let t=0;t<reducedPolygonList.length&&h.boolean(reducedPolygonList[t]);t++);h.draw(n),o&&function(n){t.push(n);const h=t.length-1,o=200/s;e.forEach((t,e)=>{const a=e%s*o-100,i=(e/s|0)*o-100,c=[a,i,a+o,i+o];c[3]<n.aabb[1]||c[1]>n.aabb[3]||c[0]>n.aabb[2]||c[2]<n.aabb[0]||t.push(h)})}(h)}
    }
}