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..3122c1d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ package-lock.json # Mac .DS_Store + +dist/* 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..8255d33 --- /dev/null +++ b/examples/cube-folded.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, 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,4,5,2], + [1,2,13,12], + [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/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/examples/vertex.fold b/examples/vertex.fold new file mode 100644 index 0000000..815270c --- /dev/null +++ b/examples/vertex.fold @@ -0,0 +1,67 @@ +{ + "file_spec": 1, + "file_creator": "A text editor", + "file_author": "Mokshit Jain", + "file_classes": ["singleModel"], + "frame_title": "3D Cube", + "frame_classes": ["creasePattern"], + "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] + ], + "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/index.html b/index.html new file mode 100644 index 0000000..bc4a7ab --- /dev/null +++ b/index.html @@ -0,0 +1,25 @@ + + + + Fold Viewer + + +

FOLD Viewer

+
+

(C) Jason S. Ku 2016

+ + + diff --git a/package.json b/package.json index 4cf9a26..17c41e6 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", @@ -39,22 +68,12 @@ }, "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.42", + "@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" diff --git a/src/convert.civet b/src/convert.civet new file mode 100644 index 0000000..d55a5a5 --- /dev/null +++ b/src/convert.civet @@ -0,0 +1,553 @@ +* as geom from "./geom.civet" +* as filter from "./filter.civet" +type { Coords, Fold, Converter } from "./types.civet" + +export function edges_vertices_to_vertices_vertices_unsorted(fold: 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 + +export function edges_vertices_to_vertices_edges_unsorted(fold: 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 + +export function edges_vertices_to_vertices_vertices_sorted(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. + ### + edges_vertices_to_vertices_vertices_unsorted fold + sort_vertices_vertices fold + +export function edges_vertices_to_vertices_edges_sorted(fold: 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. + ### + edges_vertices_to_vertices_vertices_sorted fold + vertices_vertices_to_vertices_edges fold + +export function sort_vertices_vertices(fold: 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" + for neighbors, v of fold.vertices_vertices + geom.sortByAngle neighbors, fold.vertices_vertices[v], (x) => fold.vertices_coords[x] + fold + +export function vertices_vertices_to_faces_vertices(fold: Fold) + ### + Given a FOLD object with counterclockwise-sorted `vertices_vertices` + property, constructs the implicitly defined faces, setting `faces_vertices` + property. + ### + next: Record := {} + for neighbors, v of fold.vertices_vertices + for u, i of neighbors + next[`${u},${v}`] = neighbors[(i-1) %% neighbors.length] + + fold.faces_vertices = [] + for uv of (key for key in 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 of face)) > 0 + fold.faces_vertices.push face + fold + +export function vertices_edges_to_faces_vertices_edges(fold: 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: Array> := [] + for neighbors, v of fold.vertices_edges + next[v] = {} + for e, i of neighbors + next[v][e] = neighbors[(i-1) %% neighbors.length] + + fold.faces_vertices = [] + fold.faces_edges = [] + + for nexts, vertex of next + for e1_str, let e2 in nexts + continue unless e2? + e1 .= parseInt e1_str + 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 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 x in vertices) > 0 + // fold.faces_vertices.push vertices + // fold.faces_edges.push edges + fold + +export function edges_vertices_to_faces_vertices(fold: 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. + ### + edges_vertices_to_vertices_vertices_sorted fold + vertices_vertices_to_faces_vertices fold + +export function edges_vertices_to_faces_vertices_edges(fold: 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. + ### + edges_vertices_to_vertices_edges_sorted fold + vertices_edges_to_faces_vertices_edges fold + +export function vertices_vertices_to_vertices_edges(fold: Fold): number[][] + ### + Given a FOLD object with `vertices_vertices` and `edges_vertices`, + fills in the corresponding `vertices_edges` property (preserving order). + ### + edgeMap: Record := {} + + for [v1, v2], edge of fold.edges_vertices + edgeMap[`${v1},${v2}`] = edge + edgeMap[`${v2},${v1}`] = edge + fold.vertices_edges = + for vertices, vertex of fold.vertices_vertices + for i of [0...vertices.length] + edgeMap[`${vertex},${vertices[i]}`] + +export function faces_vertices_to_faces_edges(fold: Fold): number[][] + ### + Given a FOLD object with `faces_vertices` and `edges_vertices`, + fills in the corresponding `faces_edges` property (preserving order). + ### + edgeMap: Record := {} + for [v1, v2], edge of fold.edges_vertices + edgeMap[`${v1},${v2}`] = edge + edgeMap[`${v2},${v1}`] = edge + fold.faces_edges = + for vertices, face of fold.faces_vertices + for i of [0...vertices.length] + edgeMap[`${vertices[i]},${vertices[(i+1) % vertices.length]}`] + +export function faces_vertices_to_edges(mesh: 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'). + 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: Record := {} + + for face_key, vertices in mesh.faces_vertices + face := parseInt face_key + mesh.faces_edges.push( + for v1_key, i of vertices + v1 := parseInt v1_key + v2 := vertices[(i+1) % vertices.length] + + key: string .= "" + if v1 <= v2 + key = `${v1},${v2}` + else + key = `${v2},${v1}` + + edge: number .= null + 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 + edge + ) + mesh + +export function edges_vertices_to_edges_faces_edges(fold: 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 of [0...fold.edges_vertices.length]) + edgeMap: Record := {} + + for edge_key, vertices in fold.edges_vertices + continue unless vertices? + edge := parseInt edge_key + edgeMap[`${vertices[0]},${vertices[1]}`] = [edge, 0] // forward + edgeMap[`${vertices[1]},${vertices[0]}`] = [edge, 1] // backward + + fold.faces_edges ?= [] + for each vertices, face of fold.faces_vertices + fold.faces_edges[face] = + for 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 unfoldedGeometry(fold: Fold, rootFace = 0) + console.log('works') + if fold.vertices_coords? and fold.edges_vertices? and not (fold.edges_faces? and fold.faces_edges?) + edges_vertices_to_edges_faces_edges fold + + maxError .= 0 + level .= [rootFace] + I := [[1,0,0],[0,1,0],[0,0,1]] + fold.faces_unfoldTransform = [] + fold.faces_unfoldTransform[rootFace] = I + fold.vertices_unfoldCoords = 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 edge of fold.faces_edges[rootFace] + for vertex of fold.edges_vertices[edge] + fold.vertices_unfoldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for face of level + for edge of fold.faces_edges[face] + for face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + + crease_edges_vertices := fold.edges_vertices[edge] + crease_coords := (for vertex of crease_edges_vertices + fold.vertices_coords[vertex] + ) as [Coords, Coords] + + transform := geom.matrixMatrix fold.faces_unfoldTransform[face], geom.matrixMatrix( + geom.matrixMatrix( + geom.matrixTranslate(crease_coords[1]) + geom.matrixRotate3D(...crease_coords, -geom.toRadian(fold.edges_foldAngle[edge])) + ), + geom.matrixTranslate(geom.mul crease_coords[1], -1) + ) + + if fold.faces_unfoldTransform[face2]? + for row, i of fold.faces_unfoldTransform[face2] + maxError = Math.max maxError, geom.dist row, transform[i] + else + fold.faces_unfoldTransform[face2] = transform + + for edge2 of fold.faces_edges[face2] + for vertex2 of fold.edges_vertices[edge2] + mapped := geom.matrixVector transform, fold.vertices_coords[vertex2] + + if fold.vertices_unfoldCoords[vertex2]? + maxError = Math.max maxError, + geom.dist fold.vertices_unfoldCoords[vertex2], mapped + else + fold.vertices_unfoldCoords[vertex2] = mapped + nextLevel.push face2 + level = nextLevel + maxError + +export function foldedGeometry(fold: Fold, rootFace = 0) + if fold.vertices_coords? and fold.edges_vertices? and not (fold.edges_faces? and fold.faces_edges?) + edges_vertices_to_edges_faces_edges fold + + maxError .= 0 + level .= [rootFace] + I := [[1,0,0],[0,1,0],[0,0,1]] + fold.faces_foldTransform = [] + fold.faces_foldTransform[rootFace] = I + 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 edge of fold.faces_edges[rootFace] + for vertex of fold.edges_vertices[edge] + fold.vertices_foldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for face of level + for edge of fold.faces_edges[face] + for face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + + crease_edges_vertices := fold.edges_vertices[edge] + crease_coords := (for 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(fold.edges_foldAngle[edge])) + ), + geom.matrixTranslate(geom.mul crease_coords[1], -1) + ) + + if fold.faces_foldTransform[face2]? + for row, i of fold.faces_foldTransform[face2] + maxError = Math.max maxError, geom.dist row, transform[i] + else + fold.faces_foldTransform[face2] = transform + + for edge2 of fold.faces_edges[face2] + for vertex2 of fold.edges_vertices[edge2] + mapped := geom.matrixVector transform, fold.vertices_coords[vertex2] + + if fold.vertices_foldCoords[vertex2]? + maxError = Math.max maxError, + geom.dist fold.vertices_foldCoords[vertex2], mapped + else + fold.vertices_foldCoords[vertex2] = mapped + nextLevel.push face2 + level = nextLevel + maxError + +export function flatUnfoldedGeometry(fold: Fold, rootFace = 0) + if fold.vertices_coords? and fold.edges_vertices? and not (fold.edges_faces? and fold.faces_edges?) + edges_vertices_to_edges_faces_edges fold + + maxError .= 0 + level .= [rootFace] + fold.faces_flatUnfoldTransform = Array(fold.faces_edges.length).fill(null) + fold.faces_flatUnfoldTransform[rootFace] = [[1,0,0],[0,1,0]] // identity + fold.faces_flatUnfoldOrientation = Array(fold.faces_edges.length).fill(null) + fold.faces_flatUnfoldOrientation[rootFace] = +1 + fold.vertices_flatUnfoldCoords = 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 edge of fold.faces_edges[rootFace] + for vertex of fold.edges_vertices[edge] + fold.vertices_flatUnfoldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for face of level + orientation := -fold.faces_flatUnfoldOrientation[face] + + for edge of fold.faces_edges[face] + for face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + transform := geom.matrixMatrix fold.faces_flatUnfoldTransform[face], + geom.matrixReflectLine(...( + for vertex of fold.edges_vertices[edge] + fold.vertices_coords[vertex] + ) as [Coords, Coords]) + + if fold.faces_flatUnfoldTransform[face2]? + for row, i of fold.faces_flatUnfoldTransform[face2] + maxError = Math.max maxError, geom.dist row, transform[i] + if orientation != fold.faces_flatUnfoldOrientation[face2] + maxError = Math.max 1, maxError + else + fold.faces_flatUnfoldTransform[face2] = transform + fold.faces_flatUnfoldOrientation[face2] = orientation + + for edge2 of fold.faces_edges[face2] + for vertex2 of fold.edges_vertices[edge2] + mapped := geom.matrixVector transform, fold.vertices_coords[vertex2] + if fold.vertices_flatUnfoldCoords[vertex2]? + maxError = Math.max maxError, + geom.dist fold.vertices_flatUnfoldCoords[vertex2], mapped + else + fold.vertices_flatUnfoldCoords[vertex2] = mapped + nextLevel.push face2 + level = nextLevel + maxError + +export function flatFoldedGeometry(fold: 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?) + edges_vertices_to_edges_faces_edges fold + + 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 edge of fold.faces_edges[rootFace] + for vertex of fold.edges_vertices[edge] + fold.vertices_flatFoldCoords[vertex] ?= fold.vertices_coords[vertex][..] + + while level.length + nextLevel := [] + + for face of level + orientation := -fold.faces_flatFoldOrientation[face] + + for edge of fold.faces_edges[face] + for face2 of fold.edges_faces[edge] + continue unless face2? and face2 != face + transform := geom.matrixMatrix fold.faces_flatFoldTransform[face], + geom.matrixReflectLine(...( + for vertex of fold.edges_vertices[edge] + fold.vertices_coords[vertex] + ) as [Coords, Coords]) + + if fold.faces_flatFoldTransform[face2]? + for 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 edge2 of fold.faces_edges[face2] + for vertex2 of 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 + +export function deepCopy(fold: T): T + // 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 of fold + deepCopy item + ) as T + else // Object + copy := {} as T + for key, value in fold as any + copy[key] = deepCopy value + copy + +export function toJSON(fold: Fold) + // Convert FOLD object into a nicely formatted JSON string. + "{\n" + + (for key, value in fold + ` ${JSON.stringify key}: ` + + if Array.isArray value + "[\n" + + (` ${JSON.stringify(obj)}` for 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) + if fromExt == toExt + (x: any) -> x + else + converters[`${fromExt}${toExt}`] + +export function setConverter(fromExt: string, toExt: string, converter: Converter) + extensions[fromExt] = true + extensions[toExt] = true + converters[`${fromExt}${toExt}`] = converter + +export function convertFromTo(data: T, fromExt: string, toExt: string) + fromExt = `.${fromExt}` unless fromExt[0] == '.' + toExt = `.${toExt}` unless toExt[0] == '.' + converter := getConverter fromExt, toExt + + unless converter? + if fromExt == toExt + return data + throw new Error `No converter from ${fromExt} to ${toExt}` + + converter data + +export function convertFrom(data: T, fromExt: string) + convertFromTo data, fromExt, '.fold' + +export function convertTo(data: T, toExt: string) + 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..75cb1eb --- /dev/null +++ b/src/file.civet @@ -0,0 +1,125 @@ +fs from fs +path from path +* as convert from "./convert.civet" +type { Fold, Converter } from "./types.civet" + +interface Options + flatFold?: boolean + converter?: Converter + fold?: boolean + +export function extensionOf(filename: string) + 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 = null) + 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 + + fs.writeFileSync output, result, 'utf-8' + +export function fileToFile(input: string, output: string, options: Options) + 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 .= fs.readFileSync input, 'utf-8' + if inFormat == '.fold' != outFormat // avoid double mogrification + result = mogrify result, options + result = converter result + if outFormat == '.fold' + result = mogrify result, options + if typeof result != 'string' + result = convert.toJSON result + fs.writeFileSync output, result, 'utf-8' + +export function mogrify(data: string, options: Options) + 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 + 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 + fold.frame_classes = fold.frame_classes.filter (x) -> x != 'creasePattern' + + delete fold.vertices_flatFoldCoords + convert.toJSON fold + +export function main(args = process.argv[2..]): void + filenames := [] + output .= '.fold' // Default behavior: convert to .fold + options: Options := + flatFold: false + fold: false + 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 + else + filenames.push arg + + for filename of filenames + fileToFile filename, output, options + +main() if require.main == module 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..ce43159 --- /dev/null +++ b/src/geom.civet @@ -0,0 +1,533 @@ +// BASIC GEOMETRY + +type { Coords } from "./types.civet" + +// Utilities + +export EPS := 0.000001 + +export function sum(a: number, b: number) + a + b + +export function min(a: number, b: number) + if a < b then a else b + +export function max(a: number, b: number) + if a > b then a else b + +export function all(a: boolean, b: boolean) + a and b + +export function next(start: number, n: number, i = 1) + // Returns the ith cyclic ordered number after start in the range [0..n]. + (start + i) %% n + +export function rangesDisjoint([a1, a2]: [number, number], [b1, b2]: [number, number]) + // Returns whether the scalar interval [a1, a2] is disjoint from the scalar + // interval [b1,b2]. + (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[]) + [v.visited, v.parent] = [false, null] for v of vs + list: Tree[] .= [] + for v of vs + continue if v.visited + list = visit(v, list) + list + +export function visit(v: Tree, list: Tree[]) + v.visited = true + for u of v.children + continue if u.visited + u.parent = v + list = visit(u, list) + [...list, v] + +export function magsq(a: Coords) + // Returns the squared magnitude of vector a having arbitrary dimension. + dot a, a + +export function mag(a: Coords) + // Returns the magnitude of vector a having arbitrary dimension. + Math.sqrt magsq a + +export function unit(a: Coords, eps = EPS) + // Returns the unit vector in the direction of vector a having arbitrary + // dimension. Returns null if magnitude of a is zero. + length := magsq a + return null if length < eps + mul(a, 1 / mag(a)) + +export function ang2D(a: Coords, eps = EPS) + // Returns the angle of a 2D vector relative to the standard + // east-is-0-degrees rule. + return null if magsq(a) < eps + Math.atan2(a[1], a[0]) + +export function mul(a: Coords, s: number): Coords + // Returns the vector a multiplied by scaler factor s. + (i * s for i of a) + +export function linearInterpolate(t: number, a: Coords, b: Coords): Coords + // Returns linear interpolation of vector a to vector b for 0 < t < 1 + plus mul(a, 1 - t), mul(b, t) + +export function plus(a: Coords, b: Coords): Coords + // Returns the vector sum between of vectors a and b having the same + // dimension. + (ai + b[i] for each ai, i of a) + +export function sub(a: Coords, b: Coords): Coords + // Returns the vector difference of vectors a and b having the same dimension. + plus a, mul(b, -1) + +export function dot(a: Coords, b: Coords): number + // Returns the dot product between two vectors a and b having the same + // dimension. + (ai * b[i] for each ai, i of a).reduce(sum) + +export function distsq(a: Coords, b: Coords) + // Returns the squared Euclidean distance between two vectors a and b having + // the same dimension. + magsq sub(a, b) + +export function dist(a: Coords, b: Coords) + // Returns the Euclidean distance between general vectors a and b having the + // same dimension. + Math.sqrt distsq(a, b) + +export function closestIndex(a: Coords, bs: Coords[]) + // 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 + let minI: number + for each b, i of bs + d := dist(a, b) + if minDist > d + minDist = d + minI = i + minI + +export function dir(a: Coords, b: Coords) + // Returns a unit vector in the direction from vector a to vector b, in the + // same dimension as a and b. + unit sub(b, a) + +export function ang(a: Coords, b: Coords) + // Returns the angle spanned by vectors a and b having the same dimension. + [ua, ub] := [unit(a), unit(b)] + return null unless ua? and ub? + Math.acos dot(ua, ub) + +export function cross(a: Coords, b: Coords): number | Coords | null + // 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] of [[1, 2], [2, 0], [0, 1]]) + return null + +export function parallel(a: Coords, b: Coords, eps = EPS) + // Return if vectors are parallel, up to accuracy eps + [ua, ub] := [unit(a), unit(b)] + return null unless ua? and ub? + 1 - Math.abs(dot ua, ub) < eps + +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] + ] + +export function rotate(a: Coords, u: Coords, t: number) + // Returns the rotation of 3D vector a about 3D unit vector u by angle t. + u = unit(u)! + + return null unless u? + + [ct, st] := [Math.cos(t), Math.sin(t)] + (for 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) + ) + +export function reflectPoint(p: Coords, q: Coords) + // Reflect point p through the point q into the "symmetric point" + sub mul(q, 2), p + +export function reflectLine(p: Coords, a: Coords, b: Coords) + // 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 := 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`. +### + +export function matrixVector(matrix: Coords[], vector: Coords, 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 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 + +export function matrixMatrix(matrix1: Coords[], matrix2: Coords[]) + // 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 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 Number(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 + +export function matrixInverseRT(matrix: Coords[]) + // 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]. + let lastCol: Coords + 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 + +export function matrixInverse(matrix: Coords[]) + // 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 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 unless 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 + +export function matrixTranslate(v: Coords) + // Transformation matrix for translating by given vector v. + // Works in any dimension, assuming v.length is that dimension. + for each x, i of v + row := + for j of [0...v.length] + Number(i == j) + row.push x + row + +export function matrixRotate2D(t: number, center?: Coords) + // 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]] + +export function matrixReflectAxis(a: number, d: number, center?: number) + // Matrix transformation negating dimension a out of d dimensions, + // or if center is specified, reflecting around that value of dimension a. + 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 + +export function matrixReflectLine(a: Coords, b: Coords) + // Matrix transformation implementing 2D reflectLine(*, a, b) + 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 +### + +export function interiorAngle(a: Coords, b: Coords, c: Coords) + // Computes the angle of three points that are, say, part of a triangle. + // Specify in counterclockwise order. + // a + // / + // / + // b/_)__ c + ang := ang2D(sub(a, b)) - ang2D(sub(c, b)) + ang + (if ang < 0 then 2*Math.PI else 0) + +export function turnAngle(a: Coords, b: Coords, c: Coords) + // Returns the turn angle, the supplement of the interior angle + Math.PI - interiorAngle(a, b, c) + +export function triangleNormal(a: Coords, b: Coords, c: Coords) + // Returns the right handed normal unit vector to triangle a, b, c in 3D. If + // the triangle is degenerate, returns null. + unit(cross(sub(b, a), sub(c, b)) as Coords) + +export function polygonNormal(points: Coords[], eps = EPS) + // Returns the right handed normal unit vector to the polygon defined by + // points in 3D. Assumes the points are planar. + return unit( + (for each p, i of points + cross(p, points[next(i, points.length)]) + ).reduce(plus), + eps + ) + +export function twiceSignedArea(points: Coords[]) + // 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 each v0, i of points + v1 := points[next(i, points.length)] + v0[0] * v1[1] - v1[0] * v0[1] + ).reduce(sum) + +export function polygonOrientation(points: Coords[]) + // 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 twiceSignedArea points + +export function sortByAngle(points: T[], origin?: number[], mapping: (x: T) => number[] = (x) => (x as any)) + if (!origin) origin = [0,0] + + // Sort a set of 2D points in place counter clockwise about origin + // under the provided mapping. + points.sort (p, q) => + pa := ang2D sub(mapping(p), origin) + qa := ang2D sub(mapping(q), origin) + pa - qa + +export function segmentsCross([p0, q0]: [Coords, Coords], [p1, q1]: [Coords, Coords]) + // 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 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]) + +export function parametricLineIntersect([p1, p2]: [Coords, Coords], [q1, q2]: [Coords, Coords]) + // 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] + +export function segmentIntersectSegment(s1: [Coords, Coords], s2: [Coords, Coords]) + [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]) + [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) + +export function basis(ps: Coords[], eps = 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 of ps).reduce(all)) + + ds: Coords[] := (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 ((parallel(d, x, eps) for d of ds).reduce(all)) + + ns := (unit(cross(d, x) as Coords) for d of ds).filter !!& + z := ns[0] + y := cross(z, x) + return [x, y] if ((parallel(n, z, eps) for n of ns).reduce(all)) + return [x, y, z] + +export function above(ps: Coords[], qs: Coords[], n: number[], eps = EPS) + [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) + // 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 + m := unit(cross(sub(p, q), n) as Coords) + if m? + sign := above(t1, t2, m, eps) + return mul(m, sign) if sign !== 0 + return null + +export function separatingDirection3D(t1: Coords[], t2: Coords[], eps = 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] 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) + 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) + +export function quadSplit(u: Coords, p: Coords, d: number, t: number) + // 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 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) + ang * Math.PI/180 + 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..3041e6e --- /dev/null +++ b/src/types.civet @@ -0,0 +1,36 @@ +export type Coords = number[] +export type Converter = (x: any) => any; + +export interface Fold + edges: Array<[number, number]> + edges_assignment: Array + faces: Array> + faces_vertices: Array> + faces_edges: Array> + faces_faces: Array> + vertices_edges: Array> + vertices_faces: Array> + vertices_coords: Coords[] + vertices_vertices: Array> + edges_vertices: [u: number, v: number][] + edges_faces: Array> + edges_foldAngle: Array + edges_length: Array + faces_flatFoldTransform: (number[][] | null)[] + faces_flatUnfoldTransform: (number[][] | null)[] + faces_foldTransform: (number[][] | null)[] + vertices_foldCoords: (number[] | null)[] + faces_unfoldTransform: (number[][] | null)[] + vertices_unfoldCoords: (number[] | null)[] + faces_flatFoldOrientation: (number | null)[] + vertices_flatFoldCoords: (number[] | null)[] + vertices_flatUnfoldCoords: Coords[] + faces_flatUnfoldOrientation: (number | null)[] + file_creator?: string + file_spec?: number + file_author?: string + file_classes?: string[] + frame_attributes?: string[] + frame_title?: string + frame_classes: any + faceOrders: any diff --git a/src/viewer.civet b/src/viewer.civet new file mode 100644 index 0000000..fb7a05f --- /dev/null +++ b/src/viewer.civet @@ -0,0 +1,410 @@ +* as geom from "./geom.civet" +type { Fold, Coords } from "./types.civet" + +interface FoldSVGProperties + path?: SVGElement + text?: SVGElement + +interface Vertex + cs: Coords + i: number + ps: Coords + vg?: SVGElement & FoldSVGProperties + +interface Edge + eg?: SVGElement + v1: Vertex + v2: Vertex + as: string + +interface Model + vs: Vertex[] + fs: Array<{ + vs?: Vertex[] + i?: number + n?: number[] + c?: Coords + ord?: Record + es?: Edge[] + g?: SVGElement + c2?: Coords + children: any[] + visited?: boolean + } & FoldSVGProperties> + es: Record + +interface ViewOptions + viewButtons: boolean + axisButtons: boolean + attrViewer: boolean + examples: any + import: boolean + export: boolean + properties: boolean + +interface Camera + 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 := { + 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)) as SVGElement + +export function makePath(coords: Coords[]) + (for c, i of coords + `${if (i is 0) then 'M' else 'L'} ${c[0]} ${c[1]} ` + ).reduce geom.sum + +### 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] + 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 + model: null + view.opts[k] = 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[`on${s}`] = (e) -> 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 = fold.faces_vertices ? ({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, f2, n) + [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]) + 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 + +export function orderFaces(view: View) + faces := view.model.fs + direction := geom.mul(view.cam.z, -1) + (f.children = [] for f of faces) + + for f1, i of faces + for f2, j of faces + continue unless i < j + f1_above := 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 of view.model.fs + view.svg.appendChild(f.g) for f of view.model.fs + +export function draw({ svg, cam, model }: { + svg: SVGElement, + cam: Camera, + model: Model +}) + 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 _, e in model.es + e.eg = appendSVG(svg, 'path') + + for v, i of model.vs + v.vg = appendSVG(svg, 'g') as any + v.vg.path = appendSVG(v.vg, 'circle', {class: 'vert'}) + v.vg.text = appendSVG(v.vg, 'text', {class: 'text', transform: t}) + v.vg.text.innerHTML = `${v.i}` + + 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}` + 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) + orderFaces(view) + show: Record := {} + + for k, v in cam.show + show[k] = if v then 'visible' else 'hidden' + + for _, e in model.es + setAttrs(e.eg, { + d: makePath([e.v1.ps, e.v2.ps]) + visibility: show['Edges'], class: `edge ${e.as}`}) + + for v, j of model.vs + setAttrs(v.vg, {visibility: show['Vertices']}) + setAttrs(v.vg.path, {cx: v.ps[0], cy: v.ps[1]}) + setAttrs(v.vg.text, {x: v.ps[0], y: v.ps[1]}) + + 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 c, v in {x: [1,0,0], y: [0,1,0], z: [0,0,1]} + 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..4d648ba 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..575995e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": [ + "src" + ], + "exclude": [ + "dist" + ], + "compilerOptions": { + "isolatedModules": true, + "esModuleInterop": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ESNext", + "strict": false, + "allowJs": true, + "skipLibCheck": true, + "jsx": "preserve" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ecf9596 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig } from 'vite'; +import civetPlugin from '@danielx/civet/vite'; +import { resolve } from 'path'; + +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: { + external: ['fs', 'path'], + }, + }, + test: { + include: ['test/**/*.test.civet'], + }, + plugins: [ + civetPlugin({ + dts: false, + }), + ], +}); 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 {} +}