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();