grid-apps-cmms/web/kiri/gyroid-test.html
2020-05-05 11:56:34 -04:00

381 lines
13 KiB
HTML

<html>
<head>
<meta charset="UTF-8">
<meta name="copyright" content="stewart allen [sa@grid.space]">
<meta name="description" content="gyroid infill code playground">
<meta name="keywords" content="gyroid,infill" />
<meta name="author" content="Stewart Allen">
<meta name="robots" content="noindex, nofollow">
<meta property="og:description" content="algorithm testing sandbox">
<meta property="og:title" content="gyroid infill playground">
<meta property="og:type" content="website">
<meta property="og:url" content="https://grid.space/kiri/gyroid-test.html">
<title>gyroid testing</title>
<style>
body {
display: flex;
flex-direction: row;
justify-content: center;
}
#test {
display: flex;
flex-direction: column;
justify-content: center;
align-items: stretch;
}
#gyroid > div {
display: flex;
flex-direction: row;
}
#gyroid > div > div {
min-width: 3px;
min-height: 3px;
}
#stats {
text-align: center;
}
#stats > input {
pointer-events: none;
outline: none;
width: 5em;
}
svg {
border: 1px solid #888;
}
</style>
<script>
class Timer {
constructor() {
this.time = 0;
this.times = {};
this.order = [];
this.start();
}
start() {
this.time = Date.now();
}
mark(label) {
let newtime = Date.now();
let oldval = this.times[label] || 0;
if (oldval === 0) {
this.order.push(label);
}
this.times[label] = oldval + (newtime - this.time);
this.time = newtime;
}
toString() {
return this.order.map(l => `${l}:${this.times[l]}`);
}
}
class Map {
constructor(x,y,type) {
this.x = x;
this.y = y;
switch (type) {
case 'f': this.map = new Float32Array(x*y); break;
case 'i': this.map = new Int32Array(x*y); break;
case 'c': this.map = new Uint8Array(x*y); break;
default: throw `invalid type: ${type}`;
}
}
pos(x,y) {
return x+y*this.x;
}
get(x,y) {
return this.map[x+y*this.x];
}
put(x,y,v) {
this.map[x+y*this.x] = v;
}
}
let PI2 = Math.PI * 2;
let rez = 200;
let inc = PI2 / rez;
let zcache = {};
let timer = new Timer();
let maxdist = rez * 0.01;
function $(id) {
return document.getElementById(id);
}
function init() {
$('zval').max = rez;
for (let z=0; z<PI2; z += inc) {
generate(z);
}
console.log(timer.toString());
render(0);
}
function update() {
let z = $('zval').value;
render((z / rez) * PI2);
}
function generate(z) {
let zkey = z.toFixed(8);
let cached = zcache[zkey];
if (cached) {
return cached;
}
timer.start();
let size = ((PI2 / inc)|0) + 1;
let edge = new Map(size,size,'c');
let vals = new Map(size,size,'f');
let points = 0;
let points_lr = 0;
let points_td = 0;
let tip = 0.2;
let xv = 0;
for (let x=0; x<size; x++) {
let yv = 0;
for (let y=0; y<size; y++) {
vals.put(x,y,
Math.sin(xv) * Math.cos(yv) +
Math.sin(yv) * Math.cos(z) +
Math.sin(z) * Math.cos(xv)
);
yv += inc;
}
xv += inc;
}
timer.mark('points');
// left-right threshold search (red)
for (let x=1; x<size; x++) {
for (let y=0; y<size; y++) {
let v0 = vals.get(x-1,y);
let v1 = vals.get(x,y);
if (
(v0 <= tip && v1 >= tip) || (v0 >= tip && v1 <= tip) ||
(v0 <= -tip && v1 >= -tip) || (v0 >= -tip && v1 <= -tip)
) {
// if ((v0 <= 0 && v1 >= 0) || (v0 >= 0 && v1 <= 0)) {
edge.put(x,y,1);
points++;
points_lr++;
}
}
}
timer.mark('gen_red');
// top-down threshold search (green)
for (let y=1; y<size; y++) {
for (let x=0; x<size; x++) {
let v0 = vals.get(x,y-1);
let v1 = vals.get(x,y);
if (
(v0 <= tip && v1 >= tip) || (v0 >= tip && v1 <= tip) ||
(v0 <= -tip && v1 >= -tip) || (v0 >= -tip && v1 <= -tip)
) {
// if ((v0 <= 0 && v1 >= 0) || (v0 >= 0 && v1 <= 0)) {
if (edge.get(x,y)) {
edge.put(x,y,3);
} else {
edge.put(x,y,2);
points++;
}
points_td++;
}
}
}
timer.mark('gen_grn');
// deterime prevailing direction for chaining
let dir = points_td > points_lr ? 'lr' : 'td';
// create sparse representation
let sparse = [];
let center = rez / 2;
let edgev = size-1;
for (let x=0; x<size; x++) {
for (let y=0; y<size; y++) {
let val = edge.get(x,y);
if (val) {
let dx = Math.abs(x - center);
let dy = Math.abs(y - center);
sparse.push({
x, y, val,
dist: Math.max(dx,dy),
edge: (x === 0 || y === 0 || x === edgev || y === edgev)
});
}
}
}
// order points farthest from center (edges, in other words)
sparse.sort((a,b) => {
return b.dist - a.dist;
});
timer.mark('sparse');
// preserve sparse points for rendering
let raw = sparse.slice();
// join sparse points array by closest distance
let polys = [];
let chain;
let added;
let cleared = 0;
do {
for (let i=0; i<sparse.length; i++) {
// find the unclaimed point farthest
// from center and start a chain
if (sparse[i]) {
chain = [ sparse[i] ];
polys.push(chain);
sparse[i] = null;
cleared++;
break;
}
}
do {
added = false;
let target = chain[chain.length - 1];
let cl_elm = null; // candidate closest element
let cl_idx = null; // candidate closest index
let cl_dst = Infinity; // candidate distance
// look for next closest point to end of chain
for (let i=0; i<sparse.length; i++) {
let test_el = sparse[i];
if (test_el) {
let dst = dist(target, test_el, dir);
if (cl_idx === null || dst < cl_dst) {
cl_idx = i;
cl_elm = test_el;
cl_dst = dst;
}
}
}
if (cl_elm) {
// if (target.edge && !cl_elm.edge) {
// break;
// }
if (cl_dst > maxdist) {
break;
}
sparse[cl_idx] = null;
cleared++;
chain.push(cl_elm);
added = true;
}
} while (added);
} while (cleared < sparse.length);
timer.mark('chain');
// simplify poly
polys = polys.map(poly => filter(poly));
return zcache[zkey] = { raw, points, dir, polys };
}
function filter(poly) {
if (poly.length <= 2) {
return poly;
}
let nuchain = [ poly[0] ];
let e1 = poly[1];
let e2 = null;
let last = poly.length - 2;
for (let i=1; i<poly.length; i++) {
let el = poly[i];
if (e1.x === el.x || e1.y === el.y) {
e2 = el;
if (i < last) {
continue;
}
}
if (e2) {
nuchain.push({x:(e1.x + e2.x)/2, y:(e1.y + e2.y)/2});
e2 = null;
} else {
nuchain.push(e1);
if (i === last) {
nuchain.push(el);
}
}
e1 = el;
}
nuchain.push(poly[poly.length-1]);
return nuchain;
}
function dist(a, b, dir) {
let dx = a.x - b.x;
let dy = a.y - b.y;
// bias distance by prevailing direction of discovery to join stragglers
if (dir === 'lr') dx = dx / 2;
if (dir === 'td') dy = dy / 2;
return Math.sqrt(dx * dx + dy * dy);
}
function render(z) {
let size = 600;
let wh = size / rez;
let html = [`<svg width=${size} height=${size}>`];
let { raw, points, dir, polys } = generate(z);
$('points').value = points;
$('direction').value = dir;
$('polys').value = polys.length;
// raw threshold points
html.push('<g>');
raw.forEach(point => {
let x = point.x;
let y = point.y;
let color = ['black','#f00a','#0f0a','#00fa'][point.val];
html.push(`<rect dist="${point.dist}" x="${x*wh}" y="${y*wh}" width="${wh}" height="${wh}" style="fill:${color};stroke-width:0"/>`);
})
// polys.forEach(poly => {
// poly.forEach(point => {
// let x = point.x;
// let y = point.y;
// let color = ['black','#f00a','#0f0a','#00fa'][point.val];
// html.push(`<rect dist="${point.dist}" x="${x*wh}" y="${y*wh}" width="${wh}" height="${wh}" style="fill:${color};stroke-width:0"/>`);
// });
// });
html.push('</g>');
// points joined into poly lines
html.push('<g>');
polys.forEach(poly => {
let points = poly
.map(point => [point.x * wh + wh / 2, point.y * wh + wh / 2]
.join(',')).join(' ');
let x = poly[0].x - 1;
let y = poly[0].y - 1;
html.push(`<rect x="${x*wh}" y="${y*wh}" width="${wh*3}" height="${wh*3}" style="fill:#f0f5;stroke-width:0"/>`);
html.push(`<polyline points="${points}"/ fill="none" stroke="black" />`);
});
html.push('</g>');
html.push('</svg>');
$('gyroid').innerHTML = html.join('');
}
</script>
</head>
<body onload="init()">
<div id="test">
<div id="gyroid"></div>
<input id="zval" type="range" min="0" max="200" value="0" oninput="update()"/>
<div id="stats">
<input id="points"></input>
<input id="direction"></input>
<input id="polys"></input>
</div>
</div>
</body>
</html>