Skip to content

Commit

Permalink
Merge pull request #67 from david-hall/staging
Browse files Browse the repository at this point in the history
Various post-processing of .shapes.json
  • Loading branch information
david-hall authored Feb 20, 2024
2 parents c56ff7c + 3744352 commit b6e7428
Show file tree
Hide file tree
Showing 92 changed files with 293,372 additions and 237,519 deletions.
50 changes: 27 additions & 23 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@
<div class="main-grid">
<div class="intro">
<h1>
Johnson Solids made with <a href="https://vzome.com" target="_blank" rel="noopener">vZome</a>
</h1>
<p>
Select a row in the table to view that Johnson solid.<br>
Check the box in the viewer to show struts and balls for
<a href="https://www.zometool.com" target="_blank" rel="noopener">Zometool</a> constructible solids listed in italics.
</p>
<p>
Use your mouse to manipulate the 3D view: left button to rotate, right button to pan, and scroll gesture to zoom.
</p>
Johnson Solids Explorer
</h1>
<div class="intro-text">
<p>
Select a row in the table to view that Johnson solid.<br>
For <a href="https://www.zometool.com" target="_blank" rel="noopener">Zometool</a>-constructible solids highlighted in blue,
check the box in the viewer to show struts and balls.
</p>
<p>
Use your mouse to manipulate the 3D view: left button to rotate, right button to pan, and scroll gesture to zoom.
</p>
</div>
</div>

<div class="table">
Expand All @@ -66,26 +68,28 @@ <h1 class="label">
<div id="index"></div>
</h1>
<div id="zome-switch">
<label id="labelForShowEdges"" for="showEdges">Show Zometool</label>
<input type="checkbox" id="showEdges">
<div>Show Zometool</div>
</div>
<vzome-viewer id="viewer" reactive="false"/>
</div>
</div>

<div class="references">
<h2>
References
</h2>
<p>
<div class="references">
<h2>
References
</h2>

<ul>
<li><a href="https://discord.com/channels/808488427400986724/1176604061294936114" target="_blank" rel="noopener">Discord</a></li>
<li><a href="https://github.com/vZome/johnson-solids/" target="_blank" rel="noopener">GitHub</a></li>
<li><a href="https://www.qfbox.info/4d/johnson" target="_blank" rel="noopener">QFBox</a></li>
<li><a href="https://en.wikipedia.org/wiki/Johnson_solid" target="_blank" rel="noopener">Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_Johnson_solids" target="_blank" rel="noopener">Wikipedia (list)</a></li>
<li><a href="https://discord.com/channels/808488427400986724/1176604061294936114" target="_blank"
rel="noopener">Discord discussion</a></li>
<li><a href="https://github.com/vZome/johnson-solids/" target="_blank" rel="noopener">GitHub source</a></li>
<li><a href="https://vzome.com" target="_blank" rel="noopener">vZome</a></li>
<li><a href="https://www.qfbox.info/4d/johnson" target="_blank" rel="noopener">More Polyhedra</a></li>
<li><a href="https://en.wikipedia.org/wiki/Johnson_solid" target="_blank" rel="noopener">Wikipedia</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_Johnson_solids" target="_blank" rel="noopener">Wikipedia
(list)</a></li>
</ul>
</p>
</div>
</div>

<div class="download">
Expand Down
256 changes: 227 additions & 29 deletions johnson-solids-listing.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const viewer = document.getElementById( "viewer" );
const showEdges = document.getElementById( "showEdges" );
const zomeSwitch = document.getElementById( "zome-switch" );
const downloadLink = document.getElementById( "download" );
const sigfig = 1000000000; // significant digits for rounding

const shapeColors = new Map();
shapeColors.set( 3, "#F0A000"); // yellow strut
Expand All @@ -16,11 +17,13 @@ shapeColors.set( 6, "#008D36"); // green strut
shapeColors.set( 8, "#DC4C00"); // orange strut
shapeColors.set(10, "#6C00C6"); // purple strut

// include a "download=true" query param in the URL to make the ID in the viewer become the recolor download link
// include a "download=true" query param in the URL to make the ID in the viewer become the .shapes.json download link
if(new URL(document.location).searchParams.get("download") == "true") {
document.getElementById( "index" ).addEventListener( "click", downloadShapesJson );
}

// https://medium.com/charisol-community/downloading-resources-in-html5-a-download-may-not-work-as-expected-bf63546e2baa
// This method works with local files as well as cross origin files.
function downloadShapesJson() {
const url = viewer.src.replace( ".vZome", ".shapes.json" );
const filename = url.substring(url.lastIndexOf( "/" ) + 1);
Expand All @@ -36,7 +39,7 @@ function downloadShapesJson() {
return response.json();
} )
.then(modelData => {
var stringifiedData = JSON.stringify( recolor(modelData), null, 2);
const stringifiedData = JSON.stringify(postProcess(modelData), null, 2);
const blobUrl = URL.createObjectURL(new Blob([stringifiedData], { type: "application/json" }));
downloadLink.href = blobUrl;
downloadLink.click();
Expand All @@ -49,36 +52,191 @@ function downloadShapesJson() {
});
}

function recolor(modelData) {
const faceMap = new Map();
function postProcess(modelData) {
recolor(modelData);
rescale(modelData);
standardizeCameras(modelData);
return modelData;
}

function standardizeCameras(modelData) {
// Adjust all camera vector settings to the same values
// and zoom levels so that any model that's the first one loaded will be zoomed to fit
// and others will use the same initial zoom level.
// Any model could be the one that sets the default camera if the "J=" queryparam is used.
const distance = getDistanceScaledToFitView(modelData);
standardizeCamera(modelData.camera, distance);
for(let scene of modelData.scenes) {
// scene views are not used by the Johnson solids app, but we'll standardize their cameras too since we're here
standardizeCamera(scene.view, distance);
}
return modelData;
}

function cameraFieldOfViewY ( width, distance ) {
const halfX = width / 2;
const halfY = halfX; // assumes aspectWtoH = 1.0;
return 360 * Math.atan( halfY / distance ) / Math.PI;
}

function getDistanceScaledToFitView(modelData) {
const snapshots = getFaceSceneSnapshots(modelData);
const shapeMap = new Map();
for(const shape of modelData.shapes) {
faceMap.set(shape.id, shape.vertices.length);
shapeMap.set(shape.id, shape);
}
// Get a list of facescene(s) of all models that use the selected jsolid's URL.
// There may be only one facescene, but there may be more than one. e.g. J38 & J39
const url = viewer.src;
const facescenes = [];
for(const model of models) {
if(model.url == url) {
facescenes.push(model.facescene);
const origin = {x:0, y:0, z:0};
var maxRadius = 0;
for(const snapshot of snapshots) {
const ss = modelData.snapshots[snapshot];
for(let i = 0; i < ss.length; i++) {
const item = ss[i];
const shapeGuid = item.shape;
const vertices = shapeMap.get(shapeGuid).vertices;
for(const vertex of vertices) {
maxRadius = Math.max( maxRadius, edgeLength(origin, vertex) );
}
}
}
const snapshots = [];
if(facescenes.includes("default scene")) {
snapshots.push(0);
// Originally, I planned to determine the distance based on the view frustum
// and a sphere with radius = maxRadius, but I determined that a simple scaling
// of maxRadius is adequate and much simpler.
// Emperically, distance ends up being
// about 12 for J1 which is the smallest solid
// and 48 for J71 which is the biggest solid.
return maxRadius * 8; // Scale factor of 8 was determined empirically as a reasonable best-fit.
}

function standardizeCamera(camera, distance) {
// Much of this is copied from camera.jsx
const NEAR_FACTOR = 0.1;
const FAR_FACTOR = 2.0;
const WIDTH_FACTOR = 0.5;
camera.viewDistance = distance;
camera.farClipDistance = distance * FAR_FACTOR;
camera.nearClipDistance = distance * NEAR_FACTOR;
camera.width = distance * WIDTH_FACTOR;
camera.fieldOfView = cameraFieldOfViewY ( camera.width, camera.viewDistance );

camera.perspective = true;
camera.stereo = false;

camera.position.x = 0;
camera.position.y = 0;
camera.position.z = camera.viewDistance;

camera.lookAtPoint.x = 0;
camera.lookAtPoint.y = 0;
camera.lookAtPoint.z = 0;

camera.upDirection.x = 0;
camera.upDirection.y = 1;
camera.upDirection.z = 0;

camera.lookDirection.x = 0;
camera.lookDirection.y = 0;
camera.lookDirection.z = -1;

// don't need to return the camera because it's passed by reference and updated in situ
}

function rescale(modelData) {
const snapshots = getFaceSceneSnapshots(modelData);
const shapeMap = new Map();
for(const shape of modelData.shapes) {
shapeMap.set(shape.id, shape);
}
for(const scene of modelData.scenes) {
if(facescenes.includes(scene.title)) {
snapshots.push(scene.snapshot);
var nTriangleEdges = 0;
var sumOfLengths = 0;
for(const snapshot of snapshots) {
const ss = modelData.snapshots[snapshot];
for(let i = 0; i < ss.length; i++) {
const item = ss[i];
const shapeGuid = item.shape;
const vertices = shapeMap.get(shapeGuid).vertices;
if(vertices.length == 3) {
// All Johnson solids have at least one equilateral triangle face.
// All other polygons are chopped into triangles that are not necessarily equilateral.
// I'll use the average length of all the edges of all the triangular faces
// to calculate the rescaling factor.
// Note that the edges will be counted twice when two triangles share an edge,
// and other triangle edges will only be counted once when a triangle shares
// an edge with a larger polygon such as a square.
// It's not worth the effort to distinguish the two cases for this application.
// In fact, it would work well enough by just using the first equilateral triangle
// edge length that we encounter.
sumOfLengths += edgeLength(vertices[0], vertices[1]); nTriangleEdges++;
sumOfLengths += edgeLength(vertices[1], vertices[2]); nTriangleEdges++;
sumOfLengths += edgeLength(vertices[2], vertices[0]); nTriangleEdges++;
}
}
}
console.log(snapshots);

const averageLength = sumOfLengths / nTriangleEdges;
console.log("averageLength = " + averageLength + " (Ideal length = 2.0.)");

// Many models have an averageLength of 8.472135952064994 = (2+4phi) corresponding to blue zometool lengths.
// The target edge length will be 2 because most of the coordinates on qfbox and wikipedia
// have edge length of 2, resulting in a half edge length of 1 on each side of the symmetry plane(s).
const scaleFactor = Math.round((2.0 / averageLength) * sigfig) / sigfig;

console.log("scaleFactor = " + scaleFactor);
if(!!modelData.scaleFactor) {
console.log("Previously calculated scaleFactor of " + modelData.scaleFactor + " will not be modified.");
} else {
// persist scaleFactor in the json
modelData.scaleFactor = scaleFactor;

const sigScaleFactor = scaleFactor * sigfig; // scaleVector() will divide by sigfig after rounding
// scale all shapes
for(let s = 0; s < modelData.shapes.length; s++) {
for(let v = 0; v < modelData.shapes[s].vertices.length; v++) {
scaleVector(sigScaleFactor, modelData.shapes[s].vertices[v]);
}
}
// scale all instances
//console.log(modelData.instances.length + " instances");
for(let i = 0; i < modelData.instances.length; i++) {
scaleVector(sigScaleFactor, modelData.instances[i].position);
}
// scale all snapshots
//console.log(modelData.snapshots.length + " snapshots");
for(let i = 0; i < modelData.snapshots.length; i++) {
//console.log(modelData.snapshots[i].length + " snapshot[" + i + "]");
for(let j = 0; j < modelData.snapshots[i].length; j++) {
scaleVector(sigScaleFactor, modelData.snapshots[i][j].position);
}
}
}
return modelData;
}

function edgeLength(v0, v1) {
const x = v0.x - v1.x;
const y = v0.y - v1.y;
const z = v0.z - v1.z;
return Math.sqrt((x*x)+(y*y)+(z*z));
}

function scaleVector(scalar, vector) {
vector.x = Math.round( vector.x * scalar ) / sigfig;
vector.y = Math.round( vector.y * scalar ) / sigfig;
vector.z = Math.round( vector.z * scalar ) / sigfig;
// don't need to return the vector because it's passed by reference and updated in situ
}

function recolor(modelData) {
const snapshots = getFaceSceneSnapshots(modelData);
const shapeMap = new Map();
for(const shape of modelData.shapes) {
shapeMap.set(shape.id, shape.vertices.length);
}
for(const snapshot of snapshots) {
const ss = modelData.snapshots[snapshot];
for(let i = 0; i < ss.length; i++) {
const item = ss[i];
const shapeGuid = item.shape;
const nVertices = faceMap.get(shapeGuid);
const nVertices = shapeMap.get(shapeGuid);
const newColor = shapeColors.get(nVertices);
if(newColor) {
modelData.snapshots[snapshot][i].color = newColor;
Expand All @@ -88,19 +246,67 @@ function recolor(modelData) {
return modelData;
}

function getFaceSceneSnapshots(modelData) {
// Get a list of facescene(s) of all models that use the selected jsolid's URL.
// There may be only one facescene, but there may be more than one. e.g. J38 & J39
const url = viewer.src;
const facescenes = [];
for(const model of models) {
if(model.url == url) {
facescenes.push(model.facescene);
}
}
const snapshots = [];
// if(facescenes.includes("default scene")) {
// snapshots.push(0);
// }
for(const scene of modelData.scenes) {
if(facescenes.includes(scene.title)) {
snapshots.push(scene.snapshot);
}
}
// console.dir(snapshots);
return snapshots;
}

viewer .addEventListener( "vzome-scenes-discovered", (e) => {
// Just logging this to the console for now. Not actually using the scenes list.
const scenes = e.detail;
//console.log( "These scenes were discovered in " + viewer.src);
console.log( JSON.stringify( scenes, null, 2 ) );
} );

// TODO: Remove this listener after the race condition is fixed
viewer .addEventListener( "vzome-scenes-discovered", (e) => {
console.log( "This event handler is only triggered the first time a vzome scene is discovered after the page is loaded.\n"
+ "It is a work around for the intermittant race condition that results in the wrong camera zoom level.\n"
+ "The same race condition may still result in showing the wrong background color, but at least the camera zoom is consistent.\n"
+ "If the vzome-viewer revision is shown ABOVE this message, then the background color was read from .shapes.json.\n"
+ "If the vzome-viewer revision is shown BELOW this message, then the default background color of the viewer is being used.\n"
);
viewer.update({ camera: true }); // force a camera update, but background color may still be wrong.
},
{once: true}); // automatically remove this listener after it is fired once

for (const jsolid of models) {
const tr = tbody.insertRow();
fillRow(tr, jsolid);
tr.addEventListener("click", () => selectJohnsonSolid( jsolid, tr ) );
}
selectJohnsonSolid( models[ 0 ], tbody .rows[ 0 ] );

var initialId = 1;
const searchParams = new URL(document.location).searchParams;
let jId = parseInt(searchParams.get("J")); // upper case
if(Number.isNaN(jId)) {
jId = parseInt(searchParams.get("j")); // lower case
}
if(jId >= 1 && jId <= 92) {
initialId = jId;
}
const initialRow = tbody.rows[ initialId - 1 ];
selectJohnsonSolid( models[ initialId - 1 ], initialRow );
initialRow.scrollIntoView({ behavior: "smooth", block: "center" });

showEdges.addEventListener("change", // use "change" here, not "click"
() => {
setScene(selectedRow.dataset);
Expand All @@ -116,14 +322,6 @@ function selectJohnsonSolid( jsolid, tr ) {
selectedRow = tr;
selectedRow.className = "selected";
document.getElementById( "index" ).textContent = "J" +id;
//const shapes = url.replace( ".vZome", ".shapes.json" );
//downloadLink.href = shapes;
//const filename = shapes.substring(shapes.lastIndexOf('/')+1);
//downloadLink.download = filename;
/*
Downloads local files as expected, but it won't directly download cross origin files in-situ.
One possible react solution is https://medium.com/charisol-community/downloading-resources-in-html5-a-download-may-not-work-as-expected-bf63546e2baa
*/
switchModel(jsolid);
} else {
alert("Johnson solid J" + id + " is not yet available.\n\nPlease help us collect the full set.");
Expand Down
Loading

0 comments on commit b6e7428

Please sign in to comment.