diff --git a/frontend/css/index.css b/frontend/css/index.css
index c1b2db8..4d244ce 100644
--- a/frontend/css/index.css
+++ b/frontend/css/index.css
@@ -42,7 +42,7 @@ input:focus {
padding 10px;
width: 40px;
height: 40px;
- background: no-repeat center / 20px var(--white) url('/img/bars-black.svg');
+ background: no-repeat center / 20px var(--white) url('../img/bars-black.svg');
transition: 0.2s;
}
@@ -52,7 +52,7 @@ input:focus {
}
#show-controls-button.selected {
- background: no-repeat center / 20px var(--black) url('/img/bars-white.svg');
+ background: no-repeat center / 20px var(--black) url('../img/bars-white.svg');
cursor: pointer;
}
@@ -92,7 +92,7 @@ input:focus {
}
#triangle-grid .triangle.selected {
- background-image: url('/img/triangle.svg');
+ background-image: url('../img/triangle.svg');
}
#triangle-grid .triangle.selected:hover {
@@ -100,7 +100,7 @@ input:focus {
}
#triangle-grid .triangle.selected.even-row {
- background-image: url('/img/triangle-flipped.svg');
+ background-image: url('../img/triangle-flipped.svg');
}
.row {
diff --git a/frontend/img/triangle-flipped.svg b/frontend/img/triangle-flipped.svg
index 234d4a2..4494ebd 100644
--- a/frontend/img/triangle-flipped.svg
+++ b/frontend/img/triangle-flipped.svg
@@ -5,7 +5,7 @@
version="1.1"
id="svg2"
sodipodi:docname="triangle-flipped.svg"
- inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -22,17 +22,21 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="7.1198816"
- inkscape:cx="50.071057"
+ inkscape:cx="50.000831"
inkscape:cy="43.259146"
- inkscape:window-width="1904"
- inkscape:window-height="995"
- inkscape:window-x="8"
- inkscape:window-y="40"
- inkscape:window-maximized="0"
- inkscape:current-layer="svg2"
- inkscape:pageshadow="2"
- showgrid="false" />
-
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+
+
+
+
+
diff --git a/frontend/img/triangle.svg b/frontend/img/triangle.svg
index 0deba55..062517e 100644
--- a/frontend/img/triangle.svg
+++ b/frontend/img/triangle.svg
@@ -5,7 +5,7 @@
version="1.1"
id="svg2"
sodipodi:docname="triangle.svg"
- inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
@@ -22,17 +22,22 @@
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="7.1198816"
- inkscape:cx="50.071057"
+ inkscape:cx="50.000832"
inkscape:cy="43.259146"
- inkscape:window-width="1904"
- inkscape:window-height="995"
- inkscape:window-x="8"
- inkscape:window-y="40"
- inkscape:window-maximized="0"
- inkscape:current-layer="svg2"
- inkscape:pageshadow="2"
- showgrid="false" />
-
+ inkscape:window-width="1920"
+ inkscape:window-height="1008"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+
+
+
+
+
diff --git a/frontend/index.html b/frontend/index.html
index ad109a7..6530b0a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,6 +2,7 @@
+
@@ -41,6 +42,11 @@
Simulate
+
+
+ Download Nmesh File
+
+
diff --git a/frontend/js/modules/matrix-lib.js b/frontend/js/modules/matrix-lib.js
index 65028e1..cfae9bf 100644
--- a/frontend/js/modules/matrix-lib.js
+++ b/frontend/js/modules/matrix-lib.js
@@ -1,6 +1,10 @@
-// Multiply two matrices together using dot product multiplication
-//
-// a and b: matrices (2d array of numbers)
+export const identity = [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, 1, 0],
+ [0, 0, 0, 1],
+]
+
export const matrixMult = (a, b) => {
if (a[0]?.length != b.length) {
throw "incompatible dimensions"
@@ -24,21 +28,6 @@ export const matrixMult = (a, b) => {
return result
}
-// An identity matrix.
-// This won't transform anything, so it's a good starting point
-export const identity = [
- [1, 0, 0, 0],
- [0, 1, 0, 0],
- [0, 0, 1, 0],
- [0, 0, 0, 1],
-]
-
-// Create a matrix that will translate a
-// coordinate in the x, y, and z dimensions
-//
-// x, y, and z: The amount to translate by (number)
-//
-// returns a matrix (2d array of numbers)
export const translate = (x, y, z) => [
[1, 0, 0, 0],
[0, 1, 0, 0],
@@ -46,12 +35,6 @@ export const translate = (x, y, z) => [
[x, y, z, 1],
]
-// Create a matrix that will scale a coordinate
-// in the x, y, and z dimensions
-//
-// x, y, and z: the amount to scale by (number)
-//
-// returns a matrix (2d array of numbers)
export const scale = (x, y = x, z = x) => [
[x, 0, 0, 0],
[0, y, 0, 0],
@@ -59,12 +42,6 @@ export const scale = (x, y = x, z = x) => [
[0, 0, 0, 1],
]
-// Create a matrix that will rotate a coordinate
-// along the x, y, and z axes
-//
-// x, y, and z: the amount to rotate by (number)
-//
-// returns a matrix (2d array of numbers)
export const rotate = (x, y, z) => {
// X rotation
@@ -105,13 +82,6 @@ export const rotate = (x, y, z) => {
return matrixMult(matrixMult(xMat, yMat), zMat)
}
-// Creates a matrix that can project a matrix
-// in 3d space to create a sense of perspective
-//
-// scale: Intensity of the effect
-// aspect: Ratio of the width and height for the viewport
-//
-// returns a matrix (2d array of numbers)
export const project = (scale, aspect) => [
[aspect > 1 ? 1 / aspect : 1, 0, 0, 0],
[0, aspect < 1 ? aspect : 1, 0, 0],
diff --git a/frontend/js/modules/model-utils.js b/frontend/js/modules/model-utils.js
index 33e9f32..ba96ca6 100644
--- a/frontend/js/modules/model-utils.js
+++ b/frontend/js/modules/model-utils.js
@@ -1,10 +1,11 @@
+// TODO: fix this so that exterior is not always true
export const extrudePoints = (points, layers) => Array(layers).fill(0)
.map((_, z) => points
.map((row, y) => row
.map((p, x) => ({
...p,
z,
- exterior: // TODO: fix this so that exterior is not always true
+ exterior:
!points[0][x - 1]?.[y]
|| !points[0][x + 1]?.[y]
|| !points[0][x]?.[y - 1]
diff --git a/frontend/js/modules/scene.js b/frontend/js/modules/scene.js
index 902514e..d9dd412 100644
--- a/frontend/js/modules/scene.js
+++ b/frontend/js/modules/scene.js
@@ -106,7 +106,7 @@ export default class Scene {
return this
}
-
+
/**
* Draws lines using the given positions and colors.
* @param {Float32Array} positions - The vertex positions.
diff --git a/frontend/js/new.js b/frontend/js/new.js
new file mode 100644
index 0000000..ff1c4ee
--- /dev/null
+++ b/frontend/js/new.js
@@ -0,0 +1,306 @@
+import { $ } from './modules/common.js'
+import Scene from './modules/scene.js'
+import { svg, path, svg2points } from './modules/svg-lib.js'
+import { triangle, invertedTriangle, arrangement } from './modules/gates.js'
+import {
+ extrudePoints,
+ createTetrahedrons,
+ drawModel,
+ centerScene,
+ arrangeModel,
+} from './modules/model-utils.js'
+
+const scene = new Scene()
+ .project(2, $('canvas').width / $('canvas').height)
+
+/**
+ * @typedef {Object} Point
+ * @property {boolean} exterior - If this point is visible when rendering.
+ * @property {number} x - The x coordinate of the point.
+ * @property {number} y - The y coordinate of the point.
+ * @property {number} z - The z coordinate of the point.
+ */
+
+/**
+ * @type {Array>}
+ * An array of tetrahedrons, where each tetrahedron is an
+ * array of 4 point objects. Each point is an object with
+ * `x`, `y`, and `z` properties representing the 3D coordinates.
+*/
+let tetrahedrons = []
+
+const renderMesh = async (positionGrid) => {
+ const componentModel = await (await fetch(`${location.origin}/res/triangle.json`)).json()
+ tetrahedrons = arrangeModel(positionGrid, componentModel)
+ centerScene(scene, tetrahedrons)
+ drawModel(scene, tetrahedrons)
+}
+
+// Triangle grid controls
+
+const toggleControls = ({ target }) => {
+ if (target.classList.contains('selected')) {
+ target.classList.remove('selected')
+ $('#show-controls-target').classList.add('hidden')
+ } else {
+ target.classList.add('selected')
+ $('#show-controls-target').classList.remove('hidden')
+ }
+}
+
+const getPositionGrid = () => {
+ let isEvenRow = false
+ const positionGrid = [[]]
+
+ Array.from($('#triangle-grid').children).forEach(c => {
+ if (isEvenRow != c.classList.contains('even-row')) {
+ positionGrid.unshift([])
+ isEvenRow = c.classList.contains('even-row')
+ }
+
+ positionGrid[0]
+ .push(c.classList.contains('selected'))
+ })
+
+ return positionGrid
+}
+
+const toggleTriangle = ({ target }) => {
+ target.classList.contains('selected')
+ ? target.classList.remove('selected')
+ : target.classList.add('selected')
+
+ renderMesh(getPositionGrid())
+}
+
+const makeTriangleGrid = (positionGrid) => {
+ const rows = positionGrid.length
+ const cols = positionGrid[0].length
+
+ $('#triangle-grid').innerHTML = '';
+ $('#triangle-grid').style['grid-template-columns'] =
+ Array(cols).fill(0).map(_ => 'auto').join(' ')
+
+ // Rows are read backwards to match the drawing coordinate system
+ for (let i = rows - 1; i >= 0; i--) {
+ for (let j = 0; j < cols; j++) {
+ const cell = document.createElement('div')
+
+ cell.classList.add(...[
+ 'triangle',
+ (i + 1) % 2 == 0 && 'even-row',
+ positionGrid[i][j] && 'selected',
+ ].filter(c => c))
+
+ cell.onclick = toggleTriangle
+
+ $('#triangle-grid').appendChild(cell)
+ }
+ }
+
+ renderMesh(positionGrid)
+}
+
+const changeNumRows = ({ target }) => {
+ if (target.value < 1) target.value = 1
+
+ const rows = target.value
+ const cols = $('#cols-input').value
+ const positionGrid = getPositionGrid()
+
+ while (positionGrid.length > rows)
+ positionGrid.splice(-1)
+
+ while (positionGrid.length < rows)
+ positionGrid.push(Array(cols).fill(0))
+
+ makeTriangleGrid(positionGrid)
+}
+
+const changeNumCols = ({ target }) => {
+ if (target.value < 1) target.value = 1
+
+ const rows = $('#rows-input').value
+ const cols = target.value
+ const positionGrid = getPositionGrid()
+
+ while (positionGrid[0].length > cols)
+ positionGrid.forEach(row => row.splice(-1))
+
+ while (positionGrid[0].length < cols)
+ positionGrid.forEach(row => row.push(0))
+
+ makeTriangleGrid(positionGrid)
+}
+
+/**
+ * Onclick function for converting tetrahedrons to Nmesh file.
+ * I haven't stress tested it, but it should fail when we have around a couple
+ * million tetrahedrons. More efficient approaches will have to be considered then.
+ * https://nmag.readthedocs.io/en/latest/finite_element_mesh_generation.html#nmesh-file-format
+ * @param {Event} e
+ */
+const downloadNmeshFile = (e) => {
+ $('#download-nmesh-file').innerHTML = 'Generating...'
+ // Delimiter to use for point -> key generation
+ const DELIM = ','
+
+ /**
+ * @param {Point} point
+ */
+ const generateKeyFromPoint = (point) => `${point.x}${DELIM}${point.y}${DELIM}${point.z}`;
+
+ /** @type {Array}
+ * An array to store unique nodes (points).
+ */
+ const nodes = []
+
+ /**
+ * @type {Map}
+ * A map to track unique nodes by their stringified coordinates, mapping to a node index.
+ */
+ const nodeMap = new Map()
+
+ /** @type {number}
+ * The index to assign to the next unique node.
+ */
+ let nodeIndex = 0
+
+ /** @type {Array>}
+ * An array to store tetrahedrons as arrays of node indices.
+ * The inner array is always of length 4 and contains the node index for each point.
+ */
+ const simplices = []
+
+ // Generate nodes and unique indexes for the node map
+ tetrahedrons.forEach((tetrahedron) => {
+ tetrahedron.forEach((point) => {
+ const key = generateKeyFromPoint(point)
+ if(!nodeMap.has(key)) {
+ nodeMap.set(key, nodeIndex)
+ nodes.push(point)
+ nodeIndex++
+ }
+ })
+ })
+
+ // Generate simplices
+ tetrahedrons.forEach((tetrahedron) => {
+ const tetrahedronAsNodeIndexes = tetrahedron.map((point) => nodeMap.get(generateKeyFromPoint(point)))
+ simplices.push(tetrahedronAsNodeIndexes);
+ })
+
+ /** @type {string} The content of the `.nmesh` file as a string. */
+ let nmeshFileContent = '# PYFEM mesh file version 1.0\n'
+
+ // TODO: If we aren't including surfaces in our file, do we need to include it here?
+ nmeshFileContent += `# dim = 3 nodes = ${nodes.length} simplices = ${simplices.length} periodic = 0\n`
+
+ nmeshFileContent += `${nodes.length}\n`;
+ nodes.forEach((node, i) => {
+ nmeshFileContent += ` ${node.x} ${node.y} ${node.z}\n`
+ })
+
+ nmeshFileContent += `${simplices.length}\n`;
+ simplices.forEach((simplex) => {
+ nmeshFileContent += ` 1 ${simplex.join(' ')}\n`; // Format simplex in NMesh format
+ });
+
+ const nmeshFileBlob = new Blob([nmeshFileContent], {type: 'text/plain'})
+
+ const a = document.createElement('a')
+ a.href = URL.createObjectURL(nmeshFileBlob)
+ a.download = 'generated.nmesh'
+ a.style.display = 'none'
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+
+ $('#download-nmesh-file').innerHTML = 'Download Nmesh File'
+};
+
+$('#show-controls-button').onclick = toggleControls
+$('#rows-input').onclick = changeNumRows
+$('#rows-input').onblur = changeNumRows
+$('#rows-input').onkeypress = (e) => e.key == 'Enter' ? changeNumRows(e) : e
+$('#cols-input').onclick = changeNumCols
+$('#cols-input').onblur = changeNumCols
+$('#cols-input').onkeypress = (e) => e.key == 'Enter' ? changeNumCols(e) : e
+$('#download-nmesh-file').onclick = downloadNmeshFile
+
+// Initial state
+
+$('#rows-input').value = 3
+$('#cols-input').value = 3
+
+makeTriangleGrid([
+ [0, 1, 0],
+ [0, 1, 0],
+ [1, 0, 1],
+])
+
+// Limit rendering calls to improve efficiency
+
+const timeBetweenDraws = 30
+let lastDrawTime = 0
+
+const canDraw = () => {
+ if (Date.now() - lastDrawTime < timeBetweenDraws) {
+ return false
+ }
+
+ lastDrawTime = Date.now()
+
+ return true
+}
+
+// Tranformation controls
+
+$('main').onmousedown = () => $('main').isClicked = true
+$('main').onmouseup = () => $('main').isClicked = false
+$('main').onmouseout = () => $('main').isClicked = false
+
+$('main').onmousemove = e => {
+ if (!$('main').isClicked) return
+ if (!canDraw()) return
+
+ scene.clear()
+
+ // Click and drag to rotate
+ !e.shiftKey && scene.rotate(
+ Math.PI * e.movementY / 100,
+ Math.PI * e.movementX / 100,
+ 0,
+ )
+
+ // Shift-Click and drag to translate
+ e.shiftKey && scene.translate(
+ 4.5 * e.movementX / $('main').clientWidth,
+ -4.5 * e.movementY / $('main').clientHeight,
+ 0,
+ )
+
+ drawModel(scene, tetrahedrons)
+}
+
+// Mouse-wheel to scale
+$('main').onwheel = e => {
+ if (!canDraw()) return
+
+ scene.clear()
+ scene.scale(e.deltaY > 0 ? 1.15 : 0.85)
+ drawModel(scene, tetrahedrons)
+}
+
+// Resize canvas when it's containers size changes
+setInterval(() => {
+ if (
+ $('canvas').width == $('main').clientWidth
+ && $('canvas').height == $('main').clientHeight
+ ) return
+
+ scene
+ .resizeCanvas($('main').clientWidth, $('main').clientHeight)
+ .project(2, $('canvas').width / $('canvas').height)
+ drawModel(scene, tetrahedrons)
+}, 100)
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..9f29865
--- /dev/null
+++ b/server.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+import http.server
+import socketserver
+import os
+
+PORT = 1234
+HTML_FOLDER_PATH = './frontend'
+
+class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=HTML_FOLDER_PATH, **kwargs)
+
+ def do_GET(self):
+ if self.path == '/':
+ self.send_response(301)
+ self.send_header('Location', '/index.html')
+ self.end_headers()
+ else:
+ super().do_GET()
+
+with socketserver.TCPServer(("", PORT), CustomHTTPRequestHandler) as httpd:
+ print(f"Serving on port {PORT} from {os.path.abspath(HTML_FOLDER_PATH)}")
+ httpd.serve_forever()