Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 10, 2018 11:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/4562c82625497273870692800ca1f2dd to your computer and use it in GitHub Desktop.
Save Kcnarf/4562c82625497273870692800ca1f2dd to your computer and use it in GitHub Desktop.
Online Impossible Figure Builder
license: lgpl-3.0

This block is a recreation which allows me to continue my journey into impossible geometries, and allows you to create some of them.

Before words

Just remember:

  • impossible geometries are optical illusions involving objects that can be drawn in 2D but are infeasible in 3D (i.e., cannot be physically constructed)
  • this experimentation deals with impossible geometries that resemble 3D objects that can be constructed out of blocks/cubes under parallel projection; cubes are drawn one after the others, some covering part of others, depending on the order they are drawn
  • impossible geometries are so called because they preserve local consistency, but not global consistency. Meaning that parts of the geometry (taken into isolation) look like valid 3D objects, but the overall figure is not a valid 3D object. This is possible with some hacks (see next paragraph).

Where does the magic come from?

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.

How expansions works?

  • by default, a cube can expand in all 6 directions
  • if no distant cube is found for a particular direction, there is no expansion in that direction
  • one can produce intricated impossible geometries by limiting the directions a cube could expand to (see below paragraphs dedicated to user interactions)
  • if a cube is allowed to expand in a certain direction and there is distant cube in that direction, but that distant cube is not allowed to expand in the direction of the initial cube, there is no expansion between the two cubes
  • all the above rules applie only to user-defined cubes and not to automatically added cubes. Expansions exist only between user-defined cubes. There is no expansion between a user-defined cube and an automatically added and already drawn cube

Mouse interactions

  • click the grid to add a cube; by default, it is allowed to expand in all 6 directions
  • double-click a cube to delete it
  • click a cube to select it (it's contour stays in green)
  • click a selected cube a second time to deselect it
  • the axes on the bottom right of the interface allow you to handle the directions a cube is allowed to expand to:
  • the allowed directions appear darker than the disallowed ones
  • when hovering a cube, axes informs you about its allowed directions
  • when a cube is selected and no cube is hovered, axes inform you about the allowed directions of the selected cube; you can also click an axe to allow/disallow the selected cube to expand to that particular direction

Input text field

The input text field allows you to define a set of cubes via a mini-language:

  • directives to add a cube:
  • M2,3: move to oblique coordinate (2,3) and add a new cube
  • X4: from the last added cube, move in x direction (i.e. to the right) by 4 units, and add a new cube
  • X-4: same as above, except in -x direction (i.e. to the left)
  • Y4, Y-4, Z4, Z-4: same as X4/X-4 along the adequate axe
  • directives to redefine the directions a cube is allowed to expand to:
  • Ex: the cube is allowed to expand in the x direction
  • E-x: the cube is allowed to expand in the -x direction
  • Ey, E-y, Ez, E-z: same as Ex and E-x along the adequate axe
  • Ex-x-yz: allow a cube to expand in severall directions
  • E (wihtout any direction): the cube is not allowed to expand in any direction
  • don't use this directive to let the cube allowed to expand to all the 6 directions

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.

Limitations

  • the algortihm fails when cube are too close (distance < 3 grid units).
  • it is not easy to define crossing lines that gives the impression that a line is drawn in front of another one. Even if expansions are drawn one after another and may lead to crossing lines, handling the way they are drawn (and hence the way they cross each others) is mesmerizing

Acknowledgments to :

Bibliography

PS

  • Scrollytelling the Penrose triangle
  • Performance/code enhancement:
  • define a Cube class, with appropriate functions/helpers
  • use the fact that z-aligned cubes all have the same (y-x) value
  • when adding/deleting a cube, don't recompute closests cubes from scratch; reuse what is already computed
  • same as above for closest-to-mouse fake cube
<!DOCTYPE html>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment