diff --git a/.github/workflows/deploy-action.yml b/.github/workflows/deploy-action.yml new file mode 100644 index 0000000..903f15a --- /dev/null +++ b/.github/workflows/deploy-action.yml @@ -0,0 +1,50 @@ +name: Deploy Fold website + +on: + # Runs on pushes targeting the default branch + push: + branches: ['main'] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: 'pages' + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: 'npm' + - name: Install dependencies + run: npm install + - name: Build + run: npm run build + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + # Upload dist repository + path: './dist' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore index 56f7029..fc1040b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# Compiled .js -lib +dist # Logs *.log diff --git a/.npmignore b/.npmignore index 52b39a3..343a07f 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,3 @@ -# Compiled .js -lib - # Logs *.log npm-debug.log* diff --git a/bin/fold-convert.js b/bin/fold-convert.js index cf915a4..7d778aa 100755 --- a/bin/fold-convert.js +++ b/bin/fold-convert.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('..').file.main() +require('../file').main(); diff --git a/dist/fold.js b/dist/fold.js deleted file mode 100644 index 8164212..0000000 --- a/dist/fold.js +++ /dev/null @@ -1,3511 +0,0 @@ -require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0) { - //console.log face - fold.faces_vertices.push(face); - } - } - //else - // console.log face, 'clockwise' - return fold; -}; - -convert.vertices_edges_to_faces_vertices_edges = function(fold) { - /* - Given a FOLD object with counterclockwise-sorted `vertices_edges` property, - constructs the implicitly defined faces, setting both `faces_vertices` - and `faces_edges` properties. Handles multiple edges to the same vertex - (unlike `FOLD.convert.vertices_vertices_to_faces_vertices`). - */ - var e, e1, e2, edges, i, j, k, l, len, len1, len2, len3, m, neighbors, next, nexts, ref, ref1, v, vertex, vertices, x; - next = []; - ref = fold.vertices_edges; - for (v = j = 0, len = ref.length; j < len; v = ++j) { - neighbors = ref[v]; - next[v] = {}; - for (i = k = 0, len1 = neighbors.length; k < len1; i = ++k) { - e = neighbors[i]; - next[v][e] = neighbors[modulo(i - 1, neighbors.length)]; - } - } - //console.log e, neighbors[(i-1) %% neighbors.length] - fold.faces_vertices = []; - fold.faces_edges = []; - for (vertex = l = 0, len2 = next.length; l < len2; vertex = ++l) { - nexts = next[vertex]; - for (e1 in nexts) { - e2 = nexts[e1]; - if (e2 == null) { - continue; - } - e1 = parseInt(e1); - nexts[e1] = null; - edges = [e1]; - vertices = [filter.edges_verticesIncident(fold.edges_vertices[e1], fold.edges_vertices[e2])]; - if (vertices[0] == null) { - throw new Error(`Confusion at edges ${e1} and ${e2}`); - } - while (e2 !== edges[0]) { - if (e2 == null) { - console.warn(`Confusion with face containing edges ${edges}`); - break; - } - edges.push(e2); - ref1 = fold.edges_vertices[e2]; - for (m = 0, len3 = ref1.length; m < len3; m++) { - v = ref1[m]; - if (v !== vertices[vertices.length - 1]) { - vertices.push(v); - break; - } - } - e1 = e2; - e2 = next[v][e1]; - next[v][e1] = null; - } - //# Move e1 to the end so that edges[0] connects vertices[0] to vertices[1] - edges.push(edges.shift()); - //# Outside face is clockwise; exclude it. - if ((e2 != null) && geom.polygonOrientation((function() { - var len4, n, results; - results = []; - for (n = 0, len4 = vertices.length; n < len4; n++) { - x = vertices[n]; - results.push(fold.vertices_coords[x]); - } - return results; - })()) > 0) { - //console.log vertices, edges - fold.faces_vertices.push(vertices); - fold.faces_edges.push(edges); - } - } - } - //else - // console.log face, 'clockwise' - return fold; -}; - -convert.edges_vertices_to_faces_vertices = function(fold) { - /* - Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, - computes a counterclockwise-sorted `vertices_vertices` property and - constructs the implicitly defined faces, setting `faces_vertices` property. - */ - convert.edges_vertices_to_vertices_vertices_sorted(fold); - return convert.vertices_vertices_to_faces_vertices(fold); -}; - -convert.edges_vertices_to_faces_vertices_edges = function(fold) { - /* - Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, - computes counterclockwise-sorted `vertices_vertices` and `vertices_edges` - properties and constructs the implicitly defined faces, setting - both `faces_vertices` and `faces_edges` property. - */ - convert.edges_vertices_to_vertices_edges_sorted(fold); - return convert.vertices_edges_to_faces_vertices_edges(fold); -}; - -convert.vertices_vertices_to_vertices_edges = function(fold) { - /* - Given a FOLD object with `vertices_vertices` and `edges_vertices`, - fills in the corresponding `vertices_edges` property (preserving order). - */ - var edge, edgeMap, i, j, len, ref, v1, v2, vertex, vertices; - edgeMap = {}; - ref = fold.edges_vertices; - for (edge = j = 0, len = ref.length; j < len; edge = ++j) { - [v1, v2] = ref[edge]; - edgeMap[`${v1},${v2}`] = edge; - edgeMap[`${v2},${v1}`] = edge; - } - return fold.vertices_edges = (function() { - var k, len1, ref1, results; - ref1 = fold.vertices_vertices; - results = []; - for (vertex = k = 0, len1 = ref1.length; k < len1; vertex = ++k) { - vertices = ref1[vertex]; - results.push((function() { - var l, ref2, results1; - results1 = []; - for (i = l = 0, ref2 = vertices.length; (0 <= ref2 ? l < ref2 : l > ref2); i = 0 <= ref2 ? ++l : --l) { - results1.push(edgeMap[`${vertex},${vertices[i]}`]); - } - return results1; - })()); - } - return results; - })(); -}; - -convert.faces_vertices_to_faces_edges = function(fold) { - /* - Given a FOLD object with `faces_vertices` and `edges_vertices`, - fills in the corresponding `faces_edges` property (preserving order). - */ - var edge, edgeMap, face, i, j, len, ref, v1, v2, vertices; - edgeMap = {}; - ref = fold.edges_vertices; - for (edge = j = 0, len = ref.length; j < len; edge = ++j) { - [v1, v2] = ref[edge]; - edgeMap[`${v1},${v2}`] = edge; - edgeMap[`${v2},${v1}`] = edge; - } - return fold.faces_edges = (function() { - var k, len1, ref1, results; - ref1 = fold.faces_vertices; - results = []; - for (face = k = 0, len1 = ref1.length; k < len1; face = ++k) { - vertices = ref1[face]; - results.push((function() { - var l, ref2, results1; - results1 = []; - for (i = l = 0, ref2 = vertices.length; (0 <= ref2 ? l < ref2 : l > ref2); i = 0 <= ref2 ? ++l : --l) { - results1.push(edgeMap[`${vertices[i]},${vertices[(i + 1) % vertices.length]}`]); - } - return results1; - })()); - } - return results; - })(); -}; - -convert.faces_vertices_to_edges = function(mesh) { - var edge, edgeMap, face, i, key, ref, v1, v2, vertices; - /* - Given a FOLD object with just `faces_vertices`, automatically fills in - `edges_vertices`, `edges_faces`, `faces_edges`, and `edges_assignment` - (indicating which edges are boundary with 'B'). - This code currently assumes an orientable manifold, and uses nulls to - represent missing neighbor faces in `edges_faces` (for boundary edges). - */ - mesh.edges_vertices = []; - mesh.edges_faces = []; - mesh.faces_edges = []; - mesh.edges_assignment = []; - edgeMap = {}; - ref = mesh.faces_vertices; - for (face in ref) { - vertices = ref[face]; - face = parseInt(face); - mesh.faces_edges.push((function() { - var j, len, results; - results = []; - for (i = j = 0, len = vertices.length; j < len; i = ++j) { - v1 = vertices[i]; - v1 = parseInt(v1); - v2 = vertices[(i + 1) % vertices.length]; - if (v1 <= v2) { - key = `${v1},${v2}`; - } else { - key = `${v2},${v1}`; - } - if (key in edgeMap) { - edge = edgeMap[key]; - // Second instance of edge means not on boundary - mesh.edges_assignment[edge] = null; - } else { - edge = edgeMap[key] = mesh.edges_vertices.length; - if (v1 <= v2) { - mesh.edges_vertices.push([v1, v2]); - } else { - mesh.edges_vertices.push([v2, v1]); - } - mesh.edges_faces.push([null, null]); - // First instance of edge might be on boundary - mesh.edges_assignment.push('B'); - } - if (v1 <= v2) { - mesh.edges_faces[edge][0] = face; - } else { - mesh.edges_faces[edge][1] = face; - } - results.push(edge); - } - return results; - })()); - } - return mesh; -}; - -convert.edges_vertices_to_edges_faces_edges = function(fold) { - var edge, edgeMap, face, i, orient, ref, ref1, v1, v2, vertices; - /* - Given a `fold` object with `edges_vertices` and `faces_vertices`, - fills in `faces_edges` and `edges_vertices`. - */ - fold.edges_faces = (function() { - var j, ref, results; - results = []; - for (edge = j = 0, ref = fold.edges_vertices.length; (0 <= ref ? j < ref : j > ref); edge = 0 <= ref ? ++j : --j) { - results.push([null, null]); - } - return results; - })(); - edgeMap = {}; - ref = fold.edges_vertices; - for (edge in ref) { - vertices = ref[edge]; - if (!(vertices != null)) { - continue; - } - edge = parseInt(edge); - edgeMap[`${vertices[0]},${vertices[1]}`] = [ - edge, - 0 // forward - ]; - edgeMap[`${vertices[1]},${vertices[0]}`] = [ - edge, - 1 // backward - ]; - } - ref1 = fold.faces_vertices; - for (face in ref1) { - vertices = ref1[face]; - face = parseInt(face); - fold.faces_edges[face] = (function() { - var j, len, results; - results = []; - for (i = j = 0, len = vertices.length; j < len; i = ++j) { - v1 = vertices[i]; - v2 = vertices[(i + 1) % vertices.length]; - [edge, orient] = edgeMap[`${v1},${v2}`]; - fold.edges_faces[edge][orient] = face; - results.push(edge); - } - return results; - })(); - } - return fold; -}; - -convert.flatFoldedGeometry = function(fold, rootFace = 0) { - var base, edge, edge2, face, face2, i, j, k, l, len, len1, len2, len3, len4, len5, len6, len7, level, m, mapped, maxError, n, nextLevel, o, orientation, p, q, ref, ref1, ref2, ref3, ref4, ref5, ref6, row, transform, vertex, vertex2; - /* - Assuming `fold` is a locally flat foldable crease pattern in the xy plane, - sets `fold.vertices_flatFoldCoords` to give the flat-folded geometry - as determined by repeated reflection relative to `rootFace`; sets - `fold.faces_flatFoldTransform` transformation matrix mapping each face's - unfolded --> folded geometry; and sets `fold.faces_flatFoldOrientation` to - +1 or -1 to indicate whether each folded face matches its original - orientation or is upside-down (so is oriented clockwise in 2D). - - Requires `fold` to have `vertices_coords` and `edges_vertices`; - `edges_faces` and `faces_edges` will be created if they do not exist. - - Returns the maximum displacement error from closure constraints (multiple - mappings of the same vertices, or multiple transformations of the same face). - */ - if ((fold.vertices_coords != null) && (fold.edges_vertices != null) && !((fold.edges_faces != null) && (fold.faces_edges != null))) { - convert.edges_vertices_to_edges_faces_edges(fold); - } - maxError = 0; - level = [rootFace]; - fold.faces_flatFoldTransform = (function() { - var j, ref, results; - results = []; - for (face = j = 0, ref = fold.faces_edges.length; (0 <= ref ? j < ref : j > ref); face = 0 <= ref ? ++j : --j) { - results.push(null); - } - return results; - })(); - fold.faces_flatFoldTransform[rootFace] = [ - [1, - 0, - 0], - [ - 0, - 1, - 0 // identity - ] - ]; - fold.faces_flatFoldOrientation = (function() { - var j, ref, results; - results = []; - for (face = j = 0, ref = fold.faces_edges.length; (0 <= ref ? j < ref : j > ref); face = 0 <= ref ? ++j : --j) { - results.push(null); - } - return results; - })(); - fold.faces_flatFoldOrientation[rootFace] = +1; - fold.vertices_flatFoldCoords = (function() { - var j, ref, results; - results = []; - for (vertex = j = 0, ref = fold.vertices_coords.length; (0 <= ref ? j < ref : j > ref); vertex = 0 <= ref ? ++j : --j) { - results.push(null); - } - return results; - })(); - ref = fold.faces_edges[rootFace]; - // Use fold.faces_edges -> fold.edges_vertices, which are both needed below, - // in case fold.faces_vertices isn't defined. - for (j = 0, len = ref.length; j < len; j++) { - edge = ref[j]; - ref1 = fold.edges_vertices[edge]; - for (k = 0, len1 = ref1.length; k < len1; k++) { - vertex = ref1[k]; - if ((base = fold.vertices_flatFoldCoords)[vertex] == null) { - base[vertex] = fold.vertices_coords[vertex].slice(0); - } - } - } - while (level.length) { - nextLevel = []; - for (l = 0, len2 = level.length; l < len2; l++) { - face = level[l]; - orientation = -fold.faces_flatFoldOrientation[face]; - ref2 = fold.faces_edges[face]; - for (m = 0, len3 = ref2.length; m < len3; m++) { - edge = ref2[m]; - ref3 = fold.edges_faces[edge]; - for (n = 0, len4 = ref3.length; n < len4; n++) { - face2 = ref3[n]; - if (!((face2 != null) && face2 !== face)) { - continue; - } - transform = geom.matrixMatrix(fold.faces_flatFoldTransform[face], geom.matrixReflectLine(...((function() { - var len5, o, ref4, results; - ref4 = fold.edges_vertices[edge]; - results = []; - for (o = 0, len5 = ref4.length; o < len5; o++) { - vertex = ref4[o]; - results.push(fold.vertices_coords[vertex]); - } - return results; - })()))); - if (fold.faces_flatFoldTransform[face2] != null) { - ref4 = fold.faces_flatFoldTransform[face2]; - for (i = o = 0, len5 = ref4.length; o < len5; i = ++o) { - row = ref4[i]; - maxError = Math.max(maxError, geom.dist(row, transform[i])); - } - if (orientation !== fold.faces_flatFoldOrientation[face2]) { - maxError = Math.max(1, maxError); - } - } else { - fold.faces_flatFoldTransform[face2] = transform; - fold.faces_flatFoldOrientation[face2] = orientation; - ref5 = fold.faces_edges[face2]; - for (p = 0, len6 = ref5.length; p < len6; p++) { - edge2 = ref5[p]; - ref6 = fold.edges_vertices[edge2]; - for (q = 0, len7 = ref6.length; q < len7; q++) { - vertex2 = ref6[q]; - mapped = geom.matrixVector(transform, fold.vertices_coords[vertex2]); - if (fold.vertices_flatFoldCoords[vertex2] != null) { - maxError = Math.max(maxError, geom.dist(fold.vertices_flatFoldCoords[vertex2], mapped)); - } else { - fold.vertices_flatFoldCoords[vertex2] = mapped; - } - } - } - nextLevel.push(face2); - } - } - } - } - level = nextLevel; - } - return maxError; -}; - -convert.deepCopy = function(fold) { - var copy, item, j, key, len, ref, results, value; - //# Given a FOLD object, make a copy that shares no pointers with the original. - if ((ref = typeof fold) === 'number' || ref === 'string' || ref === 'boolean') { - return fold; - } else if (Array.isArray(fold)) { - results = []; - for (j = 0, len = fold.length; j < len; j++) { - item = fold[j]; - results.push(convert.deepCopy(item)); // Object - } - return results; - } else { - copy = {}; - for (key in fold) { - if (!hasProp.call(fold, key)) continue; - value = fold[key]; - copy[key] = convert.deepCopy(value); - } - return copy; - } -}; - -convert.toJSON = function(fold) { - var key, obj, value; - //# Convert FOLD object into a nicely formatted JSON string. - return "{\n" + ((function() { - var results; - results = []; - for (key in fold) { - value = fold[key]; - results.push(` ${JSON.stringify(key)}: ` + (Array.isArray(value) ? "[\n" + ((function() { - var j, len, results1; - results1 = []; - for (j = 0, len = value.length; j < len; j++) { - obj = value[j]; - results1.push(` ${JSON.stringify(obj)}`); - } - return results1; - })()).join(',\n') + "\n ]" : JSON.stringify(value))); - } - return results; - })()).join(',\n') + "\n}\n"; -}; - -convert.extensions = {}; - -convert.converters = {}; - -convert.getConverter = function(fromExt, toExt) { - if (fromExt === toExt) { - return function(x) { - return x; - }; - } else { - return convert.converters[`${fromExt}${toExt}`]; - } -}; - -convert.setConverter = function(fromExt, toExt, converter) { - convert.extensions[fromExt] = true; - convert.extensions[toExt] = true; - return convert.converters[`${fromExt}${toExt}`] = converter; -}; - -convert.convertFromTo = function(data, fromExt, toExt) { - var converter; - if (fromExt[0] !== '.') { - fromExt = `.${fromExt}`; - } - if (toExt[0] !== '.') { - toExt = `.${toExt}`; - } - converter = convert.getConverter(fromExt, toExt); - if (converter == null) { - if (fromExt === toExt) { - return data; - } - throw new Error(`No converter from ${fromExt} to ${toExt}`); - } - return converter(data); -}; - -convert.convertFrom = function(data, fromExt) { - return convert.convertFromTo(data, fromExt, '.fold'); -}; - -convert.convertTo = function(data, toExt) { - return convert.convertFromTo(data, '.fold', toExt); -}; - -convert.oripa = require('./oripa'); - - -},{"./filter":3,"./geom":4,"./oripa":5}],3:[function(require,module,exports){ -var RepeatedPointsDS, filter, geom, - indexOf = [].indexOf; - -geom = require('./geom'); - -filter = exports; - -filter.edgesAssigned = function(fold, target) { - var assignment, i, k, len, ref, results; - ref = fold.edges_assignment; - results = []; - for (i = k = 0, len = ref.length; k < len; i = ++k) { - assignment = ref[i]; - if (assignment === target) { - results.push(i); - } - } - return results; -}; - -filter.mountainEdges = function(fold) { - return filter.edgesAssigned(fold, 'M'); -}; - -filter.valleyEdges = function(fold) { - return filter.edgesAssigned(fold, 'V'); -}; - -filter.flatEdges = function(fold) { - return filter.edgesAssigned(fold, 'F'); -}; - -filter.boundaryEdges = function(fold) { - return filter.edgesAssigned(fold, 'B'); -}; - -filter.unassignedEdges = function(fold) { - return filter.edgesAssigned(fold, 'U'); -}; - -filter.cutEdges = function(fold) { - return filter.edgesAssigned(fold, 'C'); -}; - -filter.joinEdges = function(fold) { - return filter.edgesAssigned(fold, 'J'); -}; - -filter.keysStartingWith = function(fold, prefix) { - var key, results; - results = []; - for (key in fold) { - if (key.slice(0, prefix.length) === prefix) { - results.push(key); - } - } - return results; -}; - -filter.keysEndingWith = function(fold, suffix) { - var key, results; - results = []; - for (key in fold) { - if (key.slice(-suffix.length) === suffix) { - results.push(key); - } - } - return results; -}; - -filter.remapField = function(fold, field, old2new) { - /* - old2new: null means throw away that object - */ - var array, i, j, k, key, l, len, len1, len2, m, new2old, old, ref, ref1; - new2old = []; -//# later overwrites earlier - for (i = k = 0, len = old2new.length; k < len; i = ++k) { - j = old2new[i]; - if (j != null) { - new2old[j] = i; - } - } - ref = filter.keysStartingWith(fold, `${field}_`); - for (l = 0, len1 = ref.length; l < len1; l++) { - key = ref[l]; - fold[key] = (function() { - var len2, m, results; - results = []; - for (m = 0, len2 = new2old.length; m < len2; m++) { - old = new2old[m]; - results.push(fold[key][old]); - } - return results; - })(); - } - ref1 = filter.keysEndingWith(fold, `_${field}`); - for (m = 0, len2 = ref1.length; m < len2; m++) { - key = ref1[m]; - fold[key] = (function() { - var len3, n, ref2, results; - ref2 = fold[key]; - results = []; - for (n = 0, len3 = ref2.length; n < len3; n++) { - array = ref2[n]; - results.push((function() { - var len4, o, results1; - results1 = []; - for (o = 0, len4 = array.length; o < len4; o++) { - old = array[o]; - results1.push(old2new[old]); - } - return results1; - })()); - } - return results; - })(); - } - return fold; -}; - -filter.remapFieldSubset = function(fold, field, keep) { - var id, old2new, value; - id = 0; - old2new = (function() { - var k, len, results; - results = []; - for (k = 0, len = keep.length; k < len; k++) { - value = keep[k]; - if (value) { - results.push(id++); - } else { - results.push(null); //# remove - } - } - return results; - })(); - filter.remapField(fold, field, old2new); - return old2new; -}; - -filter.remove = function(fold, field, index) { - var i; - /* - Remove given index from given field ('vertices', 'edges', 'faces'), in place. - */ - return filter.remapFieldSubset(fold, field, (function() { - var k, ref, results; - results = []; - for (i = k = 0, ref = filter.numType(fold, field); (0 <= ref ? k < ref : k > ref); i = 0 <= ref ? ++k : --k) { - results.push(i !== index); - } - return results; - })()); -}; - -filter.removeVertex = function(fold, index) { - return filter.remove(fold, 'vertices', index); -}; - -filter.removeEdge = function(fold, index) { - return filter.remove(fold, 'edges', index); -}; - -filter.removeFace = function(fold, index) { - return filter.remove(fold, 'faces', index); -}; - -filter.transform = function(fold, matrix) { - var coords, k, key, l, len, len1, ref, ref1, transform; - ref = filter.keysEndingWith(fold, "_coords"); - /* - Transforms all fields ending in _coords (in particular, vertices_coords) - and all fields ending in FoldTransform (in particular, - faces_flatFoldTransform generated by convert.flat_folded_geometry) - according to the given transformation matrix. - */ - for (k = 0, len = ref.length; k < len; k++) { - key = ref[k]; - fold[key] = (function() { - var l, len1, ref1, results; - ref1 = fold[key]; - results = []; - for (l = 0, len1 = ref1.length; l < len1; l++) { - coords = ref1[l]; - results.push(geom.matrixVector(matrix, coords)); - } - return results; - })(); - } - ref1 = filter.keysEndingWith(fold, "FoldTransform"); - for (l = 0, len1 = ref1.length; l < len1; l++) { - key = ref1[l]; - if (indexOf.call(key, '_') >= 0) { - fold[key] = (function() { - var len2, m, ref2, results; - ref2 = fold[key]; - results = []; - for (m = 0, len2 = ref2.length; m < len2; m++) { - transform = ref2[m]; - results.push(geom.matrixMatrix(matrix, transform)); - } - return results; - })(); - } - } - return fold; -}; - -filter.numType = function(fold, type) { - /* - Count the maximum number of objects of a given type, by looking at all - fields with key of the form `type_...`, and if that fails, looking at all - fields with key of the form `..._type`. Returns `0` if nothing found. - */ - var counts, key, value; - counts = (function() { - var k, len, ref, results; - ref = filter.keysStartingWith(fold, `${type}_`); - results = []; - for (k = 0, len = ref.length; k < len; k++) { - key = ref[k]; - value = fold[key]; - if (value.length == null) { - continue; - } - results.push(value.length); - } - return results; - })(); - if (!counts.length) { - counts = (function() { - var k, len, ref, results; - ref = filter.keysEndingWith(fold, `_${type}`); - results = []; - for (k = 0, len = ref.length; k < len; k++) { - key = ref[k]; - results.push(1 + Math.max(...fold[key])); - } - return results; - })(); - } - if (counts.length) { - return Math.max(...counts); - } else { - return 0; //# nothing of this type - } -}; - -filter.numVertices = function(fold) { - return filter.numType(fold, 'vertices'); -}; - -filter.numEdges = function(fold) { - return filter.numType(fold, 'edges'); -}; - -filter.numFaces = function(fold) { - return filter.numType(fold, 'faces'); -}; - -filter.removeDuplicateEdges_vertices = function(fold) { - var edge, id, key, old2new, seen, v, w; - seen = {}; - id = 0; - old2new = (function() { - var k, len, ref, results; - ref = fold.edges_vertices; - results = []; - for (k = 0, len = ref.length; k < len; k++) { - edge = ref[k]; - [v, w] = edge; - if (v < w) { - key = `${v},${w}`; - } else { - key = `${w},${v}`; - } - if (!(key in seen)) { - seen[key] = id; - id += 1; - } - results.push(seen[key]); - } - return results; - })(); - filter.remapField(fold, 'edges', old2new); - return old2new; -}; - -filter.edges_verticesIncident = function(e1, e2) { - var k, len, v; - for (k = 0, len = e1.length; k < len; k++) { - v = e1[k]; - if (indexOf.call(e2, v) >= 0) { - return v; - } - } - return null; -}; - -//# Use hashing to find points within an epsilon > 0 distance from each other. -//# Each integer cell will have O(1) distinct points before matching -//# (number of disjoint half-unit disks that fit in a unit square). -RepeatedPointsDS = class RepeatedPointsDS { - constructor(vertices_coords, epsilon1) { - var base, coord, k, len, name, ref, v; - this.vertices_coords = vertices_coords; - this.epsilon = epsilon1; - //# Note: if vertices_coords has some duplicates in the initial state, - //# then we will detect them but won't remove them here. Rather, - //# future duplicate inserts will return the higher-index vertex. - this.hash = {}; - ref = this.vertices_coords; - for (v = k = 0, len = ref.length; k < len; v = ++k) { - coord = ref[v]; - ((base = this.hash)[name = this.key(coord)] != null ? base[name] : base[name] = []).push(v); - } - null; - } - - lookup(coord) { - var k, key, l, len, len1, len2, m, ref, ref1, ref2, ref3, v, x, xr, xt, y, yr, yt; - [x, y] = coord; - xr = Math.round(x / this.epsilon); - yr = Math.round(y / this.epsilon); - ref = [xr, xr - 1, xr + 1]; - for (k = 0, len = ref.length; k < len; k++) { - xt = ref[k]; - ref1 = [yr, yr - 1, yr + 1]; - for (l = 0, len1 = ref1.length; l < len1; l++) { - yt = ref1[l]; - key = `${xt},${yt}`; - ref3 = (ref2 = this.hash[key]) != null ? ref2 : []; - for (m = 0, len2 = ref3.length; m < len2; m++) { - v = ref3[m]; - if (this.epsilon > geom.dist(this.vertices_coords[v], coord)) { - return v; - } - } - } - } - return null; - } - - key(coord) { - var key, x, xr, y, yr; - [x, y] = coord; - xr = Math.round(x / this.epsilon); - yr = Math.round(y / this.epsilon); - return key = `${xr},${yr}`; - } - - insert(coord) { - var base, name, v; - v = this.lookup(coord); - if (v != null) { - return v; - } - ((base = this.hash)[name = this.key(coord)] != null ? base[name] : base[name] = []).push(v = this.vertices_coords.length); - this.vertices_coords.push(coord); - return v; - } - -}; - -filter.collapseNearbyVertices = function(fold, epsilon) { - var coords, old2new, vertices; - vertices = new RepeatedPointsDS([], epsilon); - old2new = (function() { - var k, len, ref, results; - ref = fold.vertices_coords; - results = []; - for (k = 0, len = ref.length; k < len; k++) { - coords = ref[k]; - results.push(vertices.insert(coords)); - } - return results; - })(); - return filter.remapField(fold, 'vertices', old2new); -}; - -//# In particular: fold.vertices_coords = vertices.vertices_coords -filter.maybeAddVertex = function(fold, coords, epsilon) { - /* - Add a new vertex at coordinates `coords` and return its (last) index, - unless there is already such a vertex within distance `epsilon`, - in which case return the closest such vertex's index. - */ - var i; - i = geom.closestIndex(coords, fold.vertices_coords); - if ((i != null) && epsilon >= geom.dist(coords, fold.vertices_coords[i])) { - return i; //# Closest point is close enough - } else { - return fold.vertices_coords.push(coords) - 1; - } -}; - -filter.addVertexLike = function(fold, oldVertexIndex) { - var k, key, len, ref, vNew; - //# Add a vertex and copy data from old vertex. - vNew = filter.numVertices(fold); - ref = filter.keysStartingWith(fold, 'vertices_'); - for (k = 0, len = ref.length; k < len; k++) { - key = ref[k]; - switch (key.slice(6)) { - case 'vertices': - break; - default: - //# Leaving these broken - fold[key][vNew] = fold[key][oldVertexIndex]; - } - } - return vNew; -}; - -filter.addEdgeLike = function(fold, oldEdgeIndex, v1, v2) { - var eNew, k, key, len, ref; - //# Add an edge between v1 and v2, and copy data from old edge. - //# If v1 or v2 are unspecified, defaults to the vertices of the old edge. - //# Must have `edges_vertices` property. - eNew = fold.edges_vertices.length; - ref = filter.keysStartingWith(fold, 'edges_'); - for (k = 0, len = ref.length; k < len; k++) { - key = ref[k]; - switch (key.slice(6)) { - case 'vertices': - fold.edges_vertices.push([v1 != null ? v1 : fold.edges_vertices[oldEdgeIndex][0], v2 != null ? v2 : fold.edges_vertices[oldEdgeIndex][1]]); - break; - case 'edges': - break; - default: - //# Leaving these broken - fold[key][eNew] = fold[key][oldEdgeIndex]; - } - } - return eNew; -}; - -filter.addVertexAndSubdivide = function(fold, coords, epsilon) { - var changedEdges, e, i, iNew, k, len, ref, s, u, v; - v = filter.maybeAddVertex(fold, coords, epsilon); - changedEdges = []; - if (v === fold.vertices_coords.length - 1) { - ref = fold.edges_vertices; - //# Similar to "Handle overlapping edges" case: - for (i = k = 0, len = ref.length; k < len; i = ++k) { - e = ref[i]; - if (indexOf.call(e, v) >= 0) { // shouldn't happen - continue; - } - s = (function() { - var l, len1, results; - results = []; - for (l = 0, len1 = e.length; l < len1; l++) { - u = e[l]; - results.push(fold.vertices_coords[u]); - } - return results; - })(); - if (geom.pointStrictlyInSegment(coords, s)) { //# implicit epsilon - //console.log coords, 'in', s - iNew = filter.addEdgeLike(fold, i, v, e[1]); - changedEdges.push(i, iNew); - e[1] = v; - } - } - } - return [v, changedEdges]; -}; - -filter.removeLoopEdges = function(fold) { - var edge; - /* - Remove edges whose endpoints are identical. After collapsing via - `filter.collapseNearbyVertices`, this removes epsilon-length edges. - */ - return filter.remapFieldSubset(fold, 'edges', (function() { - var k, len, ref, results; - ref = fold.edges_vertices; - results = []; - for (k = 0, len = ref.length; k < len; k++) { - edge = ref[k]; - results.push(edge[0] !== edge[1]); - } - return results; - })()); -}; - -filter.subdivideCrossingEdges_vertices = function(fold, epsilon, involvingEdgesFrom) { - /* - Using just `vertices_coords` and `edges_vertices` and assuming all in 2D, - subdivides all crossing/touching edges to form a planar graph. - In particular, all duplicate and loop edges are also removed. - - If called without `involvingEdgesFrom`, does all subdivision in quadratic - time. xxx Should be O(n log n) via plane sweep. - In this case, returns an array of indices of all edges that were subdivided - (both modified old edges and new edges). - - If called with `involvingEdgesFrom`, does all subdivision involving an - edge numbered `involvingEdgesFrom` or higher. For example, after adding an - edge with largest number, call with `involvingEdgesFrom = - edges_vertices.length - 1`; then this will run in linear time. - In this case, returns two arrays of edges: the first array are all subdivided - from the "involved" edges, while the second array is the remaining subdivided - edges. - */ - var addEdge, changedEdges, cross, crossI, e, e1, e2, i, i1, i2, k, l, len, len1, len2, len3, m, n, old2new, p, ref, ref1, ref2, ref3, s, s1, s2, u, v, vertices; - changedEdges = [[], []]; - addEdge = function(v1, v2, oldEdgeIndex, which) { - var eNew; - //console.log 'adding', oldEdgeIndex, fold.edges_vertices.length, 'to', which - eNew = filter.addEdgeLike(fold, oldEdgeIndex, v1, v2); - return changedEdges[which].push(oldEdgeIndex, eNew); - }; - //# Handle overlapping edges by subdividing edges at any vertices on them. - //# We use a while loop instead of a for loop to process newly added edges. - i = involvingEdgesFrom != null ? involvingEdgesFrom : 0; - while (i < fold.edges_vertices.length) { - e = fold.edges_vertices[i]; - s = (function() { - var k, len, results; - results = []; - for (k = 0, len = e.length; k < len; k++) { - u = e[k]; - results.push(fold.vertices_coords[u]); - } - return results; - })(); - ref = fold.vertices_coords; - for (v = k = 0, len = ref.length; k < len; v = ++k) { - p = ref[v]; - if (indexOf.call(e, v) >= 0) { - continue; - } - if (geom.pointStrictlyInSegment(p, s)) { //# implicit epsilon - //console.log p, 'in', s - addEdge(v, e[1], i, 0); - e[1] = v; - } - } - i++; - } - //# Handle crossing edges - //# We use a while loop instead of a for loop to process newly added edges. - vertices = new RepeatedPointsDS(fold.vertices_coords, epsilon); - i1 = involvingEdgesFrom != null ? involvingEdgesFrom : 0; - while (i1 < fold.edges_vertices.length) { - e1 = fold.edges_vertices[i1]; - s1 = (function() { - var l, len1, results; - results = []; - for (l = 0, len1 = e1.length; l < len1; l++) { - v = e1[l]; - results.push(fold.vertices_coords[v]); - } - return results; - })(); - ref1 = fold.edges_vertices.slice(0, i1); - for (i2 = l = 0, len1 = ref1.length; l < len1; i2 = ++l) { - e2 = ref1[i2]; - s2 = (function() { - var len2, m, results; - results = []; - for (m = 0, len2 = e2.length; m < len2; m++) { - v = e2[m]; - results.push(fold.vertices_coords[v]); - } - return results; - })(); - if (!filter.edges_verticesIncident(e1, e2) && geom.segmentsCross(s1, s2)) { - //# segment intersection is too sensitive a test; - //# segmentsCross more reliable - //cross = segmentIntersectSegment s1, s2 - cross = geom.lineIntersectLine(s1, s2); - if (cross == null) { - continue; - } - crossI = vertices.insert(cross); - //console.log e1, s1, 'intersects', e2, s2, 'at', cross, crossI - if (!(indexOf.call(e1, crossI) >= 0 && indexOf.call(e2, crossI) >= 0)) { //# don't add endpoint again - //console.log e1, e2, '->' - if (indexOf.call(e1, crossI) < 0) { - addEdge(crossI, e1[1], i1, 0); - e1[1] = crossI; - s1[1] = fold.vertices_coords[crossI]; - } - //console.log '->', e1, fold.edges_vertices[fold.edges_vertices.length-1] - if (indexOf.call(e2, crossI) < 0) { - addEdge(crossI, e2[1], i2, 1); - e2[1] = crossI; - } - } - } - } - //console.log '->', e2, fold.edges_vertices[fold.edges_vertices.length-1] - i1++; - } - old2new = filter.removeDuplicateEdges_vertices(fold); - ref2 = [0, 1]; - for (m = 0, len2 = ref2.length; m < len2; m++) { - i = ref2[m]; - changedEdges[i] = (function() { - var len3, n, ref3, results; - ref3 = changedEdges[i]; - results = []; - for (n = 0, len3 = ref3.length; n < len3; n++) { - e = ref3[n]; - results.push(old2new[e]); - } - return results; - })(); - } - old2new = filter.removeLoopEdges(fold); - ref3 = [0, 1]; - for (n = 0, len3 = ref3.length; n < len3; n++) { - i = ref3[n]; - changedEdges[i] = (function() { - var len4, o, ref4, results; - ref4 = changedEdges[i]; - results = []; - for (o = 0, len4 = ref4.length; o < len4; o++) { - e = ref4[o]; - results.push(old2new[e]); - } - return results; - })(); - } - //fold - if (involvingEdgesFrom != null) { - return changedEdges; - } else { - return changedEdges[0].concat(changedEdges[1]); - } -}; - -filter.addEdgeAndSubdivide = function(fold, v1, v2, epsilon) { - var changedEdges, changedEdges1, changedEdges2, e, i, iNew, k, len, ref; - /* - Add an edge between vertex indices or points `v1` and `v2`, subdivide - as necessary, and return two arrays: all the subdivided parts of this edge, - and all the other edges that change. - If the edge is a loop or a duplicate, both arrays will be empty. - */ - if (v1.length != null) { - [v1, changedEdges1] = filter.addVertexAndSubdivide(fold, v1, epsilon); - } - if (v2.length != null) { - [v2, changedEdges2] = filter.addVertexAndSubdivide(fold, v2, epsilon); - } - if (v1 === v2) { //# Ignore loop edges - return [[], []]; - } - ref = fold.edges_vertices; - for (i = k = 0, len = ref.length; k < len; i = ++k) { - e = ref[i]; - if ((e[0] === v1 && e[1] === v2) || (e[0] === v2 && e[1] === v1)) { - return [[i], []]; //# Ignore duplicate edges - } - } - iNew = fold.edges_vertices.push([v1, v2]) - 1; - if (iNew) { - changedEdges = filter.subdivideCrossingEdges_vertices(fold, epsilon, iNew); - if (indexOf.call(changedEdges[0], iNew) < 0) { - changedEdges[0].push(iNew); - } - } else { - changedEdges = [[iNew], []]; - } - if (changedEdges1 != null) { - changedEdges[1].push(...changedEdges1); - } - if (changedEdges2 != null) { - changedEdges[1].push(...changedEdges2); - } - return changedEdges; -}; - -filter.splitCuts = function(fold, es = filter.cutEdges(fold)) { - var b, b1, b2, boundaries, e, e1, e2, ev, i, i1, i2, ie, ie1, ie2, k, l, len, len1, len2, len3, len4, len5, len6, len7, len8, m, n, neighbor, neighbors, o, q, r, ref, ref1, ref10, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, t, u1, u2, v, v1, v2, ve, vertices_boundaries, z; - if (!es.length) { - /* - Given a FOLD object with `edges_vertices`, `edges_assignment`, and - counterclockwise-sorted `vertices_edges` - (see `FOLD.convert.edges_vertices_to_vertices_edges_sorted`), - cuts apart ("unwelds") all edges in `es` into pairs of boundary edges. - When an endpoint of a cut edge ends up on n boundaries, - it splits into n vertices. - Preserves above-mentioned properties (so you can then compute faces via - `FOLD.convert.edges_vertices_to_faces_vertices_edges`), - and recomputes `vertices_vertices` if present, - but ignores face properties. - `es` is unspecified, cuts all edges with an assignment of `"C"`, - effectively switching from FOLD 1.2's `"C"` assignments to - FOLD 1.1's `"B"` assignments. - */ - return fold; - } - //# Maintain map from every vertex to array of incident boundary edges - vertices_boundaries = []; - ref = filter.boundaryEdges(fold); - for (k = 0, len = ref.length; k < len; k++) { - e = ref[k]; - ref1 = fold.edges_vertices[e]; - for (l = 0, len1 = ref1.length; l < len1; l++) { - v = ref1[l]; - (vertices_boundaries[v] != null ? vertices_boundaries[v] : vertices_boundaries[v] = []).push(e); - } - } - for (m = 0, len2 = es.length; m < len2; m++) { - e1 = es[m]; - //# Split e1 into two edges {e1, e2} - e2 = filter.addEdgeLike(fold, e1); - ref2 = fold.edges_vertices[e1]; - for (i = n = 0, len3 = ref2.length; n < len3; i = ++n) { - v = ref2[i]; - ve = fold.vertices_edges[v]; - //# Insert e2 before e1 in first vertex and after e1 in second vertex - //# to represent valid counterclockwise ordering - ve.splice(ve.indexOf(e1) + i, 0, e2); - } - ref3 = fold.edges_vertices[e1]; - //# Check for endpoints of {e1, e2} to split, when they're on the boundary - for (i = o = 0, len4 = ref3.length; o < len4; i = ++o) { - v1 = ref3[i]; - u1 = fold.edges_vertices[e1][1 - i]; - u2 = fold.edges_vertices[e2][1 - i]; - boundaries = (ref4 = vertices_boundaries[v1]) != null ? ref4.length : void 0; - if (boundaries >= 2) { //# vertex already on boundary - if (boundaries > 2) { - throw new Error(`${vertices_boundaries[v1].length} boundary edges at vertex ${v1}`); - } - [b1, b2] = vertices_boundaries[v1]; - neighbors = fold.vertices_edges[v1]; - i1 = neighbors.indexOf(b1); - i2 = neighbors.indexOf(b2); - if (i2 === (i1 + 1) % neighbors.length) { - if (i2 !== 0) { - neighbors = neighbors.slice(i2).concat(neighbors.slice(0, +i1 + 1 || 9e9)); - } - } else if (i1 === (i2 + 1) % neighbors.length) { - if (i1 !== 0) { - neighbors = neighbors.slice(i1).concat(neighbors.slice(0, +i2 + 1 || 9e9)); - } - } else { - throw new Error(`Nonadjacent boundary edges at vertex ${v1}`); - } - //# Find first vertex among e1, e2 among neighbors, so other is next - ie1 = neighbors.indexOf(e1); - ie2 = neighbors.indexOf(e2); - ie = Math.min(ie1, ie2); - fold.vertices_edges[v1] = neighbors.slice(0, +ie + 1 || 9e9); - v2 = filter.addVertexLike(fold, v1); - fold.vertices_edges[v2] = neighbors.slice(1 + ie); - ref5 = fold.vertices_edges[v2]; - //console.log "Split #{neighbors} into #{fold.vertices_edges[v1]} for #{v1} and #{fold.vertices_edges[v2]} for #{v2}" - //# Update relevant incident edges to use v2 instead of v1 - for (q = 0, len5 = ref5.length; q < len5; q++) { - neighbor = ref5[q]; - ev = fold.edges_vertices[neighbor]; - ev[ev.indexOf(v1)] = v2; - } - //# Partition boundary edges incident to v1 - vertices_boundaries[v1] = []; - vertices_boundaries[v2] = []; - ref6 = [b1, b2]; - for (r = 0, len6 = ref6.length; r < len6; r++) { - b = ref6[r]; - if (indexOf.call(fold.vertices_edges[v1], b) >= 0) { - vertices_boundaries[v1].push(b); //if b in fold.vertices_edges[v2] - } else { - vertices_boundaries[v2].push(b); - } - } - } - } - //# e1 and e2 are new boundary edges - if ((ref7 = fold.edges_assignment) != null) { - ref7[e1] = 'B'; - } - if ((ref8 = fold.edges_assignment) != null) { - ref8[e2] = 'B'; - } - ref9 = fold.edges_vertices[e1]; - for (i = t = 0, len7 = ref9.length; t < len7; i = ++t) { - v = ref9[i]; - (vertices_boundaries[v] != null ? vertices_boundaries[v] : vertices_boundaries[v] = []).push(e1); - } - ref10 = fold.edges_vertices[e2]; - for (i = z = 0, len8 = ref10.length; z < len8; i = ++z) { - v = ref10[i]; - (vertices_boundaries[v] != null ? vertices_boundaries[v] : vertices_boundaries[v] = []).push(e2); - } - } - if (fold.vertices_vertices != null) { - fold.vertices_vertices = filter.edges_vertices_to_vertices_vertices(fold); // would be out-of-date - } - return fold; -}; - -filter.edges_vertices_to_vertices_vertices = function(fold) { - /* - Works for abstract structures, so NOT SORTED. - Use sort_vertices_vertices to sort in counterclockwise order. - */ - var k, len, numVertices, ref, v, vertices_vertices, w; - numVertices = filter.numVertices(fold); - vertices_vertices = (function() { - var k, ref, results; - results = []; - for (v = k = 0, ref = numVertices; (0 <= ref ? k < ref : k > ref); v = 0 <= ref ? ++k : --k) { - results.push([]); - } - return results; - })(); - ref = fold.edges_vertices; - for (k = 0, len = ref.length; k < len; k++) { - [v, w] = ref[k]; - while (v >= vertices_vertices.length) { - vertices_vertices.push([]); - } - while (w >= vertices_vertices.length) { - vertices_vertices.push([]); - } - vertices_vertices[v].push(w); - vertices_vertices[w].push(v); - } - return vertices_vertices; -}; - -filter.edges_vertices_to_vertices_edges = function(fold) { - /* - Invert edges_vertices into vertices_edges. - Works for abstract structures, so NOT SORTED in any sense. - */ - var edge, k, l, len, len1, numVertices, ref, v, vertex, vertices, vertices_edges; - numVertices = filter.numVertices(fold); - vertices_edges = (function() { - var k, ref, results; - results = []; - for (v = k = 0, ref = numVertices; (0 <= ref ? k < ref : k > ref); v = 0 <= ref ? ++k : --k) { - results.push([]); - } - return results; - })(); - ref = fold.edges_vertices; - for (edge = k = 0, len = ref.length; k < len; edge = ++k) { - vertices = ref[edge]; - for (l = 0, len1 = vertices.length; l < len1; l++) { - vertex = vertices[l]; - vertices_edges[vertex].push(edge); - } - } - return vertices_edges; -}; - - -},{"./geom":4}],4:[function(require,module,exports){ - /* BASIC GEOMETRY */ -var geom, - modulo = function(a, b) { return (+a % (b = +b) + b) % b; }; - -geom = exports; - -/* - Utilities -*/ -geom.EPS = 0.000001; - -geom.sum = function(a, b) { - return a + b; -}; - -geom.min = function(a, b) { - if (a < b) { - return a; - } else { - return b; - } -}; - -geom.max = function(a, b) { - if (a > b) { - return a; - } else { - return b; - } -}; - -geom.all = function(a, b) { - return a && b; -}; - -geom.next = function(start, n, i = 1) { - /* - Returns the ith cyclic ordered number after start in the range [0..n]. - */ - return modulo(start + i, n); -}; - -geom.rangesDisjoint = function([a1, a2], [b1, b2]) { - var ref, ref1; - //# Returns whether the scalar interval [a1, a2] is disjoint from the scalar - //# interval [b1,b2]. - return ((b1 < (ref = Math.min(a1, a2)) && ref > b2)) || ((b1 > (ref1 = Math.max(a1, a2)) && ref1 < b2)); -}; - -geom.topologicalSort = function(vs) { - var l, len, list, v; - (function() { - var l, len, results; - results = []; - for (l = 0, len = vs.length; l < len; l++) { - v = vs[l]; - results.push([v.visited, v.parent] = [false, null]); - } - return results; - })(); - list = []; - for (l = 0, len = vs.length; l < len; l++) { - v = vs[l]; - if (!v.visited) { - list = geom.visit(v, list); - } - } - return list; -}; - -geom.visit = function(v, list) { - var l, len, ref, u; - v.visited = true; - ref = v.children; - for (l = 0, len = ref.length; l < len; l++) { - u = ref[l]; - if (!(!u.visited)) { - continue; - } - u.parent = v; - list = geom.visit(u, list); - } - return list.concat([v]); -}; - -//# -//# Vector operations -//# -geom.magsq = function(a) { - //# Returns the squared magnitude of vector a having arbitrary dimension. - return geom.dot(a, a); -}; - -geom.mag = function(a) { - //# Returns the magnitude of vector a having arbitrary dimension. - return Math.sqrt(geom.magsq(a)); -}; - -geom.unit = function(a, eps = geom.EPS) { - var length; - //# Returns the unit vector in the direction of vector a having arbitrary - //# dimension. Returns null if magnitude of a is zero. - length = geom.magsq(a); - if (length < eps) { - return null; - } - return geom.mul(a, 1 / geom.mag(a)); -}; - -geom.ang2D = function(a, eps = geom.EPS) { - if (geom.magsq(a) < eps) { - //# Returns the angle of a 2D vector relative to the standard - //# east-is-0-degrees rule. - return null; - } - return Math.atan2(a[1], a[0]); -}; - -geom.mul = function(a, s) { - var i, l, len, results; - results = []; - for (l = 0, len = a.length; l < len; l++) { - i = a[l]; - //# Returns the vector a multiplied by scaler factor s. - results.push(i * s); - } - return results; -}; - -geom.linearInterpolate = function(t, a, b) { - //# Returns linear interpolation of vector a to vector b for 0 < t < 1 - return geom.plus(geom.mul(a, 1 - t), geom.mul(b, t)); -}; - -geom.plus = function(a, b) { - var ai, i, l, len, results; - results = []; - for (i = l = 0, len = a.length; l < len; i = ++l) { - ai = a[i]; - //# Returns the vector sum between of vectors a and b having the same - //# dimension. - results.push(ai + b[i]); - } - return results; -}; - -geom.sub = function(a, b) { - //# Returns the vector difference of vectors a and b having the same dimension. - return geom.plus(a, geom.mul(b, -1)); -}; - -geom.dot = function(a, b) { - var ai, i; - return ((function() { - var l, len, results; - results = []; - for (i = l = 0, len = a.length; l < len; i = ++l) { - ai = a[i]; - //# Returns the dot product between two vectors a and b having the same - //# dimension. - results.push(ai * b[i]); - } - return results; - })()).reduce(geom.sum); -}; - -geom.distsq = function(a, b) { - //# Returns the squared Euclidean distance between two vectors a and b having - //# the same dimension. - return geom.magsq(geom.sub(a, b)); -}; - -geom.dist = function(a, b) { - //# Returns the Euclidean distance between general vectors a and b having the - //# same dimension. - return Math.sqrt(geom.distsq(a, b)); -}; - -geom.closestIndex = function(a, bs) { - var b, dist, i, l, len, minDist, minI; - //# Finds the closest point to `a` among points in `bs`, and returns the - //# index of that point in `bs`. Returns `undefined` if `bs` is empty. - minDist = 2e308; - for (i = l = 0, len = bs.length; l < len; i = ++l) { - b = bs[i]; - if (minDist > (dist = geom.dist(a, b))) { - minDist = dist; - minI = i; - } - } - return minI; -}; - -geom.dir = function(a, b) { - //# Returns a unit vector in the direction from vector a to vector b, in the - //# same dimension as a and b. - return geom.unit(geom.sub(b, a)); -}; - -geom.ang = function(a, b) { - var ua, ub, v; - //# Returns the angle spanned by vectors a and b having the same dimension. - [ua, ub] = (function() { - var l, len, ref, results; - ref = [a, b]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - v = ref[l]; - results.push(geom.unit(v)); - } - return results; - })(); - if (!((ua != null) && (ub != null))) { - return null; - } - return Math.acos(geom.dot(ua, ub)); -}; - -geom.cross = function(a, b) { - var i, j, ref, ref1; - //# Returns the cross product of two 2D or 3D vectors a, b. - if ((a.length === (ref = b.length) && ref === 2)) { - return a[0] * b[1] - a[1] * b[0]; - } - if ((a.length === (ref1 = b.length) && ref1 === 3)) { - return (function() { - var l, len, ref2, results; - ref2 = [[1, 2], [2, 0], [0, 1]]; - results = []; - for (l = 0, len = ref2.length; l < len; l++) { - [i, j] = ref2[l]; - results.push(a[i] * b[j] - a[j] * b[i]); - } - return results; - })(); - } - return null; -}; - -geom.parallel = function(a, b, eps = geom.EPS) { - var ua, ub, v; - //# Return if vectors are parallel, up to accuracy eps - [ua, ub] = (function() { - var l, len, ref, results; - ref = [a, b]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - v = ref[l]; - results.push(geom.unit(v)); - } - return results; - })(); - if (!((ua != null) && (ub != null))) { - return null; - } - return 1 - Math.abs(geom.dot(ua, ub)) < eps; -}; - -geom.rotate = function(a, u, t) { - var ct, i, l, len, p, q, ref, results, st; - //# Returns the rotation of 3D vector a about 3D unit vector u by angle t. - u = geom.unit(u); - if (u == null) { - return null; - } - [ct, st] = [Math.cos(t), Math.sin(t)]; - ref = [[0, 1, 2], [1, 2, 0], [2, 0, 1]]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - p = ref[l]; - results.push(((function() { - var len1, o, ref1, results1; - ref1 = [ct, -st * u[p[2]], st * u[p[1]]]; - results1 = []; - for (i = o = 0, len1 = ref1.length; o < len1; i = ++o) { - q = ref1[i]; - results1.push(a[p[i]] * (u[p[0]] * u[p[i]] * (1 - ct) + q)); - } - return results1; - })()).reduce(geom.sum)); - } - return results; -}; - -geom.reflectPoint = function(p, q) { - //# Reflect point p through the point q into the "symmetric point" - return geom.sub(geom.mul(q, 2), p); -}; - -geom.reflectLine = function(p, a, b) { - var dot, lenSq, projection, vec; - //# Reflect point p through line through points a and b - // [based on https://math.stackexchange.com/a/11532] - // projection = a + (b - a) * [(b - a) dot (p - a)] / ||b - a||^2 - vec = geom.sub(b, a); - lenSq = geom.magsq(vec); - dot = geom.dot(vec, geom.sub(p, a)); - projection = geom.plus(a, geom.mul(vec, dot / lenSq)); - // reflection = 2*projection - p (symmetric point of p opposite projection) - return geom.sub(geom.mul(projection, 2), p); -}; - -/* -Matrix transformations - -2D transformation matrices are of the form (where last column is optional): - [[a, b, c], - [d, e, f]] - -3D transformation matrices are of the form (where last column is optional): - [[a, b, c, d], - [e, f, g, h], - [i, j, k, l]] - -Transformation matrices are designed to be multiplied on the left of points, -i.e., T*x gives vector x transformed by matrix T, where x has an implicit 1 -at the end (homogeneous coordinates) when T has the optional last column. -See `geom.matrixVector`. -*/ -geom.matrixVector = function(matrix, vector, implicitLast = 1) { - var j, l, len, results, row, val, x; -//# Returns matrix-vector product, matrix * vector. -//# Requires the number of matrix columns to be <= vector length. -//# If the matrix has more columns than the vector length, then the vector -//# is assumed to be padded with zeros at the end, EXCEPT when the matrix -//# has more columns than rows (as in transformation matrices above), -//# in which case the final vector padding is implicitLast, -//# which defaults to 1 (point); set to 0 for treating like a vector. - results = []; - for (l = 0, len = matrix.length; l < len; l++) { - row = matrix[l]; - val = ((function() { - var len1, o, results1; - results1 = []; - for (j = o = 0, len1 = vector.length; o < len1; j = ++o) { - x = vector[j]; - results1.push(row[j] * x); - } - return results1; - })()).reduce(geom.sum); - if (row.length > vector.length && row.length > matrix.length) { - val += row[row.length - 1] * implicitLast; - } - results.push(val); - } - return results; -}; - -geom.matrixMatrix = function(matrix1, matrix2) { - var j, k, l, len, product, ref, ref1, results, row1, row2, val; -//# Returns matrix-matrix product, matrix1 * matrix2. -//# Requires number of matrix1 columns equal to or 1 more than matrix2 rows. -//# In the latter case, treats matrix2 as having an extra row [0,0,...,0,0,1], -//# which may involve adding an implicit column to matrix2 as well. - results = []; - for (l = 0, len = matrix1.length; l < len; l++) { - row1 = matrix1[l]; - if ((matrix2.length !== (ref = row1.length) && ref !== matrix2.length + 1)) { - throw new Error(`Invalid matrix dimension ${row1.length} vs. matrix dimension ${matrix2.length}`); - } - product = (function() { - var o, ref1, ref2, results1; - results1 = []; - for (j = o = 0, ref1 = matrix2[0].length; (0 <= ref1 ? o < ref1 : o > ref1); j = 0 <= ref1 ? ++o : --o) { - val = ((function() { - var len1, r, results2; - results2 = []; - for (k = r = 0, len1 = matrix2.length; r < len1; k = ++r) { - row2 = matrix2[k]; - results2.push(row1[k] * row2[j]); - } - return results2; - })()).reduce(geom.sum); - if ((j === (ref2 = row1.length - 1) && ref2 === matrix2.length)) { - val += row1[j]; - } - results1.push(val); - } - return results1; - })(); - if ((row1.length - 1 === (ref1 = matrix2.length) && ref1 === matrix2[0].length)) { - product.push(row1[row1.length - 1]); - } - results.push(product); - } - return results; -}; - -geom.matrixInverseRT = function(matrix) { - var i, invRow, j, l, lastCol, len, results, row; - //# Returns inverse of a matrix consisting of rotations and/or translations, - //# where the inverse can be found by a transpose and dot products - //# [http://www.graphics.stanford.edu/courses/cs248-98-fall/Final/q4.html]. - if (matrix[0].length === matrix.length + 1) { - lastCol = (function() { - var l, len, results; - results = []; - for (l = 0, len = matrix.length; l < len; l++) { - row = matrix[l]; - results.push(row[row.length - 1]); - } - return results; - })(); - } else if (matrix[0].length !== matrix.length) { - throw new Error(`Invalid matrix dimensions ${matrix.length}x${matrix[0].length}`); - } - results = []; - for (i = l = 0, len = matrix.length; l < len; i = ++l) { - row = matrix[i]; - invRow = (function() { - var o, ref, results1; -// transpose - results1 = []; - for (j = o = 0, ref = matrix.length; (0 <= ref ? o < ref : o > ref); j = 0 <= ref ? ++o : --o) { - results1.push(matrix[j][i]); - } - return results1; - })(); - if (lastCol != null) { - invRow.push(-geom.dot(row.slice(0, matrix.length), lastCol)); - } - results.push(invRow); - } - return results; -}; - -geom.matrixInverse = function(matrix) { - var bestRow, i, inverse, j, l, o, r, ref, ref1, ref2, ref3, ref4, ref5, row, w; - //# Returns inverse of a matrix computed via Gauss-Jordan elimination method. - if ((matrix.length !== (ref = matrix[0].length) && ref !== matrix.length + 1)) { - throw new Error(`Invalid matrix dimensions ${matrix.length}x${matrix[0].length}`); - } - matrix = (function() { - var l, len, results; -// copy before elimination - results = []; - for (l = 0, len = matrix.length; l < len; l++) { - row = matrix[l]; - results.push(row.slice(0)); - } - return results; - })(); - inverse = (function() { - var l, len, results; - results = []; - for (i = l = 0, len = matrix.length; l < len; i = ++l) { - row = matrix[i]; - results.push((function() { - var o, ref1, results1; - results1 = []; - for (j = o = 0, ref1 = row.length; (0 <= ref1 ? o < ref1 : o > ref1); j = 0 <= ref1 ? ++o : --o) { - results1.push(0 + (i === j)); - } - return results1; - })()); - } - return results; - })(); - for (j = l = 0, ref1 = matrix.length; (0 <= ref1 ? l < ref1 : l > ref1); j = 0 <= ref1 ? ++l : --l) { - // Pivot to maximize absolute value in jth column - bestRow = j; - for (i = o = ref2 = j + 1, ref3 = matrix.length; (ref2 <= ref3 ? o < ref3 : o > ref3); i = ref2 <= ref3 ? ++o : --o) { - if (Math.abs(matrix[i][j]) > Math.abs(matrix[bestRow][j])) { - bestRow = i; - } - } - if (bestRow !== j) { - [matrix[bestRow], matrix[j]] = [matrix[j], matrix[bestRow]]; - [inverse[bestRow], inverse[j]] = [inverse[j], inverse[bestRow]]; - } - // Scale row to unity in jth column - inverse[j] = geom.mul(inverse[j], 1 / matrix[j][j]); - matrix[j] = geom.mul(matrix[j], 1 / matrix[j][j]); -// Eliminate other rows in jth column - for (i = r = 0, ref4 = matrix.length; (0 <= ref4 ? r < ref4 : r > ref4); i = 0 <= ref4 ? ++r : --r) { - if (!(i !== j)) { - continue; - } - inverse[i] = geom.plus(inverse[i], geom.mul(inverse[j], -matrix[i][j])); - matrix[i] = geom.plus(matrix[i], geom.mul(matrix[j], -matrix[i][j])); - } - } - if (matrix[0].length === matrix.length + 1) { - for (i = w = 0, ref5 = matrix.length; (0 <= ref5 ? w < ref5 : w > ref5); i = 0 <= ref5 ? ++w : --w) { - if (!(i !== j)) { - continue; - } - inverse[i][inverse[i].length - 1] -= matrix[i][matrix[i].length - 1]; - matrix[i][matrix[i].length - 1] -= matrix[i][matrix[i].length - 1]; - } - } - return inverse; -}; - -geom.matrixTranslate = function(v) { - var i, j, l, len, results, row, x; -//# Transformation matrix for translating by given vector v. -//# Works in any dimension, assuming v.length is that dimension. - results = []; - for (i = l = 0, len = v.length; l < len; i = ++l) { - x = v[i]; - row = (function() { - var o, ref, results1; - results1 = []; - for (j = o = 0, ref = v.length; (0 <= ref ? o < ref : o > ref); j = 0 <= ref ? ++o : --o) { - results1.push(0 + (i === j)); - } - return results1; - })(); - row.push(x); - results.push(row); - } - return results; -}; - -geom.matrixRotate2D = function(t, center) { - var ct, st, x, y; - //# 2D rotation matrix around center, which defaults to origin, - //# counterclockwise by t radians. - [ct, st] = [Math.cos(t), Math.sin(t)]; - if (center != null) { - [x, y] = center; - return [[ct, -st, -x * ct + y * st + x], [st, ct, -x * st - y * ct + y]]; - } else { - return [[ct, -st], [st, ct]]; - } -}; - -geom.matrixReflectAxis = function(a, d, center) { - var i, j, l, ref, results, row; -//# Matrix transformation negating dimension a out of d dimensions, -//# or if center is specified, reflecting around that value of dimension a. - results = []; - for (i = l = 0, ref = d; (0 <= ref ? l < ref : l > ref); i = 0 <= ref ? ++l : --l) { - row = (function() { - var o, ref1, results1; - results1 = []; - for (j = o = 0, ref1 = d; (0 <= ref1 ? o < ref1 : o > ref1); j = 0 <= ref1 ? ++o : --o) { - if (i === j) { - if (a === i) { - results1.push(-1); - } else { - results1.push(1); - } - } else { - results1.push(0); - } - } - return results1; - })(); - if (center != null) { - if (a === i) { - row.push(2 * center); - } else { - row.push(0); - } - } - results.push(row); - } - return results; -}; - -geom.matrixReflectLine = function(a, b) { - var dot2, lenSq, vec; - //# Matrix transformation implementing 2D geom.reflectLine(*, a, b) - vec = geom.sub(b, a); - lenSq = geom.magsq(vec); - // dot = vec dot (p - a) = vec dot p - vec dot a - dot2 = geom.dot(vec, a); - //proj = (a[i] + vec[i] * dot / lenSq for i in [0...2]) - //[[vec[0] * vec[0] / lenSq, - // vec[0] * vec[1] / lenSq, - // a[0] - vec[0] * dot2 / lenSq] - // [vec[1] * vec[0] / lenSq, - // vec[1] * vec[1] / lenSq, - // a[1] - vec[1] * dot2 / lenSq]] - return [[2 * (vec[0] * vec[0] / lenSq) - 1, 2 * (vec[0] * vec[1] / lenSq), 2 * (a[0] - vec[0] * dot2 / lenSq)], [2 * (vec[1] * vec[0] / lenSq), 2 * (vec[1] * vec[1] / lenSq) - 1, 2 * (a[1] - vec[1] * dot2 / lenSq)]]; -}; - -//# -//# Polygon Operations -//# -geom.interiorAngle = function(a, b, c) { - var ang; - //# Computes the angle of three points that are, say, part of a triangle. - //# Specify in counterclockwise order. - //# a - //# / - //# / - //# b/_)__ c - ang = geom.ang2D(geom.sub(a, b)) - geom.ang2D(geom.sub(c, b)); - return ang + (ang < 0 ? 2 * Math.PI : 0); -}; - -geom.turnAngle = function(a, b, c) { - //# Returns the turn angle, the supplement of the interior angle - return Math.PI - geom.interiorAngle(a, b, c); -}; - -geom.triangleNormal = function(a, b, c) { - //# Returns the right handed normal unit vector to triangle a, b, c in 3D. If - //# the triangle is degenerate, returns null. - return geom.unit(geom.cross(geom.sub(b, a), geom.sub(c, b))); -}; - -geom.polygonNormal = function(points, eps = geom.EPS) { - var i, p; - //# Returns the right handed normal unit vector to the polygon defined by - //# points in 3D. Assumes the points are planar. - return geom.unit(((function() { - var l, len, results; - results = []; - for (i = l = 0, len = points.length; l < len; i = ++l) { - p = points[i]; - results.push(geom.cross(p, points[geom.next(i, points.length)])); - } - return results; - })()).reduce(geom.plus), eps); -}; - -geom.twiceSignedArea = function(points) { - var i, v0, v1; - return ((function() { - var l, len, results; -//# Returns twice signed area of polygon defined by input points. -//# Calculates and sums twice signed area of triangles in a fan from the first -//# vertex. - results = []; - for (i = l = 0, len = points.length; l < len; i = ++l) { - v0 = points[i]; - v1 = points[geom.next(i, points.length)]; - results.push(v0[0] * v1[1] - v1[0] * v0[1]); - } - return results; - })()).reduce(geom.sum); -}; - -geom.polygonOrientation = function(points) { - //# Returns the orientation of the 2D polygon defined by the input points. - //# +1 for counterclockwise, -1 for clockwise - //# via computing sum of signed areas of triangles formed with origin - return Math.sign(geom.twiceSignedArea(points)); -}; - -geom.sortByAngle = function(points, origin = [0, 0], mapping = function(x) { - return x; - }) { - //# Sort a set of 2D points in place counter clockwise about origin - //# under the provided mapping. - origin = mapping(origin); - return points.sort(function(p, q) { - var pa, qa; - pa = geom.ang2D(geom.sub(mapping(p), origin)); - qa = geom.ang2D(geom.sub(mapping(q), origin)); - return pa - qa; - }); -}; - -geom.segmentsCross = function([p0, q0], [p1, q1]) { - //# May not work if the segments are collinear. - //# First do rough overlap check in x and y. This helps with - //# near-collinear segments. (Inspired by oripa/geom/GeomUtil.java) - if (geom.rangesDisjoint([p0[0], q0[0]], [p1[0], q1[0]]) || geom.rangesDisjoint([p0[1], q0[1]], [p1[1], q1[1]])) { - return false; - } - //# Now do orientation test. - return geom.polygonOrientation([p0, q0, p1]) !== geom.polygonOrientation([p0, q0, q1]) && geom.polygonOrientation([p1, q1, p0]) !== geom.polygonOrientation([p1, q1, q0]); -}; - -geom.parametricLineIntersect = function([p1, p2], [q1, q2]) { - var denom; - //# Returns the parameters s,t for the equations s*p1+(1-s)*p2 and - //# t*q1+(1-t)*q2. Used Maple's result of: - //# solve({s*p2x+(1-s)*p1x=t*q2x+(1-t)*q1x, - //# s*p2y+(1-s)*p1y=t*q2y+(1-t)*q1y}, {s,t}); - //# Returns null, null if the intersection couldn't be found - //# because the lines are parallel. - //# Input points must be 2D. - denom = (q2[1] - q1[1]) * (p2[0] - p1[0]) + (q1[0] - q2[0]) * (p2[1] - p1[1]); - if (denom === 0) { - return [null, null]; - } else { - return [(q2[0] * (p1[1] - q1[1]) + q2[1] * (q1[0] - p1[0]) + q1[1] * p1[0] - p1[1] * q1[0]) / denom, (q1[0] * (p2[1] - p1[1]) + q1[1] * (p1[0] - p2[0]) + p1[1] * p2[0] - p2[1] * p1[0]) / denom]; - } -}; - -geom.segmentIntersectSegment = function(s1, s2) { - var s, t; - [s, t] = geom.parametricLineIntersect(s1, s2); - if ((s != null) && ((0 <= s && s <= 1)) && ((0 <= t && t <= 1))) { - return geom.linearInterpolate(s, s1[0], s1[1]); - } else { - return null; - } -}; - -geom.lineIntersectLine = function(l1, l2) { - var s, t; - [s, t] = geom.parametricLineIntersect(l1, l2); - if (s != null) { - return geom.linearInterpolate(s, l1[0], l1[1]); - } else { - return null; - } -}; - -geom.pointStrictlyInSegment = function(p, s, eps = geom.EPS) { - var v0, v1; - v0 = geom.sub(p, s[0]); - v1 = geom.sub(p, s[1]); - return geom.parallel(v0, v1, eps) && geom.dot(v0, v1) < 0; -}; - -geom.centroid = function(points) { - //# Returns the centroid of a set of points having the same dimension. - return geom.mul(points.reduce(geom.plus), 1.0 / points.length); -}; - -geom.basis = function(ps, eps = geom.EPS) { - var d, ds, n, ns, p, x, y, z; - if (((function() { - var l, len, results; - results = []; - for (l = 0, len = ps.length; l < len; l++) { - p = ps[l]; - results.push(p.length !== 3); - } - return results; - })()).reduce(geom.all)) { - //# Returns a basis of a 3D point set. - //# - [] if the points are all the same point (0 dimensional) - //# - [x] if the points lie on a line with basis direction x - //# - [x,y] if the points lie in a plane with basis directions x and y - //# - [x,y,z] if the points span three dimensions - return null; - } - ds = (function() { - var l, len, results; - results = []; - for (l = 0, len = ps.length; l < len; l++) { - p = ps[l]; - if (geom.distsq(p, ps[0]) > eps) { - results.push(geom.dir(p, ps[0])); - } - } - return results; - })(); - if (ds.length === 0) { - return []; - } - x = ds[0]; - if (((function() { - var l, len, results; - results = []; - for (l = 0, len = ds.length; l < len; l++) { - d = ds[l]; - results.push(geom.parallel(d, x, eps)); - } - return results; - })()).reduce(geom.all)) { - return [x]; - } - ns = (function() { - var l, len, results; - results = []; - for (l = 0, len = ds.length; l < len; l++) { - d = ds[l]; - results.push(geom.unit(geom.cross(d, x))); - } - return results; - })(); - ns = (function() { - var l, len, results; - results = []; - for (l = 0, len = ns.length; l < len; l++) { - n = ns[l]; - if (n != null) { - results.push(n); - } - } - return results; - })(); - z = ns[0]; - y = geom.cross(z, x); - if (((function() { - var l, len, results; - results = []; - for (l = 0, len = ns.length; l < len; l++) { - n = ns[l]; - results.push(geom.parallel(n, z, eps)); - } - return results; - })()).reduce(geom.all)) { - return [x, y]; - } - return [x, y, z]; -}; - -geom.above = function(ps, qs, n, eps = geom.EPS) { - var pn, qn, v, vs; - [pn, qn] = (function() { - var l, len, ref, results; - ref = [ps, qs]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - vs = ref[l]; - results.push((function() { - var len1, o, results1; - results1 = []; - for (o = 0, len1 = vs.length; o < len1; o++) { - v = vs[o]; - results1.push(geom.dot(v, n)); - } - return results1; - })()); - } - return results; - })(); - if (qn.reduce(geom.max) - pn.reduce(geom.min) < eps) { - return 1; - } - if (pn.reduce(geom.max) - qn.reduce(geom.min) < eps) { - return -1; - } - return 0; -}; - -geom.separatingDirection2D = function(t1, t2, n, eps = geom.EPS) { - var i, j, l, len, len1, len2, m, o, p, q, r, ref, sign, t; - ref = [t1, t2]; - //# If points are contained in a common plane with normal n and a separating - //# direction exists, a direction perpendicular to some pair of points from - //# the same set is also a separating direction. - for (l = 0, len = ref.length; l < len; l++) { - t = ref[l]; - for (i = o = 0, len1 = t.length; o < len1; i = ++o) { - p = t[i]; - for (j = r = 0, len2 = t.length; r < len2; j = ++r) { - q = t[j]; - if (!(i < j)) { - continue; - } - m = geom.unit(geom.cross(geom.sub(p, q), n)); - if (m != null) { - sign = geom.above(t1, t2, m, eps); - if (sign !== 0) { - return geom.mul(m, sign); - } - } - } - } - } - return null; -}; - -geom.separatingDirection3D = function(t1, t2, eps = geom.EPS) { - var i, j, l, len, len1, len2, len3, m, o, p, q1, q2, r, ref, sign, w, x1, x2; - ref = [[t1, t2], [t2, t1]]; - //# If points are not contained in a common plane and a separating direction - //# exists, a plane spanning two points from one set and one point from the - //# other set is a separating plane, with its normal a separating direction. - for (l = 0, len = ref.length; l < len; l++) { - [x1, x2] = ref[l]; - for (o = 0, len1 = x1.length; o < len1; o++) { - p = x1[o]; - for (i = r = 0, len2 = x2.length; r < len2; i = ++r) { - q1 = x2[i]; - for (j = w = 0, len3 = x2.length; w < len3; j = ++w) { - q2 = x2[j]; - if (!(i < j)) { - continue; - } - m = geom.unit(geom.cross(geom.sub(p, q1), geom.sub(p, q2))); - if (m != null) { - sign = geom.above(t1, t2, m, eps); - if (sign !== 0) { - return geom.mul(m, sign); - } - } - } - } - } - } - return null; -}; - -//# -//# Hole Filling Methods -//# -geom.circleCross = function(d, r1, r2) { - var x, y; - x = (d * d - r2 * r2 + r1 * r1) / d / 2; - y = Math.sqrt(r1 * r1 - x * x); - return [x, y]; -}; - -geom.creaseDir = function(u1, u2, a, b, eps = geom.EPS) { - var b1, b2, x, y, z, zmag; - b1 = Math.cos(a) + Math.cos(b); - b2 = Math.cos(a) - Math.cos(b); - x = geom.plus(u1, u2); - y = geom.sub(u1, u2); - z = geom.unit(geom.cross(y, x)); - x = geom.mul(x, b1 / geom.magsq(x)); - y = geom.mul(y, geom.magsq(y) < eps ? 0 : b2 / geom.magsq(y)); - zmag = Math.sqrt(1 - geom.magsq(x) - geom.magsq(y)); - z = geom.mul(z, zmag); - return [x, y, z].reduce(geom.plus); -}; - -geom.quadSplit = function(u, p, d, t) { - // Split from origin in direction U subject to external point P whose - // shortest path on the surface is distance D and projecting angle is T - if (geom.magsq(p) > d * d) { - throw new Error("STOP! Trying to split expansive quad."); - } - return geom.mul(u, (d * d - geom.magsq(p)) / 2 / (d * Math.cos(t) - geom.dot(u, p))); -}; - - -},{}],5:[function(require,module,exports){ -//#TODO: match spec (no frame_designer, no frame_reference, fix cw -> ccw) -//#TODO: oripa folded state format -var DOMParser, convert, filter, oripa, ref, x, y; - -if (typeof DOMParser === "undefined" || DOMParser === null) { - DOMParser = require('@xmldom/xmldom').DOMParser; -} - -//XMLSerializer = require('@xmldom/xmldom').XMLSerializer unless XMLSerializer? -//DOMImplementation = require('@xmldom/xmldom').DOMImplementation unless DOMImplementation? -convert = require('./convert'); - -filter = require('./filter'); - -oripa = exports; - -//# Based on src/oripa/geom/OriLine.java -oripa.type2fold = { - 0: 'F', //# TYPE_NONE = flat - 1: 'B', //# TYPE_CUT = boundary - 2: 'M', //# TYPE_RIDGE = mountain - 3: 'V' //# TYPE_VALLEY = valley -}; - -oripa.fold2type = {}; - -ref = oripa.type2fold; -for (x in ref) { - y = ref[x]; - oripa.fold2type[y] = x; -} - -oripa.fold2type_default = 0; - -oripa.prop_xml2fold = { - 'editorName': 'frame_author', - 'originalAuthorName': 'frame_designer', - 'reference': 'frame_reference', - 'title': 'frame_title', - 'memo': 'frame_description', - 'paperSize': null, - 'mainVersion': null, - 'subVersion': null -}; - -//oripa.prop_fold2xml = {} -//for x, y of oripa.prop_xml2fold -// oripa.prop_fold2xml[y] = x if y? -oripa.POINT_EPS = 1.0; - -oripa.toFold = function(oripaStr) { - var children, fold, j, k, l, len, len1, len2, len3, len4, line, lines, m, n, nodeSpec, object, oneChildSpec, oneChildText, prop, property, ref1, ref2, ref3, ref4, ref5, subproperty, top, type, vertex, x0, x1, xml, y0, y1; - fold = { - vertices_coords: [], - edges_vertices: [], - edges_assignment: [], - file_creator: 'oripa2fold' - }; - vertex = function(x, y) { - var v; - v = fold.vertices_coords.length; - fold.vertices_coords.push([parseFloat(x), parseFloat(y)]); - return v; - }; - nodeSpec = function(node, type, key, value) { - if ((type != null) && node.tagName !== type) { - console.warn(`ORIPA file has ${node.tagName} where ${type} was expected`); - return null; - } else if ((key != null) && (!node.hasAttribute(key) || ((value != null) && node.getAttribute(key) !== value))) { - console.warn(`ORIPA file has ${node.tagName} with ${key} = ${node.getAttribute(key)} where ${value} was expected`); - return null; - } else { - return node; - } - }; - children = function(node) { - var child, j, len, ref1, results; - if (node) { - ref1 = node.childNodes; - //# element - results = []; - for (j = 0, len = ref1.length; j < len; j++) { - child = ref1[j]; - if (child.nodeType === 1) { - results.push(child); - } - } - return results; - } else { - return []; - } - }; - oneChildSpec = function(node, type, key, value) { - var sub; - sub = children(node); - if (sub.length !== 1) { - console.warn(`ORIPA file has ${node.tagName} with ${node.childNodes.length} children, not 1`); - return null; - } else { - return nodeSpec(sub[0], type, key, value); - } - }; - oneChildText = function(node) { - var child; - if (node.childNodes.length > 1) { - console.warn(`ORIPA file has ${node.tagName} with ${node.childNodes.length} children, not 0 or 1`); - return null; - } else if (node.childNodes.length === 0) { - return ''; - } else { - child = node.childNodes[0]; - if (child.nodeType !== 3) { - return console.warn(`ORIPA file has nodeType ${child.nodeType} where 3 (text) was expected`); - } else { - return child.data; - } - } - }; - xml = new DOMParser().parseFromString(oripaStr, 'text/xml'); - ref1 = children(xml.documentElement); - for (j = 0, len = ref1.length; j < len; j++) { - top = ref1[j]; - if (nodeSpec(top, 'object', 'class', 'oripa.DataSet')) { - ref2 = children(top); - for (k = 0, len1 = ref2.length; k < len1; k++) { - property = ref2[k]; - if (property.getAttribute('property') === 'lines') { - lines = oneChildSpec(property, 'array', 'class', 'oripa.OriLineProxy'); - ref3 = children(lines); - for (l = 0, len2 = ref3.length; l < len2; l++) { - line = ref3[l]; - if (nodeSpec(line, 'void', 'index')) { - ref4 = children(line); - for (m = 0, len3 = ref4.length; m < len3; m++) { - object = ref4[m]; - if (nodeSpec(object, 'object', 'class', 'oripa.OriLineProxy')) { - //# Java doesn't encode the default value, 0 - x0 = x1 = y0 = y1 = type = 0; - ref5 = children(object); - for (n = 0, len4 = ref5.length; n < len4; n++) { - subproperty = ref5[n]; - if (nodeSpec(subproperty, 'void', 'property')) { - switch (subproperty.getAttribute('property')) { - case 'x0': - x0 = oneChildText(oneChildSpec(subproperty, 'double')); - break; - case 'x1': - x1 = oneChildText(oneChildSpec(subproperty, 'double')); - break; - case 'y0': - y0 = oneChildText(oneChildSpec(subproperty, 'double')); - break; - case 'y1': - y1 = oneChildText(oneChildSpec(subproperty, 'double')); - break; - case 'type': - type = oneChildText(oneChildSpec(subproperty, 'int')); - } - } - } - if ((x0 != null) && (x1 != null) && (y0 != null) && (y1 != null)) { - fold.edges_vertices.push([vertex(x0, y0), vertex(x1, y1)]); - if (type != null) { - type = parseInt(type); - } - fold.edges_assignment.push(oripa.type2fold[type]); - } else { - console.warn(`ORIPA line has missing data: ${x0} ${x1} ${y0} ${y1} ${type}`); - } - } - } - } - } - } else if (property.getAttribute('property') in oripa.prop_xml2fold) { - prop = oripa.prop_xml2fold[property.getAttribute('property')]; - if (prop != null) { - fold[prop] = oneChildText(oneChildSpec(property, 'string')); - } - } else { - console.warn(`Ignoring ${property.tagName} ${top.getAttribute('property')} in ORIPA file`); - } - } - } - } - //# src/oripa/Doc.java uses absolute distance POINT_EPS = 1.0 to detect - //# points being the same. - filter.collapseNearbyVertices(fold, oripa.POINT_EPS); - filter.subdivideCrossingEdges_vertices(fold, oripa.POINT_EPS); - //# In particular, convert.removeLoopEdges fold - convert.edges_vertices_to_faces_vertices(fold); - return fold; -}; - -oripa.fromFold = function(fold) { - var coord, edge, ei, fp, i, j, len, line, lines, ref1, s, vertex, vs, xp, z; - if (typeof fold === 'string') { - fold = JSON.parse(fold); - } - s = ` - - - - 1 - - - 1 - - - 400.0 - -`; - ref1 = oripa.prop_xml2fold; - for (xp in ref1) { - fp = ref1[xp]; - //if fp of fold - s += `. - - ${fold[fp] || ''} - -`.slice(2); - } - z = 0; - lines = (function() { - var j, len, ref2, results; - ref2 = fold.edges_vertices; - results = []; - for (ei = j = 0, len = ref2.length; j < len; ei = ++j) { - edge = ref2[ei]; - vs = (function() { - var k, l, len1, len2, ref3, results1; - results1 = []; - for (k = 0, len1 = edge.length; k < len1; k++) { - vertex = edge[k]; - ref3 = fold.vertices_coords[vertex].slice(2); - for (l = 0, len2 = ref3.length; l < len2; l++) { - coord = ref3[l]; - if (coord !== 0) { - z += 1; - } - } - results1.push(fold.vertices_coords[vertex]); - } - return results1; - })(); - results.push({ - x0: vs[0][0], - y0: vs[0][1], - x1: vs[1][0], - y1: vs[1][1], - type: oripa.fold2type[fold.edges_assignment[ei]] || oripa.fold2type_default - }); - } - return results; - })(); - s += `. - - -`.slice(2); - for (i = j = 0, len = lines.length; j < len; i = ++j) { - line = lines[i]; - s += `. - - - - ${line.type} - - - ${line.x0} - - - ${line.x1} - - - ${line.y0} - - - ${line.y1} - - - -`.slice(2); - } - s += `. - - - - -`.slice(2); - return s; -}; - -convert.setConverter('.fold', '.opx', oripa.fromFold); - -convert.setConverter('.opx', '.fold', oripa.toFold); - - -},{"./convert":2,"./filter":3,"@xmldom/xmldom":1}],6:[function(require,module,exports){ -var DEFAULTS, STYLES, SVGNS, geom, viewer; - -geom = require('./geom'); - -viewer = exports; - -STYLES = { - vert: "fill: white; r: 0.03; stroke: black; stroke-width: 0.005;", - face: "stroke: none; fill-opacity: 0.8;", - top: "fill: cyan;", - bot: "fill: yellow;", - edge: "fill: none; stroke-width: 0.01; stroke-linecap: round;", - axis: "fill: none; stroke-width: 0.01; stroke-linecap: round;", - text: "fill: black; font-size: 0.04; text-anchor: middle; font-family: sans-serif;", - B: "stroke: black;", - V: "stroke: blue;", - M: "stroke: red;", - U: "stroke: white;", - F: "stroke: gray;", - ax: "stroke: blue;", - ay: "stroke: red;", - az: "stroke: green;" -}; - -/* UTILITIES */ -viewer.setAttrs = function(el, attrs) { - var k, v; - (function() { - var results; - results = []; - for (k in attrs) { - v = attrs[k]; - results.push(el.setAttribute(k, v)); - } - return results; - })(); - return el; -}; - -viewer.appendHTML = function(el, tag, attrs) { - return el.appendChild(viewer.setAttrs(document.createElement(tag), attrs)); -}; - -SVGNS = 'http://www.w3.org/2000/svg'; - -viewer.appendSVG = function(el, tag, attrs) { - return el.appendChild(viewer.setAttrs(document.createElementNS(SVGNS, tag), attrs)); -}; - -viewer.makePath = function(coords) { - var c, i; - return ((function() { - var l, len, results; - results = []; - for (i = l = 0, len = coords.length; l < len; i = ++l) { - c = coords[i]; - results.push(`${i === 0 ? 'M' : 'L'} ${c[0]} ${c[1]} `); - } - return results; - })()).reduce(geom.sum); -}; - -/* INTERFACE */ -viewer.processInput = function(input, view) { - var k; - if (typeof input === 'string') { - view.fold = JSON.parse(input); - } else { - view.fold = input; - } - view.model = viewer.makeModel(view.fold); - viewer.addRotation(view); - viewer.draw(view); - viewer.update(view); - if (view.opts.properties) { - view.properties.innerHTML = ''; - for (k in view.fold) { - if (view.opts.properties) { - viewer.appendHTML(view.properties, 'option', { - value: k - }).innerHTML = k; - } - } - return viewer.updateProperties(view); - } -}; - -viewer.updateProperties = function(view) { - var s, v; - v = view.fold[view.properties.value]; - s = v.length != null ? `${v.length} elements: ` : ''; - return view.data.innerHTML = s + JSON.stringify(v); -}; - -viewer.importURL = function(url, view) { - var xhr; - xhr = new XMLHttpRequest(); - xhr.onload = (e) => { - return viewer.processInput(e.target.responseText, view); - }; - xhr.open('GET', url); - return xhr.send(); -}; - -viewer.importFile = function(file, view) { - var file_reader; - file_reader = new FileReader(); - file_reader.onload = (e) => { - return viewer.processInput(e.target.result, view); - }; - return file_reader.readAsText(file); -}; - -DEFAULTS = { - viewButtons: true, - axisButtons: true, - attrViewer: true, - examples: false, - import: true, - export: true, - properties: true -}; - -viewer.addViewer = function(div, opts = {}) { - var buttonDiv, i, inputDiv, k, l, len, ref, ref1, ref2, select, t, title, toggleDiv, url, v, val, view; - view = { - cam: viewer.initCam(), - opts: DEFAULTS - }; - for (k in opts) { - v = opts[k]; - view.opts[k] = v; - } - if (view.opts.viewButtons) { - toggleDiv = viewer.appendHTML(div, 'div'); - toggleDiv.innerHtml = ''; - toggleDiv.innerHtml += 'Toggle: '; - ref = view.cam.show; - for (k in ref) { - v = ref[k]; - t = viewer.appendHTML(toggleDiv, 'input', { - type: 'checkbox', - value: k - }); - if (v) { - t.setAttribute('checked', ''); - } - toggleDiv.innerHTML += k + ' '; - } - } - if (view.opts.axisButtons) { - buttonDiv = viewer.appendHTML(div, 'div'); - buttonDiv.innerHTML += 'View: '; - ref1 = ['x', 'y', 'z']; - for (i = l = 0, len = ref1.length; l < len; i = ++l) { - val = ref1[i]; - viewer.appendHTML(buttonDiv, 'input', { - type: 'button', - value: val - }); - } - } - if (view.opts.properties) { - buttonDiv.innerHTML += ' Property:'; - view.properties = viewer.appendHTML(buttonDiv, 'select'); - view.data = viewer.appendHTML(buttonDiv, 'div', { - style: 'width: 300; padding: 10px; overflow: auto; border: 1px solid black; display: inline-block; white-space: nowrap;' - }); - } - if (view.opts.examples || view.opts.import) { - inputDiv = viewer.appendHTML(div, 'div'); - if (view.opts.examples) { - inputDiv.innerHTML = 'Example: '; - select = viewer.appendHTML(inputDiv, 'select'); - ref2 = view.opts.examples; - for (title in ref2) { - url = ref2[title]; - viewer.appendHTML(select, 'option', { - value: url - }).innerHTML = title; - } - viewer.importURL(select.value, view); - } - if (view.opts.import) { - inputDiv.innerHTML += ' Import: '; - viewer.appendHTML(inputDiv, 'input', { - type: 'file' - }); - } - } - div.onclick = (e) => { - if (e.target.type === 'checkbox') { - if (e.target.hasAttribute('checked')) { - e.target.removeAttribute('checked'); - } else { - e.target.setAttribute('checked', ''); - } - view.cam.show[e.target.value] = e.target.hasAttribute('checked'); - viewer.update(view); - } - if (e.target.type === 'button') { - switch (e.target.value) { - case 'x': - viewer.setCamXY(view.cam, [0, 1, 0], [0, 0, 1]); - break; - case 'y': - viewer.setCamXY(view.cam, [0, 0, 1], [1, 0, 0]); - break; - case 'z': - viewer.setCamXY(view.cam, [1, 0, 0], [0, 1, 0]); - } - return viewer.update(view); - } - }; - div.onchange = (e) => { - if (e.target.type === 'file') { - viewer.importFile(e.target.files[0], view); - } - if (e.target.type === 'select-one') { - if (e.target === view.properties) { - return viewer.updateProperties(view); - } else { - return viewer.importURL(e.target.value, view); - } - } - }; - view.svg = viewer.appendSVG(div, 'svg', { - xmlns: SVGNS, - width: 600 - }); - return view; -}; - -/* CAMERA */ -viewer.initCam = function() { - return { - c: [0, 0, 0], - x: [1, 0, 0], - y: [0, 1, 0], - z: [0, 0, 1], - r: 1, - last: null, - show: { - 'Faces': true, - 'Edges': true, - 'Vertices': false, - 'Face Text': false - } - }; -}; - -viewer.proj = function(p, cam) { - var q; - q = geom.mul(geom.sub(p, cam.c), 1 / cam.r); - return [geom.dot(q, cam.x), -geom.dot(q, cam.y), 0]; -}; - -viewer.setCamXY = function(cam, x, y) { - return [cam.x, cam.y, cam.z] = [x, y, geom.cross(x, y)]; -}; - -viewer.addRotation = function(view) { - var cam, l, len, ref, s, svg; - ({ - svg: svg, - cam: cam - } = view); - ref = ['contextmenu', 'selectstart', 'dragstart']; - for (l = 0, len = ref.length; l < len; l++) { - s = ref[l]; - svg[`on${s}`] = function(e) { - return e.preventDefault(); - }; - } - svg.onmousedown = (e) => { - return cam.last = [e.clientX, e.clientY]; - }; - svg.onmousemove = (e) => { - return viewer.rotateCam([e.clientX, e.clientY], view); - }; - return svg.onmouseup = (e) => { - viewer.rotateCam([e.clientX, e.clientY], view); - return cam.last = null; - }; -}; - -viewer.rotateCam = function(p, view) { - var cam, d, e, u, x, y; - cam = view.cam; - if (cam.last == null) { - return; - } - d = geom.sub(p, cam.last); - if (!geom.mag(d) > 0) { - return; - } - u = geom.unit(geom.plus(geom.mul(cam.x, -d[1]), geom.mul(cam.y, -d[0]))); - [x, y] = (function() { - var l, len, ref, results; - ref = ['x', 'y']; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - e = ref[l]; - results.push(geom.rotate(cam[e], u, geom.mag(d) * 0.01)); - } - return results; - })(); - viewer.setCamXY(cam, x, y); - cam.last = p; - return viewer.update(view); -}; - -/* RENDERING */ -viewer.makeModel = function(fold) { - var a, as, b, cs, edge, f, f1, f2, i, i1, j, j1, l, len, len1, len2, len3, len4, m, normRel, o, r, ref, ref1, ref2, ref3, ref4, ref5, v, vs, w, z; - m = { - vs: null, - fs: null, - es: {} - }; - m.vs = (function() { - var l, len, ref, results; - ref = fold.vertices_coords; - results = []; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - cs = ref[i]; - results.push({ - i: i, - cs: cs - }); - } - return results; - })(); - (function() { - var l, len, ref, results; - ref = m.vs; - results = []; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - v = ref[i]; - if (v.cs.length === 2) { - results.push(m.vs[i].cs[2] = 0); - } - } - return results; - })(); - m.fs = (function() { - var l, len, ref, results; - ref = fold.faces_vertices; - results = []; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - vs = ref[i]; - results.push({ - i: i, - vs: (function() { - var len1, r, results1; - results1 = []; - for (r = 0, len1 = vs.length; r < len1; r++) { - v = vs[r]; - results1.push(m.vs[v]); - } - return results1; - })() - }); - } - return results; - })(); - if (fold.edges_vertices != null) { - ref = fold.edges_vertices; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - v = ref[i]; - [a, b] = v[0] > v[1] ? [v[1], v[0]] : [v[0], v[1]]; - as = ((ref1 = fold.edges_assignment) != null ? ref1[i] : void 0) != null ? fold.edges_assignment[i] : 'U'; - m.es[`e${a}e${b}`] = { - v1: m.vs[a], - v2: m.vs[b], - as: as - }; - } - } else { - ref2 = m.fs; - for (i = r = 0, len1 = ref2.length; r < len1; i = ++r) { - f = ref2[i]; - ref3 = f.vs; - for (j = z = 0, len2 = ref3.length; z < len2; j = ++z) { - v = ref3[j]; - w = f.vs[geom.next(j, f.vs.length)]; - [a, b] = v.i > w.i ? [w, v] : [v, w]; - m.es[`e${a.i}e${b.i}`] = { - v1: a, - v2: b, - as: 'U' - }; - } - } - } - ref4 = m.fs; - for (i = i1 = 0, len3 = ref4.length; i1 < len3; i = ++i1) { - f = ref4[i]; - m.fs[i].n = geom.polygonNormal((function() { - var j1, len4, ref5, results; - ref5 = f.vs; - results = []; - for (j1 = 0, len4 = ref5.length; j1 < len4; j1++) { - v = ref5[j1]; - results.push(v.cs); - } - return results; - })()); - m.fs[i].c = geom.centroid((function() { - var j1, len4, ref5, results; - ref5 = f.vs; - results = []; - for (j1 = 0, len4 = ref5.length; j1 < len4; j1++) { - v = ref5[j1]; - results.push(v.cs); - } - return results; - })()); - m.fs[i].es = {}; - m.fs[i].es = (function() { - var j1, len4, ref5, results; - ref5 = f.vs; - results = []; - for (j = j1 = 0, len4 = ref5.length; j1 < len4; j = ++j1) { - v = ref5[j]; - w = f.vs[geom.next(j, f.vs.length)]; - [a, b] = v.i > w.i ? [w, v] : [v, w]; - edge = m.es[`e${a.i}e${b.i}`]; - if (edge == null) { - edge = { - v1: a, - v2: b, - as: 'U' - }; - } - results.push(edge); - } - return results; - })(); - m.fs[i].ord = {}; - } - if (fold.faceOrders != null) { - ref5 = fold.faceOrders; - for (j1 = 0, len4 = ref5.length; j1 < len4; j1++) { - [f1, f2, o] = ref5[j1]; - if (o !== 0) { - if (geom.parallel(m.fs[f1].n, m.fs[f2].n)) { - normRel = geom.dot(m.fs[f1].n, m.fs[f2].n) > 0 ? 1 : -1; - if (m.fs[f1].ord[`f${f2}`] != null) { - console.log(`Warning: duplicate ordering input information for faces ${f1} and ${f2}. Using first found in the faceOrder list.`); - if (m.fs[f1].ord[`f${f2}`] !== o) { - console.log(`Error: duplicate ordering [${f1},${f2},${o}] is inconsistent with a previous entry.`); - } - } else { - m.fs[f1].ord[`f${f2}`] = o; - m.fs[f2].ord[`f${f1}`] = -o * normRel; - } - } else { - console.log(`Warning: order for non-parallel faces [${f1},${f2}]`); - } - } - } - } - return m; -}; - -viewer.faceAbove = function(f1, f2, n) { - var basis, dir, f, ord, p1, p2, sepDir, v, v1, v2; - [p1, p2] = (function() { - var l, len, ref, results; - ref = [f1, f2]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - f = ref[l]; - results.push((function() { - var len1, r, ref1, results1; - ref1 = f.vs; - results1 = []; - for (r = 0, len1 = ref1.length; r < len1; r++) { - v = ref1[r]; - results1.push(v.ps); - } - return results1; - })()); - } - return results; - })(); - sepDir = geom.separatingDirection2D(p1, p2, [0, 0, 1]); - if (sepDir != null) { - return null; // projections do not overlap - } - [v1, v2] = (function() { - var l, len, ref, results; - ref = [f1, f2]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - f = ref[l]; - results.push((function() { - var len1, r, ref1, results1; - ref1 = f.vs; - results1 = []; - for (r = 0, len1 = ref1.length; r < len1; r++) { - v = ref1[r]; - results1.push(v.cs); - } - return results1; - })()); - } - return results; - })(); - basis = geom.basis(v1.concat(v2)); - if (basis.length === 3) { - dir = geom.separatingDirection3D(v1, v2); - if (dir != null) { - return 0 > geom.dot(n, dir); // faces are separable in 3D - } else { - console.log(`Warning: faces ${f1.i} and ${f2.i} properly intersect. Ordering is unresolved.`); - } - } - if (basis.length === 2) { - ord = f1.ord[`f${f2.i}`]; - if (ord != null) { - return 0 > geom.dot(f2.n, n) * ord; // faces coplanar and have order - } - } - return null; -}; - -viewer.orderFaces = function(view) { - var c, direction, f, f1, f1_above, f2, faces, i, i1, j, l, len, len1, len2, len3, p, r, ref, ref1, results, z; - faces = view.model.fs; - direction = geom.mul(view.cam.z, -1); - (function() { - var l, len, results; - results = []; - for (l = 0, len = faces.length; l < len; l++) { - f = faces[l]; - results.push(f.children = []); - } - return results; - })(); - for (i = l = 0, len = faces.length; l < len; i = ++l) { - f1 = faces[i]; - for (j = r = 0, len1 = faces.length; r < len1; j = ++r) { - f2 = faces[j]; - if (!(i < j)) { - continue; - } - f1_above = viewer.faceAbove(f1, f2, direction); - if (f1_above != null) { - [p, c] = f1_above ? [f1, f2] : [f2, f1]; - p.children = p.children.concat([c]); - } - } - } - view.model.fs = geom.topologicalSort(faces); - ref = view.model.fs; - for (z = 0, len2 = ref.length; z < len2; z++) { - f = ref[z]; - f.g.parentNode.removeChild(f.g); - } - ref1 = view.model.fs; - results = []; - for (i1 = 0, len3 = ref1.length; i1 < len3; i1++) { - f = ref1[i1]; - results.push(view.svg.appendChild(f.g)); - } - return results; -}; - -viewer.draw = function({ - svg: svg, - cam: cam, - model: model - }) { - var c, e, f, i, i1, j, k, l, len, len1, len2, len3, max, min, r, ref, ref1, ref2, ref3, results, style, t, v, z; - svg.innerHTML = ''; - style = viewer.appendSVG(svg, 'style'); - for (k in STYLES) { - v = STYLES[k]; - style.innerHTML += `.${k}{${v}}\n`; - } - min = (function() { - var l, len, ref, results; - ref = [0, 1, 2]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - i = ref[l]; - results.push(((function() { - var len1, r, ref1, results1; - ref1 = model.vs; - results1 = []; - for (r = 0, len1 = ref1.length; r < len1; r++) { - v = ref1[r]; - results1.push(v.cs[i]); - } - return results1; - })()).reduce(geom.min)); - } - return results; - })(); - max = (function() { - var l, len, ref, results; - ref = [0, 1, 2]; - results = []; - for (l = 0, len = ref.length; l < len; l++) { - i = ref[l]; - results.push(((function() { - var len1, r, ref1, results1; - ref1 = model.vs; - results1 = []; - for (r = 0, len1 = ref1.length; r < len1; r++) { - v = ref1[r]; - results1.push(v.cs[i]); - } - return results1; - })()).reduce(geom.max)); - } - return results; - })(); - cam.c = geom.mul(geom.plus(min, max), 0.5); - cam.r = geom.mag(geom.sub(max, min)) / 2 * 1.05; - c = viewer.proj(cam.c, cam); - viewer.setAttrs(svg, { - viewBox: "-1,-1,2,2" - }); - t = "translate(0,0.01)"; - ref = model.fs; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - f = ref[i]; - f.g = viewer.appendSVG(svg, 'g'); - f.path = viewer.appendSVG(f.g, 'path'); - f.text = viewer.appendSVG(f.g, 'text', { - class: 'text', - transform: t - }); - f.text.innerHTML = `f${f.i}`; - f.eg = []; - ref1 = f.es; - for (j = r = 0, len1 = ref1.length; r < len1; j = ++r) { - e = ref1[j]; - f.eg[j] = viewer.appendSVG(f.g, 'path'); - } - f.vg = []; - ref2 = f.vs; - for (j = z = 0, len2 = ref2.length; z < len2; j = ++z) { - v = ref2[j]; - f.vg[j] = viewer.appendSVG(f.g, 'g'); - f.vg[j].path = viewer.appendSVG(f.vg[j], 'circle', { - class: 'vert' - }); - f.vg[j].text = viewer.appendSVG(f.vg[j], 'text', { - transform: 'translate(0, 0.01)', - class: 'text' - }); - f.vg[j].text.innerHTML = `${v.i}`; - } - } - cam.axis = viewer.appendSVG(svg, 'g', { - transform: 'translate(-0.9,-0.9)' - }); - ref3 = ['x', 'y', 'z']; - results = []; - for (i1 = 0, len3 = ref3.length; i1 < len3; i1++) { - c = ref3[i1]; - results.push(cam.axis[c] = viewer.appendSVG(cam.axis, 'path', { - id: `a${c}`, - class: `a${c} axis` - })); - } - return results; -}; - -viewer.update = function(view) { - var c, cam, e, end, f, i, j, k, l, len, len1, len2, model, p, r, ref, ref1, ref2, ref3, ref4, results, show, svg, v, visibleSide, z; - ({ - model: model, - cam: cam, - svg: svg - } = view); - (function() { - var l, len, ref, results; - ref = model.vs; - results = []; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - v = ref[i]; - results.push(model.vs[i].ps = viewer.proj(v.cs, cam)); - } - return results; - })(); - (function() { - var l, len, ref, results; - ref = model.fs; - results = []; - for (i = l = 0, len = ref.length; l < len; i = ++l) { - f = ref[i]; - results.push(model.fs[i].c2 = viewer.proj(f.c, cam)); - } - return results; - })(); - viewer.orderFaces(view); - show = {}; - ref = cam.show; - for (k in ref) { - v = ref[k]; - show[k] = v ? 'visible' : 'hidden'; - } - ref1 = model.fs; - for (i = l = 0, len = ref1.length; l < len; i = ++l) { - f = ref1[i]; - if (!(f.path != null)) { - continue; - } - visibleSide = geom.dot(f.n, cam.z) > 0 ? 'top' : 'bot'; - viewer.setAttrs(f.text, { - x: f.c2[0], - y: f.c2[1], - visibility: show['Face Text'] - }); - viewer.setAttrs(f.path, { - d: viewer.makePath((function() { - var len1, r, ref2, results; - ref2 = f.vs; - results = []; - for (r = 0, len1 = ref2.length; r < len1; r++) { - v = ref2[r]; - results.push(v.ps); - } - return results; - })()) + 'Z', - visibility: show['Faces'], - class: `face ${visibleSide}` - }); - ref2 = f.es; - for (j = r = 0, len1 = ref2.length; r < len1; j = ++r) { - e = ref2[j]; - viewer.setAttrs(f.eg[j], { - d: viewer.makePath([e.v1.ps, e.v2.ps]), - visibility: show['Edges'], - class: `edge ${e.as}` - }); - } - ref3 = f.vs; - for (j = z = 0, len2 = ref3.length; z < len2; j = ++z) { - v = ref3[j]; - viewer.setAttrs(f.vg[j], { - visibility: show['Vertices'] - }); - viewer.setAttrs(f.vg[j].path, { - cx: v.ps[0], - cy: v.ps[1] - }); - viewer.setAttrs(f.vg[j].text, { - x: v.ps[0], - y: v.ps[1] - }); - } - } - ref4 = { - x: [1, 0, 0], - y: [0, 1, 0], - z: [0, 0, 1] - }; - results = []; - for (c in ref4) { - v = ref4[c]; - end = geom.plus(geom.mul(v, 0.05 * cam.r), cam.c); - results.push(viewer.setAttrs(cam.axis[c], { - d: viewer.makePath((function() { - var i1, len3, ref5, results1; - ref5 = [cam.c, end]; - results1 = []; - for (i1 = 0, len3 = ref5.length; i1 < len3; i1++) { - p = ref5[i1]; - results1.push(viewer.proj(p, cam)); - } - return results1; - })()) - })); - } - return results; -}; - - -},{"./geom":4}],"fold":[function(require,module,exports){ -module.exports = { - geom: require('./geom'), - viewer: require('./viewer'), - filter: require('./filter'), - convert: require('./convert'), - file: require('./file') -}; - - -},{"./convert":2,"./file":1,"./filter":3,"./geom":4,"./viewer":6}]},{},[]); diff --git a/examples/cube-cp.fold b/examples/cube-cp.fold new file mode 100644 index 0000000..eb6e965 --- /dev/null +++ b/examples/cube-cp.fold @@ -0,0 +1,96 @@ +{ + "file_spec": 1, + "file_creator": "A text editor", + "file_author": "Mokshit Jain", + "file_classes": ["singleModel"], + "frame_title": "3D Cube", + "frame_classes": ["foldedForm"], + "frame_attributes": ["3D"], + "vertices_coords": [ + [0,0,0], + [1,0,0], + [1,1,0], + [0,1,0], + [0,2,0], + [1,2,0], + [-1,1,0], + [-1,0,0], + [0,-1,0], + [0,-2,0], + [1,-2,0], + [1,-1,0], + [2,0,0], + [2,1,0] + ], + "faces_vertices": [ + [0,1,2,3], + [3,2,5,4], + [1,12,13,2], + [8,11,1,0], + [9,10,11,8], + [7,0,3,6] + ], + "edges_vertices": [ + [0,1], + [1,2], + [2,3], + [3,0], + [3,4], + [4,5], + [5,2], + [2,13], + [13,12], + [12,1], + [0,7], + [7,6], + [6,3], + [0,8], + [1,11], + [8,11], + [8,9], + [9,10], + [10,11] + ], + "edges_assignment": [ + "V", + "V", + "V", + "V", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "V", + "B", + "B", + "B" + ], + "edges_foldAngle": [ + 90, + 90, + 90, + 90, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 90, + 0, + 0, + 0 + ] +} diff --git a/examples/cube-folded.fold b/examples/cube-folded.fold new file mode 100644 index 0000000..c5aa118 --- /dev/null +++ b/examples/cube-folded.fold @@ -0,0 +1,75 @@ +{ + "file_spec": 1, + "file_creator": "A text editor", + "file_author": "Mokshit Jain", + "file_classes": ["singleModel"], + "frame_title": "3D Cube", + "frame_classes": ["foldedForm"], + "frame_attributes": ["3D"], + "vertices_coords": [ + [ 0, 0, 0 ], + [ 1, 0, 0 ], + [ 1, 1, 0 ], + [ 0, 1, 0 ], + [ 0, 1, 1 ], + [ 1, 1, 1 ], + [ 0, 1, 1 ], + [ 0, 0, 1 ], + [ 0, 0, 1 ], + [ 0, 1, 1 ], + [ 1, 1, 1 ], + [ 1, 0, 1 ], + [ 1, 0, 1 ], + [ 1, 1, 1 ] + ], + "faces_vertices": [ + [0, 1, 2, 3], + [3, 2, 5, 4], + [1, 12, 13, 2], + [8, 11, 1, 0], + [9, 10, 11, 8], + [7, 0, 3, 6] + ], + "edges_vertices": [ + [0,1], + [1,2], + [2,3], + [3,0], + [3,4], + [4,5], + [5,2], + [2,13], + [13,12], + [12,1], + [0,7], + [7,6], + [6,3], + [0,8], + [1,11], + [8,11], + [8,9], + [9,10], + [10,11] + ], + "edges_assignment": [ + "V", + "V", + "V", + "V", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "B", + "V", + "B", + "B", + "B" + ] +} diff --git a/examples/foldviewer.html b/examples/foldviewer.html deleted file mode 100644 index 2444dcc..0000000 --- a/examples/foldviewer.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -Fold Viewer - - - - -

FOLD Viewer

-
-

(C) Jason S. Ku 2016

- - diff --git a/examples/test.html b/examples/test.html deleted file mode 100644 index f1d7323..0000000 --- a/examples/test.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -FOLD - - - -

Welcome to FOLD!

- - diff --git a/index.html b/index.html new file mode 100644 index 0000000..c8a10f1 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + Fold Viewer + + +

FOLD Viewer

+
+

(C) Jason S. Ku 2016

+ + + diff --git a/package.json b/package.json index 4cf9a26..13d069f 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,45 @@ { "name": "fold", - "version": "0.12.0", + "version": "0.13.0", "description": "FOLD file format for origami models, crease patterns, etc.", - "main": "lib/index.js", + "main": "dist/index.civet.js", "bin": { "fold-convert": "bin/fold-convert.js" }, "scripts": { - "test": "npm run coffee && jest", - "prepare": "npm run coffee && npm run dist", - "coffee": "coffee --no-header --bare -o lib -c src", - "dist": "browserify -t coffeeify --extension=.coffee -r ./src/index.coffee:fold -o dist/fold.js" + "test": "vitest", + "build": "vite build", + "dev": "vite dev" + }, + "exports": { + ".": { + "import": "./dist/index.civet.js", + "require": "./dist/index.civet.mjs" + }, + "./convert": { + "import": "./dist/convert.civet.js", + "require": "./dist/convert.civet.mjs" + }, + "./file": { + "import": "./dist/file.civet.js", + "require": "./dist/file.civet.mjs" + }, + "./filter": { + "import": "./dist/filter.civet.js", + "require": "./dist/filter.civet.mjs" + }, + "./geom": { + "import": "./dist/geom.civet.js", + "require": "./dist/geom.civet.mjs" + }, + "./viewer": { + "import": "./dist/viewer.civet.js", + "require": "./dist/viewer.civet.mjs" + }, + "./oripa": { + "import": "./dist/oripa.civet.js", + "require": "./dist/oripa.civet.mjs" + } }, "repository": { "type": "git", @@ -28,7 +57,7 @@ { "name": "Erik Demaine", "email": "edemaine@mit.edu", - "url": "http://erikdemaine.org" + "url": "https://erikdemaine.org" }, "Jason Ku", "Robert Lang" @@ -39,24 +68,16 @@ }, "homepage": "https://github.com/edemaine/fold#readme", "devDependencies": { - "browserify": "^16.5.0", - "coffeeify": "^3.0.1", - "coffeescript": "^2.4.1", - "jest": "^26.6.3", - "jest-matcher-deep-close-to": "^2.0.1", - "jest-preset-coffeescript": "1.1.1" - }, - "browser": { - "@xmldom/xmldom": false, - "./src/file.coffee": false - }, - "jest": { - "preset": "jest-preset-coffeescript", - "testPathIgnorePatterns": [ - "/node_modules/" - ] + "@danielx/civet": "^0.6.61", + "@types/functional-red-black-tree": "^1.0.6", + "@types/node": "^20.8.2", + "jest-matcher-deep-close-to": "^3.0.2", + "typescript": "^5.1.6", + "vite": "^4.5.0", + "vitest": "^0.34.6" }, "dependencies": { - "@xmldom/xmldom": "^0.7.2" + "@xmldom/xmldom": "^0.7.2", + "functional-red-black-tree": "^1.0.1" } } diff --git a/src/convert.civet b/src/convert.civet new file mode 100644 index 0000000..55d6238 --- /dev/null +++ b/src/convert.civet @@ -0,0 +1,531 @@ +* as geom from "./geom.civet" +* as filter from "./filter.civet" +type { Coords, Fold, Converter } from "./types.civet" + +/** + * Works for abstract structures, so NOT SORTED. + * Use sort_vertices_vertices to sort in counterclockwise order. + */ +export function edges_vertices_to_vertices_vertices_unsorted(fold: Fold): Fold + fold.vertices_vertices = filter.edges_vertices_to_vertices_vertices fold + fold + +/** + * Invert edges_vertices into vertices_edges. + * Works for abstract structures, so NOT SORTED. + */ +export function edges_vertices_to_vertices_edges_unsorted(fold: Fold) + fold.vertices_edges = filter.edges_vertices_to_vertices_edges fold + fold + +/** + * Given a FOLD object with 2D `vertices_coords` and `edges_vertices` property + * (defining edge endpoints), automatically computes the `vertices_vertices` + * property and sorts them counterclockwise by angle in the plane. + */ +export function edges_vertices_to_vertices_vertices_sorted(fold: Fold): Fold + edges_vertices_to_vertices_vertices_unsorted fold + sort_vertices_vertices fold + +/** + * Given a FOLD object with 2D `vertices_coords` and `edges_vertices` property + * (defining edge endpoints), automatically computes the `vertices_edges` + * and `vertices_vertices` property and sorts them counterclockwise by angle + * in the plane. + */ +export function edges_vertices_to_vertices_edges_sorted(fold: Fold): Fold + edges_vertices_to_vertices_vertices_sorted fold + vertices_vertices_to_vertices_edges fold + +export function sort_vertices_vertices(fold: Fold): Fold + ### + Sorts `fold.vertices_vertices` in counterclockwise order using `fold.vertices_coords`. + * 2D only. Constructs `fold.vertices_vertices` if absent, via + `convert.edges_vertices_to_vertices_vertices`. + ### + coords := fold.vertices_coords + unless coords? + throw new Error "sort_vertices_vertices: Vertex coordinates missing" + unless coords.every .length == 2 + throw new Error "sort_vertices_vertices: Vertex coordinates not two dimensional" + unless fold.vertices_vertices? + edges_vertices_to_vertices_vertices_unsorted fold + fold.vertices_vertices = fold.vertices_vertices! + for v, neighbors in fold.vertices_vertices + geom.sortByAngle neighbors, coords[v], (x: number) => coords[x] + fold + +export function vertices_vertices_to_faces_vertices(fold: Fold): Fold + ### + Given a 2D FOLD object with counterclockwise-sorted `vertices_vertices` + property, constructs the implicitly defined faces, setting `faces_vertices` + property. Requires `vertices_coords` to exclude the outside face. + ### + unless fold.vertices_vertices? + throw new Error "vertices_vertices_to_faces_vertices needs vertices_vertices" + unless fold.vertices_coords? + throw new Error "vertices_vertices_to_faces_vertices needs vertices_coords" + next: Record := {} + for each neighbors, v of fold.vertices_vertices + for each u, i of neighbors + next[`${u},${v}`] = neighbors[(i-1) %% neighbors.length] + + fold.faces_vertices = [] + for each uv of Object.keys next + w .= next[uv] + continue unless w? + next[uv] = null + [uStr, vStr] := uv.split ',' + u .= parseInt uStr + v .= parseInt vStr + face := [u, v] + until w == face[0] + unless w? + console.warn `Confusion with face ${face}` + break + face.push w + [u, v] = [v, w] + w = next[`${u},${v}`] + next[`${u},${v}`] = null + next[`${face[face.length-1]},${face[0]}`] = null + // Outside face is clockwise; exclude it. + if w? and geom.polygonOrientation((fold.vertices_coords[x] for each x of face)) > 0 + fold.faces_vertices.push face + fold + +/** + * Given a FOLD object with counterclockwise-sorted `vertices_edges` property, + * constructs the implicitly defined faces, setting both `faces_vertices` + * and `faces_edges` properties. Handles multiple edges to the same vertex + * (unlike `FOLD.convert.vertices_vertices_to_faces_vertices`). + * Requires `vertices_coords` to exclude the outside face. + */ +export function vertices_edges_to_faces_vertices_edges(fold: Fold): Fold + unless fold.vertices_edges? + throw new Error "vertices_edges_to_faces_vertices_edges needs vertices_edges" + unless fold.edges_vertices? + throw new Error "vertices_edges_to_faces_vertices_edges needs edges_vertices" + unless fold.vertices_coords? + throw new Error "vertices_edges_to_faces_vertices_edges needs vertices_coords" + next: Record[] := [] + for each neighbors, v of fold.vertices_edges + next[v] = {} + for each e, i of neighbors + next[v][e] = neighbors[(i-1) %% neighbors.length] + + fold.faces_vertices = [] + fold.faces_edges = [] + + for each nexts, vertex of next + for e1Str, let e2 in nexts + continue unless e2? + e1 .= parseInt e1Str + nexts[e1] = null + edges := [e1] + vertices: number[] := [ + filter.edges_verticesIncident fold.edges_vertices[e1], + fold.edges_vertices[e2] + ] + unless vertices[0]? + throw new Error `Confusion at edges ${e1} and ${e2}` + until e2 == edges[0] + unless e2? + console.warn `Confusion with face containing edges ${edges}` + break + edges.push e2 + for each var v of fold.edges_vertices[e2] + if v != vertices[vertices.length-1] + vertices.push v + break + e1 = e2 + e2 = next[v][e1] + next[v][e1] = null + // Move e1 to the end so that edges[0] connects vertices[0] to vertices[1] + edges.push edges.shift()! + // Outside face is clockwise; exclude it. + if e2? and geom.polygonOrientation((fold.vertices_coords[x] for each x of vertices)) > 0 + fold.faces_vertices.push vertices + fold.faces_edges.push edges + fold + +/** + * Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, + * computes a counterclockwise-sorted `vertices_vertices` property and + * constructs the implicitly defined faces, setting `faces_vertices` property. + */ +export function edges_vertices_to_faces_vertices(fold: Fold): Fold + edges_vertices_to_vertices_vertices_sorted fold + vertices_vertices_to_faces_vertices fold + +/** + * Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, + * computes counterclockwise-sorted `vertices_vertices` and `vertices_edges` + * properties and constructs the implicitly defined faces, setting + * both `faces_vertices` and `faces_edges` property. + */ +export function edges_vertices_to_faces_vertices_edges(fold: Fold): Fold + edges_vertices_to_vertices_edges_sorted fold + vertices_edges_to_faces_vertices_edges fold + +/** + * Given a FOLD object with `vertices_vertices` and `edges_vertices`, + * fills in the corresponding `vertices_edges` property (preserving order). + */ +export function vertices_vertices_to_vertices_edges(fold: Fold): Fold + unless fold.vertices_vertices? + throw new Error "vertices_vertices_to_vertices_edges needs vertices_vertices" + unless fold.edges_vertices? + throw new Error "vertices_vertices_to_vertices_edges needs edges_vertices" + edgeMap: Record := {} + for each [v1, v2], edge of fold.edges_vertices + edgeMap[`${v1},${v2}`] = edge + edgeMap[`${v2},${v1}`] = edge + fold.vertices_edges = + for each vertices, vertex of fold.vertices_vertices + for i of [0...vertices.length] + edgeMap[`${vertex},${vertices[i]}`] + fold + +/** + * Given a FOLD object with `faces_vertices` and `edges_vertices`, + * fills in the corresponding `faces_edges` property (preserving order). + */ +export function faces_vertices_to_faces_edges(fold: Fold): Fold + unless fold.faces_vertices? + throw new Error "faces_vertices_to_faces_edges needs faces_vertices" + unless fold.edges_vertices? + throw new Error "faces_vertices_to_faces_edges needs edges_vertices" + edgeMap: Record := {} + for each [v1, v2], edge of fold.edges_vertices + edgeMap[`${v1},${v2}`] = edge + edgeMap[`${v2},${v1}`] = edge + fold.faces_edges = + for each vertices, face of fold.faces_vertices + for i of [0...vertices.length] + edgeMap[`${vertices[i]},${vertices[(i+1) % vertices.length]}`] + fold + +/** + * Given a FOLD object with just `faces_vertices`, automatically fills in + * `edges_vertices`, `edges_faces`, `faces_edges`, and `edges_assignment` + * (indicating which edges are boundary with 'B' and 'U'). + * This code currently assumes an orientable manifold, and uses nulls to + * represent missing neighbor faces in `edges_faces` (for boundary edges). + */ +export function faces_vertices_to_edges(mesh: Fold): Fold + unless mesh.faces_vertices? + throw new Error "faces_vertices_to_edges needs faces_vertices" + mesh.edges_vertices = [] + mesh.edges_faces = [] + mesh.faces_edges = [] + mesh.edges_assignment = [] + + edgeMap: Record := {} + + for faceStr, vertices in mesh.faces_vertices + face := parseInt faceStr + mesh.faces_edges.push( + for each v1, i of vertices + v2 := vertices[(i+1) % vertices.length] + + let key: string + if v1 <= v2 + key = `${v1},${v2}` + else + key = `${v2},${v1}` + + let edge: number + if key in edgeMap + edge = edgeMap[key] + // Second instance of edge means not on boundary + mesh.edges_assignment[edge] = 'U' + else + edge = edgeMap[key] = mesh.edges_vertices.length + if v1 <= v2 + mesh.edges_vertices.push [v1, v2] + else + mesh.edges_vertices.push [v2, v1] + mesh.edges_faces.push [null, null] + // First instance of edge might be on boundary + mesh.edges_assignment.push 'B' + + if v1 <= v2 + mesh.edges_faces[edge][0] = face + else + mesh.edges_faces[edge][1] = face + edge + ) + mesh + +export function edges_vertices_to_edges_faces_edges(fold: Fold): Fold + ### + Given a `fold` object with `edges_vertices` and `faces_vertices`, + fills in `faces_edges` and `edges_faces`. + ### + unless fold.edges_vertices? + throw new Error "edges_vertices_to_edges_faces_edges needs edges_vertices" + unless fold.faces_vertices? + throw new Error "edges_vertices_to_edges_faces_edges needs faces_vertices" + fold.edges_faces = ([null, null] for edge of [0...fold.edges_vertices.length]) + edgeMap: Record := {} + + for edgeStr, vertices in fold.edges_vertices + continue unless vertices? + edge := parseInt edgeStr + edgeMap[`${vertices[0]},${vertices[1]}`] = [edge, 0] // forward + edgeMap[`${vertices[1]},${vertices[0]}`] = [edge, 1] // backward + + fold.faces_edges ?= [] + for faceStr, vertices in fold.faces_vertices + face := parseInt faceStr + fold.faces_edges[face] = + for each v1, i of vertices + v2 := vertices[(i+1) % vertices.length] + [edge, orient] := edgeMap[`${v1},${v2}`] + fold.edges_faces[edge][orient] = face + edge + fold + +export function computeFoldAngles(fold: Fold) + unless fold.vertices_coords? + throw new Error "computeFoldAngles needs vertices_coords" + unless fold.edges_vertices? + throw new Error "computeFoldAngles needs edges_vertices" + unless fold.faces_vertices? + throw new Error "computeFoldAngles needs faces_vertices" + unless fold.edges_faces? and fold.faces_edges? + edges_vertices_to_edges_faces_edges fold + fold.faces_edges = fold.faces_edges! as number[][] + fold.edges_faces = fold.edges_faces! as number[][] + + return fold.edges_foldAngle if fold.edges_foldAngle? + + foldAngles: number[] := [] + faceNormals: Coords[] := [] + + for edge in fold.edges_vertices + edgesFaces := (fold.edges_faces[edge].filter (x) => x?) as number[] + + // Not a boundary edge + if edgesFaces.length == 2 + normals := for each face of edgesFaces + faceCoords := for each vertex of fold.faces_vertices[face] + fold.vertices_coords[vertex] + + faceNormals[face] ?= geom.polygonNormal(faceCoords) + + if normals[0] == null or normals[1] == null + foldAngles[edge] = 0 + else + toThree := (x: number[]) => Array.from({length: 3}, (_, i) => x[i] ?? 0) + ang := geom.ang(toThree(normals[0]), toThree(normals[1]))! + foldAngles[edge] = geom.toDegree(ang) + else + foldAngles[edge] = 0 + + foldAngles + +export function foldedGeometry(fold: Fold, rootFace = 0, edges_foldAngle = fold.edges_foldAngle): number + unless fold.vertices_coords? + throw new Error "foldedGeometry needs vertices_coords" + unless fold.edges_vertices? + throw new Error "foldedGeometry needs edges_vertices" + unless edges_foldAngle? + throw new Error "foldedGeometry needs edges_foldAngle" + unless fold.edges_faces? and fold.faces_edges? + edges_vertices_to_edges_faces_edges fold + fold.faces_edges = fold.faces_edges! as number[][] + fold.edges_faces = fold.edges_faces! as number[][] + + maxError .= 0 + level .= [rootFace] + fold.faces_foldTransform = [] + fold.faces_foldTransform[rootFace] = [[1,0,0],[0,1,0],[0,0,1]] + fold.vertices_foldCoords = Array(fold.vertices_coords.length).fill(null) + + // Use fold.faces_edges -> fold.edges_vertices, which are both needed below, + // in case fold.faces_vertices isn't defined. + for each edge of fold.faces_edges[rootFace] + for each vertex of fold.edges_vertices[edge] + fold.vertices_foldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for each face of level + for each edge of fold.faces_edges[face] + for each face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + + crease_edges_vertices := fold.edges_vertices[edge] + crease_coords := (for each vertex of crease_edges_vertices + fold.vertices_coords[vertex] + ) as [Coords, Coords] + + transform := geom.matrixMatrix fold.faces_foldTransform[face]!, geom.matrixMatrix( + geom.matrixMatrix( + geom.matrixTranslate(crease_coords[1]) + geom.matrixRotate3D(...crease_coords, geom.toRadian(edges_foldAngle[edge])) + ), + geom.matrixTranslate(geom.mul crease_coords[1], -1) + ) + + if fold.faces_foldTransform[face2]? + for each row, i of fold.faces_foldTransform[face2]! + maxError = Math.max maxError, geom.dist row, transform[i] + else + fold.faces_foldTransform[face2] = transform + + for each edge2 of fold.faces_edges[face2] + for each vertex2 of fold.edges_vertices[edge2] + mapped := geom.matrixVector transform, fold.vertices_coords[vertex2] + if coords := fold.vertices_foldCoords[vertex2] + maxError = Math.max maxError, geom.dist coords, mapped + else + fold.vertices_foldCoords[vertex2] = mapped + nextLevel.push face2 + level = nextLevel + maxError + +export function unfoldedGeometry(fold: Fold, rootFace = 0): number + foldAngles := computeFoldAngles fold + unfoldAngles := foldAngles.map (ang) => -ang + foldedGeometry fold, rootFace, unfoldAngles + +/** + * Assuming `fold` is a locally flat foldable crease pattern in the xy plane, + * sets `fold.vertices_flatFoldCoords` to give the flat-folded geometry + * as determined by repeated reflection relative to `rootFace`; sets + * `fold.faces_flatFoldTransform` transformation matrix mapping each face's + * unfolded --> folded geometry; and sets `fold.faces_flatFoldOrientation` to + * +1 or -1 to indicate whether each folded face matches its original + * orientation or is upside-down (so is oriented clockwise in 2D). + * + * Requires `fold` to have `vertices_coords` and `edges_vertices`; + * `edges_faces` and `faces_edges` will be created if they do not exist. + * + * Returns the maximum displacement error from closure constraints (multiple + * mappings of the same vertices, or multiple transformations of the same face). + */ +export function flatFoldedGeometry(fold: Fold, rootFace = 0): number + unless fold.vertices_coords? + throw new Error "flatUnfoldedGeometry needs vertices_coords" + unless fold.edges_vertices? + throw new Error "flatUnfoldedGeometry needs edges_vertices" + unless fold.edges_faces? and fold.faces_edges? + edges_vertices_to_edges_faces_edges fold + fold.faces_edges = fold.faces_edges! as number[][] + fold.edges_faces = fold.edges_faces! as number[][] + + maxError .= 0 + level .= [rootFace] + fold.faces_flatFoldTransform = Array(fold.faces_edges.length).fill(null) + fold.faces_flatFoldTransform[rootFace] = [[1,0,0],[0,1,0]] // identity + fold.faces_flatFoldOrientation = Array(fold.faces_edges.length).fill(null) + fold.faces_flatFoldOrientation[rootFace] = +1 + fold.vertices_flatFoldCoords = Array(fold.vertices_coords.length).fill(null) + + // Use fold.faces_edges -> fold.edges_vertices, which are both needed below, + // in case fold.faces_vertices isn't defined. + for each edge of fold.faces_edges[rootFace] + for each vertex of fold.edges_vertices[edge] + fold.vertices_flatFoldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for each face of level + orientation := -fold.faces_flatFoldOrientation[face]! + + for each edge of fold.faces_edges[face] + for each face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + transform := geom.matrixMatrix fold.faces_flatFoldTransform[face]!, + geom.matrixReflectLine(...( + for each vertex of fold.edges_vertices[edge] + fold.vertices_coords[vertex] + ) as [Coords, Coords]) + + if fold.faces_flatFoldTransform[face2]? + for each row, i of fold.faces_flatFoldTransform[face2]! + maxError = Math.max maxError, geom.dist row, transform[i] + if orientation != fold.faces_flatFoldOrientation[face2] + maxError = Math.max 1, maxError + else + fold.faces_flatFoldTransform[face2] = transform + fold.faces_flatFoldOrientation[face2] = orientation + + for each edge2 of fold.faces_edges[face2] + for each vertex2 of fold.edges_vertices[edge2] + mapped := geom.matrixVector transform, fold.vertices_coords[vertex2] + if coords := fold.vertices_flatFoldCoords[vertex2] + maxError = Math.max maxError, geom.dist coords, mapped + else + fold.vertices_flatFoldCoords[vertex2] = mapped + nextLevel.push face2 + level = nextLevel + maxError + +// `foldedGeometry` is self-inverse +export function flatUnfoldedGeometry(fold: Fold, rootFace = 0): number + flatFoldedGeometry fold, rootFace + +export function deepCopy(fold: T): T + // Given a FOLD object, make a copy that shares no pointers with the original. + if typeof fold is in ['number', 'string', 'boolean'] + fold + else if Array.isArray fold + (for each item of fold + deepCopy item + ) as T + else // Object + copy := {} as T + for key, value in fold as object + copy[key] = deepCopy value + copy + +/** Convert FOLD object into a nicely formatted JSON string. */ +export function toJSON(fold: Fold): string + "{\n" + + (for key, value in fold + ` ${JSON.stringify key}: ` + + if Array.isArray value + "[\n" + + (` ${JSON.stringify(obj)}` for each obj of value).join(',\n') + + "\n ]" + else + JSON.stringify value + ).join(',\n') + + "\n}\n" + +export extensions: Record := {} +export converters: Record := {} + +export function getConverter(fromExt: string, toExt: string): Converter | undefined + if fromExt == toExt + (x: any) => x + else + converters[`${fromExt}${toExt}`] + +export function setConverter(fromExt: string, toExt: string, converter: Converter): void + extensions[fromExt] = true + extensions[toExt] = true + converters[`${fromExt}${toExt}`] = converter + +export function convertFromTo(data: T, fromExt: string, toExt: string): unknown + fromExt = `.${fromExt}` unless fromExt[0] == '.' + toExt = `.${toExt}` unless toExt[0] == '.' + converter := getConverter fromExt, toExt + + unless converter? + throw new Error `No converter from ${fromExt} to ${toExt}` + + converter data + +export function convertFrom(data: T, fromExt: string): unknown + convertFromTo data, fromExt, '.fold' + +export function convertTo(data: T, toExt: string): unknown + convertFromTo data, '.fold', toExt + +// export * as oripa from './oripa.civet' diff --git a/src/convert.coffee b/src/convert.coffee deleted file mode 100644 index 93effaf..0000000 --- a/src/convert.coffee +++ /dev/null @@ -1,375 +0,0 @@ -### FOLD FORMAT MANIPULATORS ### - -geom = require './geom' -filter = require './filter' -convert = exports - -convert.edges_vertices_to_vertices_vertices_unsorted = (fold) -> - ### - Works for abstract structures, so NOT SORTED. - Use sort_vertices_vertices to sort in counterclockwise order. - ### - fold.vertices_vertices = filter.edges_vertices_to_vertices_vertices fold - fold - -convert.edges_vertices_to_vertices_edges_unsorted = (fold) -> - ### - Invert edges_vertices into vertices_edges. - Works for abstract structures, so NOT SORTED. - ### - fold.vertices_edges = filter.edges_vertices_to_vertices_edges fold - fold - -convert.edges_vertices_to_vertices_vertices_sorted = (fold) -> - ### - Given a FOLD object with 2D `vertices_coords` and `edges_vertices` property - (defining edge endpoints), automatically computes the `vertices_vertices` - property and sorts them counterclockwise by angle in the plane. - ### - convert.edges_vertices_to_vertices_vertices_unsorted fold - convert.sort_vertices_vertices fold - -convert.edges_vertices_to_vertices_edges_sorted = (fold) -> - ### - Given a FOLD object with 2D `vertices_coords` and `edges_vertices` property - (defining edge endpoints), automatically computes the `vertices_edges` - and `vertices_vertices` property and sorts them counterclockwise by angle - in the plane. - ### - convert.edges_vertices_to_vertices_vertices_sorted fold - convert.vertices_vertices_to_vertices_edges fold - -convert.sort_vertices_vertices = (fold) -> - ### - Sorts `fold.vertices_neighbords` in counterclockwise order using - `fold.vertices_coordinates`. 2D only. - Constructs `fold.vertices_neighbords` if absent, via - `convert.edges_vertices_to_vertices_vertices`. - ### - unless fold.vertices_coords?[0]?.length == 2 - throw new Error "sort_vertices_vertices: Vertex coordinates missing or not two dimensional" - unless fold.vertices_vertices? - convert.edges_vertices_to_vertices_vertices fold - for v, neighbors of fold.vertices_vertices - geom.sortByAngle neighbors, v, (x) -> fold.vertices_coords[x] - fold - -convert.vertices_vertices_to_faces_vertices = (fold) -> - ### - Given a FOLD object with counterclockwise-sorted `vertices_vertices` - property, constructs the implicitly defined faces, setting `faces_vertices` - property. - ### - next = {} - for neighbors, v in fold.vertices_vertices - for u, i in neighbors - next["#{u},#{v}"] = neighbors[(i-1) %% neighbors.length] - #console.log u, v, neighbors[(i-1) %% neighbors.length] - fold.faces_vertices = [] - #for uv, w of next - for uv in (key for key of next) - w = next[uv] - continue unless w? - next[uv] = null - [u, v] = uv.split ',' - u = parseInt u - v = parseInt v - face = [u, v] - until w == face[0] - unless w? - console.warn "Confusion with face #{face}" - break - face.push w - [u, v] = [v, w] - w = next["#{u},#{v}"] - next["#{u},#{v}"] = null - next["#{face[face.length-1]},#{face[0]}"] = null - ## Outside face is clockwise; exclude it. - if w? and geom.polygonOrientation(fold.vertices_coords[x] for x in face) > 0 - #console.log face - fold.faces_vertices.push face - #else - # console.log face, 'clockwise' - fold - -convert.vertices_edges_to_faces_vertices_edges = (fold) -> - ### - Given a FOLD object with counterclockwise-sorted `vertices_edges` property, - constructs the implicitly defined faces, setting both `faces_vertices` - and `faces_edges` properties. Handles multiple edges to the same vertex - (unlike `FOLD.convert.vertices_vertices_to_faces_vertices`). - ### - next = [] - for neighbors, v in fold.vertices_edges - next[v] = {} - for e, i in neighbors - next[v][e] = neighbors[(i-1) %% neighbors.length] - #console.log e, neighbors[(i-1) %% neighbors.length] - fold.faces_vertices = [] - fold.faces_edges = [] - for nexts, vertex in next - for e1, e2 of nexts - continue unless e2? - e1 = parseInt e1 - nexts[e1] = null - edges = [e1] - vertices = [filter.edges_verticesIncident fold.edges_vertices[e1], - fold.edges_vertices[e2]] - unless vertices[0]? - throw new Error "Confusion at edges #{e1} and #{e2}" - until e2 == edges[0] - unless e2? - console.warn "Confusion with face containing edges #{edges}" - break - edges.push e2 - for v in fold.edges_vertices[e2] - if v != vertices[vertices.length-1] - vertices.push v - break - e1 = e2 - e2 = next[v][e1] - next[v][e1] = null - ## Move e1 to the end so that edges[0] connects vertices[0] to vertices[1] - edges.push edges.shift() - ## Outside face is clockwise; exclude it. - if e2? and geom.polygonOrientation(fold.vertices_coords[x] for x in vertices) > 0 - #console.log vertices, edges - fold.faces_vertices.push vertices - fold.faces_edges.push edges - #else - # console.log face, 'clockwise' - fold - -convert.edges_vertices_to_faces_vertices = (fold) -> - ### - Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, - computes a counterclockwise-sorted `vertices_vertices` property and - constructs the implicitly defined faces, setting `faces_vertices` property. - ### - convert.edges_vertices_to_vertices_vertices_sorted fold - convert.vertices_vertices_to_faces_vertices fold - -convert.edges_vertices_to_faces_vertices_edges = (fold) -> - ### - Given a FOLD object with 2D `vertices_coords` and `edges_vertices`, - computes counterclockwise-sorted `vertices_vertices` and `vertices_edges` - properties and constructs the implicitly defined faces, setting - both `faces_vertices` and `faces_edges` property. - ### - convert.edges_vertices_to_vertices_edges_sorted fold - convert.vertices_edges_to_faces_vertices_edges fold - -convert.vertices_vertices_to_vertices_edges = (fold) -> - ### - Given a FOLD object with `vertices_vertices` and `edges_vertices`, - fills in the corresponding `vertices_edges` property (preserving order). - ### - edgeMap = {} - for [v1, v2], edge in fold.edges_vertices - edgeMap["#{v1},#{v2}"] = edge - edgeMap["#{v2},#{v1}"] = edge - fold.vertices_edges = - for vertices, vertex in fold.vertices_vertices - for i in [0...vertices.length] - edgeMap["#{vertex},#{vertices[i]}"] - -convert.faces_vertices_to_faces_edges = (fold) -> - ### - Given a FOLD object with `faces_vertices` and `edges_vertices`, - fills in the corresponding `faces_edges` property (preserving order). - ### - edgeMap = {} - for [v1, v2], edge in fold.edges_vertices - edgeMap["#{v1},#{v2}"] = edge - edgeMap["#{v2},#{v1}"] = edge - fold.faces_edges = - for vertices, face in fold.faces_vertices - for i in [0...vertices.length] - edgeMap["#{vertices[i]},#{vertices[(i+1) % vertices.length]}"] - -convert.faces_vertices_to_edges = (mesh) -> - ### - Given a FOLD object with just `faces_vertices`, automatically fills in - `edges_vertices`, `edges_faces`, `faces_edges`, and `edges_assignment` - (indicating which edges are boundary with 'B'). - This code currently assumes an orientable manifold, and uses nulls to - represent missing neighbor faces in `edges_faces` (for boundary edges). - ### - mesh.edges_vertices = [] - mesh.edges_faces = [] - mesh.faces_edges = [] - mesh.edges_assignment = [] - edgeMap = {} - for face, vertices of mesh.faces_vertices - face = parseInt face - mesh.faces_edges.push( - for v1, i in vertices - v1 = parseInt v1 - v2 = vertices[(i+1) % vertices.length] - if v1 <= v2 - key = "#{v1},#{v2}" - else - key = "#{v2},#{v1}" - if key of edgeMap - edge = edgeMap[key] - # Second instance of edge means not on boundary - mesh.edges_assignment[edge] = null - else - edge = edgeMap[key] = mesh.edges_vertices.length - if v1 <= v2 - mesh.edges_vertices.push [v1, v2] - else - mesh.edges_vertices.push [v2, v1] - mesh.edges_faces.push [null, null] - # First instance of edge might be on boundary - mesh.edges_assignment.push 'B' - if v1 <= v2 - mesh.edges_faces[edge][0] = face - else - mesh.edges_faces[edge][1] = face - edge - ) - mesh - -convert.edges_vertices_to_edges_faces_edges = (fold) -> - ### - Given a `fold` object with `edges_vertices` and `faces_vertices`, - fills in `faces_edges` and `edges_vertices`. - ### - fold.edges_faces = ([null, null] for edge in [0...fold.edges_vertices.length]) - edgeMap = {} - for edge, vertices of fold.edges_vertices when vertices? - edge = parseInt edge - edgeMap["#{vertices[0]},#{vertices[1]}"] = [edge, 0] # forward - edgeMap["#{vertices[1]},#{vertices[0]}"] = [edge, 1] # backward - for face, vertices of fold.faces_vertices - face = parseInt face - fold.faces_edges[face] = - for v1, i in vertices - v2 = vertices[(i+1) % vertices.length] - [edge, orient] = edgeMap["#{v1},#{v2}"] - fold.edges_faces[edge][orient] = face - edge - fold - -convert.flatFoldedGeometry = (fold, rootFace = 0) -> - ### - Assuming `fold` is a locally flat foldable crease pattern in the xy plane, - sets `fold.vertices_flatFoldCoords` to give the flat-folded geometry - as determined by repeated reflection relative to `rootFace`; sets - `fold.faces_flatFoldTransform` transformation matrix mapping each face's - unfolded --> folded geometry; and sets `fold.faces_flatFoldOrientation` to - +1 or -1 to indicate whether each folded face matches its original - orientation or is upside-down (so is oriented clockwise in 2D). - - Requires `fold` to have `vertices_coords` and `edges_vertices`; - `edges_faces` and `faces_edges` will be created if they do not exist. - - Returns the maximum displacement error from closure constraints (multiple - mappings of the same vertices, or multiple transformations of the same face). - ### - if fold.vertices_coords? and fold.edges_vertices? and not - (fold.edges_faces? and fold.faces_edges?) - convert.edges_vertices_to_edges_faces_edges fold - maxError = 0 - level = [rootFace] - fold.faces_flatFoldTransform = - (null for face in [0...fold.faces_edges.length]) - fold.faces_flatFoldTransform[rootFace] = [[1,0,0],[0,1,0]] # identity - fold.faces_flatFoldOrientation = - (null for face in [0...fold.faces_edges.length]) - fold.faces_flatFoldOrientation[rootFace] = +1 - fold.vertices_flatFoldCoords = - (null for vertex in [0...fold.vertices_coords.length]) - # Use fold.faces_edges -> fold.edges_vertices, which are both needed below, - # in case fold.faces_vertices isn't defined. - for edge in fold.faces_edges[rootFace] - for vertex in fold.edges_vertices[edge] - fold.vertices_flatFoldCoords[vertex] ?= fold.vertices_coords[vertex][..] - while level.length - nextLevel = [] - for face in level - orientation = -fold.faces_flatFoldOrientation[face] - for edge in fold.faces_edges[face] - for face2 in fold.edges_faces[edge] when face2? and face2 != face - transform = geom.matrixMatrix fold.faces_flatFoldTransform[face], - geom.matrixReflectLine ...( - for vertex in fold.edges_vertices[edge] - fold.vertices_coords[vertex] - ) - if fold.faces_flatFoldTransform[face2]? - for row, i in fold.faces_flatFoldTransform[face2] - maxError = Math.max maxError, geom.dist row, transform[i] - if orientation != fold.faces_flatFoldOrientation[face2] - maxError = Math.max 1, maxError - else - fold.faces_flatFoldTransform[face2] = transform - fold.faces_flatFoldOrientation[face2] = orientation - for edge2 in fold.faces_edges[face2] - for vertex2 in fold.edges_vertices[edge2] - mapped = geom.matrixVector transform, fold.vertices_coords[vertex2] - if fold.vertices_flatFoldCoords[vertex2]? - maxError = Math.max maxError, - geom.dist fold.vertices_flatFoldCoords[vertex2], mapped - else - fold.vertices_flatFoldCoords[vertex2] = mapped - nextLevel.push face2 - level = nextLevel - maxError - -convert.deepCopy = (fold) -> - ## Given a FOLD object, make a copy that shares no pointers with the original. - if typeof fold in ['number', 'string', 'boolean'] - fold - else if Array.isArray fold - for item in fold - convert.deepCopy item - else # Object - copy = {} - for own key, value of fold - copy[key] = convert.deepCopy value - copy - -convert.toJSON = (fold) -> - ## Convert FOLD object into a nicely formatted JSON string. - "{\n" + - (for key, value of fold - " #{JSON.stringify key}: " + - if Array.isArray value - "[\n" + - (" #{JSON.stringify(obj)}" for obj in value).join(',\n') + - "\n ]" - else - JSON.stringify value - ).join(',\n') + - "\n}\n" - -convert.extensions = {} -convert.converters = {} -convert.getConverter = (fromExt, toExt) -> - if fromExt == toExt - (x) -> x - else - convert.converters["#{fromExt}#{toExt}"] -convert.setConverter = (fromExt, toExt, converter) -> - convert.extensions[fromExt] = true - convert.extensions[toExt] = true - convert.converters["#{fromExt}#{toExt}"] = converter - -convert.convertFromTo = (data, fromExt, toExt) -> - fromExt = ".#{fromExt}" unless fromExt[0] == '.' - toExt = ".#{toExt}" unless toExt[0] == '.' - converter = convert.getConverter fromExt, toExt - unless converter? - if fromExt == toExt - return data - throw new Error "No converter from #{fromExt} to #{toExt}" - converter data - -convert.convertFrom = (data, fromExt) -> - convert.convertFromTo data, fromExt, '.fold' - -convert.convertTo = (data, toExt) -> - convert.convertFromTo data, '.fold', toExt - -convert.oripa = require './oripa' diff --git a/src/file.civet b/src/file.civet new file mode 100644 index 0000000..74adb01 --- /dev/null +++ b/src/file.civet @@ -0,0 +1,136 @@ +fs from fs +path from path +{ fileURLToPath } from url +* as convert from "./convert.civet" +type { Fold, Converter } from "./types.civet" + +interface Options + converter?: Converter + flatFold?: boolean + fold?: boolean + unfold?: boolean + +export function extensionOf(filename: string): string | null + parsed := path.parse filename + if parsed.ext + parsed.ext + else if parsed.base[0] == '.' + parsed.base + else if `.${filename}` in convert.extensions + `.${filename}` + else + null + +export function toFile(fold: Fold, output: string, converter?: Converter): void + outFormat := extensionOf output + unless outFormat + console.warn `Could not detect extension of ${output}` + return + unless converter? + converter = convert.getConverter '.fold', outFormat + unless converter? + console.warn `No converter from .fold to ${outFormat}` + return + + result .= converter fold + if typeof result != 'string' + result = convert.toJSON result as Fold + + fs.writeFileSync output, result as string, 'utf-8' + +export function fileToFile(input: string, output: string, options: Options): void + inFormat := extensionOf input + outFormat := extensionOf output + unless inFormat + console.warn `Could not detect extension of ${input}` + return + unless outFormat + console.warn `Could not detect extension of ${output}` + return + converter .= options.converter + unless converter? + converter = convert.getConverter inFormat, outFormat + unless converter? + console.warn `No converter from ${inFormat} to ${outFormat}` + return + if outFormat == output or outFormat == `.${output}` + // just extension => concatenate + parsed_output := path.parse input + parsed_output.ext = outFormat + parsed_output.base = parsed_output.name + parsed_output.ext + output = path.format parsed_output + if input == output + console.warn `Attempt to convert ${input} to same filename` + else + console.log input, '->', output + result: unknown .= fs.readFileSync input, 'utf-8' + if inFormat == '.fold' != outFormat // avoid double mogrification + result = mogrify result as string, options + result = converter result + if outFormat == '.fold' + result = mogrify result as string, options + if typeof result != 'string' + result = convert.toJSON result as Fold + fs.writeFileSync output, result as string, 'utf-8' + +export function mogrify(data: string, options: Options): string | undefined + return unless options.flatFold // or any options set + fold: Fold := JSON.parse data + fold.file_creator = "fold-convert" + if options.flatFold + fold.file_creator += " --flat-fold" + error := convert.flatFoldedGeometry fold + console.log ` -- Flat folding error: ${error}` + fold.vertices_flatUnfoldCoords = fold.vertices_coords + fold.vertices_coords = fold.vertices_flatFoldCoords + if fold.frame_classes? + fold.frame_classes = fold.frame_classes.filter (x) => x != 'creasePattern' + .concat 'foldedForm' + if options.fold + fold.file_creator += " --fold" + error := convert.foldedGeometry fold + console.log ` -- 3D folding error: ${error}` + fold.vertices_unfoldCoords = fold.vertices_coords + fold.vertices_coords = fold.vertices_foldCoords + if fold.frame_classes? + fold.frame_classes = fold.frame_classes.filter (x) => x != 'creasePattern' + .concat 'foldedForm' + if options.unfold + fold.file_creator += " --unfold" + error := convert.flatUnfoldedGeometry fold + console.log ` -- Unfolding flat error: ${error}` + fold.vertices_foldCoords = fold.vertices_coords + fold.vertices_coords = fold.vertices_flatUnfoldCoords + if fold.frame_classes? + fold.frame_classes = fold.frame_classes.filter (x) -> x != 'foldedForm' + .concat 'creasePattern' + convert.toJSON fold + +export function main(args = process.argv[2..]): void + filenames := [] + output .= '.fold' // Default behavior: convert to .fold + options: Options := {} + mode .= null + + for arg of args + switch mode + when 'output' + output = arg + mode = null + else + switch arg + when '-o', '--output' + mode = 'output' + when '--flat-fold' + options.flatFold = true + when '--fold' + options.fold = true + when '--unfold' + options.unfold = true + else + filenames.push arg + + for each filename of filenames + fileToFile filename, output, options + +main() if import.meta.url.startsWith('file:') and process.argv[1] == fileURLToPath(import.meta.url) diff --git a/src/file.coffee b/src/file.coffee deleted file mode 100644 index d196421..0000000 --- a/src/file.coffee +++ /dev/null @@ -1,104 +0,0 @@ -fs = require 'fs' -path = require 'path' -convert = require './convert' -file = exports - -file.extensionOf = (filename) -> - parsed = path.parse filename - if parsed.ext - parsed.ext - else if parsed.base[0] == '.' - parsed.base - else if ".#{filename}" of convert.extensions - ".#{filename}" - else - null - -file.toFile = (fold, output, converter = null) -> - outFormat = file.extensionOf output - unless outFormat - console.warn "Could not detect extension of #{output}" - return - unless converter? - converter = convert.getConverter '.fold', outFormat - unless converter? - console.warn "No converter from .fold to #{outFormat}" - return - result = converter fold - if typeof result != 'string' - result = convert.toJSON result - fs.writeFileSync output, result, 'utf-8' - -file.fileToFile = (input, output, options) -> - inFormat = file.extensionOf input - outFormat = file.extensionOf output - unless inFormat - console.warn "Could not detect extension of #{input}" - return - unless outFormat - console.warn "Could not detect extension of #{output}" - return - converter = options.converter - unless converter? - converter = convert.getConverter inFormat, outFormat - unless converter? - console.warn "No converter from #{inFormat} to #{outFormat}" - return - if outFormat == output or outFormat == ".#{output}" - ## just extension => concatenate - output = path.parse input - output.ext = outFormat - output.base = output.name + output.ext - output = path.format output - if input == output - console.warn "Attempt to convert #{input} to same filename" - else - console.log input, '->', output - result = fs.readFileSync input, 'utf-8' - if inFormat == '.fold' != outFormat # avoid double mogrification - result = file.mogrify result, options - result = converter result - if outFormat == '.fold' - result = file.mogrify result, options - if typeof result != 'string' - result = convert.toJSON result - fs.writeFileSync output, result, 'utf-8' - -file.mogrify = (data, options) -> - return unless options.flatFold # or any options set - fold = JSON.parse data - fold.file_creator = "fold-convert" - if options.flatFold - fold.file_creator += " --flat-fold" - error = convert.flatFoldedGeometry fold - console.log " -- Flat folding error: #{error}" - fold.vertices_flatUnfoldCoords = fold.vertices_coords - fold.vertices_coords = fold.vertices_flatFoldCoords - fold.frame_classes = fold.frame_classes.filter (x) -> x != 'creasePattern' - .concat 'foldedForm' - delete fold.vertices_flatFoldCoords - convert.toJSON fold - -file.main = (args = process.argv[2..]) -> - filenames = [] - output = '.fold' ## Default behavior: convert to .fold - options = - flatFold: false - mode = null - for arg in args - switch mode - when 'output' - output = arg - mode = null - else - switch arg - when '-o', '--output' - mode = 'output' - when '--flat-fold' - options.flatFold = true - else - filenames.push arg - for filename in filenames - file.fileToFile filename, output, options - -file.main() if require.main == module diff --git a/src/filter.civet b/src/filter.civet new file mode 100644 index 0000000..eea2e70 --- /dev/null +++ b/src/filter.civet @@ -0,0 +1,490 @@ +* as geom from "./geom.civet" +type { Fold, Coords } from "./types.civet" + +export function edgesAssigned(fold: Fold, target: string): number[] + for assignment, i of fold.edges_assignment + continue unless assignment == target + i + +export function mountainEdges(fold: Fold) + edgesAssigned fold, 'M' + +export function valleyEdges(fold: Fold) + edgesAssigned fold, 'V' + +export function flatEdges(fold: Fold) + edgesAssigned fold, 'F' + +export function boundaryEdges(fold: Fold) + edgesAssigned fold, 'B' + +export function unassignedEdges(fold: Fold) + edgesAssigned fold, 'U' + +export function cutEdges(fold: Fold) + edgesAssigned fold, 'C' + +export function joinEdges(fold: Fold) + edgesAssigned fold, 'J' + +export function keysStartingWith(fold: Fold, prefix: string) + for key in fold + continue unless key[...prefix.length] == prefix + key + +export function keysEndingWith(fold: Fold, suffix: string) + for key in fold + continue unless key[-suffix.length..] == suffix + key + +export function remapField(fold: Fold, field: string, old2new: number[]) + ### + old2new: null means throw away that object + ### + new2old := [] + for each j, i of old2new // later overwrites earlier + new2old[j] = i if j? + for key of keysStartingWith fold, `${field}_` + fold[key] = (fold[key][old] for old of new2old) + for key of keysEndingWith fold, `_${field}` + fold[key] = ((old2new[old] for old of array) for array of fold[key]) + fold + +export function remapFieldSubset(fold: Fold, field: string, keep: number[]) + id .= 0 + old2new := + for value of keep + if value + id++ + else + null // remove + remapField fold, field, old2new + old2new + +export function remove(fold: Fold, field: string, index: number) + ### + Remove given index from given field ('vertices', 'edges', 'faces'), in place. + ### + remapFieldSubset fold, field, + for i of [0...numType fold, field] + i != index + +export function removeVertex(fold: Fold, index: number) + remove fold, 'vertices', index +export function removeEdge(fold: Fold, index: number) + remove fold, 'edges', index +export function removeFace(fold: Fold, index: number) + remove fold, 'faces', index + +export function transform(fold: Fold, matrix: Coords[]) + ### + Transforms all fields ending in _coords (in particular, vertices_coords) + and all fields ending in FoldTransform (in particular, + faces_flatFoldTransform generated by convert.flat_folded_geometry) + according to the given transformation matrix. + ### + for key of keysEndingWith fold, "_coords" + fold[key] = (geom.matrixVector(matrix, coords) for coords of fold[key]) + for key of keysEndingWith fold, "FoldTransform" + continue unless '_' in key + fold[key] = (geom.matrixMatrix(matrix, transform) for transform of fold[key]) + fold + +export function numType(fold: Fold, type: string) + ### + Count the maximum number of objects of a given type, by looking at all + fields with key of the form `type_...`, and if that fails, looking at all + fields with key of the form `..._type`. Returns `0` if nothing found. + ### + counts .= + for key of keysStartingWith fold, `${type}_` + value := fold[key] + continue unless value.length? + value.length + unless counts.length + counts = + for key of keysEndingWith fold, `_${type}` + 1 + Math.max fold[key]... + if counts.length + Math.max counts... + else + 0 // nothing of this type + +export function numVertices(fold: Fold) + numType fold, 'vertices' +export function numEdges(fold: Fold) + numType fold, 'edges' +export function numFaces(fold: Fold) + numType fold, 'faces' + +export function removeDuplicateEdges_vertices(fold: Fold) + seen: Record := {} + id .= 0 + old2new: number[] := + for edge of fold.edges_vertices + [v, w] := edge + key .= "" + if v < w + key = `${v},${w}` + else + key = `${w},${v}` + unless key in seen + seen[key] = id + id += 1 + seen[key] + remapField fold, 'edges', old2new + old2new + +export function edges_verticesIncident(e1: Coords, e2: Coords) + for v of e1 + if v is in e2 + return v + null + +class RepeatedPointsDS + hash: Record + epsilon: number + vertices_coords:Coords[] + + @(@vertices_coords:Coords[], @epsilon: number) + // Note: if vertices_coords has some duplicates in the initial state, + // then we will detect them but won't remove them here. Rather, + // future duplicate inserts will return the higher-index vertex. + @hash = {} + for coord, v of @vertices_coords + (@hash[@key coord] ?= []).push v + null + + lookup(coord:Coords) + [x, y] := coord + xr := Math.round(x / @epsilon) + yr := Math.round(y / @epsilon) + for xt of [xr, xr-1, xr+1] + for yt of [yr, yr-1, yr+1] + key := `${xt},${yt}` + for v of @hash[key] ?? [] + if @epsilon > geom.dist @vertices_coords[v], coord + return v + null + + key(coord:Coords) + [x, y] := coord + xr := Math.round x / @epsilon + yr := Math.round y / @epsilon + key := `${xr},${yr}` + key + + insert(coord:Coords) + v .= @lookup coord + return v if v? + (@hash[@key coord] ?= []).push v = @vertices_coords.length + @vertices_coords.push coord + v + +export function collapseNearbyVertices(fold: Fold, epsilon: number) + vertices := new RepeatedPointsDS [], epsilon + old2new := + for coords of fold.vertices_coords + vertices.insert coords + remapField fold, 'vertices', old2new + // In particular: fold.vertices_coords = vertices.vertices_coords + +export function maybeAddVertex(fold: Fold, coords:Coords, epsilon: number) + ### + Add a new vertex at coordinates `coords` and return its (last) index, + unless there is already such a vertex within distance `epsilon`, + in which case return the closest such vertex's index. + ### + i := geom.closestIndex coords, fold.vertices_coords + if i? and epsilon >= geom.dist coords, fold.vertices_coords[i] + i // Closest point is close enough + else + fold.vertices_coords.push(coords) - 1 + +export function addVertexLike(fold: Fold, oldVertexIndex: number) + // Add a vertex and copy data from old vertex. + vNew := numVertices fold + for key of keysStartingWith fold, 'vertices_' + switch key[6..] + when 'vertices' + // Leaving these broken + else + fold[key][vNew] = fold[key][oldVertexIndex] + vNew + +export function addEdgeLike(fold: Fold, oldEdgeIndex: number, v1?: number, v2?: number) + // Add an edge between v1 and v2, and copy data from old edge. + // If v1 or v2 are unspecified, defaults to the vertices of the old edge. + // Must have `edges_vertices` property. + eNew := fold.edges_vertices.length + for key in keysStartingWith fold, 'edges_' + switch key[6..] + when 'vertices' + fold.edges_vertices.push [ + v1 ?? fold.edges_vertices[oldEdgeIndex][0] + v2 ?? fold.edges_vertices[oldEdgeIndex][1] + ] + when 'edges' + // Leaving these broken + else + fold[key][eNew] = fold[key][oldEdgeIndex] + eNew + +export function addVertexAndSubdivide(fold: Fold, coords:Coords, epsilon: number) + v := maybeAddVertex fold, coords, epsilon + changedEdges: number[] := [] + if v == fold.vertices_coords.length - 1 + // Similar to "Handle overlapping edges" case: + for e, i of fold.edges_vertices + continue if v in e // shouldn't happen + s := (fold.vertices_coords[u] for u of e) as [Coords, Coords] + if geom.pointStrictlyInSegment coords, s // implicit epsilon + // console.log coords, 'in', s + iNew := addEdgeLike fold, i, v, e[1] + changedEdges.push i, iNew + e[1] = v + [v, changedEdges] as const + +export function removeLoopEdges(fold: Fold) + ### + Remove edges whose endpoints are identical. After collapsing via + `filter.collapseNearbyVertices`, this removes epsilon-length edges. + ### + remapFieldSubset fold, 'edges', + for edge of fold.edges_vertices + edge[0] != edge[1] + +export function subdivideCrossingEdges_vertices(fold: Fold, epsilon: number): number[] +export function subdivideCrossingEdges_vertices(fold: Fold, epsilon: number, involvingEdgesFrom: number): [number[], number[]] +export function subdivideCrossingEdges_vertices(fold: Fold, epsilon: number, involvingEdgesFrom?: number) + ### + Using just `vertices_coords` and `edges_vertices` and assuming all in 2D, + subdivides all crossing/touching edges to form a planar graph. + In particular, all duplicate and loop edges are also removed. + + If called without `involvingEdgesFrom`, does all subdivision in quadratic + time. xxx Should be O(n log n) via plane sweep. + In this case, returns an array of indices of all edges that were subdivided + (both modified old edges and new edges). + + If called with `involvingEdgesFrom`, does all subdivision involving an + edge numbered `involvingEdgesFrom` or higher. For example, after adding an + edge with largest number, call with `involvingEdgesFrom = + edges_vertices.length - 1`; then this will run in linear time. + In this case, returns two arrays of edges: the first array are all subdivided + from the "involved" edges, while the second array is the remaining subdivided + edges. + ### + + changedEdges: [number[], number[]] := [[], []] + addEdge := (v1: number, v2: number, oldEdgeIndex: number, which: number) -> + // console.log 'adding', oldEdgeIndex, fold.edges_vertices.length, 'to', which + eNew := addEdgeLike fold, oldEdgeIndex, v1, v2 + changedEdges[which].push oldEdgeIndex, eNew + + // Handle overlapping edges by subdividing edges at any vertices on them. + // We use a while loop instead of a for loop to process newly added edges. + i .= involvingEdgesFrom ?? 0 + while i < fold.edges_vertices.length + e := fold.edges_vertices[i] + s := (fold.vertices_coords[u] for u of e) as [Coords, Coords] + for p, v of fold.vertices_coords + continue if v is in e + if geom.pointStrictlyInSegment p, s // implicit epsilon + // console.log p, 'in', s + addEdge v, e[1], i, 0 + e[1] = v + i++ + + // Handle crossing edges + // We use a while loop instead of a for loop to process newly added edges. + vertices := new RepeatedPointsDS fold.vertices_coords, epsilon + i1 .= involvingEdgesFrom ?? 0 + while i1 < fold.edges_vertices.length + e1 := fold.edges_vertices[i1] + s1 := (fold.vertices_coords[v] for v of e1) as [Coords, Coords] + for e2, i2 of fold.edges_vertices[...i1] + s2 := (fold.vertices_coords[v] for v of e2) as [Coords, Coords] + if not edges_verticesIncident(e1, e2) and geom.segmentsCross s1, s2 + // segment intersection is too sensitive a test; + // segmentsCross more reliable + // cross = segmentIntersectSegment s1, s2 + cross := geom.lineIntersectLine s1, s2 + continue unless cross? + crossI := vertices.insert cross as Coords + // console.log e1, s1, 'intersects', e2, s2, 'at', cross, crossI + unless crossI is in e1 and (crossI is in e2) // don't add endpoint again + // console.log e1, e2, '->' + unless crossI is in e1 + addEdge crossI, e1[1], i1, 0 + e1[1] = crossI + s1[1] = fold.vertices_coords[crossI] // update for future iterations + // console.log '->', e1, fold.edges_vertices[fold.edges_vertices.length-1] + unless crossI is in e2 + addEdge crossI, e2[1], i2, 1 + e2[1] = crossI + // console.log '->', e2, fold.edges_vertices[fold.edges_vertices.length-1] + i1++ + + old2new .= removeDuplicateEdges_vertices fold + for i of [0, 1] + changedEdges[i] = (old2new[e] for e of changedEdges[i]) + old2new = removeLoopEdges fold + for i of [0, 1] + changedEdges[i] = (old2new[e] for e of changedEdges[i]) + + if involvingEdgesFrom? + changedEdges + else + changedEdges[0].concat changedEdges[1] + +// ------------------------- PORTED ----------------------------- + +export function addEdgeAndSubdivide(fold: Fold, v1Coord:Coords, v2Coord:Coords, epsilon: number) + ### + Add an edge between vertex indices or points `v1` and `v2`, subdivide + as necessary, and return two arrays: all the subdivided parts of this edge, + and all the other edges that change. + If the edge is a loop or a duplicate, both arrays will be empty. + ### + changedEdges1: number[] .= [] + changedEdges2: number[] .= [] + v1: number | undefined .= undefined + v2: number | undefined .= undefined + + if v1Coord.length? + [v1, changedEdges1] = addVertexAndSubdivide fold, v1Coord, epsilon + if v2Coord.length? + [v2, changedEdges2] = addVertexAndSubdivide fold, v2Coord, epsilon + if v1 == v2 // Ignore loop edges + return [[], []] + + for e, i of fold.edges_vertices + if (e[0] == v1 and e[1] == v2) or + (e[0] == v2 and e[1] == v1) + return [[i], []] // Ignore duplicate edges + + iNew := fold.edges_vertices.push([v1, v2]) - 1 + changedEdges: [number[], number[]] .= [[],[]] + + if iNew + changedEdges = subdivideCrossingEdges_vertices(fold, epsilon, iNew) + changedEdges[0].push iNew unless iNew in changedEdges[0] + else + changedEdges = [[iNew], []] + changedEdges[1].push changedEdges1... if changedEdges1? + changedEdges[1].push changedEdges2... if changedEdges2? + changedEdges + +export function splitCuts(fold: Fold, es = cutEdges(fold)) + ### + Given a FOLD object with `edges_vertices`, `edges_assignment`, and + counterclockwise-sorted `vertices_edges` + (see `FOLD.convert.edges_vertices_to_vertices_edges_sorted`), + cuts apart ("unwelds") all edges in `es` into pairs of boundary edges. + When an endpoint of a cut edge ends up on n boundaries, + it splits into n vertices. + Preserves above-mentioned properties (so you can then compute faces via + `FOLD.convert.edges_vertices_to_faces_vertices_edges`), + and recomputes `vertices_vertices` if present, + but ignores face properties. + `es` is unspecified, cuts all edges with an assignment of `"C"`, + effectively switching from FOLD 1.2's `"C"` assignments to + FOLD 1.1's `"B"` assignments. + ### + return fold unless es.length + + // Maintain map from every vertex to array of incident boundary edges + vertices_boundaries: number[][] := [] + for e of boundaryEdges fold + for v of fold.edges_vertices[e] + (vertices_boundaries[v] ?= []).push e + + for e1 of es + // Split e1 into two edges {e1, e2} + e2 := addEdgeLike fold, e1 + for v, i of fold.edges_vertices[e1] + ve := fold.vertices_edges[v] + // Insert e2 before e1 in first vertex and after e1 in second vertex + // to represent valid counterclockwise ordering + ve.splice ve.indexOf(e1) + i, 0, e2 + + // Check for endpoints of {e1, e2} to split, when they're on the boundary + for v1 of fold.edges_vertices[e1] + boundaries := vertices_boundaries[v1]?.length + if boundaries >= 2 // vertex already on boundary + if boundaries > 2 + throw new Error `${vertices_boundaries[v1].length} boundary edges at vertex ${v1}` + [b1, b2] := vertices_boundaries[v1] + neighbors .= fold.vertices_edges[v1] + i1 := neighbors.indexOf b1 + i2 := neighbors.indexOf b2 + if i2 == (i1+1) % neighbors.length + neighbors = neighbors[i2..].concat neighbors[..i1] unless i2 == 0 + else if i1 == (i2+1) % neighbors.length + neighbors = neighbors[i1..].concat neighbors[..i2] unless i1 == 0 + else + throw new Error `Nonadjacent boundary edges at vertex ${v1}` + + // Find first vertex among e1, e2 among neighbors, so other is next + ie1 := neighbors.indexOf e1 + ie2 := neighbors.indexOf e2 + ie := Math.min ie1, ie2 + fold.vertices_edges[v1] = neighbors[..ie] + v2 := addVertexLike fold, v1 + fold.vertices_edges[v2] = neighbors[1+ie..] + // console.log "Split ${neighbors} into ${fold.vertices_edges[v1]} for ${v1} and ${fold.vertices_edges[v2]} for ${v2}" + // Update relevant incident edges to use v2 instead of v1 + for neighbor of fold.vertices_edges[v2] // including e2 + ev := fold.edges_vertices[neighbor] + ev[ev.indexOf v1] = v2 + // Partition boundary edges incident to v1 + vertices_boundaries[v1] = [] + vertices_boundaries[v2] = [] + for b of [b1, b2] + if b in fold.vertices_edges[v1] + vertices_boundaries[v1].push b + else //if b in fold.vertices_edges[v2] + vertices_boundaries[v2].push b + + // e1 and e2 are new boundary edges + if fold.edges_assignment? + fold.edges_assignment[e1] = 'B' + fold.edges_assignment[e2] = 'B' + + for v of fold.edges_vertices[e1] + (vertices_boundaries[v] ?= []).push e1 + for v of fold.edges_vertices[e2] + (vertices_boundaries[v] ?= []).push e2 + + if fold.vertices_vertices? // would be out-of-date + fold.vertices_vertices = edges_vertices_to_vertices_vertices fold + fold + +export function edges_vertices_to_vertices_vertices(fold: Fold) + ### + Works for abstract structures, so NOT SORTED. + Use sort_vertices_vertices to sort in counterclockwise order. + ### + num_vertices := numVertices fold + vertices_vertices: number[][] := ([] for v of [0...num_vertices]) + for [v, w] of fold.edges_vertices + while v >= vertices_vertices.length + vertices_vertices.push [] + while w >= vertices_vertices.length + vertices_vertices.push [] + vertices_vertices[v].push w + vertices_vertices[w].push v + vertices_vertices + +export function edges_vertices_to_vertices_edges(fold: Fold) + ### + Invert edges_vertices into vertices_edges. + Works for abstract structures, so NOT SORTED in any sense. + ### + num_vertices := numVertices fold + vertices_edges: number[][] := ([] for _ in [0...num_vertices]) + for vertices, edge of fold.edges_vertices + for vertex of vertices + vertices_edges[vertex].push edge + vertices_edges diff --git a/src/filter.coffee b/src/filter.coffee deleted file mode 100644 index 637c2bf..0000000 --- a/src/filter.coffee +++ /dev/null @@ -1,450 +0,0 @@ -geom = require './geom' -filter = exports - -filter.edgesAssigned = (fold, target) -> - i for assignment, i in fold.edges_assignment when assignment == target -filter.mountainEdges = (fold) -> - filter.edgesAssigned fold, 'M' -filter.valleyEdges = (fold) -> - filter.edgesAssigned fold, 'V' -filter.flatEdges = (fold) -> - filter.edgesAssigned fold, 'F' -filter.boundaryEdges = (fold) -> - filter.edgesAssigned fold, 'B' -filter.unassignedEdges = (fold) -> - filter.edgesAssigned fold, 'U' -filter.cutEdges = (fold) -> - filter.edgesAssigned fold, 'C' -filter.joinEdges = (fold) -> - filter.edgesAssigned fold, 'J' - -filter.keysStartingWith = (fold, prefix) -> - key for key of fold when key[...prefix.length] == prefix - -filter.keysEndingWith = (fold, suffix) -> - key for key of fold when key[-suffix.length..] == suffix - -filter.remapField = (fold, field, old2new) -> - ### - old2new: null means throw away that object - ### - new2old = [] - for j, i in old2new ## later overwrites earlier - new2old[j] = i if j? - for key in filter.keysStartingWith fold, "#{field}_" - fold[key] = (fold[key][old] for old in new2old) - for key in filter.keysEndingWith fold, "_#{field}" - fold[key] = (old2new[old] for old in array for array in fold[key]) - fold - -filter.remapFieldSubset = (fold, field, keep) -> - id = 0 - old2new = - for value in keep - if value - id++ - else - null ## remove - filter.remapField fold, field, old2new - old2new - -filter.remove = (fold, field, index) -> - ### - Remove given index from given field ('vertices', 'edges', 'faces'), in place. - ### - filter.remapFieldSubset fold, field, - for i in [0...filter.numType fold, field] - i != index - -filter.removeVertex = (fold, index) -> filter.remove fold, 'vertices', index -filter.removeEdge = (fold, index) -> filter.remove fold, 'edges', index -filter.removeFace = (fold, index) -> filter.remove fold, 'faces', index - -filter.transform = (fold, matrix) -> - ### - Transforms all fields ending in _coords (in particular, vertices_coords) - and all fields ending in FoldTransform (in particular, - faces_flatFoldTransform generated by convert.flat_folded_geometry) - according to the given transformation matrix. - ### - for key in filter.keysEndingWith fold, "_coords" - fold[key] = (geom.matrixVector(matrix, coords) for coords in fold[key]) - for key in filter.keysEndingWith fold, "FoldTransform" when '_' in key - fold[key] = (geom.matrixMatrix(matrix, transform) for transform in fold[key]) - fold - -filter.numType = (fold, type) -> - ### - Count the maximum number of objects of a given type, by looking at all - fields with key of the form `type_...`, and if that fails, looking at all - fields with key of the form `..._type`. Returns `0` if nothing found. - ### - counts = - for key in filter.keysStartingWith fold, "#{type}_" - value = fold[key] - continue unless value.length? - value.length - unless counts.length - counts = - for key in filter.keysEndingWith fold, "_#{type}" - 1 + Math.max fold[key]... - if counts.length - Math.max counts... - else - 0 ## nothing of this type - -filter.numVertices = (fold) -> filter.numType fold, 'vertices' -filter.numEdges = (fold) -> filter.numType fold, 'edges' -filter.numFaces = (fold) -> filter.numType fold, 'faces' - -filter.removeDuplicateEdges_vertices = (fold) -> - seen = {} - id = 0 - old2new = - for edge in fold.edges_vertices - [v, w] = edge - if v < w - key = "#{v},#{w}" - else - key = "#{w},#{v}" - unless key of seen - seen[key] = id - id += 1 - seen[key] - filter.remapField fold, 'edges', old2new - old2new - -filter.edges_verticesIncident = (e1, e2) -> - for v in e1 - if v in e2 - return v - null - -## Use hashing to find points within an epsilon > 0 distance from each other. -## Each integer cell will have O(1) distinct points before matching -## (number of disjoint half-unit disks that fit in a unit square). - -class RepeatedPointsDS - constructor: (@vertices_coords, @epsilon) -> - ## Note: if vertices_coords has some duplicates in the initial state, - ## then we will detect them but won't remove them here. Rather, - ## future duplicate inserts will return the higher-index vertex. - @hash = {} - for coord, v in @vertices_coords - (@hash[@key coord] ?= []).push v - null - - lookup: (coord) -> - [x, y] = coord - xr = Math.round(x / @epsilon) - yr = Math.round(y / @epsilon) - for xt in [xr, xr-1, xr+1] - for yt in [yr, yr-1, yr+1] - key = "#{xt},#{yt}" - for v in @hash[key] ? [] - if @epsilon > geom.dist @vertices_coords[v], coord - return v - null - - key: (coord) -> - [x, y] = coord - xr = Math.round x / @epsilon - yr = Math.round y / @epsilon - key = "#{xr},#{yr}" - - insert: (coord) -> - v = @lookup coord - return v if v? - (@hash[@key coord] ?= []).push v = @vertices_coords.length - @vertices_coords.push coord - v - -filter.collapseNearbyVertices = (fold, epsilon) -> - vertices = new RepeatedPointsDS [], epsilon - old2new = - for coords in fold.vertices_coords - vertices.insert coords - filter.remapField fold, 'vertices', old2new - ## In particular: fold.vertices_coords = vertices.vertices_coords - -filter.maybeAddVertex = (fold, coords, epsilon) -> - ### - Add a new vertex at coordinates `coords` and return its (last) index, - unless there is already such a vertex within distance `epsilon`, - in which case return the closest such vertex's index. - ### - i = geom.closestIndex coords, fold.vertices_coords - if i? and epsilon >= geom.dist coords, fold.vertices_coords[i] - i ## Closest point is close enough - else - fold.vertices_coords.push(coords) - 1 - -filter.addVertexLike = (fold, oldVertexIndex) -> - ## Add a vertex and copy data from old vertex. - vNew = filter.numVertices fold - for key in filter.keysStartingWith fold, 'vertices_' - switch key[6..] - when 'vertices' - ## Leaving these broken - else - fold[key][vNew] = fold[key][oldVertexIndex] - vNew - -filter.addEdgeLike = (fold, oldEdgeIndex, v1, v2) -> - ## Add an edge between v1 and v2, and copy data from old edge. - ## If v1 or v2 are unspecified, defaults to the vertices of the old edge. - ## Must have `edges_vertices` property. - eNew = fold.edges_vertices.length - for key in filter.keysStartingWith fold, 'edges_' - switch key[6..] - when 'vertices' - fold.edges_vertices.push [ - v1 ? fold.edges_vertices[oldEdgeIndex][0] - v2 ? fold.edges_vertices[oldEdgeIndex][1] - ] - when 'edges' - ## Leaving these broken - else - fold[key][eNew] = fold[key][oldEdgeIndex] - eNew - -filter.addVertexAndSubdivide = (fold, coords, epsilon) -> - v = filter.maybeAddVertex fold, coords, epsilon - changedEdges = [] - if v == fold.vertices_coords.length - 1 - ## Similar to "Handle overlapping edges" case: - for e, i in fold.edges_vertices - continue if v in e # shouldn't happen - s = (fold.vertices_coords[u] for u in e) - if geom.pointStrictlyInSegment coords, s ## implicit epsilon - #console.log coords, 'in', s - iNew = filter.addEdgeLike fold, i, v, e[1] - changedEdges.push i, iNew - e[1] = v - [v, changedEdges] - -filter.removeLoopEdges = (fold) -> - ### - Remove edges whose endpoints are identical. After collapsing via - `filter.collapseNearbyVertices`, this removes epsilon-length edges. - ### - filter.remapFieldSubset fold, 'edges', - for edge in fold.edges_vertices - edge[0] != edge[1] - -filter.subdivideCrossingEdges_vertices = (fold, epsilon, involvingEdgesFrom) -> - ### - Using just `vertices_coords` and `edges_vertices` and assuming all in 2D, - subdivides all crossing/touching edges to form a planar graph. - In particular, all duplicate and loop edges are also removed. - - If called without `involvingEdgesFrom`, does all subdivision in quadratic - time. xxx Should be O(n log n) via plane sweep. - In this case, returns an array of indices of all edges that were subdivided - (both modified old edges and new edges). - - If called with `involvingEdgesFrom`, does all subdivision involving an - edge numbered `involvingEdgesFrom` or higher. For example, after adding an - edge with largest number, call with `involvingEdgesFrom = - edges_vertices.length - 1`; then this will run in linear time. - In this case, returns two arrays of edges: the first array are all subdivided - from the "involved" edges, while the second array is the remaining subdivided - edges. - ### - - changedEdges = [[], []] - addEdge = (v1, v2, oldEdgeIndex, which) -> - #console.log 'adding', oldEdgeIndex, fold.edges_vertices.length, 'to', which - eNew = filter.addEdgeLike fold, oldEdgeIndex, v1, v2 - changedEdges[which].push oldEdgeIndex, eNew - - ## Handle overlapping edges by subdividing edges at any vertices on them. - ## We use a while loop instead of a for loop to process newly added edges. - i = involvingEdgesFrom ? 0 - while i < fold.edges_vertices.length - e = fold.edges_vertices[i] - s = (fold.vertices_coords[u] for u in e) - for p, v in fold.vertices_coords - continue if v in e - if geom.pointStrictlyInSegment p, s ## implicit epsilon - #console.log p, 'in', s - addEdge v, e[1], i, 0 - e[1] = v - i++ - - ## Handle crossing edges - ## We use a while loop instead of a for loop to process newly added edges. - vertices = new RepeatedPointsDS fold.vertices_coords, epsilon - i1 = involvingEdgesFrom ? 0 - while i1 < fold.edges_vertices.length - e1 = fold.edges_vertices[i1] - s1 = (fold.vertices_coords[v] for v in e1) - for e2, i2 in fold.edges_vertices[...i1] - s2 = (fold.vertices_coords[v] for v in e2) - if not filter.edges_verticesIncident(e1, e2) and geom.segmentsCross s1, s2 - ## segment intersection is too sensitive a test; - ## segmentsCross more reliable - #cross = segmentIntersectSegment s1, s2 - cross = geom.lineIntersectLine s1, s2 - continue unless cross? - crossI = vertices.insert cross - #console.log e1, s1, 'intersects', e2, s2, 'at', cross, crossI - unless crossI in e1 and crossI in e2 ## don't add endpoint again - #console.log e1, e2, '->' - unless crossI in e1 - addEdge crossI, e1[1], i1, 0 - e1[1] = crossI - s1[1] = fold.vertices_coords[crossI] # update for future iterations - #console.log '->', e1, fold.edges_vertices[fold.edges_vertices.length-1] - unless crossI in e2 - addEdge crossI, e2[1], i2, 1 - e2[1] = crossI - #console.log '->', e2, fold.edges_vertices[fold.edges_vertices.length-1] - i1++ - - old2new = filter.removeDuplicateEdges_vertices fold - for i in [0, 1] - changedEdges[i] = (old2new[e] for e in changedEdges[i]) - old2new = filter.removeLoopEdges fold - for i in [0, 1] - changedEdges[i] = (old2new[e] for e in changedEdges[i]) - - #fold - if involvingEdgesFrom? - changedEdges - else - changedEdges[0].concat changedEdges[1] - -filter.addEdgeAndSubdivide = (fold, v1, v2, epsilon) -> - ### - Add an edge between vertex indices or points `v1` and `v2`, subdivide - as necessary, and return two arrays: all the subdivided parts of this edge, - and all the other edges that change. - If the edge is a loop or a duplicate, both arrays will be empty. - ### - if v1.length? - [v1, changedEdges1] = filter.addVertexAndSubdivide fold, v1, epsilon - if v2.length? - [v2, changedEdges2] = filter.addVertexAndSubdivide fold, v2, epsilon - if v1 == v2 ## Ignore loop edges - return [[], []] - for e, i in fold.edges_vertices - if (e[0] == v1 and e[1] == v2) or - (e[0] == v2 and e[1] == v1) - return [[i], []] ## Ignore duplicate edges - iNew = fold.edges_vertices.push([v1, v2]) - 1 - if iNew - changedEdges = filter.subdivideCrossingEdges_vertices(fold, epsilon, iNew) - changedEdges[0].push iNew unless iNew in changedEdges[0] - else - changedEdges = [[iNew], []] - changedEdges[1].push changedEdges1... if changedEdges1? - changedEdges[1].push changedEdges2... if changedEdges2? - changedEdges - -filter.splitCuts = (fold, es = filter.cutEdges(fold)) -> - ### - Given a FOLD object with `edges_vertices`, `edges_assignment`, and - counterclockwise-sorted `vertices_edges` - (see `FOLD.convert.edges_vertices_to_vertices_edges_sorted`), - cuts apart ("unwelds") all edges in `es` into pairs of boundary edges. - When an endpoint of a cut edge ends up on n boundaries, - it splits into n vertices. - Preserves above-mentioned properties (so you can then compute faces via - `FOLD.convert.edges_vertices_to_faces_vertices_edges`), - and recomputes `vertices_vertices` if present, - but ignores face properties. - `es` is unspecified, cuts all edges with an assignment of `"C"`, - effectively switching from FOLD 1.2's `"C"` assignments to - FOLD 1.1's `"B"` assignments. - ### - return fold unless es.length - ## Maintain map from every vertex to array of incident boundary edges - vertices_boundaries = [] - for e in filter.boundaryEdges fold - for v in fold.edges_vertices[e] - (vertices_boundaries[v] ?= []).push e - for e1 in es - ## Split e1 into two edges {e1, e2} - e2 = filter.addEdgeLike fold, e1 - for v, i in fold.edges_vertices[e1] - ve = fold.vertices_edges[v] - ## Insert e2 before e1 in first vertex and after e1 in second vertex - ## to represent valid counterclockwise ordering - ve.splice ve.indexOf(e1) + i, 0, e2 - ## Check for endpoints of {e1, e2} to split, when they're on the boundary - for v1, i in fold.edges_vertices[e1] - u1 = fold.edges_vertices[e1][1-i] - u2 = fold.edges_vertices[e2][1-i] - boundaries = vertices_boundaries[v1]?.length - if boundaries >= 2 ## vertex already on boundary - if boundaries > 2 - throw new Error "#{vertices_boundaries[v1].length} boundary edges at vertex #{v1}" - [b1, b2] = vertices_boundaries[v1] - neighbors = fold.vertices_edges[v1] - i1 = neighbors.indexOf b1 - i2 = neighbors.indexOf b2 - if i2 == (i1+1) % neighbors.length - neighbors = neighbors[i2..].concat neighbors[..i1] unless i2 == 0 - else if i1 == (i2+1) % neighbors.length - neighbors = neighbors[i1..].concat neighbors[..i2] unless i1 == 0 - else - throw new Error "Nonadjacent boundary edges at vertex #{v1}" - ## Find first vertex among e1, e2 among neighbors, so other is next - ie1 = neighbors.indexOf e1 - ie2 = neighbors.indexOf e2 - ie = Math.min ie1, ie2 - fold.vertices_edges[v1] = neighbors[..ie] - v2 = filter.addVertexLike fold, v1 - fold.vertices_edges[v2] = neighbors[1+ie..] - #console.log "Split #{neighbors} into #{fold.vertices_edges[v1]} for #{v1} and #{fold.vertices_edges[v2]} for #{v2}" - ## Update relevant incident edges to use v2 instead of v1 - for neighbor in fold.vertices_edges[v2] # including e2 - ev = fold.edges_vertices[neighbor] - ev[ev.indexOf v1] = v2 - ## Partition boundary edges incident to v1 - vertices_boundaries[v1] = [] - vertices_boundaries[v2] = [] - for b in [b1, b2] - if b in fold.vertices_edges[v1] - vertices_boundaries[v1].push b - else #if b in fold.vertices_edges[v2] - vertices_boundaries[v2].push b - ## e1 and e2 are new boundary edges - fold.edges_assignment?[e1] = 'B' - fold.edges_assignment?[e2] = 'B' - for v, i in fold.edges_vertices[e1] - (vertices_boundaries[v] ?= []).push e1 - for v, i in fold.edges_vertices[e2] - (vertices_boundaries[v] ?= []).push e2 - if fold.vertices_vertices? # would be out-of-date - fold.vertices_vertices = filter.edges_vertices_to_vertices_vertices fold - fold - -filter.edges_vertices_to_vertices_vertices = (fold) -> - ### - Works for abstract structures, so NOT SORTED. - Use sort_vertices_vertices to sort in counterclockwise order. - ### - numVertices = filter.numVertices fold - vertices_vertices = ([] for v in [0...numVertices]) - for [v, w] in fold.edges_vertices - while v >= vertices_vertices.length - vertices_vertices.push [] - while w >= vertices_vertices.length - vertices_vertices.push [] - vertices_vertices[v].push w - vertices_vertices[w].push v - vertices_vertices - -filter.edges_vertices_to_vertices_edges = (fold) -> - ### - Invert edges_vertices into vertices_edges. - Works for abstract structures, so NOT SORTED in any sense. - ### - numVertices = filter.numVertices fold - vertices_edges = ([] for v in [0...numVertices]) - for vertices, edge in fold.edges_vertices - for vertex in vertices - vertices_edges[vertex].push edge - vertices_edges diff --git a/src/geom.civet b/src/geom.civet new file mode 100644 index 0000000..a3efca7 --- /dev/null +++ b/src/geom.civet @@ -0,0 +1,603 @@ +// BASIC GEOMETRY + +type { Coords, Coords2D, Coords3D, Matrix } from "./types.civet" + +// Utilities + +export EPS := 0.000001 + +export function sum(a: number, b: number): number + a + b + +export function min(a: number, b: number): number + if a < b then a else b + +export function max(a: number, b: number): number + if a > b then a else b + +/** Returns the ith cyclic ordered number after start in the range [0..n]. */ +export function next(start: number, n: number, i = 1): number + (start + i) %% n + +/** + * Returns whether the scalar interval [a1, a2] is disjoint from the scalar + * interval [b1,b2]. + */ +export function rangesDisjoint([a1, a2]: [number, number], [b1, b2]: [number, number]): boolean + (b1 < Math.min(a1, a2) > b2) or (b1 > Math.max(a1, a2) < b2) + +interface Tree + visited?: boolean + parent?: Tree | null + children: Array + +export function topologicalSort(vs: Tree[]): Tree[] + [v.visited, v.parent] = [false, null] for v of vs + list: Tree[] .= [] + for each v of vs + continue if v.visited + list = visit(v, list) + list + +function visit(v: Tree, list: Tree[]) + v.visited = true + for each u of v.children + continue if u.visited + u.parent = v + list = visit(u, list) + list.push v + list + +// Vector operations + +/** Returns the squared magnitude of vector of arbitrary dimension. */ +export function magsq(a: Coords): number + dot a, a + +/** Returns the magnitude of vector of arbitrary dimension. */ +export function mag(a: Coords): number + Math.sqrt magsq a + +/** + * Returns the unit vector in the direction of vector of arbitrary dimension. + * Returns `null` if magnitude is (near) zero. + */ +export function unit(a: Coords, eps = EPS): Coords | null + length := magsq a + return null if length < eps + mul(a, 1 / mag(a)) + +/** + * Returns the angle of a 2D vector relative to the standard + * east-is-0-degrees rule. Returns `null` if magnitude is (near) zero. + */ +export function ang2D(a: Coords, eps = EPS): number | null + return null if magsq(a) < eps + Math.atan2(a[1], a[0]) + +/** Multiplies a vector by a scalar factor. */ +export function mul(a: Coords2D, s: number): Coords2D +export function mul(a: Coords3D, s: number): Coords3D +export function mul(a: Coords, s: number): Coords +export function mul(a: Coords, s: number): Coords + (i * s for each i of a) + +/** Returns linear interpolation of vector `a` to vector `b` for 0 < t < 1 */ +export function linearInterpolate(t: number, a: Coords, b: Coords): Coords + plus mul(a, 1 - t), mul(b, t) + +/** Returns the vector sum of two vectors having the same dimension. */ +export function plus(a: Coords2D, b: Coords2D): Coords2D +export function plus(a: Coords3D, b: Coords3D): Coords3D +export function plus(a: Coords, b: Coords): Coords +export function plus(a: Coords, b: Coords): Coords + (ai + b[i] for each ai, i of a) + +/** Returns the vector difference of two vectors having the same dimension. */ +export function sub(a: Coords2D, b: Coords2D): Coords2D +export function sub(a: Coords3D, b: Coords3D): Coords3D +export function sub(a: Coords, b: Coords): Coords +export function sub(a: Coords, b: Coords): Coords + (ai - b[i] for each ai, i of a) + +/** Returns the dot product of two vectors a and b having the same dimension. */ +export function dot(a: Coords, b: Coords): number + (ai * b[i] for each ai, i of a).reduce(sum) + +/** + * Returns the squared Euclidean distance between two vectors a and b + * having the same dimension. + */ +export function distsq(a: Coords, b: Coords): number + magsq sub(a, b) + +/** + * Returns the Euclidean distance between two vectors a and b + * having the same dimension. + */ +export function dist(a: Coords, b: Coords): number + Math.sqrt distsq(a, b) + +/** + * Finds the closest point to `a` among points in `bs`, and returns the + * index of that point in `bs`. Returns `undefined` if `bs` is empty. + */ +export function closestIndex(a: Coords, bs: Coords[]): number | undefined + minDist .= Infinity + let minI: number | undefined + for each b, i of bs + d := dist(a, b) + if minDist > d + minDist = d + minI = i + minI + +/** + * Returns a unit vector in the direction from vector `a` to vector `b`, + * in the same dimension as `a` and `b`. + */ +export function dir(a: Coords, b: Coords): Coords + unit sub(b, a) + +/** Returns the angle spanned by vectors `a` and `b` of the same dimension. */ +export function ang(a: Coords, b: Coords, eps = EPS): number | null + [ua, ub] := [unit(a), unit(b)] + return null unless ua? and ub? + + dotProd .= dot(ua, ub) + + if dotProd < -1 or dotProd > 1 + // this is only possible when floating point error causes dotProd to be + // slightly outside the range [-1,1] + dotProd = Math.round dotProd + + Math.acos dotProd + +/** Returns the cross product of two 2D or 3D vectors. */ +export function cross(a: Coords2D, b: Coords2D): number +export function cross(a: Coords3D, b: Coords3D): Coords3D +export function cross(a: Coords, b: Coords): number | Coords | null +export function cross(a: Coords, b: Coords): number | Coords | null + if a.length == b.length == 2 + return (a[0] * b[1] - a[1] * b[0]) + if a.length == b.length == 3 + return (a[i] * b[j] - a[j] * b[i] for [i, j] of [[1, 2], [2, 0], [0, 1]]) + return null + +/** Returns whether vectors are parallel, up to accuracy eps */ +export function parallel(a: Coords, b: Coords, eps = EPS): boolean | null + [ua, ub] := [unit(a), unit(b)] + return null unless ua? and ub? + 1 - Math.abs(dot ua, ub) < eps + +/** + * Returns the rotation of 3D vector `a` about 3D vector `axis` by angle `t`. + */ +export function rotate(a: Coords, axis: Coords, t: number): Coords | null + u := unit(axis) + return null unless u? + + [ct, st] := [Math.cos(t), Math.sin(t)] + (for each p of [[0,1,2],[1,2,0],[2,0,1]] + (for each q, i of [ct, -st * u[p[2]], st * u[p[1]]] + a[p[i]] * (u[p[0]] * u[p[i]] * (1 - ct) + q) + ).reduce(sum) + ) + +/** Reflect point `p` through the point `q` into the "symmetric point" */ +export function reflectPoint(p: Coords, q: Coords): Coords + sub mul(q, 2), p + +/** + * Reflect point `p` through line through points `a` and `b` + * [based on https://math.stackexchange.com/a/11532] + */ +export function reflectLine(p: Coords, a: Coords, b: Coords): Coords + // projection = a + (b - a) * [(b - a) dot (p - a)] / ||b - a||^2 + vec := sub(b, a) + lenSq := magsq(vec) + d := dot(vec, sub(p, a)) + projection := plus(a, mul(vec, d / lenSq)) + + // reflection = 2*projection - p (symmetric point of p opposite projection) + sub mul(projection, 2), p + +### +Matrix transformations + +2D transformation matrices are of the form (where last column is optional): + [[a, b, c], + [d, e, f]] + +3D transformation matrices are of the form (where last column is optional): + [[a, b, c, d], + [e, f, g, h], + [i, j, k, l]] + +Transformation matrices are designed to be multiplied on the left of points, +i.e., T*x gives vector x transformed by matrix T, where x has an implicit 1 +at the end (homogeneous coordinates) when T has the optional last column. +See `matrixCoords`. +### + +/** + * Returns matrix-vector product, `matrix` * `vector`. + * Requires the number of `matrix` columns to be <= `vector` length. + * If the matrix has more columns than the vector length, then the vector + * is assumed to be padded with zeros at the end, EXCEPT when the matrix + * has more columns than rows (as in transformation matrices above), + * in which case the final vector padding is `implicitLast`, + * which defaults to 1 (point); set to 0 for treating like a vector. + */ +export function matrixVector(matrix: Matrix, vector: Coords, implicitLast = 1): Coords + for each row of matrix + val .= (row[j] * x for each x, j of vector).reduce(sum) + if row.length > vector.length and row.length > matrix.length + val += row[row.length-1] * implicitLast + val + +/** + * Returns matrix-matrix product, `matrix1` * `matrix2`. + * Requires number of `matrix1` columns equal to or 1 more than `matrix2` rows. + * In the latter case, treats `matrix2` as having an extra row [0,0,...,0,0,1], + * which may involve adding an implicit column to `matrix2` as well. + */ +export function matrixMatrix(matrix1: Matrix, matrix2: Matrix): Matrix + for each row1 of matrix1 + if matrix2.length != row1.length != matrix2.length + 1 + throw new Error `Invalid matrix dimension ${row1.length} vs. matrix dimension ${matrix2.length}` + product := + for j of [0...matrix2[0].length] + val .= (row1[k] * row2[j] for each row2, k of matrix2).reduce(sum) + if j == row1.length - 1 == matrix2.length + val += row1[j] + val + if row1.length - 1 == matrix2.length == matrix2[0].length + product.push row1[row1.length - 1] + product + +/** + * Returns inverse of a matrix consisting of rotations and/or translations, + * where the inverse can be found by a transpose and dot products + * [http://www.graphics.stanford.edu/courses/cs248-98-fall/Final/q4.html]. + */ +export function matrixInverseRT(matrix: Matrix): Matrix + let lastCol: Coords | undefined + if matrix[0].length == matrix.length+1 + lastCol = (row[row.length-1] for row of matrix) + else if matrix[0].length != matrix.length + throw new Error `Invalid matrix dimensions ${matrix.length}x${matrix[0].length}` + for each row, i of matrix + invRow := (matrix[j][i] for j of [0...matrix.length]) // transpose + if lastCol? + invRow.push -dot row[...matrix.length], lastCol + invRow + +/** Returns inverse of a matrix computed via Gauss-Jordan elimination method. */ +export function matrixInverse(matrix: Matrix): Matrix + if matrix.length != matrix[0].length != matrix.length+1 + throw new Error `Invalid matrix dimensions ${matrix.length}x${matrix[0].length}` + matrix = (row[..] for row of matrix) // copy before elimination + inverse := + for each row, i of matrix + for j of [0...row.length] + Number(i == j) + for j of [0...matrix.length] + // Pivot to maximize absolute value in jth column + bestRow .= j + for i of [j+1...matrix.length] + if Math.abs(matrix[i][j]) > Math.abs(matrix[bestRow][j]) + bestRow = i + if bestRow != j + [matrix[bestRow], matrix[j]] = [matrix[j], matrix[bestRow]] + [inverse[bestRow], inverse[j]] = [inverse[j], inverse[bestRow]] + // Scale row to unity in jth column + inverse[j] = mul inverse[j], 1/matrix[j][j] + matrix[j] = mul matrix[j], 1/matrix[j][j] + // Eliminate other rows in jth column + for i of [0...matrix.length] + continue if i == j + inverse[i] = plus inverse[i], mul inverse[j], -matrix[i][j] + matrix[i] = plus matrix[i], mul matrix[j], -matrix[i][j] + if matrix[0].length == matrix.length+1 + for i of [0...matrix.length] + inverse[i][inverse[i].length-1] -= matrix[i][matrix[i].length-1] + matrix[i][matrix[i].length-1] -= matrix[i][matrix[i].length-1] + inverse + +/** + * Transformation matrix for translating by given vector v. + * Works in any dimension, assuming v.length is that dimension. + */ +export function matrixTranslate(v: Coords): Matrix + for each x, i of v + row := + for j of [0...v.length] + Number(i == j) + row.push x + row + +/** + * 2D rotation matrix around `center`, which defaults to origin, + * counterclockwise by `t` radians. + */ +export function matrixRotate2D(t: number, center?: Coords): Matrix + [ct, st] := [Math.cos(t), Math.sin(t)] + if center? + [x, y] := center + [[ct, -st, -x*ct + y*st + x] + [st, ct, -x*st - y*ct + y]] + else + [[ct, -st] + [st, ct]] + +/** + * 3D rotation matrix around axis through points `a` and `b`, + * counterclockwise by `t` radians. Based on `rotate`. + */ +export function matrixRotate3D(a: Coords, b: Coords, t: number) + u := sub b, a + [ct, st] := [Math.cos(t), Math.sin(t)] + [ux, uy, uz = 0] := u + omct := 1 - ct + + [ + [ux*ux*omct+ct, uy*ux*omct+st*uz, uz*ux*omct-st*uy] + [ux*uy*omct-st*uz, uy*uy*omct+ct, uz*uy*omct+st*ux] + [ux*uz*omct+st*uy, uy*uz*omct-st*ux, uz*uz*omct+ct] + ] + +/** + * Matrix transformation negating dimension `a` out of `d` dimensions, + * or if `center` is specified, reflecting around that value of dimension `a`. + */ +export function matrixReflectAxis(a: number, d: number, center?: number): Matrix + for i of [0...d] + row := + for j of [0...d] + if i == j + if a == i + -1 + else + 1 + else + 0 + if center? + if a == i + row.push 2*center + else + row.push 0 + row + +/** Matrix transformation implementing 2D `reflectLine(*, a, b)` */ +export function matrixReflectLine(a: Coords, b: Coords): Matrix + vec := sub(b, a) + lenSq := magsq(vec) + dot2 := dot(vec, a) + + [[2*(vec[0] * vec[0] / lenSq) - 1, + 2*(vec[0] * vec[1] / lenSq), + 2*(a[0] - vec[0] * dot2 / lenSq)] + [2*(vec[1] * vec[0] / lenSq), + 2*(vec[1] * vec[1] / lenSq) - 1, + 2*(a[1] - vec[1] * dot2 / lenSq)]] + +// Polygon Operations + +/** + * Computes the angle of three points that are, say, part of a triangle. + * Specify in counterclockwise order. + * ``` + * a + * / + * / + * b/_)__ c + * ``` + */ +export function interiorAngle(a: Coords, b: Coords, c: Coords): number + ang .= ang2D(sub(a, b)) - ang2D(sub(c, b)) + ang += 2*Math.PI if ang < 0 + ang + +/** Returns the turn angle, the supplement of the interior angle */ +export function turnAngle(a: Coords, b: Coords, c: Coords): number + Math.PI - interiorAngle(a, b, c) + +/** + * Returns the right handed normal unit vector to triangle `a`, `b`, `c` in 3D. + * If the triangle is degenerate, returns `null`. + */ +export function triangleNormal(a: Coords3D, b: Coords3D, c: Coords3D): Coords3D | null + unit(cross(sub(b, a), sub(c, b))) as Coords3D + +/** + * Returns the right handed normal unit vector to the polygon defined by + * points in 3D. Assumes the points are planar. + */ +export function polygonNormal(points: Coords[], eps = EPS): Coords + unit( + (for each p, i of points + cross(p, points[next(i, points.length)]) + ).reduce(plus), + eps + ) + +/** + * Returns twice signed area of polygon defined by input points. + * Calculates and sums twice signed area of triangles in a fan from the first + * vertex. + */ +export function twiceSignedArea(points: Coords[]): number + (for each v0, i of points + v1 := points[next(i, points.length)] + v0[0] * v1[1] - v1[0] * v0[1] + ).reduce(sum) + +/** + * Returns the orientation of the 2D polygon defined by the input points -- + * +1 for counterclockwise, -1 for clockwise -- + * via computing sum of signed areas of triangles formed with origin. + */ +export function polygonOrientation(points: Coords[]): number + Math.sign twiceSignedArea points + +/** + * Sort a set of 2D points in place counter clockwise about origin + * under the provided mapping. + */ +export function sortByAngle(points: Coords[], origin: Coords) +export function sortByAngle(points: T[], origin?: Coords, mapping?: (x: T) => Coords) +export function sortByAngle(points: T[], origin: Coords = [0,0], mapping: (x: T) => Coords = (x) => (x as Coords)) + points.sort (p, q) => + pa := ang2D sub(mapping(p), origin) + qa := ang2D sub(mapping(q), origin) + pa - qa + +/** + * Check whether two segments (specified by endpoints) cross. + * May not work if the segments are collinear. + * First do rough overlap check in x and y. This helps with + * near-collinear segments. (Inspired by oripa/geom/GeomUtil.java) + */ +export function segmentsCross([p0, q0]: [Coords, Coords], [p1, q1]: [Coords, Coords]): boolean + if rangesDisjoint([p0[0], q0[0]], [p1[0], q1[0]]) or + rangesDisjoint([p0[1], q0[1]], [p1[1], q1[1]]) + return false + // Now do orientation test. + polygonOrientation([p0,q0,p1]) != polygonOrientation([p0,q0,q1]) and + polygonOrientation([p1,q1,p0]) != polygonOrientation([p1,q1,q0]) + +/** + * Returns the parameters s,t for the equations s*p1+(1-s)*p2 and + * t*q1+(1-t)*q2. Used Maple's result of: + * ```maple + * solve({s*p2x+(1-s)*p1x=t*q2x+(1-t)*q1x, + * s*p2y+(1-s)*p1y=t*q2y+(1-t)*q1y}, {s,t}); + * ``` + * Returns null, null if the intersection couldn't be found + * because the lines are parallel. + * Input points must be 2D. + */ +export function parametricLineIntersect([p1, p2]: [Coords2D, Coords2D], [q1, q2]: [Coords2D | null, Coords2D | null]) + denom := (q2[1]-q1[1])*(p2[0]-p1[0]) + (q1[0]-q2[0])*(p2[1]-p1[1]) + if denom == 0 + [null, null] + else + [(q2[0]*(p1[1]-q1[1])+q2[1]*(q1[0]-p1[0])+q1[1]*p1[0]-p1[1]*q1[0])/denom, + (q1[0]*(p2[1]-p1[1])+q1[1]*(p1[0]-p2[0])+p1[1]*p2[0]-p2[1]*p1[0])/denom] + +export function segmentIntersectSegment(s1: [Coords, Coords], s2: [Coords, Coords]): Coords | null + [s, t] := parametricLineIntersect(s1, s2) + if s? and (0 <= s <= 1) and (0 <= t <= 1) + linearInterpolate(s, s1[0], s1[1]) + else + null + +export function lineIntersectLine(l1: [Coords, Coords], l2: [Coords, Coords]): Coords | null + [s, t] := parametricLineIntersect(l1, l2) + if s? + linearInterpolate(s, l1[0], l1[1]) + else + null + +export function pointStrictlyInSegment(p: Coords, s: [Coords, Coords], eps = EPS) + v0 := sub p, s[0] + v1 := sub p, s[1] + parallel(v0, v1, eps) and dot(v0, v1) < 0 + +export function centroid(points: Coords[]) + // Returns the centroid of a set of points having the same dimension. + mul(points.reduce(plus), 1.0 / points.length) + +/** + * Returns a basis of a 3D point set. + * - [] if the points are all the same point (0 dimensional) + * - [u] if the points lie on a line with basis direction u + * - [u,v] if the points lie in a plane with basis directions u and v + * - [u,v,w] if the points span three dimensions + * - null if some point is not 3D + */ +export function basis(ps: Coords3D[], eps = EPS): Coords[] | null + return null if ps.some .length != 3 + + ds: Coords3D[] := for p of ps + continue unless distsq(p,ps[0]) > eps + dir(p,ps[0]) + + return [] if ds.length is 0 + + x := ds[0] + return [x] if ds.every (d) => parallel(d, x, eps) + ns := (unit(cross(d, x) as Coords) for d of ds).filter &? + z := ns[0] + y := cross(z, x) + return [x, y] if ns.every (n) => parallel(n, z, eps) + return [x, y, z] + +export function above(ps: Coords[], qs: Coords[], n: number[], eps = EPS): number + [pn,qn] := ((dot(v, n) for v of vs) for vs of [ps,qs]) + return 1 if qn.reduce(max) - pn.reduce(min) < eps + return -1 if pn.reduce(max) - qn.reduce(min) < eps + return 0 + +export function separatingDirection2D(t1: Coords[], t2: Coords[], n: number[], eps = EPS): Coords | null + // If points are contained in a common plane with normal `n` and a separating + // direction exists, a direction perpendicular to some pair of points from + // the same set is also a separating direction. + for t of [t1, t2] + for each p, i of t + for each q, j of t + continue unless i < j + if m := unit(cross(sub(p, q), n) as Coords) + sign := above(t1, t2, m, eps) + return mul(m, sign) if sign !== 0 + return null + +export function separatingDirection3D(t1: Coords[], t2: Coords[], eps = EPS): Coords | null + // If points are not contained in a common plane and a separating direction + // exists, a plane spanning two points from one set and one point from the + // other set is a separating plane, with its normal a separating direction. + for [x1, x2] of [[t1, t2], [t2, t1]] + for p of x1 + for each q1, i of x2 + for each q2, j of x2 + continue unless i < j + m := unit(cross(sub(p, q1), sub(p, q2)) as Coords) + if m? + sign := above(t1, t2, m, eps) + return mul(m, sign) if sign !== 0 + return null + +// Hole Filling Methods + +export function circleCross(d: number, r1: number, r2: number) + x := (d * d - r2 * r2 + r1 * r1) / d / 2 + y := Math.sqrt(r1 * r1 - x * x) + [x, y] + +export function creaseDir(u1: Coords, u2: Coords, a: number, b: number, eps = EPS): Coords + b1 := Math.cos(a) + Math.cos(b) + b2 := Math.cos(a) - Math.cos(b) + x .= plus(u1, u2) + y .= sub(u1, u2) + z .= unit(cross(y, x) as Coords) + x = mul(x, b1 / magsq(x)) + y = mul(y, if magsq(y) < eps then 0 else b2 / magsq(y)) + zmag := Math.sqrt(1 - magsq(x) - magsq(y)) + z = mul(z, zmag) + [x, y, z].reduce(plus) + +/** + * Split from origin in direction `u` subject to external point `p` whose + * shortest path on the surface is distance `d` and projecting angle is `t`. + */ +export function quadSplit(u: Coords, p: Coords, d: number, t: number): Coords + if magsq(p) > d * d + throw new Error "STOP! Trying to split expansive quad." + return mul(u, (d*d - magsq(p))/2/(d*Math.cos(t) - dot(u, p))) + +export function toRadian(ang: number): number + ang * Math.PI/180 + +export function toDegree(ang: number): number + ang * 180/Math.PI diff --git a/src/geom.coffee b/src/geom.coffee deleted file mode 100644 index 1130e2d..0000000 --- a/src/geom.coffee +++ /dev/null @@ -1,497 +0,0 @@ -### BASIC GEOMETRY ### - -geom = exports - -### - Utilities -### - -geom.EPS = 0.000001 - -geom.sum = (a, b) -> a + b - -geom.min = (a, b) -> if a < b then a else b - -geom.max = (a, b) -> if a > b then a else b - -geom.all = (a, b) -> a and b - -geom.next = (start, n, i = 1) -> - ### - Returns the ith cyclic ordered number after start in the range [0..n]. - ### - (start + i) %% n - -geom.rangesDisjoint = ([a1, a2], [b1, b2]) -> - ## Returns whether the scalar interval [a1, a2] is disjoint from the scalar - ## interval [b1,b2]. - return (b1 < Math.min(a1, a2) > b2) or (b1 > Math.max(a1, a2) < b2) - -geom.topologicalSort = (vs) -> - ([v.visited, v.parent] = [false, null] for v in vs) - list = [] - for v in vs when (not v.visited) - list = geom.visit(v, list) - return list - -geom.visit = (v, list) -> - v.visited = true - for u in v.children when not u.visited - u.parent = v - list = geom.visit(u,list) - return list.concat([v]) - -## -## Vector operations -## - -geom.magsq = (a) -> - ## Returns the squared magnitude of vector a having arbitrary dimension. - geom.dot(a, a) - -geom.mag = (a) -> - ## Returns the magnitude of vector a having arbitrary dimension. - Math.sqrt(geom.magsq(a)) - -geom.unit = (a, eps = geom.EPS) -> - ## Returns the unit vector in the direction of vector a having arbitrary - ## dimension. Returns null if magnitude of a is zero. - length = geom.magsq(a) - return null if length < eps - geom.mul(a, 1 / geom.mag(a)) - -geom.ang2D = (a, eps = geom.EPS) -> - ## Returns the angle of a 2D vector relative to the standard - ## east-is-0-degrees rule. - return null if geom.magsq(a) < eps - Math.atan2(a[1], a[0]) - -geom.mul = (a, s) -> - ## Returns the vector a multiplied by scaler factor s. - (i * s for i in a) - -geom.linearInterpolate = (t, a, b) -> - ## Returns linear interpolation of vector a to vector b for 0 < t < 1 - geom.plus geom.mul(a, 1 - t), geom.mul(b, t) - -geom.plus = (a, b) -> - ## Returns the vector sum between of vectors a and b having the same - ## dimension. - (ai + b[i] for ai, i in a) - -geom.sub = (a, b) -> - ## Returns the vector difference of vectors a and b having the same dimension. - geom.plus(a, geom.mul(b, -1)) - -geom.dot = (a, b) -> - ## Returns the dot product between two vectors a and b having the same - ## dimension. - (ai * b[i] for ai, i in a).reduce(geom.sum) - -geom.distsq = (a, b) -> - ## Returns the squared Euclidean distance between two vectors a and b having - ## the same dimension. - geom.magsq(geom.sub(a, b)) - -geom.dist = (a, b) -> - ## Returns the Euclidean distance between general vectors a and b having the - ## same dimension. - Math.sqrt(geom.distsq(a, b)) - -geom.closestIndex = (a, bs) -> - ## Finds the closest point to `a` among points in `bs`, and returns the - ## index of that point in `bs`. Returns `undefined` if `bs` is empty. - minDist = Infinity - for b, i in bs - if minDist > dist = geom.dist a, b - minDist = dist - minI = i - minI - -geom.dir = (a, b) -> - ## Returns a unit vector in the direction from vector a to vector b, in the - ## same dimension as a and b. - geom.unit(geom.sub(b, a)) - -geom.ang = (a, b) -> - ## Returns the angle spanned by vectors a and b having the same dimension. - [ua, ub] = (geom.unit(v) for v in [a,b]) - return null unless ua? and ub? - Math.acos geom.dot(ua, ub) - -geom.cross = (a, b) -> - ## Returns the cross product of two 2D or 3D vectors a, b. - if a.length == b.length == 2 - return (a[0] * b[1] - a[1] * b[0]) - if a.length == b.length == 3 - return (a[i] * b[j] - a[j] * b[i] for [i, j] in [[1, 2], [2, 0], [0, 1]]) - return null - -geom.parallel = (a, b, eps = geom.EPS) -> - ## Return if vectors are parallel, up to accuracy eps - [ua, ub] = (geom.unit(v) for v in [a,b]) - return null unless ua? and ub? - 1 - Math.abs(geom.dot ua, ub) < eps - -geom.rotate = (a, u, t) -> - ## Returns the rotation of 3D vector a about 3D unit vector u by angle t. - u = geom.unit(u) - return null unless u? - [ct, st] = [Math.cos(t), Math.sin(t)] - (for p in [[0,1,2],[1,2,0],[2,0,1]] - (for q, i in [ct, -st * u[p[2]], st * u[p[1]]] - a[p[i]] * (u[p[0]] * u[p[i]] * (1 - ct) + q)).reduce(geom.sum)) - -geom.reflectPoint = (p, q) -> - ## Reflect point p through the point q into the "symmetric point" - geom.sub(geom.mul(q, 2), p) - -geom.reflectLine = (p, a, b) -> - ## Reflect point p through line through points a and b - # [based on https://math.stackexchange.com/a/11532] - # projection = a + (b - a) * [(b - a) dot (p - a)] / ||b - a||^2 - vec = geom.sub(b, a) - lenSq = geom.magsq(vec) - dot = geom.dot(vec, geom.sub(p, a)) - projection = geom.plus(a, geom.mul(vec, dot / lenSq)) - # reflection = 2*projection - p (symmetric point of p opposite projection) - geom.sub(geom.mul(projection, 2), p) - -### -Matrix transformations - -2D transformation matrices are of the form (where last column is optional): - [[a, b, c], - [d, e, f]] - -3D transformation matrices are of the form (where last column is optional): - [[a, b, c, d], - [e, f, g, h], - [i, j, k, l]] - -Transformation matrices are designed to be multiplied on the left of points, -i.e., T*x gives vector x transformed by matrix T, where x has an implicit 1 -at the end (homogeneous coordinates) when T has the optional last column. -See `geom.matrixVector`. -### - -geom.matrixVector = (matrix, vector, implicitLast = 1) -> - ## Returns matrix-vector product, matrix * vector. - ## Requires the number of matrix columns to be <= vector length. - ## If the matrix has more columns than the vector length, then the vector - ## is assumed to be padded with zeros at the end, EXCEPT when the matrix - ## has more columns than rows (as in transformation matrices above), - ## in which case the final vector padding is implicitLast, - ## which defaults to 1 (point); set to 0 for treating like a vector. - for row in matrix - val = (row[j] * x for x, j in vector).reduce(geom.sum) - if row.length > vector.length and row.length > matrix.length - val += row[row.length-1] * implicitLast - val - -geom.matrixMatrix = (matrix1, matrix2) -> - ## Returns matrix-matrix product, matrix1 * matrix2. - ## Requires number of matrix1 columns equal to or 1 more than matrix2 rows. - ## In the latter case, treats matrix2 as having an extra row [0,0,...,0,0,1], - ## which may involve adding an implicit column to matrix2 as well. - for row1 in matrix1 - if matrix2.length != row1.length != matrix2.length + 1 - throw new Error "Invalid matrix dimension #{row1.length} vs. matrix dimension #{matrix2.length}" - product = - for j in [0...matrix2[0].length] - val = (row1[k] * row2[j] for row2, k in matrix2).reduce(geom.sum) - if j == row1.length - 1 == matrix2.length - val += row1[j] - val - if row1.length - 1 == matrix2.length == matrix2[0].length - product.push row1[row1.length - 1] - product - -geom.matrixInverseRT = (matrix) -> - ## Returns inverse of a matrix consisting of rotations and/or translations, - ## where the inverse can be found by a transpose and dot products - ## [http://www.graphics.stanford.edu/courses/cs248-98-fall/Final/q4.html]. - if matrix[0].length == matrix.length+1 - lastCol = (row[row.length-1] for row in matrix) - else if matrix[0].length != matrix.length - throw new Error "Invalid matrix dimensions #{matrix.length}x#{matrix[0].length}" - for row, i in matrix - invRow = (matrix[j][i] for j in [0...matrix.length]) # transpose - if lastCol? - invRow.push -geom.dot row[...matrix.length], lastCol - invRow - -geom.matrixInverse = (matrix) -> - ## Returns inverse of a matrix computed via Gauss-Jordan elimination method. - if matrix.length != matrix[0].length != matrix.length+1 - throw new Error "Invalid matrix dimensions #{matrix.length}x#{matrix[0].length}" - matrix = (row[..] for row in matrix) # copy before elimination - inverse = - for row, i in matrix - for j in [0...row.length] - 0 + (i == j) - for j in [0...matrix.length] - # Pivot to maximize absolute value in jth column - bestRow = j - for i in [j+1...matrix.length] - if Math.abs(matrix[i][j]) > Math.abs(matrix[bestRow][j]) - bestRow = i - if bestRow != j - [matrix[bestRow], matrix[j]] = [matrix[j], matrix[bestRow]] - [inverse[bestRow], inverse[j]] = [inverse[j], inverse[bestRow]] - # Scale row to unity in jth column - inverse[j] = geom.mul inverse[j], 1/matrix[j][j] - matrix[j] = geom.mul matrix[j], 1/matrix[j][j] - # Eliminate other rows in jth column - for i in [0...matrix.length] when i != j - inverse[i] = geom.plus inverse[i], geom.mul inverse[j], -matrix[i][j] - matrix[i] = geom.plus matrix[i], geom.mul matrix[j], -matrix[i][j] - if matrix[0].length == matrix.length+1 - for i in [0...matrix.length] when i != j - inverse[i][inverse[i].length-1] -= matrix[i][matrix[i].length-1] - matrix[i][matrix[i].length-1] -= matrix[i][matrix[i].length-1] - inverse - -geom.matrixTranslate = (v) -> - ## Transformation matrix for translating by given vector v. - ## Works in any dimension, assuming v.length is that dimension. - for x, i in v - row = - for j in [0...v.length] - 0 + (i == j) - row.push x - row - -geom.matrixRotate2D = (t, center) -> - ## 2D rotation matrix around center, which defaults to origin, - ## counterclockwise by t radians. - [ct, st] = [Math.cos(t), Math.sin(t)] - if center? - [x, y] = center - [[ct, -st, -x*ct + y*st + x] - [st, ct, -x*st - y*ct + y]] - else - [[ct, -st] - [st, ct]] - -geom.matrixReflectAxis = (a, d, center) -> - ## Matrix transformation negating dimension a out of d dimensions, - ## or if center is specified, reflecting around that value of dimension a. - for i in [0...d] - row = - for j in [0...d] - if i == j - if a == i - -1 - else - 1 - else - 0 - if center? - if a == i - row.push 2*center - else - row.push 0 - row - -geom.matrixReflectLine = (a, b) -> - ## Matrix transformation implementing 2D geom.reflectLine(*, a, b) - vec = geom.sub(b, a) - lenSq = geom.magsq(vec) - # dot = vec dot (p - a) = vec dot p - vec dot a - dot2 = geom.dot(vec, a) - #proj = (a[i] + vec[i] * dot / lenSq for i in [0...2]) - #[[vec[0] * vec[0] / lenSq, - # vec[0] * vec[1] / lenSq, - # a[0] - vec[0] * dot2 / lenSq] - # [vec[1] * vec[0] / lenSq, - # vec[1] * vec[1] / lenSq, - # a[1] - vec[1] * dot2 / lenSq]] - [[2*(vec[0] * vec[0] / lenSq) - 1, - 2*(vec[0] * vec[1] / lenSq), - 2*(a[0] - vec[0] * dot2 / lenSq)] - [2*(vec[1] * vec[0] / lenSq), - 2*(vec[1] * vec[1] / lenSq) - 1, - 2*(a[1] - vec[1] * dot2 / lenSq)]] - -## -## Polygon Operations -## - -geom.interiorAngle = (a, b, c) -> - ## Computes the angle of three points that are, say, part of a triangle. - ## Specify in counterclockwise order. - ## a - ## / - ## / - ## b/_)__ c - ang = geom.ang2D(geom.sub(a, b)) - geom.ang2D(geom.sub(c, b)) - ang + (if ang < 0 then 2*Math.PI else 0) - -geom.turnAngle = (a, b, c) -> - ## Returns the turn angle, the supplement of the interior angle - Math.PI - geom.interiorAngle(a, b, c) - -geom.triangleNormal = (a, b, c) -> - ## Returns the right handed normal unit vector to triangle a, b, c in 3D. If - ## the triangle is degenerate, returns null. - geom.unit geom.cross(geom.sub(b, a), geom.sub(c, b)) - -geom.polygonNormal = (points, eps = geom.EPS) -> - ## Returns the right handed normal unit vector to the polygon defined by - ## points in 3D. Assumes the points are planar. - return geom.unit((for p, i in points - geom.cross(p, points[geom.next(i, points.length)])).reduce(geom.plus), eps) - -geom.twiceSignedArea = (points) -> - ## Returns twice signed area of polygon defined by input points. - ## Calculates and sums twice signed area of triangles in a fan from the first - ## vertex. - (for v0, i in points - v1 = points[geom.next(i, points.length)] - v0[0] * v1[1] - v1[0] * v0[1] - ).reduce(geom.sum) - -geom.polygonOrientation = (points) -> - ## Returns the orientation of the 2D polygon defined by the input points. - ## +1 for counterclockwise, -1 for clockwise - ## via computing sum of signed areas of triangles formed with origin - Math.sign geom.twiceSignedArea points - -geom.sortByAngle = (points, origin = [0,0], mapping = (x) -> x) -> - ## Sort a set of 2D points in place counter clockwise about origin - ## under the provided mapping. - origin = mapping(origin) - points.sort (p, q) -> - pa = geom.ang2D geom.sub(mapping(p), origin) - qa = geom.ang2D geom.sub(mapping(q), origin) - pa - qa - -geom.segmentsCross = ([p0, q0], [p1, q1]) -> - ## May not work if the segments are collinear. - ## First do rough overlap check in x and y. This helps with - ## near-collinear segments. (Inspired by oripa/geom/GeomUtil.java) - if geom.rangesDisjoint([p0[0], q0[0]], [p1[0], q1[0]]) or - geom.rangesDisjoint([p0[1], q0[1]], [p1[1], q1[1]]) - return false - ## Now do orientation test. - geom.polygonOrientation([p0,q0,p1]) != geom.polygonOrientation([p0,q0,q1]) and - geom.polygonOrientation([p1,q1,p0]) != geom.polygonOrientation([p1,q1,q0]) - -geom.parametricLineIntersect = ([p1, p2], [q1, q2]) -> - ## Returns the parameters s,t for the equations s*p1+(1-s)*p2 and - ## t*q1+(1-t)*q2. Used Maple's result of: - ## solve({s*p2x+(1-s)*p1x=t*q2x+(1-t)*q1x, - ## s*p2y+(1-s)*p1y=t*q2y+(1-t)*q1y}, {s,t}); - ## Returns null, null if the intersection couldn't be found - ## because the lines are parallel. - ## Input points must be 2D. - denom = (q2[1]-q1[1])*(p2[0]-p1[0]) + (q1[0]-q2[0])*(p2[1]-p1[1]) - if denom == 0 - [null, null] - else - [(q2[0]*(p1[1]-q1[1])+q2[1]*(q1[0]-p1[0])+q1[1]*p1[0]-p1[1]*q1[0])/denom, - (q1[0]*(p2[1]-p1[1])+q1[1]*(p1[0]-p2[0])+p1[1]*p2[0]-p2[1]*p1[0])/denom] - -geom.segmentIntersectSegment = (s1, s2) -> - [s, t] = geom.parametricLineIntersect(s1, s2) - if s? and (0 <= s <= 1) and (0 <= t <= 1) - geom.linearInterpolate(s, s1[0], s1[1]) - else - null - -geom.lineIntersectLine = (l1, l2) -> - [s, t] = geom.parametricLineIntersect(l1, l2) - if s? - geom.linearInterpolate(s, l1[0], l1[1]) - else - null - -geom.pointStrictlyInSegment = (p, s, eps = geom.EPS) -> - v0 = geom.sub p, s[0] - v1 = geom.sub p, s[1] - geom.parallel(v0, v1, eps) and geom.dot(v0, v1) < 0 - -geom.centroid = (points) -> - ## Returns the centroid of a set of points having the same dimension. - geom.mul(points.reduce(geom.plus), 1.0 / points.length) - -geom.basis = (ps, eps = geom.EPS) -> - ## Returns a basis of a 3D point set. - ## - [] if the points are all the same point (0 dimensional) - ## - [x] if the points lie on a line with basis direction x - ## - [x,y] if the points lie in a plane with basis directions x and y - ## - [x,y,z] if the points span three dimensions - return null if (p.length != 3 for p in ps).reduce(geom.all) - ds = (geom.dir(p,ps[0]) for p in ps when geom.distsq(p,ps[0]) > eps) - return [] if ds.length is 0 - x = ds[0] - return [x] if (geom.parallel(d, x, eps) for d in ds).reduce(geom.all) - ns = (geom.unit(geom.cross(d, x)) for d in ds) - ns = (n for n in ns when n?) - z = ns[0] - y = geom.cross(z, x) - return [x, y] if (geom.parallel(n, z, eps) for n in ns).reduce(geom.all) - return [x, y, z] - -geom.above = (ps, qs, n, eps = geom.EPS) -> - [pn,qn] = ((geom.dot(v, n) for v in vs) for vs in [ps,qs]) - return 1 if qn.reduce(geom.max) - pn.reduce(geom.min) < eps - return -1 if pn.reduce(geom.max) - qn.reduce(geom.min) < eps - return 0 - -geom.separatingDirection2D = (t1, t2, n, eps = geom.EPS) -> - ## If points are contained in a common plane with normal n and a separating - ## direction exists, a direction perpendicular to some pair of points from - ## the same set is also a separating direction. - for t in [t1, t2] - for p, i in t - for q, j in t when i < j - m = geom.unit(geom.cross(geom.sub(p, q), n)) - if m? - sign = geom.above(t1, t2, m, eps) - return geom.mul(m, sign) if sign isnt 0 - return null - -geom.separatingDirection3D = (t1, t2, eps = geom.EPS) -> - ## If points are not contained in a common plane and a separating direction - ## exists, a plane spanning two points from one set and one point from the - ## other set is a separating plane, with its normal a separating direction. - for [x1, x2] in [[t1, t2], [t2, t1]] - for p in x1 - for q1, i in x2 - for q2, j in x2 when i < j - m = geom.unit(geom.cross(geom.sub(p, q1), geom.sub(p, q2))) - if m? - sign = geom.above(t1, t2, m, eps) - return geom.mul(m, sign) if sign isnt 0 - return null - -## -## Hole Filling Methods -## - -geom.circleCross = (d, r1, r2) -> - x = (d * d - r2 * r2 + r1 * r1) / d / 2 - y = Math.sqrt(r1 * r1 - x * x) - return [x, y] - -geom.creaseDir = (u1, u2, a, b, eps = geom.EPS) -> - b1 = Math.cos(a) + Math.cos(b) - b2 = Math.cos(a) - Math.cos(b) - x = geom.plus(u1, u2) - y = geom.sub(u1, u2) - z = geom.unit(geom.cross(y, x)) - x = geom.mul(x, b1 / geom.magsq(x)) - y = geom.mul(y, if geom.magsq(y) < eps then 0 else b2 / geom.magsq(y)) - zmag = Math.sqrt(1 - geom.magsq(x) - geom.magsq(y)) - z = geom.mul(z, zmag) - return [x, y, z].reduce(geom.plus) - -geom.quadSplit = (u, p, d, t) -> - # Split from origin in direction U subject to external point P whose - # shortest path on the surface is distance D and projecting angle is T - if geom.magsq(p) > d * d - throw new Error "STOP! Trying to split expansive quad." - return geom.mul(u, (d*d - geom.magsq(p))/2/(d*Math.cos(t) - geom.dot(u, p))) - diff --git a/src/index.civet b/src/index.civet new file mode 100644 index 0000000..c8b7126 --- /dev/null +++ b/src/index.civet @@ -0,0 +1,6 @@ +export * as geom from "./geom.civet" +export * as viewer from "./viewer.civet" +export * as filter from "./filter.civet" +export * as convert from "./convert.civet" +export * as file from "./file.civet" +export * as oripa from './oripa.civet' diff --git a/src/index.coffee b/src/index.coffee deleted file mode 100644 index ab19f1f..0000000 --- a/src/index.coffee +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = - geom: require './geom' - viewer: require './viewer' - filter: require './filter' - convert: require './convert' - file: require './file' diff --git a/src/oripa.civet b/src/oripa.civet new file mode 100644 index 0000000..6cc4429 --- /dev/null +++ b/src/oripa.civet @@ -0,0 +1,229 @@ +* as XmlDOM from "@xmldom/xmldom" +* as convert from "./convert.civet" +* as filter from "./filter.civet" +type { Fold } from "./types.civet" + +{ DOMParser } := XmlDOM +export type2fold := + 0: 'F' // TYPE_NONE = flat + 1: 'B' // TYPE_CUT = boundary + 2: 'M' // TYPE_RIDGE = mountain + 3: 'V' // TYPE_VALLEY = valley + +export fold2type: Record := {} + +for x, y in type2fold + fold2type[y] = x + +export fold2type_default := 0 + +export prop_xml2fold := + editorName: 'frame_author' + originalAuthorName: 'frame_designer' + reference: 'frame_reference' + title: 'frame_title' + memo: 'frame_description' + paperSize: null + mainVersion: null + subVersion: null + +export POINT_EPS := 1.0 + +export function toFold(oripaStr: string) + fold := + vertices_coords: [] + edges_vertices: [] + edges_assignment: [] + file_creator: 'oripa2fold' + + vertex := (x: string, y: string) => + v := fold.vertices_coords.length + fold.vertices_coords.push [ + parseFloat x + parseFloat y + ] + v + + nodeSpec := (node: Element, type: string, key?: string, value?: any) => + if type? and node.tagName != type + console.warn `ORIPA file has ${node.tagName} where ${type} was expected` + null + else if key? and (not node.hasAttribute(key) or (value? and node.getAttribute(key) != value)) + console.warn `ORIPA file has ${node.tagName} with ${key} = ${node.getAttribute key} where ${value} was expected` + null + else + node + + children := (node: Element): Element[] => + if node + (for each child of node.childNodes + continue unless child.nodeType == 1 + child + ) + else + [] + + oneChildSpec := (node: Element, type: string, key?: string, value?: any) => + sub := children node + if sub.length != 1 + console.warn `ORIPA file has ${node.tagName} with ${node.childNodes.length} children, not 1` + null + else + nodeSpec sub[0], type, key, value + + oneChildText := (node: Element): string | undefined => + if node.childNodes.length > 1 + console.warn `ORIPA file has ${node.tagName} with ${node.childNodes.length} children, not 0 or 1` + null + else if node.childNodes.length == 0 + '' + else + child := node.childNodes[0] + if child.nodeType != 3 + console.warn `ORIPA file has nodeType ${child.nodeType} where 3 (text) was expected` + undefined + else + (child as any).data + + xml := new DOMParser().parseFromString oripaStr, 'text/xml' + for top of children xml.documentElement + if nodeSpec top, 'object', 'class', 'oripa.DataSet' + for property of children top + if property.getAttribute('property') == 'lines' + lines := oneChildSpec property, 'array', 'class', 'oripa.OriLineProxy' + for line of children lines + if nodeSpec line, 'void', 'index' + for object of children line + if nodeSpec object, 'object', 'class', 'oripa.OriLineProxy' + // Java doesn't encode the default value, 0 + [x0, x1, y0, y1, type]: (string | undefined)[] .= [] + x0 = x1 = y0 = y1 = type = '0' + + for subproperty of children object + if nodeSpec subproperty, 'void', 'property' + switch subproperty.getAttribute 'property' + when 'x0' + x0 = oneChildText(oneChildSpec(subproperty, 'double')) + when 'x1' + x1 = oneChildText(oneChildSpec(subproperty, 'double')) + when 'y0' + y0 = oneChildText(oneChildSpec(subproperty, 'double')) + when 'y1' + y1 = oneChildText(oneChildSpec(subproperty, 'double')) + when 'type' + type = oneChildText(oneChildSpec(subproperty, 'int')) + if x0? and x1? and y0? and y1? + fold.edges_vertices.push [ + vertex x0, y0 + vertex x1, y1 + ] + + if type? + type_int := parseInt type + fold.edges_assignment.push type2fold[type_int] + else + console.warn `ORIPA line has missing data: ${x0} ${x1} ${y0} ${y1} ${type}` + else if property.getAttribute('property') in prop_xml2fold + prop := prop_xml2fold[property.getAttribute 'property'] + if prop? + fold[prop] = oneChildText oneChildSpec(property, 'string') + else + console.warn `Ignoring ${property.tagName} ${top.getAttribute 'property'} in ORIPA file` + + // src/oripa/Doc.java uses absolute distance POINT_EPS = 1.0 to detect + // points being the same. + filter.collapseNearbyVertices(fold as Fold, POINT_EPS) + filter.subdivideCrossingEdges_vertices(fold as Fold, POINT_EPS) + // In particular, convert.removeLoopEdges fold + convert.edges_vertices_to_faces_vertices fold as Fold + + fold + +export function fromFold(fold: Fold) + if typeof fold == 'string' + fold = JSON.parse fold + + s .= """ + + + + + 1 + + + 1 + + + 400.0 + + +""" + + for xp, fp in prop_xml2fold + s += ` +. + + ${fold[fp] or ''} + + +`[2..] + + z .= 0 + lines := + for edge, ei of fold.edges_vertices + vs := for vertex of edge + for coord of fold.vertices_coords[vertex][2..] + if coord != 0 + z += 1 + fold.vertices_coords[vertex] + x0: vs[0][0] + y0: vs[0][1] + x1: vs[1][0] + y1: vs[1][1] + type: fold2type[fold.edges_assignment[ei]] or fold2type_default + + s += ` +. + + + +`[2..] + + for line, i of lines + s += ` +. + + + + ${line.type} + + + ${line.x0} + + + ${line.x1} + + + ${line.y0} + + + ${line.y1} + + + + +`[2..] + + s += """ +. + + + + + +"""[2..] + + s + +convert.setConverter '.fold', '.opx', fromFold +convert.setConverter '.opx', '.fold', toFold diff --git a/src/oripa.coffee b/src/oripa.coffee deleted file mode 100644 index d84e7ff..0000000 --- a/src/oripa.coffee +++ /dev/null @@ -1,214 +0,0 @@ -##TODO: match spec (no frame_designer, no frame_reference, fix cw -> ccw) -##TODO: oripa folded state format - -DOMParser = require('@xmldom/xmldom').DOMParser unless DOMParser? -#XMLSerializer = require('@xmldom/xmldom').XMLSerializer unless XMLSerializer? -#DOMImplementation = require('@xmldom/xmldom').DOMImplementation unless DOMImplementation? -convert = require './convert' -filter = require './filter' -oripa = exports - -## Based on src/oripa/geom/OriLine.java -oripa.type2fold = - 0: 'F' ## TYPE_NONE = flat - 1: 'B' ## TYPE_CUT = boundary - 2: 'M' ## TYPE_RIDGE = mountain - 3: 'V' ## TYPE_VALLEY = valley -oripa.fold2type = {} -for x, y of oripa.type2fold - oripa.fold2type[y] = x -oripa.fold2type_default = 0 - -oripa.prop_xml2fold = - 'editorName': 'frame_author' - 'originalAuthorName': 'frame_designer' - 'reference': 'frame_reference' - 'title': 'frame_title' - 'memo': 'frame_description' - 'paperSize': null - 'mainVersion': null - 'subVersion': null -#oripa.prop_fold2xml = {} -#for x, y of oripa.prop_xml2fold -# oripa.prop_fold2xml[y] = x if y? - -oripa.POINT_EPS = 1.0 -oripa.toFold = (oripaStr) -> - fold = - vertices_coords: [] - edges_vertices: [] - edges_assignment: [] - file_creator: 'oripa2fold' - vertex = (x,y) -> - v = fold.vertices_coords.length - fold.vertices_coords.push [ - parseFloat x - parseFloat y - ] - v - - nodeSpec = (node, type, key, value) -> - if type? and node.tagName != type - console.warn "ORIPA file has #{node.tagName} where #{type} was expected" - null - else if key? and (not node.hasAttribute(key) or (value? and node.getAttribute(key) != value)) - console.warn "ORIPA file has #{node.tagName} with #{key} = #{node.getAttribute key} where #{value} was expected" - null - else - node - children = (node) -> - if node - child for child in node.childNodes when child.nodeType == 1 ## element - else - [] - oneChildSpec = (node, type, key, value) -> - sub = children node - if sub.length != 1 - console.warn "ORIPA file has #{node.tagName} with #{node.childNodes.length} children, not 1" - null - else - nodeSpec sub[0], type, key, value - oneChildText = (node) -> - if node.childNodes.length > 1 - console.warn "ORIPA file has #{node.tagName} with #{node.childNodes.length} children, not 0 or 1" - null - else if node.childNodes.length == 0 - '' - else - child = node.childNodes[0] - if child.nodeType != 3 - console.warn "ORIPA file has nodeType #{child.nodeType} where 3 (text) was expected" - else - child.data - - xml = new DOMParser().parseFromString oripaStr, 'text/xml' - for top in children xml.documentElement - if nodeSpec top, 'object', 'class', 'oripa.DataSet' - for property in children top - if property.getAttribute('property') == 'lines' - lines = oneChildSpec property, 'array', 'class', 'oripa.OriLineProxy' - for line in children lines - if nodeSpec line, 'void', 'index' - for object in children line - if nodeSpec object, 'object', 'class', 'oripa.OriLineProxy' - ## Java doesn't encode the default value, 0 - x0 = x1 = y0 = y1 = type = 0 - for subproperty in children object - if nodeSpec subproperty, 'void', 'property' - switch subproperty.getAttribute 'property' - when 'x0' - x0 = oneChildText oneChildSpec(subproperty, 'double') - when 'x1' - x1 = oneChildText oneChildSpec(subproperty, 'double') - when 'y0' - y0 = oneChildText oneChildSpec(subproperty, 'double') - when 'y1' - y1 = oneChildText oneChildSpec(subproperty, 'double') - when 'type' - type = oneChildText oneChildSpec(subproperty, 'int') - if x0? and x1? and y0? and y1? - fold.edges_vertices.push [ - vertex x0, y0 - vertex x1, y1 - ] - type = parseInt type if type? - fold.edges_assignment.push oripa.type2fold[type] - else - console.warn "ORIPA line has missing data: #{x0} #{x1} #{y0} #{y1} #{type}" - else if property.getAttribute('property') of oripa.prop_xml2fold - prop = oripa.prop_xml2fold[property.getAttribute 'property'] - if prop? - fold[prop] = oneChildText oneChildSpec(property, 'string') - else - console.warn "Ignoring #{property.tagName} #{top.getAttribute 'property'} in ORIPA file" - - ## src/oripa/Doc.java uses absolute distance POINT_EPS = 1.0 to detect - ## points being the same. - filter.collapseNearbyVertices fold, oripa.POINT_EPS - filter.subdivideCrossingEdges_vertices fold, oripa.POINT_EPS - ## In particular, convert.removeLoopEdges fold - convert.edges_vertices_to_faces_vertices fold - fold - -oripa.fromFold = (fold) -> - if typeof fold == 'string' - fold = JSON.parse fold - s = """ - - - - - 1 - - - 1 - - - 400.0 - - -""" - for xp, fp of oripa.prop_xml2fold - #if fp of fold - s += """ -. - - #{fold[fp] or ''} - - -"""[2..] - z = 0 - lines = - for edge, ei in fold.edges_vertices - vs = for vertex in edge - for coord in fold.vertices_coords[vertex][2..] - if coord != 0 - z += 1 - fold.vertices_coords[vertex] - x0: vs[0][0] - y0: vs[0][1] - x1: vs[1][0] - y1: vs[1][1] - type: oripa.fold2type[fold.edges_assignment[ei]] or oripa.fold2type_default - s += """ -. - - - -"""[2..] - for line, i in lines - s += """ -. - - - - #{line.type} - - - #{line.x0} - - - #{line.x1} - - - #{line.y0} - - - #{line.y1} - - - - -"""[2..] - s += """ -. - - - - - -"""[2..] - s - -convert.setConverter '.fold', '.opx', oripa.fromFold -convert.setConverter '.opx', '.fold', oripa.toFold diff --git a/src/types.civet b/src/types.civet new file mode 100644 index 0000000..5280568 --- /dev/null +++ b/src/types.civet @@ -0,0 +1,244 @@ +export type Coords = number[] +export type Coords2D = [x: number, y: number] +export type Coords3D = [x: number, y: number, z: number] +export type Matrix = Coords[] + +export type Converter = (x: unknown) => unknown + +export interface FileMetadata + /** The version of the FOLD spec that the file assumes. + * **Strongly recommended**, in case we ever have to make + * backward-incompatible changes. + */ + file_spec?: number + /** The software that created the file. + * **Recommended** for files output by computer software; + * less important for files made by hand. + */ + file_creator?: string + /** The human author. */ + file_author?: string + /** A title for the entire file. */ + file_title?: string + /** A description of the entire file. */ + file_description?: string + /** A subjective interpretation about what the entire file represents. */ + file_classes?: Array< + ('singleModel' | 'multiModel') | 'animation' | 'diagrams' | string + > + +export interface FrameMetadata + /** The human author. */ + frame_author?: string + /** A title for the frame. */ + frame_title?: string + /** A description of the frame. */ + frame_description?: string + /** A subjective interpretation about what the frame represents. */ + frame_classes?: Array< + 'creasePattern' | 'foldedForm' | 'graph' | 'linkage' | string + > + /** Attributes that objectively describe properties of the + * folded structure being represented. + */ + frame_attributes?: Array< + | ('2D' | '3D') + | 'abstract' + | ('manifold' | 'nonManifold') + | ('orientable' | 'nonOrientable') + | ('selfTouching' | 'nonSelfTouching') + | ('selfIntersecting' | 'nonSelfIntersecting') + | ('cuts' | 'noCuts') + | ('joins' | 'noJoins') + | ('convexFaces' | 'nonConvexFaces') + | string + > + /** Physical or logical unit that all coordinates are relative to. */ + frame_unit?: + | 'unit' + | 'in' + | 'pt' + | 'm' + | 'cm' + | 'mm' + | 'um' + | 'nm' + | string + +export interface RootFrame extends FrameMetadata + /** For each vertex, an array of coordinates, + * such as `[x, y, z]` or `[x, y]` (where `z` is implicitly zero). + * In higher dimensions, all trailing unspecified coordinates are implicitly + * zero. **Recommended** except for frames with attribute `"abstract"`. + */ + vertices_coords?: Coords[] + /** For each vertex, an array of vertices (vertex IDs) + * that are adjacent along edges. If the frame represents an orientable + * manifold or planar linkage, this list should be ordered counterclockwise + * around the vertex (possibly repeating a vertex more than once). + * If the frame is a nonorientable manifold, this list should be cyclically + * ordered around the vertex (possibly repeating a vertex). + * Otherwise, the order is arbitrary. + * **Recommended** in any frame lacking `edges_vertices` property + * (otherwise `vertices_vertices` can easily be computed from + * `edges_vertices` as needed). + */ + vertices_vertices?: number[][] + /** For each vertex, an array of edge IDs for the edges + * incident to the vertex. If the frame represents an orientable manifold, + * this list should be ordered counterclockwise around the vertex. + * If the frame is a nonorientable manifold, this list should be cyclically + * ordered around the vertex. + * In all cases, the linear order should match `vertices_vertices` if both + * are specified: `vertices_edges[v][i]` should be an edge connecting vertices + * `v` and `vertices_vertices[v][i]`. + */ + vertices_edges?: number[][] + /** For each vertex, an array of face IDs for the faces + * incident to the vertex, possibly including `null`s. + * If the frame represents a manifold, `vertices_faces` should align with + * `vertices_vertices` and/or `vertices_edges`: + * `vertices_faces[v][i]` should be either + * + * * the face containing vertices + * `vertices_vertices[v][i]` and `vertices_vertices[v][(i+1)%d]` and + * containing edges `vertices_edges[v][i]` and `vertices_edges[v][(i+1)%d]`, + * where `d` is the degree of vertex `v`; or + * * `null` if such a face doesn't exist. + * + * If the frame represents an orientable manifold, + * this list should be ordered counterclockwise around the vertex + * (possibly repeating a face more than once). If the frame is a + * nonorientable manifold, this list should be cyclically ordered around the + * vertex (possibly repeating a vertex), and matching the cyclic order of + * `vertices_vertices` and/or `vertices_edges` (if either is specified). + */ + vertices_faces?: (number | null)[][] + + /** `edges_vertices`: For each edge, an array `[u, v]` of two vertex IDs for + * the two endpoints of the edge. This effectively defines the *orientation* + * of the edge, from `u` to `v`. (This orientation choice is arbitrary, + * but is used to define the ordering of `edges_faces`.) + * **Recommended** in frames having any `edges_...` property + * (e.g., to represent mountain-valley assignment). + */ + edges_vertices?: [u: number, v: number][] + /** For each edge, an array of face IDs for the faces incident + * to the edge, possibly including `null`s. + * For nonmanifolds in particular, the (nonnull) faces should be listed in + * counterclockwise order around the edge, + * relative to the orientation of the edge. + * For manifolds, the array for each edge should be an array of length 2, + * where the first entry is the face locally to the "left" of the edge + * (or `null` if there is no such face) and the second entry is the face + * locally to the "right" of the edge (or `null` if there is no such face); + * for orientable manifolds, "left" and "right" must be consistent with the + * manifold orientation given by the counterclockwise orientation of faces. + * However, a boundary edge may also be represented by a length-1 array, with + * the `null` omitted, to be consistent with the nonmanifold representation. + */ + edges_faces?: (number | null)[][] + /** For each edge, a string representing its fold direction assignment: + * * `"B"`: border/boundary edge (only one incident face) + * * `"M"`: mountain crease + * * `"V"`: valley crease + * * `"F"`: flat (unfolded) crease + * * `"U"`: unassigned/unknown crease + * * `"C"`: cut/slit edge (should be treated as multiple `"B"` edges) + * * `"J"`: join edge (incident faces should be treated as a single face) + */ + edges_assignment?: Array< + "B" | "M" | "V" | "F" | "U" | "C" | "J" + > + /** For each edge, the fold angle (deviation from flatness) + * along each edge of the pattern. The fold angle is a number in degrees + * lying in the range [−180, 180]. The fold angle is positive for + * valley folds, negative for mountain folds, and zero for flat, unassigned, + * and border folds. Accordingly, the sign of `edge_foldAngle` should match + * `edges_assignment` if both are specified. + * *Renamed from `edges_foldAngles` in version 1.1.* + */ + edges_foldAngle?: number[] + /** For each edge, the length of the edge. + * This is mainly useful for defining the intrinsic geometry of + * abstract complexes where `vertices_coords` are unspecified; + * otherwise, `edges_length` can be computed from `vertices_coords`. + * *Renamed from `edges_lengths` in version 1.1.* + */ + edges_length?: number[] + + /** For each face, an array of vertex IDs for the vertices + * around the face *in counterclockwise order*. This array can repeat the + * same vertex multiple times (e.g., if the face has a "slit" in it). + * **Recommended** in any frame having faces. + */ + faces_vertices?: number[][] + /** For each face, an array of edge IDs for the edges around + * the face *in counterclockwise order*. In addition to the matching cyclic + * order, `faces_vertices` and `faces_edges` should align in start so that + * `faces_edges[f][i]` is the edge connecting `faces_vertices[f][i]` and + * `faces_vertices[f][(i+1)%d]` where `d` is the degree of face `f`. + */ + faces_edges?: number[][] + /** `faces_faces`: For each face, an array of face IDs for the faces *sharing + * edges* around the face, possibly including `null`s. + * If the frame is a manifold, the faces should be listed in counterclockwise + * order and in the same linear order as `faces_edges` (if it is specified): + * `f` and `faces_faces[f][i]` should be the faces incident to the edge + * `faces_edges[f][i]`, unless that edge has no face on the other side, + * in which case `faces_faces[f][i]` should be `null`. + */ + faces_faces?: (number | null)[][] + + /** An array of triples `[f, g, s]` where `f` and `g` are face IDs + * and `s` is an integer between −1 and 1: + * * +1 indicates that face `f` lies *above* face `g`, + * i.e., on the side pointed to by `g`'s normal vector in the folded state. + * * −1 indicates that face `f` lies *below* face `g`, + * i.e., on the side opposite `g`'s normal vector in the folded state. + * * 0 indicates that `f` and `g` have unknown stacking order + * (e.g., they do not overlap in their interiors). + * + * **Recommended** for frames with interior-overlapping faces. + */ + faceOrders?: [f: number, g: number, s: number][] + /** An array of triples `[e, f, s]` where `e` and `f` are edge IDs + * and `s` is an integer between −1 and 1: + * * +1 indicates that edge `e` lies locally on the *left* side of edge `f` + * (relative to edge `f`'s orientation given by `edges_vertices`) + * * −1 indicates that edge `e` lies locally on the *right* side of edge + * `f` (relative to edge `f`'s orientation given by `edges_vertices`) + * * 0 indicates that `e` and `f` have unknown stacking order + * (e.g., they do not overlap in their interiors). + * + * This property makes sense only in 2D. + * **Recommended** for linkage configurations with interior-overlapping edges. + */ + edgeOrders?: [e: number, f: number, s: number][] + + // Nonstandard (not in spec) extensions used by the FOLD library + faces_flatFoldTransform?: (number[][] | null)[] + faces_flatUnfoldTransform?: (number[][] | null)[] + faces_foldTransform?: (number[][] | null)[] + vertices_foldCoords?: (number[] | null)[] + faces_flatFoldOrientation?: (number | null)[] + vertices_flatFoldCoords?: (number[] | null)[] + vertices_flatUnfoldCoords?: Coords[] + faces_flatUnfoldOrientation?: (number | null)[] + +export interface Frame extends RootFrame + /** Parent frame ID. Intuitively, this frame (the child) + * is a modification (or, in general, is related to) the parent frame. + * This property is optional, but enables organizing frames into a tree + * structure. + */ + frame_parent?: number + /** If true, any properties in the parent frame + * (or recursively inherited from an ancestor) that is not overridden in + * this frame are automatically inherited, allowing you to avoid duplicated + * data in many cases. + */ + frame_inherit?: boolean + +export interface Fold extends FileMetadata, RootFrame + file_frames?: Frame[] diff --git a/src/viewer.civet b/src/viewer.civet new file mode 100644 index 0000000..97d27b0 --- /dev/null +++ b/src/viewer.civet @@ -0,0 +1,501 @@ +// @ts-nocheck +* as geom from "./geom.civet" +type { Fold, Coords, Coords3D } from "./types.civet" +createRBTree from 'functional-red-black-tree' + +// vs -> vertices +// fs -> faces +// c2 -> projected centroid +// es -> edges +// g -> group +// vg -> vertex group +// eg -> edge group +// ps -> projected coords +// cs -> coords +// n -> normal +// c -> centroid +interface Face + vs?: Array<{ cs: Coords; i: number; ps: number[] }> + i?: number + n?: any + c?: any + ord?: any + es?: any + g?: any + c2?: any + children: any[] + path?: any + text?: any + eg?: any + vg?: any + +interface Model + vs: Array<{ cs: Coords; i: number; ps: number[] }> + fs: Array + es: any + +interface ViewOptions extends Record + viewButtons: boolean + axisButtons: boolean + attrViewer: boolean + examples: any + import: boolean + export: boolean + properties: boolean + +interface Camera extends Record + x: Coords + y: Coords + z: Coords + c: Coords + r: number + last: Coords | null + show: Record + axis?: any + +interface View + fold?: Fold + model: Model + properties?: HTMLSelectElement + data?: Element + svg: SVGElement + cam: Camera + opts: ViewOptions + +STYLES: Record := { + vert: "fill: white; r: 0.03; stroke: black; stroke-width: 0.005;" + face: "stroke: none; fill-opacity: 0.8;" + top: "fill: cyan;", bot: "fill: yellow;" + edge: "fill: none; stroke-width: 0.01; stroke-linecap: round;" + axis: "fill: none; stroke-width: 0.01; stroke-linecap: round;" + text: "fill: black; font-size: 0.04px; text-anchor: middle; + font-family: sans-serif;" + B: "stroke: black;", V: "stroke: blue;" + M: "stroke: red;", U: "stroke: white;", F: "stroke: gray;" + ax: "stroke: blue;", ay: "stroke: red;", az: "stroke: green;" +} + +### UTILITIES ### + +export function setAttrs(el: Element, attrs?: Record) + (el.setAttribute(k, v) for k, v in attrs); el + +export function appendHTML(el: Element, tag: string, attrs?: Record) + el.appendChild(setAttrs(document.createElement(tag), attrs)) + +SVGNS := 'http://www.w3.org/2000/svg' + +export function appendSVG(el: Element, tag: string, attrs?: Record) + el.appendChild(setAttrs(document.createElementNS(SVGNS, tag), attrs)) + +export function makePath(coords: Coords[]) + (for c, i of coords + `${if (i is 0) then 'M' else 'L'} ${c[0]} ${c[1]} ` + ).reduce((a, b) -> a + b) + +### INTERFACE ### + +export function processInput(input: string | Fold, view: View) + if typeof input == 'string' + view.fold = JSON.parse(input) + else + view.fold = input + + view.model = makeModel(view.fold!) + addRotation(view) + draw(view) + update(view) + + if view.opts.properties + view.properties!.innerHTML = '' + for k in view.fold + continue unless view.opts.properties + appendHTML(view.properties!, 'option', value: k) + .innerHTML = k + updateProperties(view) + +export function updateProperties(view: View) + v := view.fold![view.properties!.value as keyof typeof view.fold] as any + s := if v.length? then `${v.length} elements: ` else '' + view.data!.innerHTML = s + JSON.stringify(v) + +export function importURL(url: string, view: View) + fetch(url) + .then(.text()) + .then((res) => processInput(res, view)) + +export function importFile(file: Blob, view: View) + file_reader := new FileReader() + file_reader.onload = (e) => processInput(e.target!.result as string, view) + file_reader.readAsText(file) + +DEFAULTS: ViewOptions := + viewButtons: true + axisButtons: true + attrViewer: true + examples: false + import: true + export: true + properties: true + +export function addViewer(div: HTMLDivElement, opts: Partial = {}) + view: View := + cam: initCam() + opts: DEFAULTS + svg: null as any + model: null as any + view.opts[k as keyof ViewOptions] = v for k, v in opts + + if view.opts.viewButtons + toggleDiv := appendHTML(div, 'div') + toggleDiv.innerHTML = '' + toggleDiv.innerHTML += 'Toggle: ' + for k, v in view.cam.show + t := appendHTML(toggleDiv, 'input', {type: 'checkbox', value: k}) + t.setAttribute('checked', '') if v + toggleDiv.innerHTML += k + ' ' + + let buttonDiv: Element; + if view.opts.axisButtons + console.log('adding axis buttons') + buttonDiv = appendHTML(div, 'div') + console.log("button div", buttonDiv) + buttonDiv.innerHTML += 'View: ' + for val, i of ['x', 'y', 'z'] + appendHTML(buttonDiv, 'input', {type: 'button', value: val}) + console.log("appended", buttonDiv, val) + if view.opts.properties + buttonDiv.innerHTML += ' Property:' + view.properties = appendHTML(buttonDiv, 'select') as HTMLSelectElement + view.data = appendHTML(buttonDiv, 'div', { + style: 'width: 300; padding: 10px; overflow: auto; \ + border: 1px solid black; display: inline-block; white-space: nowrap;'}) + if view.opts.examples or view.opts.import + inputDiv := appendHTML(div, 'div') + if view.opts.examples + inputDiv.innerHTML = 'Example: ' + select := appendHTML(inputDiv, 'select') as HTMLSelectElement + for title, url in view.opts.examples + appendHTML(select, 'option', {value: url}).innerHTML = title + importURL(select.value, view) + if view.opts.import + inputDiv.innerHTML += ' Import: ' + appendHTML(inputDiv, 'input', {type: 'file'}) + + div.addEventListener 'click', (e) => + input := e.target as HTMLInputElement + if input.type is 'checkbox' + if input.hasAttribute('checked') + input.removeAttribute('checked') + else + input.setAttribute('checked', '') + view.cam.show[input.value] = input.hasAttribute('checked') + update view + if input.type is 'button' + switch input.value + when 'x' then setCamXY(view.cam, [0,1,0], [0,0,1]) + when 'y' then setCamXY(view.cam, [0,0,1], [1,0,0]) + when 'z' then setCamXY(view.cam, [1,0,0], [0,1,0]) + update view + + div.addEventListener 'change', (e) => + input := e.target as any + if input.type is 'file' + importFile(input.files[0], view) + if input.type is 'select-one' + if input is view.properties + updateProperties(view) + else + importURL(input.value, view) + + view.svg = appendSVG(div, 'svg', {xmlns: SVGNS, width: '600'}) as SVGElement + view + +### CAMERA ### + +export function initCam() + c: [0,0,0], x: [1,0,0], y: [0,1,0], z: [0,0,1], r: 1, last: null + show: {'Faces': true, 'Edges': true, 'Vertices': false, 'Face Text': false} + +export function proj(p: Coords, cam: Camera) + q := geom.mul(geom.sub(p, cam.c), 1/cam.r) + [geom.dot(q, cam.x), -geom.dot(q, cam.y), 0] + +export function setCamXY(cam: Camera, x: Coords, y: Coords) + [cam.x, cam.y, cam.z] = [x, y, geom.cross(x, y) as Coords] + +export function addRotation(view: View) + {svg, cam} := view + for s in ['contextmenu', 'selectstart', 'dragstart'] + (svg as any)[`on${s}`] = (e: Event) -> e.preventDefault() + svg.onmousedown = (e) => cam.last = [e.clientX, e.clientY] + svg.onmousemove = (e) => rotateCam([e.clientX, e.clientY], view) + svg.onmouseup = (e) => + rotateCam([e.clientX, e.clientY], view) + cam.last = null + +export function rotateCam(p: Coords, view: View) + cam := view.cam + return if not cam.last? + d := geom.sub(p, cam.last) + return if not (geom.mag(d) > 0) + + u := geom.unit(geom.plus(geom.mul(cam.x, -d[1]), geom.mul(cam.y, -d[0])))! + [x, y] := (geom.rotate(cam[e], u, geom.mag(d) * 0.01)! for e of ['x','y']) + setCamXY(cam, x, y) + cam.last = p + update(view) + +### RENDERING ### + +export function makeModel(fold: Fold) + m: Model := {vs: null, fs: null, es: {}} + m.vs = ({i: i, cs: cs} for cs, i of fold.vertices_coords!) + + for v, i of m.vs + continue unless v.cs.length is 2 + m.vs[i].cs[2] = 0 + + m.fs = ({i: i, vs: (m.vs[v] for v of vs)} for vs, i of fold.faces_vertices!) + + if fold.edges_vertices? + for v, i of fold.edges_vertices + [a,b] := if v[0] > v[1] then [v[1],v[0]] else [v[0],v[1]] + as := if fold.edges_assignment?[i]? then fold.edges_assignment[i] else 'U' + m.es[`e${a}e${b}`] = {v1: m.vs[a], v2: m.vs[b], as: as} + else + for f, i of m.fs + for v, j of f.vs! + w := f.vs![geom.next(j,f.vs!.length)] + [a,b] := if v.i > w.i then [w,v] else [v,w] + m.es[`e${a.i}e${b.i}`] = {v1: a, v2: b, as: 'U'} + + for f, i of m.fs + m.fs[i].n = geom.polygonNormal((v.cs for v of f.vs!)) + m.fs[i].c = geom.centroid((v.cs for v of f.vs!)) + // m.fs[i].es = {} + m.fs[i].es = (for v, j of f.vs! + w := f.vs![geom.next(j, f.vs!.length)] + [a,b] := if v.i > w.i then [w,v] else [v,w] + edge .= m.es[`e${a.i}e${b.i}`] + unless edge? + edge = {v1: a, v2: b, as: 'U'} + edge + ) + m.fs[i].ord = {} + + if fold.faceOrders? + for [f1, f2, o] of fold.faceOrders + continue unless o is not 0 + if geom.parallel(m.fs[f1].n, m.fs[f2].n) + normRel := if geom.dot(m.fs[f1].n, m.fs[f2].n) > 0 then 1 else -1 + if m.fs[f1].ord[`f${f2}`]? + console.log `Warning: duplicate ordering input information for \ + faces ${f1} and ${f2}. Using first found in the faceOrder list.` + if m.fs[f1].ord[`f${f2}`] != o + console.log `Error: duplicate ordering [${f1},${f2},${o}] \ + is inconsistent with a previous entry.` + else + m.fs[f1].ord[`f${f2}`] = o + m.fs[f2].ord[`f${f1}`] = -o * normRel + else + console.log `Warning: order for non-parallel faces [${f1},${f2}]` + return m + +export function faceAbove(f1: Face, f2: Face, n: Coords) + [p1, p2] := ((v.ps for v of f.vs!) for f of [f1,f2]) + sepDir := geom.separatingDirection2D(p1, p2, [0,0,1]) + if sepDir? // projections do not overlap + return null + [v1,v2] := ((v.cs for v of f.vs!) for f of [f1,f2]) as Coords3D[][] + basis := geom.basis(v1.concat(v2))! + if basis.length is 3 + dir := geom.separatingDirection3D(v1, v2) + if dir? + return 0 > geom.dot(n, dir) // faces are separable in 3D + else + console.log `Warning: faces ${f1.i} and ${f2.i} properly intersect. + Ordering is unresolved.` + if basis.length is 2 + ord := f1.ord[`f${f2.i}`] + if ord? + return 0 > geom.dot(f2.n, n) * ord // faces coplanar and have order + return null + +function handleFaceOrdering(f1: Face, f2: Face, direction: Coords) + f1_above := faceAbove(f1, f2, direction) + + console.log `comparing (${f1.i}, ${f2.i})` + + if f1_above? + [p,c] := if f1_above then [f1,f2] else [f2,f1] + p.children = p.children.concat([c]) + +function getPredecessorAndSuccessor(tree: any, key: number) + var cmp = tree._compare; + var n = tree.root; + var stack = []; + var successor = null; + var predecessor = null; + + while(n) { + var d = cmp(key, n.key); + stack.push(n); + if(d < 0) { + if(!successor || cmp(n.key, successor.key) < 0) { + successor = n; + } + n = n.left; + } else if(d > 0) { + if(!predecessor || cmp(n.key, predecessor.key) > 0) { + predecessor = n; + } + n = n.right; + } else { + if(n.right) { + var temp = n.right; + while(temp.left) { + temp = temp.left; + } + successor = temp; + } + if(n.left) { + var temp = n.left; + while(temp.right) { + temp = temp.right; + } + predecessor = temp; + } + break; + } + } + + return { successor, predecessor } + +export function orderFaces(view: View) + direction := geom.mul(view.cam.z, -1) + faces := view.model.fs as any[] + (f.children = [] for f of faces) + + tree .= createRBTree() as any + + for each f of faces + max .= [0, -Infinity, 0] + min .= [0, Infinity, 0] + for v of f.vs! + if v.ps[1] > max[1] + max = f.end = v.ps + f.end_i = v.i + if v.ps[1] < min[1] + min = f.begin = v.ps + f.begin_i = v.i + + events := faces.flatMap((f) => [ + { i: f.i, vs: f.begin_i, face: f, coords: f.begin, type: 'begin' }, + { i: f.i, vs: f.end_i, face: f, coords: f.begin, type: 'end' } + ]) + + events.sort((e1, e2) => { + if (e1.coords[1] !== e2.coords[1]) { + return e1.coords[1] - e2.coords[1]; + } + // If coords[1] are the same, sort 'begin' events before 'end' events + if (e1.type !== e2.type) { + return e1.type === 'begin' ? -1 : 1; + } + // If both events are of the same type, sort based on x or z coordinates + return e1.coords[0] - e2.coords[0] || e1.coords[2] - e2.coords[2]; + }); + + f_to_i := new Map(events.filter((e) => e.type === 'begin').map((e, i) => [e.i!, i])) + + for E of events + face := E.face + if E.type === 'begin' + i := f_to_i.get(face.i!)! + tree = tree.insert(i, face) + + {successor, predecessor} := tree.getSuccessorAndPredecessor(i) + if successor? + handleFaceOrdering(face, successor.value, direction) + if predecessor? + handleFaceOrdering(face, predecessor.value, direction) + if E.type === 'end' + i := f_to_i.get(face.i!)! + {successor, predecessor} := tree.getSuccessorAndPredecessor(i!) + if successor? + if predecessor? + handleFaceOrdering(predecessor.value, successor.value, direction) + tree = tree.remove(i) + + orderedFaces := geom.topologicalSort(faces) + + view.model.fs = orderedFaces + f.g.parentNode.removeChild(f.g) for f of view.model.fs + view.svg.appendChild(f.g) for f of view.model.fs + +export function draw({svg, cam, model}: View) + svg.innerHTML = '' + style := appendSVG(svg, 'style') + for k, v in STYLES + style.innerHTML += `.${k}{${v}}\n` + min := ((v.cs[i] for v of model.vs).reduce(geom.min) for i of [0,1,2]) + max := ((v.cs[i] for v of model.vs).reduce(geom.max) for i of [0,1,2]) + cam.c = geom.mul(geom.plus(min, max), 0.5) + cam.r = geom.mag(geom.sub(max, min)) / 2 * 1.05 + c := proj(cam.c, cam) + setAttrs(svg, {viewBox: "-1,-1,2,2"}) + t := "translate(0,0.01)" + for f, i of model.fs + f.g = appendSVG(svg, 'g') + f.path = appendSVG(f.g, 'path') + f.text = appendSVG(f.g, 'text', {class: 'text', transform: t}) + f.text.innerHTML = `f${f.i}` + f.eg = [] + for e, j of f.es + f.eg[j] = appendSVG(f.g, 'path') + f.vg = [] + for v, j of f.vs! + f.vg[j] = appendSVG(f.g, 'g') + f.vg[j].path = appendSVG(f.vg[j], 'circle', {class: 'vert'}) + f.vg[j].text = appendSVG(f.vg[j], 'text', + {transform: 'translate(0, 0.01)', class: 'text'}) + f.vg[j].text.innerHTML = `${v.i}` + cam.axis = appendSVG(svg,'g',{transform: 'translate(-0.9,-0.9)'}) + for c of ['x','y','z'] + cam.axis[c] = appendSVG(cam.axis,'path', { + id: `a${c}`, class: `a${c} axis`}) + +export function update(view: View) + {model, cam, svg} := view + (model.vs[i].ps = proj(v.cs, cam) for v, i of model.vs) + (model.fs[i].c2 = proj(f.c, cam) for f, i of model.fs) + console.time(); + orderFaces(view) + console.timeEnd() + show: Record := {} + + for k, v in cam.show + show[k] = if v then 'visible' else 'hidden' + + for f, i of model.fs + continue unless f.path? + visibleSide := if geom.dot(f.n, cam.z) > 0 then 'top' else 'bot' + setAttrs(f.text, { + x: f.c2[0], y: f.c2[1], visibility: show['Face Text']}) + setAttrs(f.path, { + d: makePath((v.ps for v of f.vs!)) + 'Z' + visibility: show['Faces'], class: `face ${visibleSide}`}) + for e, j of f.es + setAttrs(f.eg[j], { + d: makePath([e.v1.ps, e.v2.ps]) + visibility: show['Edges'], class: `edge ${e.as}`}) + for v, j of f.vs! + setAttrs(f.vg[j], {visibility: show['Vertices']}) + setAttrs(f.vg[j].path, {cx: String(v.ps[0]), cy: String(v.ps[1])}) + setAttrs(f.vg[j].text, {x: String(v.ps[0]), y: String(v.ps[1])}) + for c, v in {x: [1,0,0], y: [0,1,0], z: [0,0,1]} as Record + end := geom.plus(geom.mul(v, 0.05 * cam.r), cam.c) + setAttrs(cam.axis[c], { + d: makePath((proj(p, cam) for p of [cam.c, end]))}) + diff --git a/src/viewer.coffee b/src/viewer.coffee deleted file mode 100644 index e920692..0000000 --- a/src/viewer.coffee +++ /dev/null @@ -1,303 +0,0 @@ -geom = require './geom' -viewer = exports - -STYLES = { - vert: "fill: white; r: 0.03; stroke: black; stroke-width: 0.005;" - face: "stroke: none; fill-opacity: 0.8;" - top: "fill: cyan;", bot: "fill: yellow;" - edge: "fill: none; stroke-width: 0.01; stroke-linecap: round;" - axis: "fill: none; stroke-width: 0.01; stroke-linecap: round;" - text: "fill: black; font-size: 0.04; text-anchor: middle; - font-family: sans-serif;" - B: "stroke: black;", V: "stroke: blue;" - M: "stroke: red;", U: "stroke: white;", F: "stroke: gray;" - ax: "stroke: blue;", ay: "stroke: red;", az: "stroke: green;" -} - -### UTILITIES ### - -viewer.setAttrs = (el, attrs) -> - (el.setAttribute(k, v) for k, v of attrs); el - -viewer.appendHTML = (el, tag, attrs) -> - el.appendChild(viewer.setAttrs(document.createElement(tag), attrs)) - -SVGNS = 'http://www.w3.org/2000/svg' -viewer.appendSVG = (el, tag, attrs) -> - el.appendChild(viewer.setAttrs(document.createElementNS(SVGNS, tag), attrs)) - -viewer.makePath = (coords) -> - (for c, i in coords - "#{if (i is 0) then 'M' else 'L'} #{c[0]} #{c[1]} " - ).reduce geom.sum - -### INTERFACE ### - -viewer.processInput = (input, view) -> - if typeof input == 'string' - view.fold = JSON.parse(input) - else - view.fold = input - view.model = viewer.makeModel(view.fold) - viewer.addRotation(view) - viewer.draw(view) - viewer.update(view) - if view.opts.properties - view.properties.innerHTML = '' - for k of view.fold when view.opts.properties - viewer.appendHTML(view.properties, 'option', {value: k}) - .innerHTML = k - viewer.updateProperties(view) - -viewer.updateProperties = (view) -> - v = view.fold[view.properties.value] - s = if v.length? then "#{v.length} elements: " else '' - view.data.innerHTML = s + JSON.stringify(v) - -viewer.importURL = (url, view) -> - xhr = new XMLHttpRequest() - xhr.onload = (e) => viewer.processInput(e.target.responseText, view) - xhr.open('GET', url); xhr.send() - -viewer.importFile = (file, view) -> - file_reader = new FileReader() - file_reader.onload = (e) => viewer.processInput(e.target.result, view) - file_reader.readAsText(file) - -DEFAULTS = { - viewButtons: true, axisButtons: true, attrViewer: true - examples: false, import: true, export: true, properties: true} - -viewer.addViewer = (div, opts = {}) -> - view = {cam: viewer.initCam(), opts: DEFAULTS} - view.opts[k] = v for k, v of opts - if view.opts.viewButtons - toggleDiv = viewer.appendHTML(div, 'div') - toggleDiv.innerHtml = '' - toggleDiv.innerHtml += 'Toggle: ' - for k, v of view.cam.show - t = viewer.appendHTML(toggleDiv, 'input', {type: 'checkbox', value: k}) - t.setAttribute('checked', '') if v - toggleDiv.innerHTML += k + ' ' - if view.opts.axisButtons - buttonDiv = viewer.appendHTML(div, 'div') - buttonDiv.innerHTML += 'View: ' - for val, i in ['x', 'y', 'z'] - viewer.appendHTML(buttonDiv, 'input', {type: 'button', value: val}) - if view.opts.properties - buttonDiv.innerHTML += ' Property:' - view.properties = viewer.appendHTML(buttonDiv, 'select') - view.data = viewer.appendHTML(buttonDiv, 'div', { - style: 'width: 300; padding: 10px; overflow: auto; \ - border: 1px solid black; display: inline-block; white-space: nowrap;'}) - if view.opts.examples or view.opts.import - inputDiv = viewer.appendHTML(div, 'div') - if view.opts.examples - inputDiv.innerHTML = 'Example: ' - select = viewer.appendHTML(inputDiv, 'select') - for title, url of view.opts.examples - viewer.appendHTML(select, 'option', {value: url}).innerHTML = title - viewer.importURL(select.value, view) - if view.opts.import - inputDiv.innerHTML += ' Import: ' - viewer.appendHTML(inputDiv, 'input', {type: 'file'}) - div.onclick = (e) => - if e.target.type is 'checkbox' - if e.target.hasAttribute('checked') - e.target.removeAttribute('checked') - else - e.target.setAttribute('checked', '') - view.cam.show[e.target.value] = e.target.hasAttribute('checked') - viewer.update view - if e.target.type is 'button' - switch e.target.value - when 'x' then viewer.setCamXY(view.cam, [0,1,0], [0,0,1]) - when 'y' then viewer.setCamXY(view.cam, [0,0,1], [1,0,0]) - when 'z' then viewer.setCamXY(view.cam, [1,0,0], [0,1,0]) - viewer.update view - div.onchange = (e) => - if e.target.type is 'file' - viewer.importFile(e.target.files[0], view) - if e.target.type is 'select-one' - if e.target is view.properties - viewer.updateProperties(view) - else - viewer.importURL(e.target.value, view) - view.svg = viewer.appendSVG(div, 'svg', {xmlns: SVGNS, width: 600}) - view - -### CAMERA ### - -viewer.initCam = () -> { - c: [0,0,0], x: [1,0,0], y: [0,1,0], z: [0,0,1], r: 1, last: null - show: {'Faces': true, 'Edges': true, 'Vertices': false, 'Face Text': false}} - -viewer.proj = (p, cam) -> - q = geom.mul(geom.sub(p, cam.c), 1/cam.r) - [geom.dot(q, cam.x), -geom.dot(q, cam.y), 0] - -viewer.setCamXY = (cam, x, y) -> - [cam.x, cam.y, cam.z] = [x, y, geom.cross(x, y)] - -viewer.addRotation = (view) -> - {svg: svg, cam: cam} = view - for s in ['contextmenu','selectstart','dragstart'] - svg["on#{s}"] = (e) -> e.preventDefault() - svg.onmousedown = (e) => cam.last = [e.clientX, e.clientY] - svg.onmousemove = (e) => viewer.rotateCam([e.clientX, e.clientY], view) - svg.onmouseup = (e) => - viewer.rotateCam([e.clientX, e.clientY], view); cam.last = null - -viewer.rotateCam = (p, view) -> - cam = view.cam - return if not cam.last? - d = geom.sub(p, cam.last) - return if not geom.mag(d) > 0 - u = geom.unit(geom.plus(geom.mul(cam.x, -d[1]), geom.mul(cam.y, -d[0]))) - [x, y] = (geom.rotate(cam[e], u, geom.mag(d) * 0.01) for e in ['x','y']) - viewer.setCamXY(cam, x, y) - cam.last = p - viewer.update(view) - -### RENDERING ### - -viewer.makeModel = (fold) -> - m = {vs: null, fs: null, es: {}} - m.vs = ({i: i, cs: cs} for cs, i in fold.vertices_coords) - (m.vs[i].cs[2] = 0 for v, i in m.vs when v.cs.length is 2) - m.fs = ({i: i, vs: (m.vs[v] for v in vs)} for vs, i in fold.faces_vertices) - if fold.edges_vertices? - for v, i in fold.edges_vertices - [a,b] = if v[0] > v[1] then [v[1],v[0]] else [v[0],v[1]] - as = if fold.edges_assignment?[i]? then fold.edges_assignment[i] else 'U' - m.es["e#{a}e#{b}"] = { - v1: m.vs[a], v2: m.vs[b], as: as} - else - for f, i in m.fs - for v, j in f.vs - w = f.vs[geom.next(j,f.vs.length)] - [a,b] = if v.i > w.i then [w,v] else [v,w] - m.es["e#{a.i}e#{b.i}"] = {v1: a, v2: b, as: 'U'} - for f, i in m.fs - m.fs[i].n = geom.polygonNormal(v.cs for v in f.vs) - m.fs[i].c = geom.centroid(v.cs for v in f.vs) - m.fs[i].es = {} - m.fs[i].es = (for v, j in f.vs - w = f.vs[geom.next(j, f.vs.length)] - [a,b] = if v.i > w.i then [w,v] else [v,w] - edge = m.es["e#{a.i}e#{b.i}"] - unless edge? - edge = {v1: a, v2: b, as: 'U'} - edge) - m.fs[i].ord = {} - if fold.faceOrders? - for [f1, f2, o] in fold.faceOrders when o isnt 0 - if geom.parallel(m.fs[f1].n, m.fs[f2].n) - normRel = if geom.dot(m.fs[f1].n, m.fs[f2].n) > 0 then 1 else -1 - if m.fs[f1].ord["f#{f2}"]? - console.log "Warning: duplicate ordering input information for \ - faces #{f1} and #{f2}. Using first found in the faceOrder list." - if m.fs[f1].ord["f#{f2}"] != o - console.log "Error: duplicate ordering [#{f1},#{f2},#{o}] \ - is inconsistent with a previous entry." - else - m.fs[f1].ord["f#{f2}"] = o - m.fs[f2].ord["f#{f1}"] = -o * normRel - else - console.log "Warning: order for non-parallel faces [#{f1},#{f2}]" - return m - -viewer.faceAbove = (f1, f2, n) -> - [p1, p2] = ((v.ps for v in f.vs) for f in [f1,f2]) - sepDir = geom.separatingDirection2D(p1, p2, [0,0,1]) - if sepDir? # projections do not overlap - return null - [v1,v2] = ((v.cs for v in f.vs) for f in [f1,f2]) - basis = geom.basis(v1.concat(v2)) - if basis.length is 3 - dir = geom.separatingDirection3D(v1, v2) - if dir? - return 0 > geom.dot(n, dir) # faces are separable in 3D - else - console.log "Warning: faces #{f1.i} and #{f2.i} properly intersect. - Ordering is unresolved." - if basis.length is 2 - ord = f1.ord["f#{f2.i}"] - if ord? - return 0 > geom.dot(f2.n, n) * ord # faces coplanar and have order - return null - -viewer.orderFaces = (view) -> - faces = view.model.fs - direction = geom.mul(view.cam.z, -1) - (f.children = [] for f in faces) - for f1, i in faces - for f2, j in faces when i < j - f1_above = viewer.faceAbove(f1, f2, direction) - if f1_above? - [p,c] = if f1_above then [f1,f2] else [f2,f1] - p.children = p.children.concat([c]) - view.model.fs = geom.topologicalSort(faces) - f.g.parentNode.removeChild(f.g) for f in view.model.fs - view.svg.appendChild(f.g) for f in view.model.fs - -viewer.draw = ({svg: svg, cam: cam, model: model}) -> - svg.innerHTML = '' - style = viewer.appendSVG(svg, 'style') - for k, v of STYLES - style.innerHTML += ".#{k}{#{v}}\n" - min = ((v.cs[i] for v in model.vs).reduce(geom.min) for i in [0,1,2]) - max = ((v.cs[i] for v in model.vs).reduce(geom.max) for i in [0,1,2]) - cam.c = geom.mul(geom.plus(min, max), 0.5) - cam.r = geom.mag(geom.sub(max, min)) / 2 * 1.05 - c = viewer.proj(cam.c, cam) - viewer.setAttrs(svg, {viewBox: "-1,-1,2,2"}) - t = "translate(0,0.01)" - for f, i in model.fs - f.g = viewer.appendSVG(svg, 'g') - f.path = viewer.appendSVG(f.g, 'path') - f.text = viewer.appendSVG(f.g, 'text', {class: 'text', transform: t}) - f.text.innerHTML = "f#{f.i}" - f.eg = [] - for e, j in f.es - f.eg[j] = viewer.appendSVG(f.g, 'path') - f.vg = [] - for v, j in f.vs - f.vg[j] = viewer.appendSVG(f.g, 'g') - f.vg[j].path = viewer.appendSVG(f.vg[j], 'circle', {class: 'vert'}) - f.vg[j].text = viewer.appendSVG(f.vg[j], 'text', - {transform: 'translate(0, 0.01)', class: 'text'}) - f.vg[j].text.innerHTML = "#{v.i}" - cam.axis = viewer.appendSVG(svg,'g',{transform: 'translate(-0.9,-0.9)'}) - for c in ['x','y','z'] - cam.axis[c] = viewer.appendSVG(cam.axis,'path', { - id: "a#{c}", class: "a#{c} axis"}) - -viewer.update = (view) -> - {model: model, cam: cam, svg: svg} = view - (model.vs[i].ps = viewer.proj(v.cs, cam) for v, i in model.vs) - (model.fs[i].c2 = viewer.proj(f.c, cam) for f, i in model.fs) - viewer.orderFaces(view) - show = {} - for k, v of cam.show - show[k] = if v then 'visible' else 'hidden' - for f, i in model.fs when f.path? - visibleSide = if geom.dot(f.n, cam.z) > 0 then 'top' else 'bot' - viewer.setAttrs(f.text, { - x: f.c2[0], y: f.c2[1], visibility: show['Face Text']}) - viewer.setAttrs(f.path, { - d: viewer.makePath(v.ps for v in f.vs) + 'Z' - visibility: show['Faces'], class: "face #{visibleSide}"}) - for e, j in f.es - viewer.setAttrs(f.eg[j], { - d: viewer.makePath([e.v1.ps, e.v2.ps]) - visibility: show['Edges'], class: "edge #{e.as}"}) - for v, j in f.vs - viewer.setAttrs(f.vg[j], {visibility: show['Vertices']}) - viewer.setAttrs(f.vg[j].path, {cx: v.ps[0], cy: v.ps[1]}) - viewer.setAttrs(f.vg[j].text, {x: v.ps[0], y: v.ps[1]}) - for c, v of {x: [1,0,0], y: [0,1,0], z: [0,0,1]} - end = geom.plus(geom.mul(v, 0.05 * cam.r), cam.c) - viewer.setAttrs(cam.axis[c], { - d: viewer.makePath(viewer.proj(p, cam) for p in [cam.c, end])}) - diff --git a/test/geom.coffee b/test/geom.test.civet similarity index 90% rename from test/geom.coffee rename to test/geom.test.civet index 132c859..bbfcb5c 100644 --- a/test/geom.coffee +++ b/test/geom.test.civet @@ -1,11 +1,11 @@ -FOLD = require '..' -{geom} = FOLD +{ geom } from '../src/index.civet' +{ expect, test, describe } from 'vitest' +{ toBeDeepCloseTo } from 'jest-matcher-deep-close-to' -{toBeDeepCloseTo,toMatchCloseTo} = require 'jest-matcher-deep-close-to'; -expect.extend {toBeDeepCloseTo, toMatchCloseTo} +expect.extend {toBeDeepCloseTo} describe 'Utilities', -> - array = [-3,-2,-1,0,1,2,3,14,-5,6,7] + array := [-3,-2,-1,0,1,2,3,14,-5,6,7] test 'Reducing with geom.sum', -> expect array.reduce geom.sum .toEqual 22 @@ -38,16 +38,16 @@ describe 'Utilities', -> expect geom.rangesDisjoint([4.5,1],[4.5,8]) .toEqual false - #TODO: geom.topologicalSort + // TODO: geom.topologicalSort undefined describe 'Vector Operations', -> - a = [1,0,0,0] - b = [1,1,1,1] - c = [0,3,2,4] - d = [0,0,0,0] - e = [0,1.5,1,2] + a := [1,0,0,0] + b := [1,1,1,1] + c := [0,3,2,4] + d := [0,0,0,0] + e := [0,1.5,1,2] test 'geom.magsq', -> expect geom.magsq a @@ -225,8 +225,8 @@ describe 'Vector Operations', -> undefined test 'geom.rotate', -> - pi8 = Math.PI / 8 - pi3 = Math.PI / 3 + pi8 := Math.PI / 8 + pi3 := Math.PI / 3 expect geom.rotate(a,c,0) .toBeDeepCloseTo [1,0,0] expect geom.rotate(a,c,pi8) @@ -278,17 +278,17 @@ describe 'Vector Operations', -> describe 'Matrix Transformations', -> test 'geom.matrixVector', -> - expect geom.matrixVector [[1, 2], [4, 5]], [6] # implicit 0 + expect geom.matrixVector [[1, 2], [4, 5]], [6] // implicit 0 .toEqual [6, 24] expect geom.matrixVector [[1, 2], [4, 5]], [6, 7] .toEqual [20, 59] - expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7], 0 # implicit 0 + expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7], 0 // implicit 0 .toEqual [20, 59] - expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7] # implicit 1 + expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7] // implicit 1 .toEqual [23, 65] - expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7], 1 # implicit 1 + expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6, 7], 1 // implicit 1 .toEqual [23, 65] - expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6] # implicit 0,1 + expect geom.matrixVector [[1, 2, 3], [4, 5, 6]], [6] // implicit 0,1 .toEqual [9, 30] test 'geom.matrixMatrix', -> @@ -301,15 +301,15 @@ describe 'Matrix Transformations', -> expect geom.matrixMatrix [[1,2], [3,4]], [[1,2], [3,4]] .toEqual [[7,10], [15,22]] expect geom.matrixMatrix [[1,2,5], [3,4,6]], [[1,2], [3,4]] - .toEqual [[7,10,5], [15,22,6]] # implicit row 0,0,1 + .toEqual [[7,10,5], [15,22,6]] // implicit row 0,0,1 expect geom.matrixMatrix [[1,2,5], [3,4,6]], [[1,2], [3,4], [1,1]] .toEqual [[12,15], [21,28]] expect geom.matrixMatrix [[1,2,5], [3,4,6]], [[1,2,5], [3,4,6]] - .toEqual [[7,10,22], [15,22,45]] # implicit row 0,0,1 + .toEqual [[7,10,22], [15,22,45]] // implicit row 0,0,1 test 'geom.matrixMatrix equivalence to geom.matrixVector', -> - m1 = geom.matrixRotate2D Math.PI/3 # 2x2 - m2 = geom.matrixTranslate [1,2] # 2x3 + m1 := geom.matrixRotate2D Math.PI/3 // 2x2 + m2 := geom.matrixTranslate [1,2] // 2x3 expect geom.matrixVector geom.matrixMatrix(m1, m2), [5,9] .toBeDeepCloseTo geom.matrixVector m1, geom.matrixVector m2, [5,9] expect geom.matrixVector geom.matrixMatrix(m2, m1), [5,9] @@ -332,7 +332,7 @@ describe 'Matrix Transformations', -> .toBeDeepCloseTo [[1,0,-2], [0,1,-3]] expect geom.matrixInverse [[0,-1,6], [1,0,2]] .toBeDeepCloseTo [[0,1,-2], [-1,0,6]] - matrix = [[1,2,3],[7,8,9]] + matrix := [[1,2,3],[7,8,9]] expect geom.matrixInverse matrix .toBeDeepCloseTo [[-4/3,1/3,1], [7/6,-1/6,-2]] expect geom.matrixMatrix matrix, geom.matrixInverse matrix @@ -355,18 +355,18 @@ describe 'Matrix Transformations', -> .toBeDeepCloseTo [[-1,0], [0,-1]] expect geom.matrixRotate2D Math.PI, [1,2] .toBeDeepCloseTo [[-1,0,2], [0,-1,4]] - rot180 = geom.matrixRotate2D Math.PI, [1,2] + rot180 := geom.matrixRotate2D Math.PI, [1,2] expect geom.matrixMatrix rot180, rot180 .toBeDeepCloseTo [[1,0,0], [0,1,0]] expect geom.matrixRotate2D Math.PI/2 .toBeDeepCloseTo [[0,-1], [1,0]] expect geom.matrixRotate2D Math.PI/2, [1,2] .toBeDeepCloseTo [[0,-1,3], [1,0,1]] - rot90 = geom.matrixRotate2D Math.PI/2, [1,2] + rot90 := geom.matrixRotate2D Math.PI/2, [1,2] expect geom.matrixMatrix rot90, rot90 .toBeDeepCloseTo rot180 expect geom.matrixVector geom.matrixRotate2D(Math.PI/2), [1,0] - .toBeDeepCloseTo [0,1] # counterclockwise test + .toBeDeepCloseTo [0,1] // counterclockwise test test 'geom.matrixReflectAxis', -> expect geom.matrixReflectAxis 0, 2 @@ -395,7 +395,7 @@ describe 'Matrix Transformations', -> .toEqual [-2, 2] expect geom.matrixVector geom.matrixReflectLine([7, 12], [-13, 4]), [1, 2] .toBeDeepCloseTo [-4.241379310344826, 15.103448275862071] - matrix = geom.matrixReflectLine([7, 12], [-13, 4]) + matrix := geom.matrixReflectLine([7, 12], [-13, 4]) expect geom.matrixVector matrix, geom.matrixVector matrix, [1, 2] .toBeDeepCloseTo [1, 2] expect geom.matrixMatrix matrix, matrix @@ -426,12 +426,12 @@ describe 'Polygon Operations', -> undefined describe '3D Triangle Operations', -> - ts = [ + ts := [ [[1,0,0], [0,1,0], [0,0,1]] [[2,0,1], [1,1,1], [-1,-1,0]] [[3,4,0], [-1,2,-3], [0,3,2]] [[0,0,0], [1,0,0], [2,0,0]] - ] + ] as [number[], number[], number[]][] test 'geom.triangleNormal', -> expect geom.triangleNormal ...ts[0] @@ -446,7 +446,7 @@ describe 'Polygon Operations', -> undefined describe '2D Polygon Operations', -> - ts = [ + ts := [ [[5,9],[2,7],[1,7]] [[0,3],[5,8],[3,1]] [[4.5,1],[4.5,8],[2,5]] @@ -499,12 +499,12 @@ describe 'Polygon Operations', -> undefined - #TODO: segmentsCross(), parametricLineIntersect(), \ - # segmentLineIntersect(), lineIntersectLine(), and pointStrictlyInSegment() + // TODO: segmentsCross(), parametricLineIntersect(), \ + // segmentLineIntersect(), lineIntersectLine(), and pointStrictlyInSegment() describe 'General Dimension Point Operations', -> - ps1 = [[0,0,0],[0,0,1],[1,0,0],[0,1,0],[1,1,0],[1,0,1],[0,1,1],[1,1,1]] - ps2 = [[0,0,0],[1,1,0],[0,1,1]] + ps1 := [[0,0,0],[0,0,1],[1,0,0],[0,1,0],[1,1,0],[1,0,1],[0,1,1],[1,1,1]] + ps2 := [[0,0,0],[1,1,0],[0,1,1]] test 'geom.centroid', -> expect geom.centroid(ps1) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..16238cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": [ + "src" + ], + "exclude": [ + "dist" + ], + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "moduleResolution": "nodenext", + "resolveJsonModule": true, + "target": "ESNext", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "jsx": "preserve" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..830bfe3 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vite'; +import civetPlugin from '@danielx/civet/vite'; +import { resolve } from 'path'; +import { builtinModules as nodeBuiltins } from 'module'; + +export default defineConfig({ + base: '/fold/', + build: { + watch: { + include: ['**/*.civet'], + }, + lib: { + entry: [ + 'index', + 'convert', + 'file', + 'filter', + 'viewer', + 'oripa', + 'geom', + ].map(name => resolve(__dirname, `src/${name}.civet`)), + name: 'Fold', + formats: ['es', 'cjs'], + }, + rollupOptions: { + // exclude node internals + external: nodeBuiltins, + }, + }, + test: { + include: ['test/**/*.test.civet'], + }, + plugins: [ + civetPlugin({ + ts: 'esbuild', + }), + ], +}); diff --git a/vitest.d.ts b/vitest.d.ts new file mode 100644 index 0000000..f601e3b --- /dev/null +++ b/vitest.d.ts @@ -0,0 +1,15 @@ +import type { Assertion, AsymmetricMatchersContaining } from 'vitest'; +import type { Iterable } from 'jest-matcher-deep-close-to/lib/types'; + +interface CustomMatchers { + toBeDeepCloseTo( + received: Iterable, + expected: Iterable, + precision?: number + ): R; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +}