3d cube tiling with premade tiles drawn in inkscape and some rules to improve the placement.
Log in to post a comment.
// https://turtletoy.net/turtle/0f50d67501 let turtle = null; let rows = 16; // min=1, max=256, step=1, let cols = 16; // min=1, max=256, step=1 let SIZE = 16; // min=1, max=256, step=0.1 let H = Math.sqrt(3)/2; let scene = null; // 0 5 3 //2 4 1 let DIR_SIDES = [ [0, 1], [3, 4], [2, 3], [5, 0], [4, 5], [1, 2] ]; let SIDE_DIRECTIONS = [ [0, 1, 3, 4], [0, 1, 3, 4], [0, 2, 3, 5], [0, 2, 3, 5], [1, 5, 4, 2], [1, 5, 4, 2] ]; let REVERSE_SIDE_DIRECTIONS = [ [0, 1, 2, 3, 4, 5], [0, 1,-1, 2, 3,-1], [0, 1,-1, 2, 3,-1], [0,-1, 1, 2,-1, 5], [0,-1, 1, 2,-1, 5], [-1,0, 3,-1, 2, 1], [-1,0, 3,-1, 2, 1], ] class Tile { constructor(d) { this.dir = d; /** @type {[Tile]} */ this.neighbours = []; this.neighbours[3] = null; /** @type {Rule} */ this.rule = null; this.mirrorVariant = false; } horizontal() { return this.dir < 2; } static OFFSETS = [ [0, 0], [0, 1], [0, 0], [H, -0.5], [0, 0], [H, -0.5], ]; offset() { let offset = Tile.OFFSETS[this.dir]; return V(offset[0], offset[1]); } canMirror() { return this.rule && this.rule.dirf == 1; } mirrored() { return this.dir >= 4 || this.mirrorVariant; } } class Cell { constructor(a, b, c) { /** @type {[Tile]} */ this.tiles = [a, b, c]; this.sides = []; for (let i=0; i<3; i++) { let tile = this.tiles[i]; for (const x of DIR_SIDES[tile.dir]) { this.sides[x] = tile; } } this.up = true; } } class TR { constructor(horizontal, ruleId=null, group=null){ this.horizontal = horizontal; this.ruleId = ruleId; this.group = group; } static contains(ids, idList){ let hasNegative = false; let hasPositive = false; for (const v of idList) { if (v < 0) { hasNegative = true; } else { hasPositive = true; } } for (const v of idList) { for (const id of ids) { if (-v == ids) { return false; } } } for (const v of idList) { if (v == ids) { return true; } } if (hasPositive) { return false; } return true; } /** * * @param {Tile} tile * @returns {boolean} */ matches(tile, tileRuleOverride) { let tileRule = tile.rule; if (tileRuleOverride) { tileRule = tileRuleOverride; } if (this.horizontal != null && this.horizontal != tile.horizontal()) { return false; } if (this.ruleId != null && tileRule && !(TR.contains([tileRule.id], this.ruleId))) { return false; } if (this.group != null && tileRule && !(TR.contains(tileRule.group, this.group))) { return false; } return true; } } class Rule { /** * * @param {TR} selfrule * @param {[TR]} neighbours * @param {string} path */ constructor(id, selfrule, neighbours, group, dirf, path) { this.id = id; this.selfrule = selfrule; this.neighbours = neighbours; this.group = group; this.dirf = dirf; /** @type {Ob} */ this.ob = Ob.fromD(path); } /** * * @param {Tile} tile * @param {TR} rule * @returns {boolean} */ checkTile(tile, rule, tileRuleOverride) { if (!tile || !rule) { return true; } return rule.matches(tile, tileRuleOverride); } /** * * @param {Tile} tile * @param {[Rule]} allrules * @returns {boolean} */ matches(tile, allrules, checkNeighbours=true) { if (!tile){ return false; } if (!this.checkTile(tile, this.selfrule)) { return false; } if (this.neighbours) { for (let i=0; i<4; i++) { if (!this.checkTile(tile.neighbours[i], this.neighbours[i])) { return false; } } } if (checkNeighbours) { for (let i=0; i<4; i++) { let neighbour = tile.neighbours[i]; if (!neighbour || !neighbour.rule) { continue; } let reverseRule = neighbour.rule; let dirf = SIDE_DIRECTIONS[tile.dir][i]; let reverseI = REVERSE_SIDE_DIRECTIONS[neighbour.dir][(dirf+3)%6]; if (!reverseRule.neighbours || !reverseRule.neighbours[reverseI]) { continue; } if (!this.checkTile(tile, reverseRule.neighbours[reverseI], this)) { return false; } } } return true; } } function cellSide(cell, side) { if (!cell) { return null; } return cell.sides[side]; } function circle(p, r) { let loop = []; const STEPS =32; for (let i=0; i<STEPS; i++) { let a = Math.PI * 2 * i / STEPS; let p2 = new V3(Math.sin(a), Math.cos(a)); loop.push(p2.mul(r).add(p)); } return Ob.fromLoop(loop); } function init2() { // Global code will be evaluated once. //const turtle = new Turtle(); if (typeof Canvas != 'undefined') { Canvas.setpenopacity(1); turtle = new Turtle(); } else { turtle = new TestTurtle(); } //new Rule(0, new TR(true), null, [],0, "{path}"), // {id} let RULES =[ new Rule(0, new TR(true), null, [], 0, "M 0,0 0.866,-0.5 0,-1 -0.866,-0.5 0,0"), // plain h new Rule(1, new TR(false), null, [], 0, "m 0,0 -0.866,-0.5 0,1 L 0,1 -0,0"), // plain v new Rule(2, new TR(false), [null, null, new TR(true, null, [-1]), null], [], 0, "M -0.577,0.667\nV 0\nL -0.289,0.167\nv 0.667\n\nM -0,0\nV 1\nL -0.866,0.5\nv -1\nL -0,0"), // door new Rule(3, new TR(false), null, [], 0,"M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.289,0.167 -0.577,0\nv 0.333\nl 0.289,0.167\nz\n\nm -0.144,-0.083 0,0.333"), // window new Rule(5, new TR(true), null, [1], 0,"m -0.866,-0.5\nc 0.577,-0.267 1.155,-0.267 1.732,0\n\nM 0,-1 0,0 0.866,-0.5 0,-1 -0.866,-0.5 0,0"), // curved new Rule(6, new TR(true), [null, null, new TR(false), new TR(false)], [], 0,"m -0.866,-0.5 0,-0.2\nL 0,-0.2 0.866,-0.7 0.866,-0.5\n\nM 0,-0.2 0,0 0.866,-0.5 0,-1 -0.866,-0.5 0,0\n\nm -0.693,-0.6\nv 0.2\n\nm 0.173,-0.1\nv 0.2\n\nm 0.173,-0.1 0,0.2\n\nm 0.173,-0.1\nv 0.2\n\nm 0.346,-0.2 0,0.2\n\nm 0.173,-0.3 0,0.2\n\nm 0.173,-0.3\nv 0.2\n\nm 0.173,-0.3 0,0.2"), // rails new Rule(7, new TR(true), [null, new TR(false, [2]), null, null], [], 0, "M 0,0 0.866,-0.5 0,-1 -0.866,-0.5 0,0\n\nm -0.26,-0.85 0.173,0.1 -0.346,0.2 -0.173,-0.1"), // doormat 1 new Rule(8, new TR(true), [new TR(false, [2]), null, null, null], [], 0, "M -0,0 -0.866,-0.5 -0,-1 0.866,-0.5 -0,0\n\nM 0.26,-0.85\nl -0.173,0.1 0.346,0.2 0.173,-0.1"), // doormat 0 new Rule(9, new TR(false), null, [], 0,"M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.289,0.167 -0.577,0\nV 0.333\nL -0.289,0.5\nZ\n\nM -0.433,0.083\nV 0.417\n\nM -0.289,0.333 -0.577,0.167"), // windowx new Rule(10, new TR(false), null, [], 0, "M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.722,0.283 -0.52,0.4\nV 0\nL -0.722,-0.117\nZ\n\nm 0.577,0.333\nV 0.217\nL -0.346,0.1\nv 0.4\nz"), // window2 new Rule(11, new TR(false), null, [], 0, "M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.144,0.617\nV 0.217\nL -0.346,0.1\nv 0.4\nz"), // window21 new Rule(12, new TR(false), null, [], 0, "M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.52,0.4\nV -0\nL -0.722,-0.117\nv 0.4\nz"), // window22 new Rule(13, new TR(false), [null, null, new TR(true, null, [-1]), null], 0, 0, "M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.144,0.617\nV 0.217\nL -0.346,0.1\nv 0.4\nz\n\nm -0.346,0.1 -0,-0.667 -0.289,-0.167 0,0.667"), // doors new Rule(14, new TR(false), [null, null, new TR(true, null, [-1]), null], 0, 0, "M -0,0 -0,1 -0.866,0.5 -0.866,-0.5 -0,0\n\nM -0.52,0.4\nV -0\nL -0.722,-0.117\nv 0.4\nz\n\nM -0.087,0.95\nl -0,-0.667 -0.289,-0.167 0,0.667"), // doors2 new Rule(15, new TR(true), null, [], 1, "m 0.067,-0.36 0.167,0.039\n\nm -0.137,-0.164 0.159,0.048\n\nm -0.134,-0.153 0.193,0.051\n\nM -0,0 0.866,-0.5 -0,-1 -0.866,-0.5 -0,0\n\nM 0.087,-0.15\nc 0.011,-0.015 0.115,-0.415 0.115,-0.415"), // antena new Rule(16, new TR(true), null, [], 1, "m -0.087,-0.55\nv -0.3\nl -0.173,0.1\nv 0.3\n\nm 0,0\nv 0.2\n\nm 0.173,0.1\nv -0.2\n\nm 0.173,-0.1\nv 0.2\n\nm -0.346,-0.2 0.173,-0.1 0.173,0.1 -0.173,0.1\nz\n\nM 0,0 0.866,-0.5 0,-1 -0.866,-0.5 0,0"), // chair1 new Rule(17, new TR(true), null, [], 1, "m 0.346,-0.5 -0.26,-0.15\n\nm -0.087,0.35 -0.087,0.15\n\nm 0.346,-0.3 0.087,0.05\n\nm -0.779,0.05 0.26,0.15 0.52,-0.3 0.087,-0.15 -0.26,-0.15 -0.087,0.15\nz\n\nM 0,-0 0.866,-0.5 0,-1 -0.866,-0.5 0,-0"), // chair_b new Rule(18, new TR(true), null, [], 1, "M 0,-0.9 -0.52,-0.6 0,-0.3 0.52,-0.6\nZ\n\nm 0,0.8 0.693,-0.4\nv -0.1\nl -0.693,0.4\n\nm -0.693,-0.3\nv -0.1\nL 0,-0.2\nv 0.1\nz\n\nM 0,-0.7 0.173,-0.6 0,-0.5 -0.173,-0.6\nZ\n\nM 0.433,-0.55 0,-0.8 -0.433,-0.55\n\nM 0,-0.8\nv -0.1\n\nm 0,0.9\nL 0.866,-0.5 0,-1 -0.866,-0.5 0,-0"), // hatch new Rule(19, new TR(true), null, [], 0, "m -0,-0.6\nv -0.1\n\nM 0.433,-0.35\nl -0.433,-0.25 -0.433,0.25\n\nm 0.953,-0.05\nL -0,-0.7 -0.52,-0.4\n\nm 1.126,-0.05\nL -0,-0.8 -0.606,-0.45\n\nM -0,-0.9\nv 0.1\n\nm 0,-0.1\nc 0,0 0.693,0.4 0.693,0.4\nL -0,-0.1 -0.693,-0.5\nZ\n\nm -0.866,0.4\nL -0,-0 0.866,-0.5 -0,-1\nZ"), // pit new Rule(20, new TR(true), null, [1], 0, "M 0 0 L 0.866025 -0.5 L 0 -1 L -0.866026 -0.5 L 0 0 M -0.866025 -0.5 L 4.09817e-08 -0.7 L 3.07363e-08 1.04636e-07 M -6.22566e-08 -1 L 4.09817e-08 -0.7 L 0.866025 -0.5"), // pointy2 new Rule(21, new TR(true), [null, null, null, new TR(true, [22])], [1],0, "m 0.866025 -0.5 l -0.866025 -0.5 l -0.866026 0.5 L 4.81963e-07 1.08172e-08 M 4.81963e-07 -1 L -0.173205 -0.8 L -0.866025 -0.5 m 1.47224 0.15 l -0.779423 -0.45"), // gable_tl new Rule(22, new TR(true), [null, new TR(true, [21]), null, null], [1],0, "M -0.866025 -0.5 L 3.14034e-07 -1.20982e-07 L 0.866025 -0.5 L 3.35859e-07 -1 M 2.92209e-07 -4.40957e-07 L 0.173205 -0.6 L 0.866025 -0.5 m -1.12583 -0.35 l 0.433013 0.25"), // gable_br new Rule(23, new TR(true), null, [],0, "m -0.566027 -0.635998 v 0.209203 l 0.566026 0.326795 l 0.566026 -0.326796 l 4e-06 -0.209202 M -1.75097e-07 -0.962793 L 0.566025 -0.635998 L -1.1751e-06 -0.309202 L -0.566026 -0.635998 Z M 0.369282 -0.578262 c -0.0552291 0.0318863 -0.144773 0.0318858 -0.200001 -1e-06 c -0.0552283 -0.0318862 -0.0552283 -0.0835838 0 -0.11547 c 0.0552285 -0.0318862 0.144771 -0.0318862 0.2 0 c 0.0552297 0.0318861 0.0552302 0.0835845 1e-06 0.115471 z m -0.269282 -0.15547 c -0.0552285 0.0318861 -0.144771 0.0318861 -0.2 0 c -0.0552297 -0.0318861 -0.0552302 -0.0835845 -1e-06 -0.115471 c 0.0552286 -0.0318865 0.144772 -0.0318865 0.200001 0 c 0.0552295 0.0318863 0.0552295 0.0835847 0 0.115471 z m -1e-06 0.31094 c -0.0552285 0.0318862 -0.144771 0.0318862 -0.2 0 c -0.0552295 -0.0318863 -0.0552295 -0.0835847 0 -0.115471 c 0.0552285 -0.0318862 0.144771 -0.0318862 0.2 0 c 0.0552295 0.0318863 0.0552295 0.0835847 0 0.115471 z m -0.269281 -0.15547 c -0.0552285 0.0318861 -0.144771 0.0318861 -0.2 0 c -0.0552297 -0.0318861 -0.0552302 -0.0835845 -1e-06 -0.115471 c 0.0552291 -0.0318863 0.144773 -0.0318858 0.200001 1e-06 c 0.0552283 0.0318862 0.0552283 0.0835838 0 0.11547 z M -1.1751e-06 -1 L 0.866025 -0.5 L -1.75097e-07 -1.43575e-07 L -0.866026 -0.5 Z"), // cooling_top new Rule(24, new TR(false), null, [],0, "m -0.54614 0.112128 c 0 0.06362 -0.0437775 0.0899184 -0.0977898 0.0587343 c -0.054005 -0.0311799 -0.09779 -0.108033 -0.09779 -0.171652 c 0 -0.06362 0.043786 -0.0899134 0.09779 -0.0587343 c 0.054005 0.0311799 0.0977898 0.108033 0.0977898 0.171652 z m -0.0977907 -0.363231 l -0.132823 0.0766799 v 0.3 l 0.433013 0.25 l 0.132823 -0.07668 v -0.3 z m -0.132823 0.0766799 l 0.43301 0.249999 v 0.3 m 3.46e-06 -0.299999 l 0.132823 -0.07668 M -1.38619e-06 -1.32693e-06 L -5.16186e-07 0.999999 L -0.866027 0.499999 v -1 L -1.38619e-06 -1.32693e-06 M -0.546137 0.521485 c 0 0.06362 -0.0437775 0.0899184 -0.0977898 0.0587343 c -0.054005 -0.0311799 -0.09779 -0.108033 -0.09779 -0.171652 c 0 -0.06362 0.043786 -0.0899134 0.09779 -0.0587343 c 0.054005 0.0311799 0.0977898 0.108033 0.0977898 0.171652 z m -0.135911 -0.34123 l -0.0947027 0.0546792 v 0.3 l 0.433013 0.25 l 0.132823 -0.07668 v -0.3 l -0.0947101 -0.0546859 m -0.471127 -0.118634 l 0.43301 0.249999 v 0.3 m 3.46e-06 -0.299999 l 0.132823 -0.07668 M -1.38619e-06 -1.32693e-06 L -5.16186e-07 0.999999 L -0.866027 0.499999 v -1 L -1.38619e-06 -1.32693e-06"), // aircon2 new Rule(25, new TR(false), null, [],0, "m -0.51183 0.315199 c 0 0.06362 -0.0437775 0.0899184 -0.0977898 0.0587343 c -0.054005 -0.0311799 -0.09779 -0.108033 -0.09779 -0.171652 c 0 -0.06362 0.043786 -0.0899134 0.09779 -0.0587343 c 0.054005 0.0311799 0.0977898 0.108033 0.0977898 0.171652 z m -0.0977907 -0.363231 l -0.132823 0.0766799 v 0.3 l 0.433013 0.25 l 0.132823 -0.07668 v -0.3 z m -0.132823 0.0766799 l 0.43301 0.249999 v 0.3 m 3.46e-06 -0.299999 l 0.132823 -0.07668 M -1.38619e-06 -1.32693e-06 L -5.16186e-07 0.999999 L -0.866027 0.499999 v -1 L -1.38619e-06 -1.32693e-06"), // aircon new Rule(26, new TR(false), [null, new TR(null, [-26]), new TR(false), new TR(null, [-26])], [],0, "M -0.173206 0.399999 V -1.37152e-06 M -0.433014 0.249999 v -0.4 m -0.0866025 0.45 l 0.0866021 -0.0500018 l 0.346411 0.200002 m -0.606218 -0.25 l -3.5e-07 -0.5 m -0.0866022 0.55 l 0.0866025 -0.05 l 0.606218 0.35 m -0.69282 -0.3 l -2.9e-07 -0.6 l 0.69282 0.4 l 2.5e-07 0.6 L -0.779424 0.249999 M -1.39066e-06 -1.32152e-06 L -5.20661e-07 0.999999 L -0.866027 0.499999 v -1 L -1.39066e-06 -1.32152e-06"), // balcony ] /** @type {[[Cell]]} */ let grid = []; for (let i=0; i<rows; i++) { grid[i] = []; for (let j=0; j<cols; j++) { let x = SIZE*(2*(j - (cols-0.5)*0.5) + (i % 2))*H; let y = SIZE*(i-rows*0.5)*1.5; turtle.jump(x, y-SIZE); turtle.setheading(30); turtle.pendown(); for (let k=0; k<6; k++) { /*turtle.forward(SIZE); turtle.right(60);*/ } turtle.jump(x, y); let cell = null; if (Math.random() > 0.5) { turtle.setheading(90); cell = new Cell(new Tile(0), new Tile(2), new Tile(4)); } else { turtle.setheading(270); cell = new Cell(new Tile(1), new Tile(3), new Tile(5)); cell.up = false; } grid[i][j] = cell; for (let k=0; k<3;k++) { /*turtle.pendown(); turtle.forward(SIZE); turtle.penup(); turtle.backward(SIZE); turtle.right(120);*/ } //turtle.goto(x) } } for (let i=0; i<rows; i++) { for (let j=0; j<cols; j++) { let cell = grid[i][j]; let dpos = null; if (i%2) { dpos = [[1, -1], [0, -1], [-1, 0], [0,1], [1,1], [1, 0]]; } else { dpos = [[0, -1], [-1, -1], [-1, 0], [-1,1], [0, 1], [1, 0]]; } let neighbours = []; for (let k=0; k<6; k++) { let tj = dpos[k][0] + j; let ti = dpos[k][1] + i; if (tj >= 0 && tj < cols && ti >= 0 && ti < rows) { neighbours[k] = grid[ti][tj]; } else { neighbours[k] = null; } } const tiles = cell.tiles; if (cell.up) { tiles[0].neighbours = [cellSide(neighbours[0], 3), cellSide(neighbours[1], 4), tiles[1], tiles[2]]; tiles[1].neighbours = [tiles[0], cellSide(neighbours[2], 5), cellSide(neighbours[3], 0), tiles[2]]; tiles[2].neighbours = [tiles[0], cellSide(neighbours[5], 2), cellSide(neighbours[4], 1), tiles[1]]; } else { tiles[0].neighbours = [tiles[1], tiles[2], cellSide(neighbours[3], 0), cellSide(neighbours[4], 1)]; tiles[1].neighbours = [cellSide(neighbours[0], 3), tiles[1], tiles[0], cellSide(neighbours[5], 2)]; tiles[2].neighbours = [cellSide(neighbours[1], 4), tiles[2], tiles[0], cellSide(neighbours[2], 5)]; } } } scene = new Scene(); scene.setOrthographic(1, V(0,0,-1), V(0, 0, 0)); scene.camera_pos = Scene.worldCameraOrbit(new V3(0, 0, 0), 1, 90, -90) //let c = Ob.fromD("m 11.405754,6.8344004 c -0.633653,1.1767841 -1.108893,1.1541535 -1.108893,1.1541535 0,0 1.001399,0.1980164 1.041002,1.3578279 C 11.296318,8.1747642 11.649496,8.2493774 12.339261,7.8075102 10.7916,8.346872 11.405754,6.8344004 11.405754,6.8344004 Z M 6.5175738,13.397235 C 11.974015,14.003528 12.808843,9.9347738 12.808843,9.9347738 L 12.627799,5.1144849 c 0,0 -4.0734834,-3.8924398 -5.657616,-2.9872212 C 5.3860506,3.0324821 5.2502678,6.291269 3.5303526,5.8160295 1.8104372,5.3407897 1.6293934,8.8711422 3.3040478,8.8485116 c 1.6746543,-0.02263 1.4483498,1.7878064 1.4483498,1.7878064 0,0 -0.6110226,3.552983 0.7241749,2.693026 1.3351973,-0.859958 0.7864086,0.0396 0.7864086,0.0396 z M 8.3506419,7.9659233 c 0,0.5124369 -0.3546201,0.9278491 -0.7920663,0.9278491 -0.4374461,-10e-8 -0.7920662,-0.4154122 -0.7920662,-0.9278491 0,-0.5124368 0.3546201,-0.9278489 0.7920662,-0.927849 0.4374461,0 0.7920663,0.4154121 0.7920663,0.927849 z M 10.786604,10.268659 8.2007462,9.8006287 6.3871012,11.897482 5.9894294,9.0256879 3.6306331,7.7608978 5.9707166,6.4540617 6.3265453,3.5755249 8.1704682,5.63965 10.749179,5.1254066 9.5487022,7.7079421 Z"); //c.transform(M4.scale(3)); //scene.addOb(c); for (let row of grid) { for (let cell of row) { for (let tile of cell.tiles) { if (!tile.rule ) { let candidates = []; for (let i=0; i<RULES.length; i++) { if (RULES[i].matches(tile)) { candidates.push(i); } } if (candidates.length > 0) { let k = Math.min(Math.floor(Math.random() * candidates.length), candidates.length - 1); tile.rule = RULES[candidates[k]]; } } if (tile.canMirror()) { tile.mirrorVariant = Math.random() > 0.5; } } } } // fallback for (let row of grid) { for (let cell of row) { for (let tile of cell.tiles) { if (!tile.rule ) { for (let i=0; i<RULES.length; i++) { if (RULES[i].matches(tile, RULES, false)) { tile.rule = RULES[i]; break; } } } } } } for (let i=0; i<rows; i++) { for (let j=0; j<cols; j++) { let x = SIZE*(2*(j - (cols-0.5)*0.5) + (i % 2))*H; let y = SIZE*(i-rows*0.5)*1.5; let pos = V(x, y); for (let tile of grid[i][j].tiles) { if (tile.rule) { let rule = tile.rule; let transform = new M4(); transform = transform.mul(M4.translate(pos)).mul(M4.scale(SIZE)); if (tile.mirrored()) { transform = transform.mul(M4.scale3(-1, 1)); } transform = transform.mul(M4.translate(tile.offset())); scene.addOb(rule.ob.transformed(transform)); } } } } scene.draw(); } // The walk function will be called until it returns false. function walk(i) { return false; } 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); } 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)); } dot(b) { return this.scale(b).xyzs(); } rotDeg(deg) { const a = deg*Math.PI/180; const s = Math.sin(a); const c = Math.cos(a); return new V2(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]]; } } 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]); } static translate(v) { let r = new M4(); r.d[0][3] = v.x; r.d[1][3] = v.y; r.d[2][3] = v.z; return r; } 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[r][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 = 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(); } get camera_pos() { return this._camera_pos; } addLine(a,b) { this.lines.push([a, b]); } addOb(x) { this.lines.push(...x.lines); } 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.x, p.y]; } draw() { let lastPoint = null; turtle.pendown(); this.lines.forEach((l) => { let debug=false; 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 (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) { let dx = lastPoint[0] - p1[0]; let dy = lastPoint[1] - p1[1]; connected = (dx*dx+dy*dy) < 0.00001; } if (!connected) { turtle.penup(); turtle.jump(p1) turtle.pendown(); } turtle.goto(p2); 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 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); } } } screenToWorld(p) { // TODO: missing camera<->screen conversion return this.camera_pos_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 Ob { constructor(linesa=[]){ this.lines=[]; if (linesa) { this.addLines(linesa); } } static fromChain(points) { let res = new Ob(); for (let i=1; i<points.length; i++) { res.addLine(points[i-1], points[i]); } return res; } static fromLoop(points) { let res = Ob.fromChain(points); if (points.length > 1) { res.addLine(points[points.length-1], points[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); } /** * * @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.addOb(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.addOb(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) { this.lines.push([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.addLine(line[0], line[1]); } } transform(m) { this.lines.forEach((v, i) => { this.lines[i] = [m.mulv(v[0]), m.mulv(v[1])]; }); } transformed(m) { let r = new Ob(); r.addOb(this); r.transform(m); return r; } } class SDF { static bind1(f) { return function(...x) { return f.bind(null, ...x); } } // combinations static unionD(a, b, x) { return Math.min(a(x), b(x)); } static union(a, b) { return SDF.unionD.bind(null, a, b); } static diffD(a, b, x) { return Math.max(a(x), -b(x)); } static diff(a, b) { return SDF.diffD.bind(null, a, b); } static intersectionD(a, b, x) { return Math.max(a(x), b(x)); } static intersection(a, b) { return SDF.intersectionD.bind(null, a, b); } static xorD(a, b, x) { let d1=a(x), d2=b(x); return Math.max(min(d1, d2), -max(d1, d2)); } static xor(a, b) { return SDF.xorD.bind(null, a, b); } static transformD(f, t, x) { return f(t.mulv(x)) } static moveD(f, p, x) { return f(x.sub(p)); } static move(f, p) { return SDF.moveD.bind(null, f, p); } static rotateD(f, euler, x) { return f(M4.euler(Util.radians(euler.x), Util.radians(euler.y), Util.radians(euler.z)).mulv(x)); } static rotate(f, euler) { return SDF.transformD.bind(null, f, M4.euler(Util.radians(euler.x), Util.radians(euler.y), Util.radians(euler.z))); } // primitives static sphereD(p, r, x) { let dv = x.sub(p); return dv.magnitude() - r; } static sphere(p, r) { //SDF.bind1(SDF.sphereD); return SDF.sphereD.bind(null, p, r); } static boxD(s, x) { let q = x.abs().sub(s); return q.maxK(0).magnitude() + Math.min(q.xyzMax(), 0); } static box = SDF.bind1(SDF.boxD); static runRay(f, p0, dir, limit) { let travel = 0; let p = p0; while (travel < limit) { let distance = f(p); if (distance < 0.001) { break; } distance = Math.min(limit-travel, distance); p = p.add(dir.mul(distance)); travel += distance; } return [p, travel]; } static clipReach(f, p0, point, mustBeOnSurface) { let dis = point.sub(p0).magnitude(); if (mustBeOnSurface && Math.abs(f(point)) > 0.001) { return false; } let dir = point.sub(p0); let len2 = dir.len2(); if (len2 <= 0.001) { return false; } dir = dir.mul(1/Math.sqrt(len2)); let [pRay, pTravel] = SDF.runRay(f, p0, dir, dis); if (pRay.sub(point).len2() > 0.0001) { return false; } return true; } static clipLines(f, camera_info, lines, subdiv=1, subdivExtra=true) { let result = []; let p0 = camera_info.screenToWorld(V(0, 0, 0)); for (let line of lines) { let prev = line[0]; for (let i=1; i<=subdiv; i++) { let p2 = V3.lerp(line[0], line[1], i/subdiv); let r1 = SDF.clipReach(f, p0, prev, false); let r2 = SDF.clipReach(f, p0, p2, false); if (r1 && r2) { result.push([prev, p2]); } else if (subdivExtra && r1 != r2) { let [l, r] = [prev, p2]; for (let iter=0; iter<10; iter++) { let m = l.add(r).mul(0.5); let good = SDF.clipReach(f, p0, m, false); if (r1 == good) { l = m; } else { r = m; } } if (r1) { result.push([prev, l]); } else { result.push([l, p2]); } } prev = p2; } } return result; } } class TestTurtle { constructor() {} jump(x, y) { } pendown(){} penup() {} forward(x) {} backward(x) {} right(x) {} setheading(x) {} goto(x, y){} } function initlib() { this.V3 = V3; this.V = (x,y,z)=>new V3(x, y, z); this.M4 = M4; this.Ob = Ob; this.Scene = Scene; this.SDF = SDF; } initlib(); init2();