Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax
//

let c_r = 15; // min = 1, max=40, step=0.1
let c_h = 8; // min = 1, max=40, step=0.1
let c_angle = 40; // min=1, max=180, step=1
let holes = 50; // min=1, max=300, step=1
let hole_size = 0.8;// min=0.01, max=10, step=0.001

let a1 = 15; // min = -720, max=720, step=1
let a2 = 30; // min = -90, max=90, step=1
let cd = 20; // min = 0, max=360, step=0.1
let perspective = 1; // min=0, max=1, step=1,  (off, on)
let viewSize = 200; // DIS min=10, max=600, step=1
let fov = 60; // min=10, max=160, step=0.1
let subdiv = 16; // DIS min=1, max=1024, step=1
let subdiv_target = 0.5; // min=0.01, max=50, step=0.01
const subdivExtra = 1; // DIS min=0, max=1, step=1
let grid_size = 1.5; // min = 0.05, max=20, step=0.05


let scene = null;
let turtle = null;

let circleSubdiv = 16; // min=3, max=256, step=1

let useGrid = 1; // min=0, max=2, step=1

const DEBUG_N_ON_SURFACE = true;
const INCREMENTAL = true;
let sceneGen = null;

function init2() {
    // Global code will be evaluated once.
    //const turtle = new Turtle();

    scene = new Scene();
    scene.w = scene.h = viewSize;
    if (perspective > 0) {
        scene.fov = [fov, fov];
        scene.setPerspective(new V3(0, 0, 0), new V3(0, 0, 0));
    } else {
        scene.setOrthographic(100 / cd, new V3(0, 0, 0), new V3(0, 0, 0));
    }
    scene.camera_pos = Scene.worldCameraOrbit(new V3(0, 0, 0), cd, a1, a2);


    let sdf2 = new SDF2();
    sdf2.SUBDIV_TARGET = subdiv_target;
    sdf2.enableGrid = useGrid;
    sdf2.grid_step = grid_size;
    let p = new SDF2.Box(V(5, 5, 5));


    let holeSphere = [];
    for (let i=0; i<holes; i++) {
        let y = Util.rndRange(-0.5*c_h, 0.5*c_h);
        let rd = Math.sqrt(Math.random()+0.01)*c_r;
        let a = Util.rndRange(0, c_angle);
        let holeR = Util.rndRange(0.8*hole_size, 1.3*hole_size);
        let pos = new V3(rd, 0).rotDeg(90-a).changez(y);
        holeSphere.push(new SDF2.Sphere(holeR).tr(M4.translate(pos)));
    }
    //let slice = 

    sdf2.addObj(
        //new SDF2.Sphere(5)
        new SDF2.Cylinder(c_r, c_h*0.5)
            .sub(new SDF2.Box(new V3(c_r, c_r, c_h*0.51)).tr(M4.translate(-c_r, 0, 0)))
            .sub(new SDF2.Box(new V3(c_r, c_r, c_h*0.51)).tr(M4.euler(0, 0, Util.radians(180-c_angle)).mul(M4.translate(-c_r, 0, 0))))
            .sub(holeSphere.reduce((a, b) => new SDF2.Union(a, b)).format({invisible_style: null}))
            .tr(M4.translate(0, -0.5*c_r, 0))
        .format({textures: [
                //{ id: "slice_local", step: 0.2, dir: V(0, 0, 1) },
                            //{ id: "slice_local", step: 0.2, dir: V(0, 1, 0) }
        ], detect_edges: 17, line_style: 1})
            

    );



    sdf2.process(scene);
    if (!INCREMENTAL) {
        sdf2.draw_to_scene(scene);
        scene.draw();
    } else {
        sceneGen = sdf2.drawIncremental(scene);
    }
}
// The walk function will be called until it returns false.
function walk(i) {
    if (!sceneGen) {
        return false;
    }
    let r = sceneGen.next();
    if (r.value) {
        scene.drawincremental(r.value);
    }
    return !r.done;
}

class V3 {
    constructor(x = 0, y = 0, z = 0) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    static V0 = new V3(0, 0, 0);
    toString() { return `V3(${this.x}, ${this.y}, ${this.z})`; }
    add(b) { return new V3(this.x + b.x, this.y + b.y, this.z + b.z); }
    sub(b) { return new V3(this.x - b.x, this.y - b.y, this.z - b.z); }
    mul(b) { return new V3(this.x * b, this.y * b, this.z * b); }
    scale(b) { return new V3(this.x * b.x, this.y * b.y, this.z * b.z); }
    flipx() { return new V3(-this.x, this.y, this.z); }
    flipy() { return new V3(this.x, -this.y, this.z); }
    copy() { return new V3(this.x, this.y, this.z); }
    changex(v) { let res = this.copy(); res.x = v; return res; }
    changey(v) { let res = this.copy(); res.y = v; return res; }
    changez(v) { let res = this.copy(); res.z = v; return res; }
    len2() { return this.x * this.x + this.y * this.y + this.z * this.z; }
    static lerp(a, b, x) { return a.mul(1 - x).add(b.mul(x)); }
    magnitude() { return Math.sqrt(this.len2()); }
    normalized() { return this.mul(1 / this.magnitude()); }
    xyzs() { return this.x + this.y + this.z }
    xyzMax() { return Math.max(this.x, this.y, this.z); }
    xyzMin() { return Math.min(this.x, this.y, this.z); }
    yzx() { return new V3(this.y, this.z, this.x); }
    zxy() { return new V3(this.z, this.x, this.y); }
    swizzleSimple(v) {
        if (v.x > 0) {
            return this.zxy();
        } else if (v.y > 0) {
            return this.yzx();
        }
        return this;
    }

    abs() { return new V3(Math.abs(this.x), Math.abs(this.y), Math.abs(this.z)); }
    max(b) { return new V3(Math.max(this.x, b.x), Math.max(this.y, b.y), Math.max(this.z, b.z)); }
    min(b) { return new V3(Math.min(this.x, b.x), Math.min(this.y, b.y), Math.min(this.z, b.z)); }
    maxK(k) { return new V3(Math.max(this.x, k), Math.max(this.y, k), Math.max(this.z, k)); }
    minK(k) { return new V3(Math.min(this.x, k), Math.min(this.y, k), Math.min(this.z, k)); }
    sgn() { return new V3(Math.sign(this.x), Math.sign(this.y), Math.sign(this.z)); }
    dot(b) { return this.scale(b).xyzs(); }
    cross(b) {
        let a = this;
        return new V3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
    }
    rotDeg(deg) {
        const a = deg * Math.PI / 180;
        const s = Math.sin(a);
        const c = Math.cos(a);
        return new V3(this.x * c - this.y * s, this.x * s + this.y * c);
    }
}
class M4 {
    constructor(d = null) {
        this.d = d;
        if (!d) {
            this.d = [[1, 0, 0, 0],
            [0, 1, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 1]];
        }
    }
    copy() {
        let result = new M4();
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                result.d[i][j] = this.d[i][j];
            }
        }
        return result;
    }

    mul(b) {
        let res = new M4();
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                let s = 0;
                for (let k = 0; k < 4; k++) {
                    s += this.d[i][k] * b.d[k][j]
                }
                res.d[i][j] = s;
            }
        }
        return res;
    }
    transpose() {
        let res = new M4();
        for (let i = 0; i < 4; i++) {
            for (let j = 0; j < 4; j++) {
                res.d[i][j] = this.d[j][i];
            }
        }
        return res;
    }
    mulv(v) {
        let vv = [v.x, v.y, v.z, 1];
        let res = [0, 0, 0, 0];
        for (let i = 0; i < 4; i++) {
            for (let k = 0; k < 4; k++) {
                res[i] += this.d[i][k] * vv[k];
            }
        }
        return new V3(res[0], res[1], res[2]);
    }
    muldir(v) {
        let vv = [v.x, v.y, v.z, 1];
        let res = [0, 0, 0, 0];
        for (let i = 0; i < 4; i++) {
            for (let k = 0; k < 3; k++) {
                res[i] += this.d[i][k] * vv[k];
            }
        }
        return new V3(res[0], res[1], res[2]);
    }
    static translate(v, y = undefined, z = undefined) {
        if (y !== undefined) {
            let r = new M4();
            r.d[0][3] = v;
            r.d[1][3] = y;
            r.d[2][3] = z;
            return r;
        }
        let r = new M4();
        r.setTranslate(v);
        return r;
    }
    setTranslate(v) {
        this.d[0][3] = v.x;
        this.d[1][3] = v.y;
        this.d[2][3] = v.z;
    }
    translation(v) {
        return new V3(this.d[0][3], this.d[1][3], this.d[2][3]);
    }
    static scale(x) {
        let r = new M4();
        r.d[0][0] = x;
        r.d[1][1] = x;
        r.d[2][2] = x;
        return r;
    }
    static scale3(x, y, z = 1) {
        let r = new M4();
        r.d[0][0] = x;
        r.d[1][1] = y;
        r.d[2][2] = z;
        return r;
    }
    static euler(a, b, c) {
        let m1 = new M4();
        let m2 = new M4();
        let m3 = new M4();
        m1.d = [[1, 0, 0, 0],
        [0, Math.cos(a), -Math.sin(a), 0],
        [0, Math.sin(a), Math.cos(a), 0],
        [0, 0, 0, 1]];
        m2.d = [[Math.cos(b), 0, Math.sin(b), 0],
        [0, 1, 0, 0],
        [-Math.sin(b), 0, Math.cos(b), 0],
        [0, 0, 0, 1]];
        m3.d = [[Math.cos(c), -Math.sin(c), 0, 0],
        [Math.sin(c), Math.cos(c), 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1]];
        return m1.mul(m2).mul(m3);
    }
    static eulerv(v) {
        return euler(v.x, v.y, v.z);
    }
    inverse() {
        let res = [];
        for (let i = 0; i < 4; i++) {
            res[i] = this.d[i].slice();
            for (let j = 0; j < 4; j++) {
                res[i].push(i == j ? 1 : 0);
            }
        }
        for (let i = 0; i < 4; i++) {
            let r = i;
            for (let j = i; j < 4; j++) {
                if (Math.abs(res[j][i]) > Math.abs(res[r][i])) {
                    r = j;
                }
            }
            [res[i], res[r]] = [res[r], res[i]];
            if (res[i][i] != 0) {
                let k = 1 / res[i][i];
                res[i] = res[i].map((x) => x * k);

                for (let j = i + 1; j < 4; j++) {
                    let k2 = res[j][i];
                    for (let column = i; column < 8; column++) {
                        res[j][column] = res[j][column] - k2 * res[i][column];
                    }
                }
            }
        }
        for (let i = 3; i >= 0; i--) {
            for (let j = 0; j < i; j++) {
                let mul = res[j][i];
                for (let k = 0; k < 8; k++) {
                    res[j][k] -= res[i][k] * mul;
                }
            }
        }
        for (let i = 0; i < 4; i++) {
            res[i] = res[i].slice(4);
        }
        return new M4(res);
    }
}
class Util {
    static rndRange(min, max) {
        return Math.random() * (max - min) + min;
    }
    static radians(a) {
        return a * Math.PI / 180;
    }
    static inverseLerp(a, b, x) {
        let d = b - a;
        if (d != 0) {
            return (x - a) / d;
        } else {
            return 0;
        }
    }
    static lerp(a, b, x) {
        return (1 - x) * a + x * b;
    }
    static planeDistance(p0, pnorm, x) {
        pnorm = pnorm.normalized();
        let k = p0.dot(pnorm);
        return x.dot(pnorm) - k;
    }
    static rectContains(r, p) {
        return p.x >= r[0] && p.x <= r[1] && p.y >= r[2] && p.y <= r[3];
    }
    static planeClip(p0, pnorm, a, b) {
        let d1 = Util.planeDistance(p0, pnorm, a);
        let d2 = Util.planeDistance(p0, pnorm, b);
        if (d1 >= 0 && d2 >= 0) {
            return [a, b];
        }
        if (d1 < 0 && d2 < 0) {
            return null;
        }
        let m = Util.inverseLerp(d1, d2, 0);
        let p = V3.lerp(a, b, m);
        if (d1 < 0) {
            return [p, b];
        } else {
            return [a, p];
        }
    }

    static clipRect(a, b, r) {
        let ca = rectContains(r, a);
        let cb = rectContains(r, b);
        if (ca && cb) {
            return [a, b];
        }
    }
    /**
     * 
     * @param {V3} p0 
     * @param {V3} p1 
     * @param {V3} p2 
     * @param {V3} p3 
     * @param {float} t 
     * @returns 
     */
    static cubic_bezier(p0, p1, p2, p3, t) {
        let t1 = 1 - t;
        return p0.mul(t1 * t1 * t1).add(p1.mul(t1 * t1 * t * 3)).add(p2.mul(t1 * t * t * 3)).add(p3.mul(t * t * t));
    }
    /**
     * 
     * @param {V3} p0 
     * @param {V3} p1 
     * @param {V3} p2 
     * @returns {[V3]}
     */
    static quad_bezier_to_cubic(p0, p1, p2) {
        return [p0, p0.add(p1.sub(p0).mul(2 / 3)), p2.add(p1.sub(p2).mul(2 / 3)), p2];
    }
}
class Scene {
    constructor() {
        this.camera_pos_inverse = new M4();
        this.camera_pos = Scene.worldCameraM(new V3(0, 0, 0), new V3(1, 0, 0));
        this.camera_inverse = new M4();
        this.camera = Scene.orthographic1();
        this.lines = [];
        this.ortho = true;
        this.w = 100;
        this.h = 100;
        this.fov = [90, 90];
    }
    set camera_pos(v) {
        this._camera_pos = v;
        this.camera_pos_inverse = v.inverse();
    }
    /**
     * @type {M4}
     */
    get camera_pos() {
        return this._camera_pos;
    }
    set camera(m) {
        this._camera = m;
        this.camera_inverse = m.inverse();
    }
    /**
     * @type {M4}
     */
    get camera() {
        return this._camera;
    }
    addLine(a, b) {
        this.lines.push([a, b]);
    }
    addOb(x) {
        for (var line of x.lines) {
            let lineData = line.data;
            for (let i = 1; i < lineData.length; i++) {
                this.lines.push([lineData[i - 1], lineData[i]]);
            }
        }
    }
    mapPoint(x) {
        let p = this.camera.mulv(x);
        if (this.ortho) {
            // ok
        } else if (p.z != 0) {
            p = p.mul(1 / Math.abs(p.z));
        } else {
            p.x = 0; p.y = 0;
        }
        return p;
    }

    mapWorldPoint(p) {
        let cam = this.camera_pos.mulv(p);
        let z = cam.z;
        return this.mapPoint(cam).changez(z);
    }

    drawincremental(ob) {
        let lastPoint = null;
        turtle.pendown();
        ob.lines.forEach((line) => {
            for (let i = 1; i < line.data.length; i++) {
                let debug = false;
                let l = [line.data[i - 1], line.data[i]];
                if (l[0].z > 0 && l[1].z > 0) {
                    debug = true;
                }
                let p1 = this.camera_pos.mulv(l[0]);
                let p2 = this.camera_pos.mulv(l[1]);

                if (!this.ortho) {
                    let p0 = new V3(0, 0, 0);
                    let norm = new V3();
                    let line = [p1, p2];
                    let a1 = Math.PI * 0.5 * this.fov[0] / 180;
                    let a2 = Math.PI * 0.5 * this.fov[1] / 180;
                    if (line) {
                        norm = new V3(Math.cos(a1), 0, -Math.sin(a1));
                        line = Util.planeClip(p0, norm, line[0], line[1]);
                    }
                    if (line) {
                        norm = new V3(-Math.cos(a1), 0, -Math.sin(a1));
                        line = Util.planeClip(p0, norm, line[0], line[1]);
                    }
                    if (line) {
                        norm = new V3(0, -Math.cos(a2), -Math.sin(a2));
                        line = Util.planeClip(p0, norm, line[0], line[1]);
                    }
                    if (line) {
                        norm = new V3(0, Math.cos(a2), -Math.sin(a2));
                        line = Util.planeClip(p0, norm, line[0], line[1]);
                    }
                    if (line) {
                        p1 = line[0];
                        p2 = line[1];
                    } else {
                        continue
                    }
                }
                p1 = this.mapPoint(p1);
                p2 = this.mapPoint(p2);
                let connected = false;
                if (lastPoint != null) {
                    connected = lastPoint.sub(p1).len2() < 0.00001;
                }
                if (!connected) {
                    turtle.penup();
                    turtle.jump(p1.x, p1.y)
                    turtle.pendown();
                }
                turtle.goto(p2.x, p2.y);
                lastPoint = p2;
            }
        });
        turtle.penup();
    }

