|
<!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> |