This block is a recreation which allows me to continue my journey into impossible geometries, and allows you to create some of them.
Just remember:
In this experimentation, when adding a cube, it automatically expands to the closest ones in each 6 directions (x/-x (right/left), y/-y (bottom left/top right) and z/-z (top left/bottom right)). Expanding means: add intermediate cubes in a straight line between 2 user-defined cubes.
In order to draw impossible geometries, all user-defined cubes and its 6 immediate neighbor cubes (i.e. distant by 1 unit) preserve local consistency: they are drawn so that they form a valid 3D object. But expansions between user-defined cubes introduce global inconsistency: all expansions are drawn behind user-defined cubes, so that the drawing precedence is broken, leading to an impossible geometry.
The input text field allows you to define a set of cubes via a mini-language:
If a particular drawing is appealing to you, you can temporarily store it by clicking the Import geometries from UI link. This will store the currently drawn user-defined cubes into the input text field using the mini-language. For longer storage, you can copy/paste between your favorite text editor and the input text field.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<title>Impossible Geometry Builder</title>
<meta content="GUI to build Impossible Figures" name="description">
</head>
<body>
<style>
#chart {
position: fixed;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
}
#wip {
display: none;
position: absolute;
top: 200px;
left: 330px;
font-size: 40px;
text-align: center;
}
input#path {
position: absolute;
bottom: 20px;
left: 5px;
width: calc(100% - 110px);
color: lightgrey;
height: 20px;
border-radius: 5px;
border-width: 1px;
border-style: solid
}
input#path:focus {
outline-color: lightgrey;
}
input#path:focus.invalid {
outline-color: red;
}
#actions {
display: flex;
justify-content: space-between;
width: 100%;
position: absolute;
bottom: 5px;
left: 5px;
color: lightgrey;
font-size: 11px;
font-family: system-ui;
font-weight: 400;
}
#import-geometries, #grid-visibility {
text-decoration: underline;
cursor: pointer;
background-color: white;
}
#grid-visibility-container {
width: 110px;
text-align: center;
}
#grid-pattern {
stroke: black;
stroke-width: 1px;
stroke-opacity: 0.1;
fill: black;
fill-opacity: 0.1;
}
#grid-lines {
fill: url(#grid-pattern);
}
#grid-lines.hide {
display: none;
}
#background-hoverer {
fill: transparent;
}
#axes #background {
fill: white;
stroke: lightgrey;
stroke-width: 1px;
}
#axes .axe {
fill: none;
stroke: lightgrey;
stroke-width: 1px;
}
#axes .axe.clickable {
cursor: pointer;
}
#axes .label {
fill: lightgrey;
}
#axes .label.clickable {
cursor: pointer;
}
#axes .axe.allowed {
stroke: grey;
}
#axes .label.allowed {
fill: grey;
}
#closest-to-mouse {
fill: transparent;
fill-opacity: 0.2;
stroke: transparent;
}
#closest-to-mouse.on-grid {
fill: limegreen;
stroke: limegreen;
}
#closest-to-mouse.on-cube {
fill: none;
stroke: none;
}
#closest-to-mouse .distance-to-closest {
text-anchor: middle;
fill-opacity: 1;
stroke: none;
stroke-width: 1px;
paint-order: stroke;
}
#closest-to-mouse.on-cube .distance-to-closest {
fill: none;
stroke: none;
}
.cube, .expansion {
stroke-width: 1px;
stroke: transparent;
stroke-linejoin: bevel;
}
.cube-hoverer .contour {
fill: transparent;
}
.cube-hoverer .contour.covered {
fill: none;
stroke: none;
stroke-dasharray: 2 4;
}
.cube-hoverer.selected .contour,
.cube-hoverer.selected .contour.covered,
.cube-hoverer.active .contour,
.cube-hoverer.active .contour.covered {
stroke: limegreen;
}
.cube-hoverer.selected .contour,
.cube-hoverer.selected .contour.covered {
stroke-width: 1.5px;
}
.face.f0 {
fill: lightgray;
stroke: lightgray;
}
.face.f1 {
fill: darkgray;
stroke: darkgray;
}
.face.f2 {
fill: black;
stroke: black;
}
</style>
<div id="chart">
<svg>
<defs>
<pattern id="grid-pattern" patternUnits="userSpaceOnUse">
</pattern>
</defs>
</svg>
<input type="text" id="path" value="Write your path, such as 'M27,5Y9X9 M30,11'" onkeyup="inputed()">
<div id="actions">
<a id="import-geometries" onclick="importGeometries()">Import geometries from UI</a>
<span id="grid-visibility-container">
<a id="grid-visibility" onclick="gridVisibilityUpdate(this)">Hide grid</a>
</span>
</div>
​
<div id="wip">
Work in progress ...
</div>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
//begin: constants
const _PI = Math.PI,
_2PI = 2*Math.PI,
halfPI = Math.PI/2,
round = Math.round,
abs = Math.abs,
cos = Math.cos,
sin = Math.sin,
tan = Math.tan,
cos60 = Math.cos(_PI/3),
sin60 = Math.sin(_PI/3),
tan60 = tan(_PI/3),
cos120 = cos(_2PI/3),
sin120 = sin(_2PI/3),
tan120 = tan(_2PI/3),
cos240 = cos(_2PI/3*2),
sin240 = sin(_2PI/3*2),
tan240 = tan(_2PI/3*2);
//end: constants
//begin: layout config
let width = 960,
height = 500;
const gridLength = 20,
axeLength = 20;
//end: layout config
//begin: variables
let showGrid = true;
let userInput = "";
let cubes = []; // user defined cubes
let nextCubeId = 0; // store id for next user-defined cube
let expansions = []; // computed expansions/lines between cubes
let closestToMouseData = { // related to closest oblique coord from mouse
coord: [0,0],
closests: {}
};
let selectedCube; // references the selected cube
let hoveredCube; // references the hovered cube
//end:
//begin: reusable d3-selections
let svg, cubeContainer, expansionContainer, axes, closestToMouse, input, gridLines, cubeHovererContainer;
//end: reusable d3-selections
/* Oblique coordinate system
. y axe goes to bottom, as the SVG coordinate system does
. x+y+z=0
. z axe is optional (x+y=-z)
z
\
\____ x
/
/
y
*/
//begin: utils mapping triangle to orthogonal coordinate systems
const orthoCoord = function(obliqueCoord){
// x = u + v*cos(120)
// y = v*sin(120)
return [obliqueCoord[0]+obliqueCoord[1]*cos120, obliqueCoord[1]*sin120];
};
const orthoCoords = function(obliqueCoords) {
return obliqueCoords.map(function(obliqueCoord) {
return orthoCoord(obliqueCoord);
})
};
const scaledOrthoCoord = function (obliqueCoord){
const coord = orthoCoord(obliqueCoord)
return [coord[0]*gridLength, coord[1]*gridLength];
};
const scaledOrthoCoords = function (obliqueCoords){
return obliqueCoords.map(function(obliqueCoord) {
return scaledOrthoCoord(obliqueCoord);
})
};
const openPath = function (obliqueCoords) {
return d3.line()(scaledOrthoCoords(obliqueCoords));
};
const closedPath = function (obliqueCoords) {
return openPath(obliqueCoords)+"z";
}
//end: utils mapping triangle to orthogonal coordinate systems
//end: utils mapping orthogonal to triangle coordinate systems
const obliqueCoord = function(orthoCoord){
// u = x - y/tan(120)
// v = y/sin(120)
return [orthoCoord[0]-orthoCoord[1]/tan120, orthoCoord[1]/sin120];
};
const downScaledObliqueCoord = function(orthoCoord){
const coord = [orthoCoord[0]/gridLength, orthoCoord[1]/gridLength];
return obliqueCoord(coord);
};
//end: utils mapping orthogonal to triangle coordinate systems
/* Cube with its verteces and faces
coord of a cube is the center 'c' of the hexagone
distance from 'c' to other 6 verteces is 1
+z_______ -y +z_______ -y
/ /\ /\ /\
/ f2 / \ / \hf21/ \
/ / \ /hf20\ /hf00\
-x /_______/ f0 \ +x -x /______\/______\ +x
\ c\ / \ /\ /
\ \ / \hf11/ \hf01/
\ f1 \ / \ /hf10\ /
\_______\/ \/______\/
+y -z +y -z
*/
const c=[0,0],cPlusX=[1,0],cMinusZ=[1,1],cPlusY=[0,1],cMinusX=[-1,0],cPlusZ=[-1,-1],cMinusY=[0,-1];
const defaultContour = [cPlusX,cMinusZ,cPlusY,cMinusX,cPlusZ,cMinusY];
const f0 = {verteces: [c, cMinusY, cPlusX, cMinusZ], type: "f0"},
f1 = {verteces: [c, cMinusZ, cPlusY, cMinusX], type: "f1"},
f2 = {verteces: [c, cMinusX, cPlusZ, cMinusY], type: "f2"};
const hf00 = {verteces: [c, cMinusY, cPlusX], type: "f0"},
hf01 = {verteces: [c, cPlusX, cMinusZ], type: "f0"},
hf10 = {verteces: [c, cMinusZ, cPlusY], type: "f1"},
hf11 = {verteces: [c, cPlusY, cMinusX], type: "f1"},
hf20 = {verteces: [c, cMinusX, cPlusZ], type: "f2"},
hf21 = {verteces: [c, cPlusZ, cMinusY], type: "f2"};
//end: utils
initLayout();
// Reinit layout whenever the browser window is resized.
window.addEventListener("resize", reinitLayout);
/***********************/
/* Mouse Interaction */
/***********************/
function backgroundEntered(){
closestToMouse.classed("on-grid", true);
};
function backgroundExited(){
closestToMouse.classed("on-grid", false);
};
function backgroundHovered(){
const oldClosestToMouseData = closestToMouseData;
//transform orthoCoord to obliqueCoord
const coord = downScaledObliqueCoord(d3.mouse(this));
computeClosestToMouseData(coord);
const newClosestToMouseData = closestToMouseData;
if (newClosestToMouseData != oldClosestToMouseData) {
redrawClosestToMouse();
}
};
function backgroundClicked(){
addCube(closestToMouseData.coord);
if (selectedCube) {
// if another cube is currently selected
d3.select(".cube.selected").classed("selected", false);
}
selectedCube = cubes[cubes.length-1];
recomputeExpansions();
recomputeAllCubeFaces()
recomputeAllContours();
redrawAxes();
redraw();
d3.select(".cube:last-of-type").classed("selected", true);
};
function cubeEntered(){
hoveredCube = d3.select(this).datum();
d3.select(this).classed("active", true);
closestToMouse.classed("on-cube", true);
redrawAxes();
};
function cubeExited(){
hoveredCube = null;
d3.select(this).classed("active", false);
closestToMouse.classed("on-cube", false);
redrawAxes();
};
function cubeClicked(){
if (selectedCube === d3.select(this).datum()) {
selectedCube = null;
d3.select(this).classed("selected", false);
redrawAxes();
} else {
if (selectedCube) {
// if another cube is currently selected
cubeHovererContainer.select(".selected").classed("selected", false);
}
selectedCube = d3.select(this).datum();
d3.select(this).classed("selected", true);
redrawAxes();
}
};
function cubeDoubleClicked(){
let coord = d3.select(this).datum().coord;
hoveredCube = null;
if (selectedCube === d3.select(this).datum()) {
selectedCube = null;
}
removeCube(d3.select(this).datum());
computeClosestToMouseData(coord);
recomputeExpansions();
recomputeAllCubeFaces()
recomputeAllContours();
closestToMouse.classed("on-cube", false);
closestToMouse.classed("on-grid", true);
redrawClosestToMouse();
redrawAxes();
redraw();
};
function axeClicked(){
const dir = d3.select(this).datum().dir;
let dirIndex;
if (selectedCube){
dirIndex = selectedCube.allowedExpansionDirections.indexOf(dir);
if (dirIndex===-1){
selectedCube.allowedExpansionDirections.push(dir);
} else {
selectedCube.allowedExpansionDirections.splice(dirIndex, 1);
}
recomputeExpansions();
recomputeAllCubeFaces()
recomputeAllContours();
redrawAxes();
redraw();
}
};
/***********************/
/* User input */
/***********************/
const miniLanguage = /^( *|((M-?\d+,-?\d+)|([XYZ]-?\d+))(E(-?[xyz])*)*)*$/g;
const groupSplitter = /(M-?\d+,-?\d+)|([XYZ]-?\d+)|(E(-?[xyz])*)/g;
const directionSplitter = /-?[xyz]/g;
function inputed(){
let newUserInput = input.node().value,
curX = 0,
curY = 0;
let groups, directive, value, increment, lastCube;
if (!newUserInput.match(miniLanguage)) {
input.classed("invalid", true)
return;
}
input.classed("invalid", false);
userInput = newUserInput;
groups = newUserInput.replace(/ /g,"").match(groupSplitter);
removeAllCubes();
//begin: add cubes
groups.forEach((g)=> {
directive = g.slice(0,1);
value = g.slice(1, g.length);
if (directive==="M") {
value = value.split(',');
curX = parseInt(value[0]);
curY = parseInt(value[1]);
addCube([curX, curY]);
lastCube = cubes[cubes.length-1];
} else if (directive==="X") {
curX += parseInt(value);
addCube([curX, curY]);
lastCube = cubes[cubes.length-1];
} else if (directive==="Y") {
curY += parseInt(value);
addCube([curX, curY]);
lastCube = cubes[cubes.length-1];
} else if (directive==="Z") {
curX -= parseInt(value);
curY -= parseInt(value);
addCube([curX, curY]);
lastCube = cubes[cubes.length-1];
} else {
if (value) {
lastCube.allowedExpansionDirections = value.match(directionSplitter);
} else {
lastCube.allowedExpansionDirections = [];
}
}
})
//end: add cubes
selectedCube = null;
recomputeExpansions();
recomputeAllCubeFaces()
recomputeAllContours();
redrawAxes();
redraw();
};
function importGeometries() {
let s = "";
cubes.forEach(c=>{
s += "M"+c.coord;
if (c.allowedExpansionDirections.length<6) {
s += "E";
c.allowedExpansionDirections.forEach(dir => s+=dir);
}
s += " ";
})
input.attr("value", s);
userInput = s;
};
function gridVisibilityUpdate(el) {
showGrid = !showGrid;
gridLines.classed("hide", !showGrid);
d3.select(el).text((showGrid? "Hide grid" : "Show grid"));
}
/***********************/
/* List of Cubes */
/* & */
/* cubes manip. */
/***********************/
function addCube(coord){
const alreadyDefinedCube = cubes.find(c=>(c.coord[0]===coord[0]) && (c.coord[1]===coord[1]));
let newCube;
if (alreadyDefinedCube) {
//move cube to last position in cubes list
const index = cubes.indexOf(alreadyDefinedCube);
cubes.splice(index, 1);
cubes.push(alreadyDefinedCube);
// cube.closests remain the same
}
else {
//add new cube iif not already existing
newCube = {
id: nextCubeId++,
coord: coord,
contour: [],
coveredContour: [],
allowedExpansionDirections: ["-x","-y","-z","x","y","z"],
closests: {},
expansionDirections: []};
cubes.push(newCube);
computeAllClosests();
}
};
function removeAllCubes(){
cubes = [];
};
function removeCube(cube){
cubes.splice(cubes.indexOf(cube), 1);
computeAllClosests();
};
const inverseDirection = {
"x": "-x",
"-x": "x",
"y": "-y",
"-y": "y",
"z": "-z",
"-z": "z"
};
const deg = {
"x": 0,
"-x": 180,
"y": 120,
"-y": -60,
"z": -120,
"-z": 60
};
const rad = {
"x": 0,
"-x": _PI,
"y": _2PI/3,
"-y": -_PI/3,
"z": -_2PI/3,
"-z": _PI/3
};
function computeAllClosests() {
cubes.forEach(c=> c.closests={});
cubes.forEach(c=>{
computeClosests(c);
});
};
function computeClosests(cube){
let dx, dy;
cubes.forEach(c=>{
if (c!=cube){
dx = c.coord[0] - cube.coord[0];
dy = c.coord[1] - cube.coord[1];
if (dx===0) {
handleClosests(cube, c, dy, "y");
}
if (dy===0) {
handleClosests(cube, c, dx, "x");
}
if (dx===dy) {
handleClosests(cube, c, -dx, "z");
}
}
})
};
function handleClosests(cube, possibleClosest, distance, direction) {
if (distance<0) {
distance = -distance;
direction = inverseDirection[direction];
}
if (!cube.closests[direction] || cube.closests[direction].distance>distance) {
cube.closests[direction] = {
cube: possibleClosest,
distance: distance
}
}
};
​
function recomputeExpansions(){
const cubeCount = cubes.length;
let oppositeDir;
cubes.forEach(c=> c.expansionDirections=[]);
expansions = [];
cubes.forEach(c=>{
["x","y","z"].forEach(dir=>{
oppositeDir = inverseDirection[dir];
if (c.allowedExpansionDirections.includes(dir)
&& c.closests[dir]
&& c.closests[dir].cube.allowedExpansionDirections.includes(oppositeDir)
){
c.expansionDirections.push(dir);
c.closests[dir].cube.expansionDirections.push(oppositeDir);
expansions.push({
coord: c.coord,
direction: dir,
distance: c.closests[dir].distance
})
}
})
})
};
function recomputeAllCubeFaces(){
cubes.forEach(c=>{
recomputeCubeFaces(c);
});
};
function recomputeCubeFaces(cube) {
const faces = [];
if (cube.expansionDirections.includes("-x")){
if (cube.expansionDirections.includes("y")){
faces.push({relativeCoord: [-1,0], face: hf11});
} else {
faces.push({relativeCoord: [-1,0], face: f1});
}
if (cube.expansionDirections.includes("z")){
faces.push({relativeCoord: [-1,0], face: hf20});
} else {
faces.push({relativeCoord: [-1,0], face: f2});
}
}
if (cube.expansionDirections.includes("-y")){
if (cube.expansionDirections.includes("x")){
faces.push({relativeCoord: [0,-1], face: hf00});
} else {
faces.push({relativeCoord: [0,-1], face: f0});
}
if (cube.expansionDirections.includes("z")){
faces.push({relativeCoord: [0,-1], face: hf21});
} else {
faces.push({relativeCoord: [0,-1], face: f2});
}
}
if (cube.expansionDirections.includes("-z")){
if (cube.expansionDirections.includes("x")){
faces.push({relativeCoord: [1,1], face: hf01});
} else {
faces.push({relativeCoord: [1,1], face: f0});
}
if (cube.expansionDirections.includes("y")){
faces.push({relativeCoord: [1,1], face: hf10});
} else {
faces.push({relativeCoord: [1,1], face: f1});
}
}
if (cube.expansionDirections.includes("x")){
faces.push({relativeCoord: [1,0], face: f1});
faces.push({relativeCoord: [1,0], face: f2});
} else {
faces.push({relativeCoord: [0,0], face: f0});
}
if (cube.expansionDirections.includes("y")){
faces.push({relativeCoord: [0,1], face: f0});
faces.push({relativeCoord: [0,1], face: f2});
} else {
faces.push({relativeCoord: [0,0], face: f1});
}
if (cube.expansionDirections.includes("z")){
faces.push({relativeCoord: [-1,-1], face: f0});
faces.push({relativeCoord: [-1,-1], face: f1});
} else {
faces.push({relativeCoord: [0,0], face: f2});
}
cube.faces = faces;
}
function recomputeAllContours(){
cubes.forEach(c=>{
recomputeContours(c);
});
};
function recomputeContours(cube){
//consider each dir in order and add adequate verteces (relative to cube's center)
const contour = [],
coveredContour = [];
if (cube.expansionDirections.includes("x")){
if (!cube.expansionDirections.includes("-y")){
contour.push([1,-1]);
coveredContour.push([1,-1]);
}
contour.push(cPlusX,[2,1]);
coveredContour.push([2,0],[2,1]);
} else {
contour.push(cPlusX);
coveredContour.push(cPlusX);
}
if (cube.expansionDirections.includes("-z")){
if (!cube.expansionDirections.includes("x")){
contour.push([2,1]);
coveredContour.push([2,1]);
}
contour.push([2,2],[1,2]);
coveredContour.push([2,2],[1,2]);
} else {
contour.push(cMinusZ);
coveredContour.push(cMinusZ);
}
if (cube.expansionDirections.includes("y")){
if (!cube.expansionDirections.includes("-z")){
contour.push([1,2]);
coveredContour.push([1,2]);
}
contour.push(cPlusY,[-1,1]);
coveredContour.push([0,2],[-1,1]);
} else {
contour.push(cPlusY);
coveredContour.push(cPlusY);
}
if (cube.expansionDirections.includes("-x")){
if (!cube.expansionDirections.includes("y")){
contour.push([-1,1]);
coveredContour.push([-1,1]);
}
contour.push([-2,0],[-2,-1]);
coveredContour.push([-2,0],[-2,-1]);
} else {
contour.push(cMinusX);
coveredContour.push(cMinusX);
}
if (cube.expansionDirections.includes("z")){
if (!cube.expansionDirections.includes("-x")){
contour.push([-2,-1]);
coveredContour.push([-2,-1]);
}
contour.push(cPlusZ,[-1,-2]);
coveredContour.push([-2,-2],[-1,-2]);
} else {
contour.push(cPlusZ);
coveredContour.push(cPlusZ);
}
if (cube.expansionDirections.includes("-y")){
if (!cube.expansionDirections.includes("z")){
contour.push([-1,-2]);
coveredContour.push([-1,-2]);
}
contour.push([0,-2],[1,-1]);
coveredContour.push([0,-2],[1,-1]);
} else {
contour.push(cMinusY);
coveredContour.push(cMinusY);
}
cube.contour = contour;
cube.coveredContour = coveredContour;
};
const computeClosestToMouseData = function(coord){
const closestCoord = [round(coord[0]), round(coord[1])];
if (closestToMouseData.coord[0] != closestCoord[0] || closestToMouseData.coord[1] != closestCoord[1]) {
const newFakeCube = {
coord: closestCoord,
closests: {}
};
//find closests cubes
computeClosests(newFakeCube);
closestToMouseData = newFakeCube;
}
};
/***********************/
/* Drawings */
/***********************/
function redraw(){
redrawCubes();
redrawExpansions();
};
function redrawCubes(){
// remove existing cubes and cube hoverers from svg
cubeContainer.selectAll(".cube").remove();
cubeHovererContainer.selectAll(".cube-hoverer").remove();
//redraw all cubes
cubes.forEach( c=>{ drawCube(c); });
};
function drawCube(cube) {
const drawnCube = cubeContainer.append("g").datum(cube);
//translate to adequate position
drawnCube.classed("cube", true)
.classed("selected", selectedCube === cube)
.attr("id", d=> "cube-"+d.id)
.attr("transform", "translate("+scaledOrthoCoord(cube.coord)+")");
//draw faces
cube.faces.forEach(f=>{
drawnCube.append("path")
.attr("class", f.face.type)
.classed("face", true)
.attr("transform", "translate("+scaledOrthoCoord(f.relativeCoord)+")")
.attr("d", closedPath(f.face.verteces));
})
//begin: create hoverer
const drawnHoverer = cubeHovererContainer.append("g").datum(cube);
//translate to adequate position
drawnHoverer.classed("cube-hoverer", true)
.classed("selected", selectedCube === cube)
.attr("id", d=> "cube-hoverer-"+d.id)
.attr("transform", "translate("+scaledOrthoCoord(cube.coord)+")");
// draw contours
drawnHoverer.append("path")
.classed("contour covered", true)
.attr("d", closedPath(cube.coveredContour));
drawnHoverer.append("path")
.classed("contour", true)
.attr("d", closedPath(cube.contour));
// add listeners
drawnHoverer.on("mouseenter", cubeEntered)
.on("mouseout", cubeExited)
.on("click", cubeClicked)
.on("dblclick", cubeDoubleClicked);
//end: create hoverer
};
function redrawExpansions(){
// remove existing expansions from svg
expansionContainer.selectAll(".expansion").remove();
expansions.forEach(e=>drawExpansion(e))
};
function drawExpansion(expansion){
const drawnExpansion = expansionContainer.append("g");
let d;
drawnExpansion.classed("expansion", true)
.attr("transform", "translate("+scaledOrthoCoord(expansion.coord)+")");
//begin: compute faces to draw
const faces = [];
d = expansion.distance;
let verteces;
if (expansion.direction==="x"){
faces.push({verteces: [[1,0], [2,1], [d-1,1], [d-2,0]], type: "f1"});
faces.push({verteces: [[1,0], [1,-1], [d-2,-1], [d-2,0]], type: "f2"});
} else if (expansion.direction==="y"){
faces.push({verteces: [[0,1], [1,2], [1,d-1], [0,d-2]], type: "f0"});
faces.push({verteces: [[0,1], [-1,1], [-1,d-2], [0,d-2]], type: "f2"});
} else {
faces.push({verteces: [[-1,-1], [-1,-2], [-d+2,-d+1], [-d+2,-d+2]], type: "f0"});
faces.push({verteces: [[-1,-1], [-2,-1], [-d+1,-d+2], [-d+2,-d+2]], type: "f1"});
}
//end: compute faces to draw
//draw faces
faces.forEach(f=>{
drawnExpansion.append("path")
.attr("class", f.type)
.classed("face", true)
.attr("d", closedPath(f.verteces));
})
};
function initLayout() {
var chartDiv = document.getElementById("chart");
width = chartDiv.clientWidth;
height = chartDiv.clientHeight;
svg = d3.select("svg");
svg.attr("width", width)
.attr("height", height);
input = d3.select("input")
.on("click", function() { this.value = (userInput==="")? "M27,5Y9X9 M30,11" : userInput; inputed(); })
.on("input", function() { inputed() });
drawGridLines();
expansionContainer = svg.append("g").attr("id", "expansions");
// add cubes on top of expansions; expansions drawn below cubes
cubeContainer = svg.append("g").attr("id", "cubes");
// add the closest-to-mouse green fake cube
drawClosestToMouse();
// add layer handling interactions with background
svg.append("rect")
.attr("id", "background-hoverer")
.attr("width", width)
.attr("height", height)
.on("mouseenter", backgroundEntered)
.on("mouseout", backgroundExited)
.on("mousemove", backgroundHovered)
.on("click", backgroundClicked)
// add layer handling interactions with cubes
cubeHovererContainer = svg.append("g").attr("id", "cube-hoverers");
// add layer handling available expanding directions of cubes
drawAxes();
};
function reinitLayout() {
var chartDiv = document.getElementById("chart");
width = chartDiv.clientWidth;
height = chartDiv.clientHeight;
svg.attr("width", width)
.attr("height", height);
svg.select("#grid-line")
.attr("width", width)
.attr("height", height);
svg.select("#background-hoverer")
.attr("width", width)
.attr("height", height);
axes.attr("transform", "translate("+[width-2.5*axeLength, height-2.5*axeLength]+")");
}
function drawGridLines() {
//define an SGV pattern, repeatidly drawn to form the grid
gridLines = svg.append("rect")
.attr("id", "grid-lines")
.attr("width", width)
.attr("height", height);
const pattern = svg.select("#grid-pattern");
/*
//begin: define lines
pattern.attr("width", gridLength).attr("height", 2*gridLength*sin120);
pattern.append("line")
.attr("x2", gridLength);
pattern.append("line")
.attr("x2", gridLength)
.attr("transform", "translate("+[0,gridLength*sin120]+")");
pattern.append("line")
.attr("y2", 2*gridLength)
.attr("transform", "rotate(-30)");
pattern.append("line")
.attr("y2", 2*gridLength)
.attr("transform", "translate("+[gridLength,0]+")rotate(30)");
//end: define lines
*/
//begin: define points
pattern.attr("width", gridLength).attr("height", 2*gridLength*sin120);
pattern.append("circle")
.attr("r", 0.5);
pattern.append("circle")
.attr("cx", gridLength)
.attr("r", 0.5);
pattern.append("circle")
.attr("cy", 2*gridLength*sin120)
.attr("r", 0.5);
pattern.append("circle")
.attr("cx", gridLength)
.attr("cy", 2*gridLength*sin120)
.attr("r", 0.5);
pattern.append("circle")
.attr("cx", gridLength*cos60)
.attr("cy", gridLength*sin60)
.attr("r", 0.5);
//end: define points
};
function drawClosestToMouse(){
closestToMouse = svg.append("g").attr("id", "closest-to-mouse");
const cube = closestToMouse.append("g");
cube.append("path")
.attr("d",closedPath(defaultContour)
+openPath([[0,0], cMinusX])
+openPath([[0,0], cMinusY])
+openPath([[0,0], cMinusZ]));
};
function redrawClosestToMouse(){
// reposition closestToMouse
closestToMouse.datum(closestToMouseData).attr("transform", "translate("+scaledOrthoCoord(closestToMouseData.coord)+")")
const closestsData = [];
for(var k in closestToMouseData.closests) {
closestsData.push({direction: k, distance: closestToMouseData.closests[k].distance});
}
//begin: draw lines to closests cubes
closestToMouse.selectAll("line").remove();
closestToMouse.selectAll("line")
.data(closestsData)
.enter()
.append("line")
.attr("x2", d=>(d.distance-1)*gridLength)
.attr("transform", (d)=>{
return "rotate("+deg[d.direction]+")translate("+[gridLength,0]+")";
})
//end: draw lines to closests cubes
//begin: draw distances to closests cubes
closestToMouse.selectAll("text").remove();
closestToMouse.selectAll("text")
.data(closestsData)
.enter()
.append("text")
.classed("distance-to-closest", true)
.text(d=>d.distance)
.attr("transform", (d)=>{
return "rotate("+deg[d.direction]+")translate("+[1.5*gridLength,0]+")rotate("+(-deg[d.direction])+")";
})
//end: draw distances to closests cubes
};
function drawAxes() {
axes = svg.append("g");
axes.attr("id", "axes")
.attr("transform", "translate("+[width-2.5*axeLength, height-2.5*axeLength-25]+")"); // -15 for grid visibility user action
const axesConf = [
{dir:"x",deg:0,rad:0},
{dir:"-z",deg:60,rad:_PI/3},
{dir:"y",deg:120,rad:_2PI/3},
{dir:"-x",deg:180,rad:_PI},
{dir:"z",deg:-120,rad:-_2PI/3},
{dir:"-y",deg:-60,rad:-_PI/3}
];
const distance = axeLength+10;
const halfAxeLength = axeLength/2;
let path = "M"+halfAxeLength+",0h"+halfAxeLength+"M"+(axeLength-2)+",-4l2,4l-2,4";
axes.append("circle")
.attr("id", "background")
.attr("r", distance+10)
//begin: draw small cube a center
const cube = axes.append("g")
.classed("cube", true)
.style("transform", "scale("+(halfAxeLength/gridLength)+")");
cube.append("path")
.classed("face", true)
.classed("f0", true)
.attr("d", closedPath(f0.verteces));
cube.append("path")
.classed("face", true)
.classed("f1", true)
.attr("d", closedPath(f1.verteces));
cube.append("path")
.classed("face", true)
.classed("f2", true)
.attr("d", closedPath(f2.verteces));
//end: draw small cube at center
//begin: add arrows and labels
axes.selectAll(".axe")
.data(axesConf)
.enter()
.append("path")
.classed("axe", true)
.attr("d", path)
.attr("transform", (d) => "rotate("+d.deg+")")
.on("click", axeClicked);
axes.selectAll(".label")
.data(axesConf)
.enter()
.append("text")
.classed("label", true)
.text((d)=>d.dir)
.attr("transform", (d) => "translate("+[(distance)*cos(d.rad)-5, (distance)*sin(d.rad)+4]+")")
.on("click", axeClicked);
//end: add arrows and labels
};
function redrawAxes() {
let allowedExpansionDirections = [],
clickable = false;
if (hoveredCube) {
allowedExpansionDirections = hoveredCube.allowedExpansionDirections;
clickable = true;
} else if (selectedCube) {
allowedExpansionDirections = selectedCube.allowedExpansionDirections;
clickable = true;
}
const axesConf = [
{dir:"x",allowed:allowedExpansionDirections.includes("x")},
{dir:"-z",allowed:allowedExpansionDirections.includes("-z")},
{dir:"y",allowed:allowedExpansionDirections.includes("y")},
{dir:"-x",allowed:allowedExpansionDirections.includes("-x")},
{dir:"z",allowed:allowedExpansionDirections.includes("z")},
{dir:"-y",allowed:allowedExpansionDirections.includes("-y")}
];
axes.selectAll(".axe")
.data(axesConf)
.classed("allowed", (d)=>d.allowed)
.classed("clickable", clickable);
axes.selectAll(".label")
.data(axesConf)
.classed("allowed", (d)=>d.allowed)
.classed("clickable", clickable);
};
</script>
</body>
https://d3js.org/d3.v5.min.js