    draw() {
        let lastPoint = null;
        turtle.pendown();
        this.lines.forEach((l) => {
            let p1 = this.camera_pos.mulv(l[0]);
            let p2 = this.camera_pos.mulv(l[1]);


            /*if (p1.z > 0 && p2.z > 0) {
                return;
            } else if (p1.z > 0 || p2.z > 0) {
                let x = Util.inverseLerp(p1.z, p2.z, 0.011);
                if (p1.z > 0) {
                    p1 = V3.lerp(p1, p2, x);
                } else {
                    p2 = V3.lerp(p1, p2, x);
                }
            }*/
            if (!this.ortho) {
                let p0 = new V3(0, 0, 0);
                let norm = new V3();
                let line = [p1, p2];
                let a1 = Math.PI * 0.5 * this.fov[0] / 180;
                let a2 = Math.PI * 0.5 * this.fov[1] / 180;
                if (line) {
                    norm = new V3(Math.cos(a1), 0, -Math.sin(a1));
                    line = Util.planeClip(p0, norm, line[0], line[1]);
                }
                if (line) {
                    norm = new V3(-Math.cos(a1), 0, -Math.sin(a1));
                    line = Util.planeClip(p0, norm, line[0], line[1]);
                }
                if (line) {
                    norm = new V3(0, -Math.cos(a2), -Math.sin(a2));
                    line = Util.planeClip(p0, norm, line[0], line[1]);
                }
                if (line) {
                    norm = new V3(0, Math.cos(a2), -Math.sin(a2));
                    line = Util.planeClip(p0, norm, line[0], line[1]);
                }
                if (line) {
                    p1 = line[0];
                    p2 = line[1];
                } else {
                    return;
                }
            }
            p1 = this.mapPoint(p1);
            p2 = this.mapPoint(p2);
            let connected = false;
            if (lastPoint != null) {
                connected = lastPoint.sub(p1).len2() < 0.00001;
            }
            if (!connected) {
                turtle.penup();
                turtle.jump(p1.x, p1.y)
                turtle.pendown();
            }
            turtle.goto(p2.x, p2.y);
            lastPoint = p2;
        });
        turtle.penup();
    }
    static worldCameraM(from, to) {
        let d = to.sub(from);
        let m1 = M4.translate(from.mul(-1));
        let a1 = Math.atan2(d.y, d.x);
        let xy = d.changez(0);

        let a2 = Math.atan2(d.z, xy.magnitude());
        //let m2 = (new M4()).mul(M4.euler(0, 0, +a1)).mul(M4.euler(-Math.PI/2 - a2, 0, 0));
        let m2 = (new M4()).mul(M4.euler(-a2, -a1, 0)).mul(M4.euler(Math.PI / 2, Math.PI, -Math.PI / 2));
        return m2.mul(m1);
    }
    static worldCameraM2(from, to) {
        let d = to.sub(from);
        let m1 = M4.translate(from.mul(-1));

        let a1 = Math.atan2(-d.x, d.y);
        let xy = d.changez(0);

        let a2 = Math.atan2(xy.magnitude(), d.z);
        let m2 = M4.euler(0, 0, a1);
        let m3 = M4.euler(-a2, 0, 0);
        return m2.mul(m3).mul(m1);
    }

    static worldCameraOrbit(to, distance, z0, x0) {
        let x = x0 * Math.PI / 180;
        let z = z0 * Math.PI / 180;
        let p = M4.euler(0, 0, z).mul(M4.euler(0, x, 0)).mulv(new V3(-1, 0, 0));
        let p2 = p.mul(distance).add(to);
        if (x0 > -90 && x0 < 90) {
            return Scene.worldCameraM(p2, p2.sub(p));
        } else {
            let m1 = M4.translate(p2.mul(-1));
            if (x0 > 0) {
                return M4.euler(0, 0, 1 * Math.PI / 2 - z).mul(m1);
            } else {
                return M4.euler(0, Math.PI, -1 * Math.PI / 2 - z).mul(m1);
            }

        }

    }
    cameraToWorld(p) {
        return this.camera_pos_inverse.mulv(p);
    }
    screenToWorld(p) {
        return this.camera_pos_inverse.mulv(this.camera_inverse.mulv(p));
    }

    static orthographic1(scale = 1) {
        let res = new M4();
        res.d = [
            [scale, 0, 0, 0],
            [0, -scale, 0, 0],
            [0, 0, 1, 0],
            [0, 0, 0, 1],
        ];
        //res = res.mul(Scene.worldCameraM(from, to));
        res = res;
        return res;
    }
    setOrthographic(scale, from, to) {
        this.camera = Scene.orthographic1(scale);
        this.camera_pos = Scene.worldCameraM(from, to);
        this.ortho = true;
    }
    static perspective(scale) {
        let res = new M4();
        res.d = [
            [scale, 0, 0, 0],
            [0, -scale, 0, 0],
            [0, 0, -1, 0],
            [0, 0, 0, 1],
        ];
        return res;
    }
    setPerspective(from, to) {

        this.camera = Scene.perspective(this.w * 0.5 * Math.tan((90 - this.fov[0] * 0.5) / 180 * Math.PI));
        this.camera_pos = Scene.worldCameraM(from, to);
        this.ortho = false;
    }
}
class StrokeInfo {
    constructor(data) {
        this.p0 = null;
        this.data = data;
    }
    copy() {
        let result = new StrokeInfo(this.data.slice());
        result.p0 = this.data;
        return result;
    }
    last() {
        if (this.data.length == 0) {
            return null;
        }
        return this.data[this.data.length - 1];
    }
}
class Ob {
    constructor() {
        /** @type [StrokeInfo] */
        this.lines = [];
    }
    static fromChain(points) {
        let res = new Ob();
        let resLine = new StrokeInfo(points.slice());
        res.lines = [resLine];
        return res;
    }
    static fromLoop(points) {
        let res = Ob.fromChain(points);
        if (points.length > 1) {
            res.lines[0].data.push(res.lines[0].data[0]);
        }
        return res;
    }
    static cubic_bezier(p0, p1, p2, p3, steps = 32) {
        let result = [p0];
        for (let i = 1; i < steps; i++) {
            result.push(Util.cubic_bezier(p0, p1, p2, p3, i / steps));
        }
        result.push(p3);
        return Ob.fromChain(result);
    }
    static circle(r, p0 = null, subdiv = circleSubdiv) {
        if (p0 == null) {
            p0 = new V3(0, 0, 0);
        }
        let points = []
        for (let i = 0; i < subdiv; i++) {
            let a = (Math.PI * 2 * i) / subdiv;
            let pos = p0.add(new V3(Math.sin(a) * r, Math.cos(a) * r, 0));
            points.push(pos);
        }
        let res = Ob.fromLoop(points);
        let line = res.lines[0];
        line.p0 = p0;
        return res;
    }

    /**
     * 
     * @param {string} text 
     * @returns {V3}
     */
    static parsePoint(text) {
        let t = text.split(/[, ]/)
        return new V3(Number.parseFloat(t[0]), Number.parseFloat(t[1]));
    }
    /**
     *
     * @function
     * @param {string} data
     * @returns {Ob}
     */
    static fromD(data) {
        let res = new Ob();
        let pos = new V3(0, 0, 0);
        let p = 0;
        data = data.trim();
        let chunks = data.split(/(?=[a-df-zA-DF-Z])/);
        let drawing = false;
        let start = pos;
        let last_cubic = null;
        let last_quad = null;
        let POINT_REGEX = /[0-9.eE+\-]+[, ][0-9.eE+\-]+/g;
        let NUMBER_REGEX = /[0-9.eE+\-]+/g;
        for (const chunk of chunks) {
            let t = chunk[0];
            if (t == 'm' || t == 'M') {
                let pointText = chunk.matchAll(POINT_REGEX);
                /**  @type {[V3]} */
                let points = Array.from(pointText.map((x) => { return Ob.parsePoint(x[0]) }));
                for (let i = 0; i < points.length; i++) {
                    let p2 = points[i];
                    if (t == 'm') {
                        p2 = p2.add(pos);
                    }
                    if (i == 0) {
                        pos = p2;
                        start = pos;
                        drawing = true;
                    } else {
                        res.addLine(pos, p2);
                        pos = p2;
                    }
                }
                last_cubic = null;
                last_quad = null;
            } else if (t == 'l' || t == 'L') {
                let pointText = chunk.matchAll(POINT_REGEX);
                /**  @type {[V3]} */
                let points = Array.from(pointText.map((x) => { return Ob.parsePoint(x[0]) }));
                if (!drawing) {
                    drawing = true;
                    start = pos;
                }
                for (let i = 0; i < points.length; i++) {
                    let p2 = points[i];
                    if (t == 'l') {
                        p2 = p2.add(pos);
                    }
                    res.addLine(pos, p2);
                    pos = p2;
                }
                last_cubic = null;
                last_quad = null;
            } else if (t == 'h' || t == 'H' || t == 'v' || t == 'V') {
                let pointText = chunk.matchAll(NUMBER_REGEX);
                /**  @type {[float]} */
                let points = Array.from(pointText.map((x) => { return Number.parseFloat(x[0]) }));
                if (!drawing) {
                    drawing = true;
                    start = pos;
                }
                for (let v of points) {
                    let p2 = pos;
                    if (t == 'h') {
                        p2 = p2.add(V(v, 0));
                    } else if (t == 'v') {
                        p2 = p2.add(V(0, v));
                    } else if (t == 'H') {
                        p2 = p2.changex(v);
                    } else if (t == 'V') {
                        p2 = p2.changey(v);
                    }
                    res.addLine(pos, p2);
                    pos = p2;
                }
                last_cubic = null;
                last_quad = null;
            } else if (t == 'c' || t == 'C' || t == 's' || t == 'S') {
                let pointText = chunk.matchAll(POINT_REGEX);
                /**  @type {[V3]} */
                let points = Array.from(pointText.map((x) => { return Ob.parsePoint(x[0]) }));
                if (!drawing) {
                    drawing = true;
                    start = pos;
                }
                let step = 3;
                if (t == 's' || t == 'S') {
                    step = 2;
                }
                for (let i = 0; i + step - 1 < points.length; i += step) {
                    let px = null;
                    if (step == 3) {
                        px = [points[i + 0], points[i + 1], points[i + 2]];
                    } else {
                        px = [points[i + 0], points[i + 0], points[i + 1]];
                    }

                    if (t == 'c' || t == 's') {
                        for (let j = 0; j < px.length; j++) {
                            px[j] = px[j].add(pos);
                        }
                    }
                    if (step == 2) {
                        if (last_cubic) {
                            px[0] = pos.mul(2).sub(last_cubic);
                        } else {
                            px[0] = pos;
                        }
                    }
                    res.addObMove(Ob.cubic_bezier(pos, px[0], px[1], px[2]));
                    pos = px[2];
                    last_cubic = px[1];
                }
                last_quad = null;
            } else if (t == 'Q' || t == 'q' || t == 'T' || t == 't') {
                let pointText = chunk.matchAll(POINT_REGEX);
                /**  @type {[V3]} */
                let points = Array.from(pointText.map((x) => { return Ob.parsePoint(x[0]) }));
                if (!drawing) {
                    drawing = true;
                    start = pos;
                }
                let step = 2;
                if (t == 't' || t == 'T') {
                    step = 1;
                }
                for (let i = 0; i + step - 1 < points.length; i += step) {
                    let px = null;
                    if (step == 2) {
                        px = [points[i + 1], points[i + 2]];
                    } else {
                        px = [points[i + 0], points[i + 0]];
                    }

                    if (t == 'q' || t == 't') {
                        for (let j = 0; j < px.length; j++) {
                            px[j] = px[j].add(pos);
                        }
                    }
                    if (step == 1) {
                        if (last_quad) {
                            px[0] = pos.mul(2).sub(last_quad);
                        } else {
                            px[0] = pos;
                        }
                    }
                    last_quad = px[0];
                    px = Util.quad_bezier_to_cubic(pos, px[0], px[1]);
                    res.addObMove(Ob.cubic_bezier(pos, px[1], px[2], px[3]));
                    pos = px[3];
                }
                last_cubic = null;
            } else if (t == 'a' || t == 'A') {
                let pointText = chunk.matchAll(NUMBER_REGEX);
                /**  @type {[V3]} */
                let numbers = Array.from(pointText.map((x) => { return Number.parseFloat(x[0]) }));
                if (!drawing) {
                    drawing = true;
                    start = pos;
                }
                for (let i = 0; i + 6 < numbers.length; i += 7) {
                    let rx = numbers[i + 0];
                    let ry = numbers[i + 1];
                    let angle = numbers[i + 2];
                    let large_arc = numbers[i + 3];
                    let sweep_flag = numbers[i + 4];
                    let p2 = new V3(numbers[i + 4], numbers[i + 5]);

                    if (t == 'a') {
                        p2 = p2.add(pos);
                    }
                    res.addLine(pos, p2); // TODO: implement arcs
                    pos = p2;
                }
                last_cubic = null;
                last_quad = null;
            }
            else if (t == 'z' || t == 'Z') {
                if (drawing) {
                    res.addLine(pos, start);
                    pos = start;
                    drawing = false;
                }
                last_cubic = null;
                last_quad = null;
            }
        }
        return res;
    }
    addLine(a, b) {
        if (this.lines.length > 0) {
            let line = this.lines[0];
            if (line.last() == a) {
                line.data.push(b);
                return;
            }
        }
        this.lines.push(new StrokeInfo([a, b]));
    }
    addLines(ar) {
        for (const x of ar) {
            this.addLine(new V3(x[0], x[1], x[2]), new V3(x[3], x[4], x[5]));
        }
    }
    addLineArray(ar) {
        for (const x of ar) {
            this.addLine(x[0], x[1]);
        }
    }
    addOb(o) {
        for (const line of o.lines) {
            this.lines.push(line.copy());
        }
    }
    addObMove(o) {
        for (const line of o.lines) {
            this.lines.push(line);
        }
    }
    transform(m) {
        for (let line of this.lines) {
            for (let i = 0; i < line.data.length; i++) {
                line.data[i] = m.mulv(line.data[i]);
            }
            if (line.p0) {
                line.p0 = m.mulv(line.p0);
            }
        }
    }
    transformed(m) {
        let r = new Ob();
        r.addOb(this);
        r.transform(m);
        return r;
    }
}

class SDFR {
    constructor(d, norm, obj) {
        /** @type {float} */
        this.d = d;
        /** @type {V3} */
        this.norm = norm;
        this.obj = obj;
    }
}
class SDFF {
    static DETECT_EDGE_EMPTY = 1;
    static DETECT_EDGE_FRONT = 2;
    static DETECT_EDGE_EXTERNALG = 4;
    static DETECT_EDGE_INTERNALG = 8;
    static DETECT_EDGE_SURFACE = 16;

    constructor(args) {
        if (!args) {
            args = {}
        }
        this.transform = new M4();
        this.format(args);
    }
    get primitive() {
        return false;
    }
    get curved() {
        return true;
    }
    tr(t) {
        this.transform = t.mul(this.transform);
        return this;
    }
    format(args) {
        if ("textures" in args) {
            this.textures = args.textures;
        }
        if ("line_style" in args) {
            this.line_style = args.line_style;
        }
        if ("invisible_style" in args) {
            this.invisible_style = args.invisible_style;
        }
        if ("detect_edges" in args) {
            this.detect_edges = args.detect_edges;
        }
        if ("groot" in args) {
            this.groot = args.groot;
        }
        return this;
    }
    /**
     * 
     * @param {Scene} camera_info 
     * @param {SDFNode} t 
     * @returns {null|Ob}
     */
    get_lines(camera_info, t) {
        return null;
    }
    get_texture(texture, node, camera_info) {
        return null;
    }
    add(x) {
        return new SDF2.Union(this, x);
    }
    sub(x) {
        return new SDF2.Diff(this, x);
    }

}

class SDFNode {
    /**
     * 
     * @param {SDFNode} parent 
     */
    constructor(parent) {
        this.a = null;
        this.b = null;
        /** @type{M4} */
        this.transform = null;
        /** @type{M4} */
        this.inverse_transform = null;
        

        this.line_style = 1;
        this.invisible_style = null;
        this.textures = null;
        this.detect_edges = 0;
        this.group = -1;
        if (parent) {
            this.line_style = parent.line_style;
            this.invisible_style = parent.invisible_style;
            this.textures = parent.textures;
            this.detect_edges = parent.detect_edges;
            this.group = parent.group;
        }

        this.sign = 1;
        this.index = -1;
        /** @type{SDFF} */
        this.func = null;
    }
}

let sdf_runs = 0;

class SDF2 {

    static Sphere = class extends SDFF {
        constructor(r, args = null) {
            super(args);
            this.r = r;
        }
        do(p, id = null) {
            let v = p;
            let m = v.magnitude();
            let d = m - this.r;
            let norm = null;
            norm = v;
            /*if (d > 0) {
                norm = v;
            } else {
                norm = v.mul(-1);
            }*/
            return new SDFR(d, norm, id);
        }

        get primitive() {
            return true;
        }
        /**
         * 
         * @param {Scene} camera_info 
         * @param {SDFNode} node
         * @returns {Ob}
         */
        get_lines(camera_info, node) {
            let result = null;
            let p0 = camera_info.cameraToWorld(V(0, 0, 0));
            let forward = camera_info.cameraToWorld(V(0, 0, -1)).sub(p0);
            if (camera_info.ortho) {
                result = Ob.circle(this.r);
                let transform = camera_info.camera_pos_inverse.copy();
                transform.setTranslate(new V3());
                let objTransform = node.inverse_transform.copy();
                objTransform.setTranslate(new V3())
                result.transform(objTransform.mul(transform));
            } else {
                let pObj = node.transform.translation();
                let posRelative = pObj.sub(p0);
                let d = posRelative.magnitude();
                if (d < this.r) {
                    return null;
                }
                let r = this.r;
                let tangent = Math.sqrt(d * d - r * r);
                let radiusOutline = r * tangent / d;
                let offset = r * r / d;
                let o = Ob.circle(radiusOutline, new V3(0, 0, offset));

                let objTransform = node.inverse_transform.copy();
                objTransform.setTranslate(new V3())
                let tr = Scene.worldCameraM2(new V3(), posRelative.mul(-1));
                tr.setTranslate(new V3());

                result = o;
                o.transform(objTransform.mul(tr));
            }
            return result;
        }
        get_texture(texture, node, camera_info) {
            let result = new Ob();
            switch (texture.id) {
                case "slice_local":
                    {
                        let dir = V(0, 0, 1);
                        if (texture.dir) {
                            dir = texture.dir;
                        }

                        let step = texture.step;
                        for (let z = -this.r + step; z < this.r; z += step) {
                            let r2 = Math.sqrt(this.r * this.r - z * z);
                            let ring = Ob.circle(r2, new V3(0, 0, z));
                            let ringData = ring.lines[0].data;
                            for (let i = 0; i < ringData.length; i++) {
                                ringData[i] = ringData[i].swizzleSimple(dir);
                            }
                            ring.lines[0].p0 = ring.lines[0].p0.swizzleSimple(dir);
                            result.addObMove(ring);
                        }
                        return result;
                    }
                    break;
            }
            return super.get_texture(texture, node, camera_info);
        }
    }
    static Cylinder = class extends SDFF {
        constructor(r, h, args = null) {
            super(args);
            this.r = r;
            this.h = h;
        }
        /**
         * 
         * @param {V3} p 
         * @param {*} id 
         * @returns {SDFR}
         */
        do(p, id = null) {
            let flip = p.z < 0 ? -1 : 1;
            p.z *= flip;
            let v = p.changez(0);
            let d = v.magnitude() - this.r;
            let dh = p.z - this.h;
            if (dh < 0) {
                //d = v.magnitude() - this.r;
            } else {
                if (d < 0) {
                    d = dh;
                    v = new V3(0, 0, d);
                } else {
                    v = v.sub(v.normalized().mul(this.r)).changez(dh)
                    d = v.magnitude();
                }
            }
            v.z = v.z * flip;
            return new SDFR(d, v, id);
        }

        get primitive() {
            return true;
        }
        /**
         * 
         * @param {Scene} camera_info 
         * @param {SDFNode} node 
         * @returns 
         */
        get_lines(camera_info, node) {
            let result = new Ob();

            result.addObMove(Ob.circle(this.r, new V3(0, 0, this.h)));
            result.addObMove(Ob.circle(this.r, new V3(0, 0, -this.h)));
            let p0 = camera_info.cameraToWorld(new V3());

            if (camera_info.ortho) {
                let forwardGlobal = camera_info.cameraToWorld(new V3(0, 0, -1))
                let flocal = node.inverse_transform.muldir(forwardGlobal);

                let side = flocal.cross(new V3(0, 0, 1));
                side = side.mul(this.r / side.magnitude());
                result.addLine(side.changez(-this.h), side.changez(this.h));
                side = side.mul(-1);
                result.addLine(side.changez(-this.h), side.changez(this.h));
            } else {
                let cameraObjLocal = node.inverse_transform.mulv(p0).changez(0);
                let d = cameraObjLocal.magnitude();
                if (d <= this.r) {
                    return result;
                }
                let r = this.r;
                let tangent = Math.sqrt(d * d - r * r);
                let radiusOutline = r * tangent / d;
                let offset = r * r / d;

                let v1 = cameraObjLocal.normalized();
                let v2 = new V3(v1.y, -v1.x, 0);
                v1 = v1.mul(offset);
                v2 = v2.mul(radiusOutline);
                let side1 = v1.add(v2);
                let side2 = v1.sub(v2);

                result.addLine(side1.changez(-this.h), side1.changez(this.h));
                result.addLine(side2.changez(-this.h), side2.changez(this.h));
            }

            return result;
        }
        get_texture(texture, node, camera_info) {
            let result = new Ob();
            switch (texture.id) {
                case "slice_local":
                    {
                        let dir = V(0, 0, 1);
                        if (texture.dir) {
                            dir = texture.dir;
                        }

                        let step = texture.step;
                        if (dir.z > 0) {
                            for (let z = -this.h; z < this.h; z += step) {
                                result.addObMove(Ob.circle(this.r, new V3(0, 0, z)));
                            }
                        }

                        return result;
                    }
                    break;
            }
            return super.get_texture(texture, node, camera_info);
        }
    }
    static Box = class extends SDFF {
        constructor(size, args = null) {
            super(args);
            /** @type{V3} */
            this.size = size;
        }
        /**
         * 
         * @param {V3} p 
         * @returns {SDFR}
         */
        do(p, id = null) {
            let sc1 = p.sgn();
            p = p.scale(sc1);
            let q = p.sub(this.size);
            let [a, b, c] = [q.x, q.y, q.z];
            let d = 0;
            let norm = null;
            if (a < 0 && b < 0 && c < 0) {
                if (a > b && a > c) {
                    d = a;
                    norm = new V3(-d, 0, 0);
                } else if (b > c) {
                    d = b;
                    norm = new V3(0, -d, 0);
                } else {
                    d = c;
                    norm = new V3(0, 0, -c);
                }
            } else {
                norm = q.maxK(0);
                d = norm.magnitude();
            }
            return new SDFR(d, norm.scale(sc1), id);
        }
        get primitive() {
            return true;
        }
        get curved() {
            return false;
        }
        corner(m) {
            let r = this.size.mul(-1);
            if (m & 1) {
                r.x = -r.x;
            }
            if (m & 2) {
                r.y = -r.y;
            }
            if (m & 4) {
                r.z = -r.z;
            }
            return r;
        }
        get_lines(camera_info, node) {
            let result = new Ob();
            for (let mask = 0; mask < 7; mask++) {
                for (let j = 0; j < 3; j++) {
                    let m2 = mask | (1 << j);
                    if (m2 == mask) {
                        continue;
                    }
                    result.addLine(this.corner(mask), this.corner(m2));
                }
            }
            return result;
        }
        get_texture(texture, node, camera_info) {
            let result = new Ob();
            switch (texture.id) {
                case "slice_local":
                    {
                        let dir = V(0, 0, 1);
                        if (texture.dir) {
                            dir = texture.dir;
                        }

                        let step = texture.step;
                        if (dir.z > 0) {
                            for (let z = -this.size.z; z < this.size.z; z += step) {
                                result.addObMove(Ob.fromLoop([
                                    new V3(this.size.x, this.size.y, z),
                                    new V3(this.size.x, -this.size.y, z),
                                    new V3(-this.size.x, -this.size.y, z),
                                    new V3(-this.size.x, this.size.y, z)
                                ]));
                            }
                        } else if (dir.y > 0) {
                            for (let z = -this.size.y; z < this.size.y; z += step) {
                                result.addObMove(Ob.fromLoop([
                                    new V3(this.size.x, z, this.size.z),
                                    new V3(this.size.x, z, -this.size.z),
                                    new V3(-this.size.x, z, -this.size.z),
                                    new V3(-this.size.x, z, this.size.z),
                                    new V3(this.size.x, z, this.size.z),
                                ]));
                            }
                        } else {
                            for (let z = -this.size.x; z < this.size.x; z += step) {
                                result.addObMove(Ob.fromLoop([
                                    new V3(z, this.size.y, this.size.z),
                                    new V3(z, this.size.y, -this.size.z),
                                    new V3(z, -this.size.y, -this.size.z),
                                    new V3(z, -this.size.y, this.size.z),
                                    new V3(z, this.size.y, this.size.z),
                                ]));
                            }
                        }
                        return result;
                    }
                    break;
            }
            return super.get_texture(texture, node, camera_info);
        }
    }
    static Union = class extends SDFF {
        constructor(a, b, args = null) {
            super(args)
            this.a = a;
            this.b = b;
        }
        /**
         * 
         * @param {V3} p 
         * @returns {SDFR}
         */
        do(p, r1, r2) {
            if (r1.d < r2.d) {
                return r1;
            } else {
                return r2;
            }
        }
        static doStatic(p, r1, r2) {
            if (r1.d < r2.d) {
                return r1;
            } else {
                return r2;
            }
        }
    }
    static Diff = class extends SDFF {
        constructor(a, b, args = null) {
            super(args)
            this.a = a;
            this.b = b;
        }
        /**
         * 
         * @param {V3} p 
         * @returns {SDFR}
         */
        do(p, r1, r2) {
            r2.d *= -1;
            if (r1.d > r2.d) {
                return r1;
            } else {
                r2.norm = r2.norm.mul(-1);
                return r2;
            }
        }
    }
    static Intersection = class extends SDFF {
        constructor(a, b, args = null) {
            super(args)
            this.a = a;
            this.b = b;
        }
        /**
         * 
         * @param {V3} p 
         * @returns {SDFR}
         */
        do(p, r1, r2) {
            if (r1.d > r2.d) {
                return r1;
            } else {
                return r2;
            }
        }
    }
    static Xor = class extends SDFF {
        constructor(a, b, args = null) {
            super(args)
            this.a = a;
            this.b = b;
        }
        /**
         * 
         * @param {V3} p 
         * @returns {SDFR}
         */
        do(p, r1, r2) {
            if (r1.d >= r2.d) {
                [r1, r2] = [r2, r1];
            }
            if (r1.d > r2.d) {
                return r1;
            } else {
                r2.d *= -1;
                r2.norm = r2.norm.mul(-1);
                return r2;
            }
        }
    }
    constructor() {
        /** @type {SDFF} */
        this.objs = []
        this.tmpd = []
        /** @type {[SDFNode]} */
        this.o2 = [];

        this.MAX_STEPS = 300;
        this.RAY_MARCH_LIMIT = 0.0001;
        this.TANGENT_HACK = 0.01;
        this.SUBDIV_TARGET = 0.5;
        this.MAX_DISTANCE = 10000;
        this.EDGE_SEARCH_ITER = 16;

        this.grid_step = 1;


        this.enableGrid = 1;

        /** @type {[[[V3, float, SDFR]]]} */
        this.grid = null;
    }

    addObj(x) {
        this.objs.push(x);
    }
    /**
     * 
     * @param {V3} p 
     * @param {SDFNode} offsetNode
     * @returns {SDFR}
     */
    calcSDF(p, offsetNode = null) {
        sdf_runs++;
        for (let i of this.primitive) {
            let node = this.o2[i];
            let p2 = node.inverse_transform.mulv(p);
            this.tmpd[i] = node.func.do(p2, this.o2[i]);
        }
        if (offsetNode) {
            this.tmpd[offsetNode.index].d += offsetNode.sign * this.TANGENT_HACK;
        }
        for (let i = this.combine.length - 1; i >= 0; --i) {
            let index = this.combine[i];
            let node = this.o2[index];
            let r1 = this.tmpd[node.a];
            let r2 = this.tmpd[node.b];
            this.tmpd[index] = node.func.do(p, r1, r2);
        }
        let res = null;
        for (let i of this.root) {
            if (res) {
                res = SDF2.Union.doStatic(p, res, this.tmpd[i]);
            } else {
                res = this.tmpd[i];
            }
        }
        return res;
    }

    runRay(p0, dir, limit, offsetNode = null) {
        let travel = 0;
        let p = p0;
        let result = null;
        let steps = 0;
        while (travel < limit) {
            steps++;
            /** @type {SDFR} */
            let hit = this.calcSDF(p, offsetNode);
            let distance = hit.d;
            if (distance < this.RAY_MARCH_LIMIT || steps > this.MAX_STEPS) {
                result = hit;
                break;
            }
            distance = Math.min(limit - travel, distance);
            p = p.add(dir.mul(distance));
            travel += distance;
        }
        return [p, travel, result];
    }
    clipReach(p0, point, ortho, forward, mustBeOnSurface, node = null) {
        if (mustBeOnSurface && DEBUG_N_ON_SURFACE) {
            let d = 0;
            if (!node) {
                d = Math.abs(this.calcSDF(point).d);
            } else {
                let hit = this.calcSDF(point);
                if (hit.obj != node) {
                    return -1;
                }
                d = Math.abs(hit.d);
            }
            if (d > 0.001) {
                return -1;
            }
        }
        let dir = point.sub(p0);
        let dis = null;
        if (ortho) {
            let fnorm = forward.normalized();
            let l = fnorm.dot(dir);
            let sideway = dir.sub(fnorm.mul(l));
            p0 = p0.add(sideway);
            dir = fnorm;
            dis = point.sub(p0).magnitude();
        } else {
            forward = point.sub(p0);
            dir = forward;
            let len2 = dir.len2();
            if (len2 <= 0.001) {
                return 0;
            }
            dis = Math.sqrt(len2);
            dir = dir.mul(1 / dis);
        }
        let [pRay, pTravel, hit] = this.runRay(p0, dir, dis, node);
        if (pRay.sub(point).len2() > 0.0001) {
            return 0;
        }
        return 1;
    }

    /**
     * 
     * @param {V3} p 
     * @param {SDFF} node 
     */
    snap_to_surface(p, node) {
        if (!DEBUG_N_ON_SURFACE) {
            return p;
        }
        let p2Local = node.inverse_transform.mulv(p);
        /** @type {SDFR} */
        let sdfi = node.func.do(p2Local);
        let norm = sdfi.norm;
        let l2 = norm.len2();
        if (l2 <= 0.00001) {
            return p;
        }
        norm = norm.mul(sdfi.d / Math.sqrt(l2));
        return p.sub(node.transform.muldir(norm));
    }

    /**
     * 
     * @param {V3} a 
     * @param {V3} b 
     * @param {float} k 
     * @param {StrokeInfo} stroke 
     * @param {SDFNode} node 
     */
    interpolate_point(a, b, k, stroke, node) {
        let p = V3.lerp(a, b, k);

        if (stroke.p0) {
            let p0 = stroke.p0;
            let r1 = a.sub(p0).len2();
            let plocal = p.sub(p0);
            let r2 = plocal.len2()
            if (r2 > 0.00001 && r1 > 0.00001) {
                p = plocal.mul(Math.sqrt(r1 / r2)).add(p0);
            }
        }

        if (node.func.curved) {
            p = this.snap_to_surface(p, node);
        }
        return p;
    }

    /**
     * 
     * @param {Scene} camera_info 
     * @param {Ob} shape 
     * @param {SDFNode} node 
     */
    clip_lines(camera_info, shape, node, style, invisible_style = null) {
        let result = new Ob();
        let p0 = camera_info.cameraToWorld(V(0, 0, 0));
        let ortho = camera_info.ortho;
        let forward = camera_info.cameraToWorld(V(0, 0, -1)).sub(p0);

        let show_invisible = false;

        if (invisible_style) {
            show_invisible = true;
        }

        for (let line of shape.lines) {
            let chain = null;
            let chain_invisible = null;

            let last_normal = null;
            let last_invisible = null;
            //TODO: camera clipping
            let addSegment = function (a, b, invisible, show_invisible = false) {
                let cur_style = invisible ? invisible_style : style;
                if (cur_style == null) {
                    chain = null;
                    last_normal = null;
                    chain_invisible = null;
                    last_invisible = null;
                    return;
                }
                let last = invisible ? last_invisible : last_normal;
                if (last && last.length > 0) {
                    last[last.length - 1] = b;
                    return;
                }
                let active_chain = invisible ? chain_invisible : chain;
                if (!active_chain) {
                    active_chain = new StrokeInfo([a]);
                    if (invisible) {
                        active_chain.style = invisible_style;
                        chain_invisible = active_chain;
                    } else {
                        active_chain.style = style;
                        chain = active_chain;
                    }
                    result.lines.push(active_chain);
                }
                active_chain.data.push(b);
                if (invisible) {
                    last_invisible = active_chain.data;
                    chain = null;
                    last_normal = null;
                } else {
                    last_normal = active_chain.data;
                    chain_invisible = null;
                    last_invisible = null;
                }
            }

            let points = line.data;
            for (let j = 1; j < points.length; j++) {
                let pa = points[j - 1];
                let pb = points[j];
                last_normal = last_invisible = null;

                let prev = pa;
                let prevOnLine = pa;
                let k = 1 / subdiv;
                let recalcSize = 0.26;
                let recalcStep = 0.0 - 0.001;
                for (let progress = 0; progress < 1; progress += k) {

                    let next = progress + k;
                    if (next > recalcStep) {
                        let sc1 = camera_info.mapWorldPoint(prevOnLine);
                        recalcStep += recalcSize;
                        let nextP = null
                        let nextScreen = null;

                        let target2 = this.SUBDIV_TARGET * this.SUBDIV_TARGET;
                        do {
                            k *= 0.5;
                            next = Math.min(progress + k, 1);
                            nextP = V3.lerp(pa, pb, next);
                            nextScreen = camera_info.mapWorldPoint(nextP);
                        } while (sc1.sub(nextScreen).changez(0).len2() > target2 && k > 0.01);
                        do {
                            k *= 2;
                            next = Math.min(progress + k, 1);
                            nextP = V3.lerp(pa, pb, next);
                            nextScreen = camera_info.mapWorldPoint(nextP)
                        } while (sc1.sub(nextScreen).changez(0).len2() < target2 && k < 1 - progress);
                        next = progress + k;
                        //console.log(`recalc ${k} ${sc1.sub(nextScreen).changez(0).magnitude()}`);
                    }
                    next = Math.min(next, 1);
                    prevOnLine = V3.lerp(pa, pb, next);
                    let p2 = this.interpolate_point(pa, pb, next, line, node);

                    if (node.func.curved || line.p0) {
                        last_normal = last_invisible = null;
                    }
                    // todo snap to surface
                    let r1 = this.clipReach(p0, prev, ortho, forward, true, node);
                    let r2 = this.clipReach(p0, p2, ortho, forward, true, node);
                    if (r1 > 0 && r2 > 0) {
                        addSegment(prev, p2, false);
                    } else if (subdivExtra && (r1 > 0) != (r2 > 0)) {
                        let [l, r] = [prev, p2];
                        let m = null;
                        for (let iter = 0; iter < 10; iter++) {
                            m = this.interpolate_point(l, r, 0.5, line, node);
                            let good = this.clipReach(p0, m, ortho, forward, true, node) > 0;
                            if ((r1 > 0) == good) {
                                l = m;
                            } else {
                                r = m;
                            }
                        }
                        if (r1 > 0) {
                            addSegment(prev, m, false);
                            if (r2 == 0) {
                                addSegment(m, p2, true, show_invisible);
                            } else {
                                chain = chain_invisible = last_normal = last_invisible = null;
                            }

                        } else {
                            if (r1 == 0) {
                                addSegment(prev, m, true, show_invisible);
                            } else {
                                chain = chain_invisible = last_normal = last_invisible = null;
                            }
                            addSegment(m, p2, false);
                        }
                    } else {
                        if (r1 == 0 && r2 == 0) {
                            addSegment(prev, p2, true, show_invisible);
                        } else {
                            chain = chain_invisible = last_normal = last_invisible = null;
                        }
                    }
                    prev = p2;
                }
            }
        }
        return result;
    }

    process(camera_info) {
        let o2 = [];
        let root = [];
        let combine = [];
        let primitive = [];

        function recursiveProc(x, transform, parent, sign) {
            let index = o2.length;
            let t2 = transform.mul(x.transform);
            if (x.primitive) {
                primitive.push(index);
            } else {
                combine.push(index);
            }

            let node = new SDFNode(parent);
            node.sign *= sign;
            node.func = x;
            node.transform = t2;

            if (x.textures !== undefined) {
                node.textures = x.textures;
            }
            if (x.line_style !== undefined) {
                node.line_style = x.line_style;
            }
            if (x.invisible_style !== undefined) {
                node.invisible_style = x.invisible_style;
            }
            if (x.detect_edges !== undefined) {
                node.detect_edges = x.detect_edges;
            }
            if (x.groot) {
                node.group = index;
            }

            node.index = index;
            o2.push(node);
            if (!x.primitive) {
                node.a = recursiveProc(x.a, t2, node, 1);
                let subSign = 1;
                if (x instanceof SDF2.Diff) {
                    subSign = -1;
                }
                node.b = recursiveProc(x.b, t2, node, subSign);
            }
            return index;
        }
        let identity_transform = new M4();
        let dummyParent = new SDFNode();
        for (let item of this.objs) {
            root.push(recursiveProc(item, identity_transform, dummyParent));
        }
        this.o2 = o2;
        this.root = root;
        this.combine = combine;
        this.primitive = primitive;
        for (let o of this.o2) {
            o.inverse_transform = o.transform.inverse();
        }
    }

    gridPos(i, j, camera_info) {
        let w = camera_info.w;
        let n = this.grid.length;
        return new V3((j * w / n) - 0.5 * w, (i * w / n) - 0.5 * w, 0);
    }

    /**
     * 
     * @param {Scene} camera_info 
     */
    *calcGrid(camera_info) {
        let c = camera_info.screenToWorld(new V3(0, 0, 1));
        let vy = camera_info.screenToWorld(new V3(0, 1, 1)).sub(c);
        let vx = camera_info.screenToWorld(new V3(1, 0, 1)).sub(c);

        let n = Math.floor(camera_info.w / this.grid_step);
        let res = [];
        for (let i = 0; i < n; i++) {
            let a = [];
            a.length = n;
            res.push(a);
        }
        this.grid = res;
        let forward = camera_info.camera_pos_inverse.muldir(new V3(0, 0, -1));
        let p0 = camera_info.cameraToWorld(new V3());
        let w = camera_info.w;

        /*let debug_points = [
            camera_info.screenToWorld(new V3(w*0.5, 0, 1)),
            camera_info.screenToWorld(new V3(0, w*0.5, 1)),
            camera_info.screenToWorld(new V3(-w*0.5, 0, 1)),
            camera_info.screenToWorld(new V3(0, -w*0.5, 1)),
        ];
        let debugOb = Ob.fromLoop(debug_points);
        camera_info.drawincremental(debugOb);*/

        for (let i = 0; i < n; i++) {
            for (let j = 0; j < n; j++) {
                let p = this.gridPos(i, j, camera_info);
                let worldP = c.add(vx.mul(p.x)).add(vy.mul(p.y));
                let f = forward;
                if (!camera_info.ortho) {
                    f = worldP.sub(p0).normalized();
                    worldP = p0;
                }
                let rayResult = this.runRay(worldP, f, this.MAX_DISTANCE, null);
                res[i][j] = rayResult;
            }
            console.log(`${i}/${n}`);
            yield null;
            
        }
        //camera_info.drawincremental(ob);
    }

    /**
     * 
     * @param {Scene} camera_info 
     */
    searchEdges(camera_info) {
        const DIR1 = [[0, 1], [1, 0]];
        let n = this.grid.length;

        let visited = new Uint8Array(n * n * 4);
        let depth = new Uint32Array(n * n * 4);
        let NEXT2 = [
            [[0, 0, 1], [0, 1, 1], [1, 0, 0],
            [-1, 0, 1], [-1, 1, 1], [-1, 0, 0]],
            [[0, 0, 0], [1, 0, 0], [0, 1, 1],
            [0, -1, 0], [1, -1, 0], [0, -1, 1]],
        ]

        function splitIndex(v) {
            let side = v % 4;
            v = Math.trunc(v / 4);
            let j = v % n;
            v = Math.trunc(v / n);
            let i = v;
            return [i, j, side];
        }
        function joinIndex(i, j, side) {
            return (((i * n) + j) * 4) + side;
        }
        function needEdge(hit1, hit2) {
            let o1 = hit1[2] ? hit1[2].obj : null;
            let o2 = hit2[2] ? hit2[2].obj : null;
            if (o1 == null) {
                [o1, o2] = [o2, o1];
                [hit1, hit2] = [hit2, hit1];
            }
            if (o1 == null) {
                return false;
            }
            if (o2 == null) {
                return (o1.detect_edges & (SDFF.DETECT_EDGE_EMPTY));
            }
            if (o1.detect_edges == 0 && o2.detect_edges == 0){ 
                return false;
            }
            let frontO1 = !(o1.detect_edges & SDFF.DETECT_EDGE_FRONT) || hit1[1] < hit2[1];
            let frontO2 = !(o2.detect_edges & SDFF.DETECT_EDGE_FRONT) || hit2[1] < hit1[1];
            let mask = 0;
            if (frontO1) {
                mask |= o1.detect_edges;
            }
            if (frontO2) {
                mask |= o2.detect_edges;
            }
            if (o1 != o2 && (mask & SDFF.DETECT_EDGE_SURFACE)) {
                return true;
            }
            if (o1.group == o2.group && (mask & SDFF.DETECT_EDGE_INTERNALG)) {
                return true;
            }
            if (o1.group != o2.group && (mask & SDFF.DETECT_EDGE_EXTERNALG)) {
                return true;
            }
            return false;
        }
        let context = this;
        function hasEdge(i, j, ni, nj) {
            return needEdge(context.grid[i][j], context.grid[ni][nj]);
        }

        let forward = camera_info.camera_pos_inverse.muldir(new V3(0, 0, -1));
        let p0 = camera_info.cameraToWorld(new V3());
        //let edges = [];
        let followPath = function (i, j, side, o1, o2) {
            let s = [];
            let i0 = joinIndex(i, j, side);
            if (visited[i0]) {
                return;
            }
            let startPos = context.grid[i][j][0];// todo 
            s.push([i0, 0, null])
            while (s.length > 0) {
                let [index, it, edgePos] = s[s.length - 1];
                let [ci, cj, cs] = splitIndex(index);
                if (it == 0) {
                    let hit1 = context.grid[ci][cj];
                    let i2 = ci + DIR1[cs][1];
                    let j2 = cj + DIR1[cs][0];
                    let hit2 = context.grid[i2][j2];

                    let p1World = camera_info.screenToWorld(context.gridPos(ci, cj, camera_info).changez(1));
                    let p2World = camera_info.screenToWorld(context.gridPos(i2, j2, camera_info).changez(1));

                    let left = p1World, right = p2World;
                    let hitleft = hit1;
                    let hitRight = hit2;
                    /*{ //DEBUG
                        let tmp = new Ob();
                        tmp.addLine(p1World, p2World);
                        camera_info.drawincremental(tmp);
                    }*/
                    for (let t = 0; t < context.EDGE_SEARCH_ITER; t++) {
                        let m = V3.lerp(left, right, 0.5);
                        let rayStart = m;
                        let f = forward;
                        if (!camera_info.ortho) {
                            f = rayStart.sub(p0).normalized();
                            rayStart = p0;
                        }
                        let rayResult = context.runRay(rayStart, f, context.MAX_DISTANCE, null);
                        if (needEdge(hit1, rayResult)) {
                            right = m;
                            hitRight = rayResult;
                        } else {
                            left = m;
                            hitleft = rayResult;
                        }
                    }

                    if (hitleft[1] < hitRight[1]) {
                        edgePos = hitleft[0];
                    } else {
                        edgePos = hitRight[0];
                    }
                    s[s.length - 1][2] = edgePos;
                    if (s.length > 1) {
                        let prevPos = s[s.length - 2][2];

                        let tmp = new Ob();
                        tmp.addLine(prevPos, edgePos);
                        camera_info.drawincremental(tmp);
                    }
                }

                if (it >= 6) {
                    s.pop();
                } else {

                    s[s.length - 1][1] = it + 1;

                    let diff = NEXT2[cs][it];
                    let ni = ci + diff[1];
                    let nj = cj + diff[0];
                    let ns = diff[2];
                    if (ni < 0 || nj < 0 || ni >= n || nj >= n) {
                        continue;
                    }
                    let nextIndex = joinIndex(ni, nj, ns);
                    let n2i = ni + DIR1[ns][1];
                    let n2j = nj + DIR1[ns][0];
                    if (n2i >= n || n2j >= n) {
                        continue;
                    }
                    //let ns = diff[2];
                    if (visited[nextIndex] ||
                        !hasEdge(ni, nj, n2i, n2j)
                    ) {
                        continue;
                    }
                    visited[nextIndex] = 1;
                    depth[nextIndex] = depth[index] + 1;

                    /*let pos1 = context.grid[ni][nj][0];
                    let pos2 = context.grid[n2i][n2j][0];


                    let posNew = V3.lerp(pos1, pos2, 0.5);
                    let tmp = new Ob();
                    tmp.addLine(prevp, posNew);
                    camera_info.drawincremental(tmp);*/
                    s.push([nextIndex, 0, null]);
                }
            }
        }

        for (let i = 0; i < n; i++) {
            for (let j = 0; j < n; j++) {
                let hit1 = this.grid[i][j];
                for (let side = 0; side < DIR1.length; side++) {
                    let tx = j + DIR1[side][0];
                    let ty = i + DIR1[side][1];
                    if (tx < 0 || ty < 0 || tx >= n || ty >= n) {
                        continue;
                    }
                    let hit2 = this.grid[ty][tx];
                    if (needEdge(hit1, hit2)) {
                        followPath(i, j, side);
                    }
                }
            }

        }
    }

    /**
     * 
     * @param {Scene} camera_info 
     * @returns {[any, [V3]]}
     */
    draw(camera_info) {
        let result = new Ob();
        for (let i of this.primitive) {
            let node = this.o2[i];
            let lines = node.func.get_lines(camera_info, node);
            if (lines) {
                lines.transform(node.transform);
                let clipped = this.clip_lines(camera_info, lines, node, node.line_style, node.invisible_style);
                result.addObMove(clipped);
            }

            let textures = node.textures;
            if (textures) {
                let textureLines = new Ob();
                for (let texture of textures) {
                    let text = node.func.get_texture(texture, node, camera_info);
                    if (text) {
                        textureLines.addObMove(text);
                    }
                }
                textureLines.transform(node.transform);
                let clipped = this.clip_lines(camera_info, textureLines, node, 1, null);
                result.addObMove(clipped);
            }
        }
        return result;
    }

    *drawIncremental(camera_info) {
        let t0 = Date.now();
        let t1 = t0;
        let tim = function () {
            let t2 = Date.now();
            //console.log(`time ${t2 - t1} ${t2 - t0}`);
            t1 = t2;
        }
        console.log(`t: ${Date.now() - t0}`);
        for (let i of this.primitive) {
            let node = this.o2[i];
            let lines = node.func.get_lines(camera_info, node);
            if (lines) {
                lines.transform(node.transform);
                let clipped = this.clip_lines(camera_info, lines, node, node.line_style, node.invisible_style);
                tim();
                yield clipped;
            }
        }
        for (let i of this.primitive) {
            let node = this.o2[i];
            let textures = node.textures;
            if (textures) {
                for (let texture of textures) {
                    let text = node.func.get_texture(texture, node, camera_info);
                    text.transform(node.transform);
                    let clipped = this.clip_lines(camera_info, text, node, 1, null);
                    //console.log(`sometext ${i}/${this.o2.length}`);
                    tim();
                    yield clipped;
                }
            }
        }
        if (this.enableGrid == 1) {
            for (let x of this.calcGrid(camera_info)) {
                yield x;
            }
            //this.calcGrid(camera_info);
            this.searchEdges(camera_info);
        }
    }

    /**
     * 
     * @param {Scene} scene 
     */
    draw_to_scene(scene) {
        let r = this.draw(scene);
        scene.addOb(r);
    }
}

function initlib() {
    this.V3 = V3;
    this.V = (x, y, z) => new V3(x, y, z);
    this.M4 = M4;
    this.Ob = Ob;
    this.Scene = Scene;
}
initlib();
if (typeof Canvas != 'undefined') {
    //console.log("init browser");
    Canvas.setpenopacity(1);
    turtle = new Turtle();
    init2();
} else {
    //console.log("init standalone");
    import('./testTurtle.js').then((mod) => {
        turtle = new mod.TestTurtle();
        init2();
        let i = 0;
        while (walk(i)) {
            ++i;
        }
        turtle.finishSVG();
    });

}
//init2();