diff --git a/CHANGELOG.md b/CHANGELOG.md index d851b77..5111c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v2.2.0 + +- make facet outline optional +- add lattice generation +- add facet generation + # v2.1.3 - revert levo/dextro bug-fix, it was correct before... diff --git a/README.md b/README.md index 16934ff..ce668cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This work implements Caspar-Klug Theory to generate high-quality, vectorized cap # Run -- Run democapsid (v2.1.3): [https://dnanto.github.io/democapsid/app.html](https://dnanto.github.io/democapsid/app.html). +- Run democapsid (v2.2.0): [https://dnanto.github.io/democapsid/app.html](https://dnanto.github.io/democapsid/app.html). ![screenshot.png](screenshot.png) diff --git a/app.html b/app.html index f1cec24..3d7a5b4 100644 --- a/app.html +++ b/app.html @@ -5,14 +5,14 @@ democapsid - +
- c= - + c=[ + + ]
- m= - + m=[ + + + + + ] +
+
+   [ + + ]
@@ -76,11 +86,13 @@
- fiber + features + +
aesthetics diff --git a/js/app.js b/js/app.js index 2151bfe..3532986 100644 --- a/js/app.js +++ b/js/app.js @@ -1,9 +1,16 @@ /*! - * democapsid v2.1.3 - Render viral capsids in the browser and export SVG. + * democapsid v2.2.0 - Render viral capsids in the browser and export SVG. * MIT License * Copyright (c) 2020 - 2024, Daniel Antonio Negrón (dnanto/remaindeer) */ +const DRAW_MODES = { + lattice: draw_lattice, + facets: draw_facets, + net: draw_net, + capsid: draw_capsid, +}; + const PARSERS = { color: (e) => e.value, alpha: (e) => Number(e.value).toString(16).padStart(2, "0"), @@ -30,8 +37,8 @@ const DEFAULTS = Object.assign( φ: (e) => parseFloat(e.value), }, Object.fromEntries([ - ...["net", "capsid"].map((e) => ["mode_" + e, PARSERS.toggle]), - ...["penton_fiber", "knob"].map((e) => [e + "_toggle", PARSERS.toggle]), + ...Object.keys(DRAW_MODES).map((e) => ["mode_" + e, PARSERS.toggle]), + ...["penton_fiber", "knob", "facet"].map((e) => [e + "_toggle", PARSERS.toggle]), ...["line", "fiber", "knob"].flatMap((e) => ["color", "alpha", "size"].map((f) => [e + "_" + f, PARSERS[f]])), ...["color", "alpha", "size", "length"].map((e) => ["fiber_" + e, PARSERS[e]]), ...["color", "alpha", "toggle"].flatMap((e) => Array.from({ length: 6 }, (_, i) => ["mer_" + e + "_" + (i + 1), PARSERS[e]])), @@ -61,15 +68,15 @@ function params_to_tag(PARAMS) { function download(e) { const PARAMS = params(); - const [mode, draw] = PARAMS.mode_capsid ? ["capsid", draw_capsid] : ["net", draw_net]; + const mode = Object.keys(DRAW_MODES).find((e) => PARAMS["mode_" + e]); + const draw = DRAW_MODES[mode]; let name = ["h", "k", "H", "K", "a", "R", "t", "s", "c"].map((k) => PARAMS[k]); name[7] = name[7].toFixed(2); name = mode + "[" + name.join("_") + "]"; const ext = e.target.id.split("_")[1]; + // only care about facet outline for SVG... + const g = draw(Object.assign({}, PARAMS, { facet_toggle: ext === "svg" ? PARAMS.facet_toggle : true })); let href; - const obj = draw(PARAMS); - obj.remove(); - // return; /****/ if (ext === "svg") { href = "data:image/svg+xml;utf8," + @@ -83,32 +90,27 @@ function download(e) { } else if (ext === "csv" || ext === "tsv") { const sep = ext === "csv" ? "," : "\t"; const mime = ext === "csv" ? "csv" : "tab-separated-values"; - const g = draw(PARAMS); const coordinates = PARAMS.mode_capsid ? g.children.filter((e) => e.data.type === "facet").flatMap((e, i) => e.children.flatMap((f, j) => f.data.segments_3D.map((g, k) => [...g, i + 1, j + 1, k + 1].join(sep)))) : g.children.flatMap((e, i) => e.children.flatMap((f, j) => f.segments.map((g, k) => [g.point.x, g.point.y, 1, i + 1, j + 1, k + 1].join(sep)))); href = `data:text/${mime};charset=utf-8,` + encodeURIComponent([["x", "y", "z", "facet", "polygon", "segment"].join(sep)].concat(coordinates).join("\r\n")); - g.remove(); } else if (ext === "json") { - const g = draw(PARAMS); href = "data:application/json;charset=utf-8," + encodeURIComponent( JSON.stringify( g.children - .filter((e) => e.data.type === "facet" || PARAMS.mode_net) - .map((e, i) => e.children.map((f, j) => (PARAMS.mode_net ? f.segments.map((e) => [e.point.x, e.point.y, 1]) : f.data.segments_3D))), + .filter((e) => e.data.type === "facet" || !PARAMS.mode_capsid) + .map((e) => e.children.map((f) => (!PARAMS.mode_capsid ? f.segments.map((e) => [e.point.x, e.point.y, 1]) : f.data.segments_3D))), null, 4 ) ); - g.remove(); } else if (ext === "py") { - const g = draw(PARAMS); const data = JSON.stringify( g.children - .filter((e) => e.data.type === "facet" || PARAMS.mode_net) - .map((e, i) => e.children.map((f, j) => (PARAMS.mode_net ? f.segments.map((e) => [e.point.x, e.point.y, 1]) : f.data.segments_3D))), + .filter((e) => e.data.type === "facet" || !PARAMS.mode_capsid) + .map((e) => e.children.map((f) => (!PARAMS.mode_capsid ? f.segments.map((e) => [e.point.x, e.point.y, 1]) : f.data.segments_3D))), null, 4 ); @@ -131,8 +133,8 @@ function download(e) { [" n += 1"], ].join("\r\n") ); - g.remove(); } + g.remove(); var link = document.createElement("a"); link.download = name + "." + ext; @@ -144,16 +146,16 @@ function update(e) { paper.clear(); paper.setup("model"); const PARAMS = params(); - const draw = PARAMS.mode_capsid ? draw_capsid : draw_net; const msg = document.getElementById("msg"); + const mode = Object.keys(DRAW_MODES).find((e) => PARAMS["mode_" + e]); try { - draw(PARAMS); + const g = DRAW_MODES[mode](PARAMS); const [h, k, H, K] = ["h", "k", "H", "K"].map((e) => PARAMS[e]); msg.children[1].innerText = [ - ["net", "capsid"][PARAMS.mode_capsid * 1] + "[" + params_to_tag(PARAMS) + "]", - "model_sa_error=" + model_sa_error(PARAMS) * 100 + "%", - `T-Number=(${h})²+(${h})(${k})+(${k})²=` + (h * h + h * k + k * k), - `Q-Number=(${H})²+(${H})(${K})+(${K})²=` + (H * H + H * K + K * K), + `${mode}[${params_to_tag(PARAMS)}]`, + `model_sa_error=${model_sa_error(PARAMS) * 100}%`, + `T-Number=(${h})²+(${h})(${k})+(${k})²=${h * h + h * k + k * k}`, + `Q-Number=(${H})²+(${H})(${K})+(${K})²=${H * H + H * K + K * K}`, ].join("\n"); if (PARAMS.c === "levo") paper.view.scale(1, -1); } catch (e) { diff --git a/js/democapsid.js b/js/democapsid.js index 64f5c30..6cdda5d 100644 --- a/js/democapsid.js +++ b/js/democapsid.js @@ -1,10 +1,10 @@ /*! - * democapsid v2.1.3 - Render viral capsids in the browser and export SVG. + * democapsid v2.2.0 - Render viral capsids in the browser and export SVG. * MIT License * Copyright (c) 2020 - 2024, Daniel Antonio Negrón (dnanto/remaindeer) */ -const VERSION = "2.1.3"; +const VERSION = "2.2.0"; const SQRT3 = Math.sqrt(3); const SQRT5 = Math.sqrt(5); @@ -84,8 +84,24 @@ Array.prototype.T = function () { return this.map((e) => [e]); }; -Array.prototype.has = function (q) { - return this.some((p) => p.length === q.length && p.every((v, i) => v === q[i])); +Array.prototype.distance = function (q) { + return this.sub(q).norm(); +}; + +Array.prototype.has = function (q, tol = TOL) { + return this.some((p) => p.length === q.length && p.distance(q) < tol); +}; + +Array.prototype.split = function (sep) { + // https://stackoverflow.com/a/34513786 + return this.reduce( + function (arr, val) { + if (val === -1) arr.push([]); + else arr[arr.length - 1].push(val); + return arr; + }, + [[]] + ).filter((e) => e.length); }; function mmul(A, B) { @@ -709,8 +725,9 @@ function ico_axis_3(ck, iter = ITER, tol = TOL) { return [pD, pF, pG, Math.abs(pD[1]) - pG.sub([0, 0, pG[2]]).norm()]; } + // TODO: parameterize increment... const delta = Math.PI / 180 / 10; - t = 0; + let t = 0; for (let i = 0; i * delta < Math.PI / 2; i++) { t = i * delta; try { @@ -768,8 +785,9 @@ function ico_axis_2(ck, iter = ITER, tol = TOL) { return [pE, pF, pG, pE.sub([0, 0, pE[2]]).norm() - pG.sub([0, 0, pG[2]]).norm()]; } + // TODO: parameterize increment... const delta = Math.PI / 180 / 10; - t = 0; + let t = 0; for (let i = 0; i * delta < Math.PI / 2; i++) { t = i * delta; try { @@ -861,7 +879,7 @@ function lattice_config(h, k, H, K, R, t) { return { tile: tile, ck: ck, lattice: lattice }; } -function calc_facets(lat_cfg) { +function calc_facets(lat_cfg, PARAMS) { const triangles = [ [3, 0], [0, 1], @@ -869,26 +887,101 @@ function calc_facets(lat_cfg) { ] .map((e) => [lat_cfg.ck[e[0]], lat_cfg.ck[e[1]]]) .map((e) => new paper.Path({ segments: [[0, 0], ...e], closed: true, data: { vectors: [[0, 0], ...e] } })); + const facets = triangles.map( - (e) => + (tri) => new paper.Group({ children: lat_cfg.lattice - .flatMap((f) => - f.map((g) => { - const x = g.intersect(e, { insert: false }); - x.data.has_centroid = e.contains(x.data.centroid); - x.data.centroid_on_vertex = e.segments.map((h) => h.point.getDistance(x.data.centroid)).some((e) => e < 1e-5); + .flatMap((tile) => + tile.map((subtile) => { + const border = subtile.curves.map((e) => [e.segment1.point, e.segment2.point]); + const x = subtile.intersect(tri, { insert: false }); + x.data.has_centroid = tri.contains(x.data.centroid); + x.data.centroid_on_vertex = x.segments.findIndex((e) => e.point.getDistance(x.data.centroid) < 1e-5) > -1; + x.style.fillColor = PARAMS["mer_color_" + x.data.offset] + PARAMS["mer_alpha_" + x.data.offset]; + x.data.strokes = x.curves + .map((e) => [e.segment1.point, e.segment2.point]) + .map((e, i) => { + // TODO: simplify... + return border.some((f) => [0, 1].every((i) => f[0].subtract(e[i]).cross(f[1].subtract(e[i])) < 1e-5) && f[0].subtract(f[1]).isCollinear(e[0].subtract(e[1]))) + ? i + : -1; + }) + .split(-1) + .map((e) => { + e.push((e[e.length - 1] + 1) % x.curves.length); + return e; + }); return x; }) ) + .sort((a, b) => a.data.offset - b.data.offset) .filter((e) => e.segments.length > 0), - data: e.data, + data: tri.data, }) ); triangles.forEach((e) => e.remove()); return facets; } +function draw_lattice(PARAMS) { + // unpack + const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + // lattice + const lat_cfg = lattice_config(h, k, H, K, R, t); + lat_cfg.lattice.flat().forEach((e) => (e.style.fillColor = PARAMS["mer_color_" + e.data.offset] + PARAMS["mer_alpha_" + e.data.offset])); + return new paper.Group( + new paper.Group({ + children: lat_cfg.lattice.flat(), + position: paper.view.center, + style: { + strokeColor: PARAMS.line_color + PARAMS.line_alpha, + strokeWidth: PARAMS.line_size, + strokeCap: "round", + strokeJoin: "round", + }, + }) + ); +} + +function draw_facets(PARAMS) { + // unpack + const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); + + // lattice + const lat_cfg = lattice_config(h, k, H, K, R, t); + const g = new paper.Group({ children: calc_facets(lat_cfg, PARAMS), position: paper.view.center }); + + lat_cfg.lattice.forEach((e) => e.forEach((f) => f.remove())); + + if (PARAMS.facet_toggle) { + g.style.strokeColor = PARAMS.line_color + PARAMS.line_alpha; + g.style.strokeWidth = PARAMS.line_size; + g.style.strokeCap = "round"; + g.style.strokeJoin = "round"; + return g; + } + g.remove(); + return new paper.Group({ + children: g.children.map( + (e) => + new paper.Group( + e.children.map((e) => { + const points = e.segments.map((e) => e.point); + const border = new paper.Group( + e.data.strokes.map((f) => new paper.Path({ segments: f.map((i) => points[i]), closed: false, style: { strokeColor: PARAMS.line_color, strokeWidth: PARAMS.line_size } })) + ); + return new paper.Group([e.clone(), border]); + }) + ) + ), + style: { + strokeCap: "round", + strokeJoin: "round", + }, + }); +} + function draw_net(PARAMS) { // unpack const [h, k, H, K, R, t] = ["h", "k", "H", "K", "R", "t"].map((e) => PARAMS[e]); @@ -896,8 +989,7 @@ function draw_net(PARAMS) { // lattice const lat_cfg = lattice_config(h, k, H, K, R, t); const ck = lat_cfg.ck; - lat_cfg.lattice.flat().forEach((e) => (e.style.fillColor = PARAMS["mer_color_" + e.data.offset] + PARAMS["mer_alpha_" + e.data.offset])); - const facets = calc_facets(lat_cfg); + const facets = calc_facets(lat_cfg, PARAMS); let g; /****/ if (PARAMS.a === 5) { @@ -919,7 +1011,6 @@ function draw_net(PARAMS) { }) .flatMap((e) => e.children), position: paper.view.center, - style: { strokeColor: PARAMS.line_color + PARAMS.line_alpha, strokeWidth: PARAMS.line_size, strokeCap: "round", strokeJoin: "round" }, }); // clean-up [unit1, unit2].forEach((e) => e.remove()); @@ -941,7 +1032,6 @@ function draw_net(PARAMS) { f.position = paper.view.center; g = new paper.Group({ children: f.children.flatMap((e) => e.children.flat()), - style: { strokeColor: PARAMS.line_color + PARAMS.line_alpha, strokeWidth: PARAMS.line_size, strokeCap: "round", strokeJoin: "round" }, }); // clean-up [unit0, unit1, unit2].forEach((e) => e.remove()); @@ -967,7 +1057,6 @@ function draw_net(PARAMS) { const children = f.children.flatMap((e) => e.children).flatMap((e) => e.children); g = new paper.Group({ children: [...children.filter((_, i) => i % 3 === 0).flatMap((e) => e.children), ...children.filter((_, i) => i % 3 !== 0)], - style: { strokeColor: PARAMS.line_color + PARAMS.line_alpha, strokeWidth: PARAMS.line_size, strokeCap: "round", strokeJoin: "round" }, }); // clean-up [2, 5, 8, 11].forEach((e) => g.children[e].remove()); @@ -975,11 +1064,38 @@ function draw_net(PARAMS) { } else { throw new Error("invalid symmetry mode!"); } + g.scale(-1, 1); + // clean-up facets.forEach((e) => e.remove()); lat_cfg.lattice.forEach((e) => e.forEach((f) => f.remove())); - return g.scale(-1, 1); + if (PARAMS.facet_toggle) { + g.style.strokeColor = PARAMS.line_color + PARAMS.line_alpha; + g.style.strokeWidth = PARAMS.line_size; + g.style.strokeCap = "round"; + g.style.strokeJoin = "round"; + return g; + } + g.remove(); + return new paper.Group({ + children: g.children.map( + (e) => + new paper.Group( + e.children.map((e) => { + const points = e.segments.map((e) => e.point); + const border = new paper.Group( + e.data.strokes.map((f) => new paper.Path({ segments: f.map((i) => points[i]), closed: false, style: { strokeColor: PARAMS.line_color, strokeWidth: PARAMS.line_size } })) + ); + return new paper.Group([e.clone(), border]); + }) + ) + ), + style: { + strokeCap: "round", + strokeJoin: "round", + }, + }); } function draw_capsid(PARAMS) { @@ -988,8 +1104,7 @@ function draw_capsid(PARAMS) { // lattice const lat_cfg = lattice_config(h, k, H, K, R, t); - lat_cfg.lattice.flat().forEach((e) => (e.style.fillColor = PARAMS["mer_color_" + e.data.offset] + PARAMS["mer_alpha_" + e.data.offset])); - const facets = calc_facets(lat_cfg); + const facets = calc_facets(lat_cfg, PARAMS); // coordinates const ico_cfg = ico_config(PARAMS.a); @@ -1022,13 +1137,13 @@ function draw_capsid(PARAMS) { ).flat(); return new paper.Path({ segments: segments.map((f) => f.slice(0, 2)), + closed: e.closed, data: Object.assign({}, e.data, { id: id, centroid: centroid, segments_3D: segments, normal: segments[1].sub(segments[0]).cross(segments[2].sub(segments[0])).uvec(), }), - closed: true, style: e.style, }); }); @@ -1093,11 +1208,14 @@ function draw_capsid(PARAMS) { const g = new paper.Group({ children: results, position: paper.view.center, - style: { strokeColor: PARAMS.line_color + PARAMS.line_alpha, strokeWidth: PARAMS.line_size, strokeCap: "round", strokeJoin: "round" }, + style: { strokeWidth: PARAMS.line_size, strokeCap: "round", strokeJoin: "round" }, }); // styling - knobs.forEach((e) => (e.style.fillColor = PARAMS.knob_color + PARAMS.knob_alpha)); + knobs.forEach((e) => { + e.style.strokeColor = PARAMS.line_color + PARAMS.line_alpha; + e.style.fillColor = PARAMS.knob_color + PARAMS.knob_alpha; + }); fibers.forEach((e) => { e.style.strokeColor = PARAMS.fiber_color + PARAMS.fiber_alpha; e.style.strokeWidth = PARAMS.fiber_size; @@ -1106,8 +1224,42 @@ function draw_capsid(PARAMS) { // clean-up facets.forEach((e) => e.remove()); lat_cfg.lattice.forEach((e) => e.forEach((f) => f.remove())); - - return g; + if (PARAMS.facet_toggle) { + g.children + .filter((e) => e.data.type === "facet") + .forEach((e) => { + e.style.strokeColor = PARAMS.line_color + PARAMS.line_alpha; + e.style.strokeWidth = PARAMS.line_size; + }); + g.style.strokeCap = "round"; + g.style.strokeJoin = "round"; + return g; + } + g.remove(); + return new paper.Group({ + children: g.children.map((e) => { + if (Object.hasOwn(e.data, "type") && e.data.type === "facet") { + return new paper.Group( + e.children.map((e) => { + const points = e.segments.map((e) => e.point); + const border = new paper.Group( + e.data.strokes.map( + (f) => + new paper.Path({ segments: f.map((i) => points[i]), closed: false, style: { strokeColor: PARAMS.line_color + PARAMS.line_alpha, strokeWidth: PARAMS.line_size } }) + ) + ); + return new paper.Group([e.clone(), border]); + }) + ); + } else { + return e; + } + }), + style: { + strokeCap: "round", + strokeJoin: "round", + }, + }); } if (typeof exports !== "undefined") { diff --git a/js/democapsid.min.js b/js/democapsid.min.js index 3bed933..679382d 100644 --- a/js/democapsid.min.js +++ b/js/democapsid.min.js @@ -1,6 +1,6 @@ /*! - * democapsid v2.1.3 - Render viral capsids in the browser and export SVG. + * democapsid v2.2.0 - Render viral capsids in the browser and export SVG. * MIT License * Copyright (c) 2020 - 2024, Daniel Antonio Negrón (dnanto/remaindeer) */ -const r="2.1.3",e=Math.sqrt(3),o=Math.sqrt(5),a=(1+o)/2,n=100,s=1e-15,i=1e-5;function mmul(t,r){const[e,o,a]=[t.length,t[0].length,r[0].length];for(var n=new Array(e),s=0;sArray.from({length:t.length},(()=>[])))),e=0;e[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}})],radius:r};else if("trihex"===t)a={basis:[[2*r,0],[r,r*e]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:2},((e,a)=>new paper.Path.RegularPolygon({center:t.coor.add([r,-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(180).rotate(60*a,t.coor)))],radius:2*o};else if("snubhex"===t)a={basis:[[2.5*r,o],[.5*r,3*o]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:6},((r,e)=>new paper.Path.RegularPolygon({center:t.coor.add([0,-o-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(60*e,t.coor))),new paper.Path.RegularPolygon({center:t.coor.add([1.5*r,-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}),new paper.Path.RegularPolygon({center:t.coor.add([-1.5*r,2/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(180)],radius:2*o};else if("rhombitrihex"===t)a={basis:[[r+o+.5*r,.5*r+o],[0,2*o+r]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:3},((o,a)=>new paper.Path.RegularPolygon({center:t.coor.add([0,r+r*(e/3)]),sides:3,radius:r/e,data:{mer:2}}).rotate(-60*a-30,t.coor))),...Array.from({length:3},((e,a)=>new paper.Path.RegularPolygon({center:t.coor.add([0,-o-.5*r]),sides:4,radius:Math.sqrt(2*r*r)/2,data:{mer:3}}).rotate(60*a,t.coor)))],radius:Math.sqrt(Math.pow(o+r,2)+Math.pow(r/2,2))};else if("dualhex"===t)a={basis:[[1.5*r,o],[0,2*o]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path.RegularPolygon({center:t.coor.add([0,o-r*e/6]),sides:3,radius:r/e,data:{mer:1}}).rotate(60*n,t.coor))),radius:r};else if("dualtrihex"===t)a={basis:[[2*o,0],[o,e*o]],tile:t=>[...Array.from({length:6},((e,a)=>new paper.Path({segments:[[0,0],[.5*o,-.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[o,0],[.5*o,.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)]].map((r=>t.coor.add(r))),closed:!0,data:{mer:1}}).rotate(60*a,t.coor))),...Array.from({length:6},((e,a)=>new paper.Path({segments:[[-.5*o,.5*r+.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[0,.5*r],[o-.5*o,.5*r+.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[0,.5*r+.25*r*Math.sin(Math.PI/6)*2/Math.cos(Math.PI/3)]].map((r=>t.coor.add(r))),closed:!0,data:{mer:2}}).rotate(60*a,t.coor)))],radius:r};else if("dualsnubhex"===t)a={basis:[[2.5*r,o],[.5*r,2*o+r*e/3*2-r*e/6]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path({segments:[[0,0],[0,o+r*e/6],[.5*r,o+r*e/3],[r,o+r*e/6],[r,r*e/3]].map((r=>t.coor.add(r))),closed:!0,data:{mer:1}}).rotate(60*n,t.coor))),radius:o+r*e/3};else{if("dualrhombitrihex"!==t)throw new Error("incorrect tile mode");a={basis:[[1.5*r,o],[0,2*o]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path({segments:[[0,0],[0,o],[.5*r,o],[e/2*o,.5*o]].map((r=>t.coor.add(r))),closed:!0,data:{mer:1}}).rotate(60*n,t.coor))),radius:r}}return a}function*tile_grid(t,r){const e=t.map((t=>mmul(T(inv2(r)),[t,1].flat().T()).flat())).map((t=>t.map(Math.round))),[o,a,n,s]=[...[0,1].map(((t,r)=>Math.min(...e.map((t=>t[r]))))),...[0,1].map(((t,r)=>Math.max(...e.map((t=>t[r])))))];for(let i=o;ir.sub(t)));return t.add(a.cross(n).mul(s.norm()**2).add(s.cross(a).mul(n.norm()**2)).add(n.cross(s).mul(a.norm()**2)).div(2*det3([a,n,s])))}function body_radius(t){return t[6].sub([0,0,t[6][2]]).norm()}function height(t){return t[0][2]-t[19][2]}function body_height(t){return t[4][2]-t[6][2]}function sd_sphere(t,r){return t.norm()-r}function spherize(t,r,e){return t.uvec().mul(Math.abs(sd_sphere(t,r))*e).add(t)}function cylinderize(t,r,e,o){const[a,n]=[body_radius(r),body_height(r)/2];let s,i;if(5===e)s=[0,0,n-a/2],i=r[0][2]+a/2-n;else if(3===e){const[t,e]=[r[0],r[3]];s=triangle_circumcircle_center(t,e,[e[0],-e[1],e[2]]),i=t.sub(s).norm()}else 2===e&&(p1=r[0],s=tetrahedron_circumsphere_center(p1,...[1,4,5].map((t=>r[t]))),i=p1.sub(s).norm());const[c,d,l,u]=[[0,0,s[2]],[0,0,-s[2]],[0,0,n],[0,0,-n]];if(n[t,r[e]])))}function ico_axis_5(t){const[r,e]=[t[0].norm(),t[1].norm()],a=r*Math.sqrt((5+o)/10),n=[0,0,(1+o)*r/(2*Math.sqrt(5+2*o))],s=[-a,0,0].roro([0,0,1],.3*Math.PI),i=s.add([r,0,0]),c=t[0].angle(t[1]),d=i.add([e,0,0].roro([0,1,0],-Math.PI-c)),l=s.add(d.sub(s).proj(i.sub(s))),u=[l[0],-Math.abs(l[1])*Math.sqrt(a*a*l[1]*l[1]-(l[0]*l[1])**2)/(l[1]*l[1]),0];if(Number.isNaN(u[1]))throw new Error("impossible construction!");const h=u.add([0,0,-Math.sqrt(d[2]*d[2]-(l[1]-u[1])**2)]);return[n,s,i].concat([1,2,3].map((t=>i.roro([0,0,1],2*t/5*Math.PI)))).concat([h]).concat([1,2,3,4].map((t=>h.roro([0,0,1],2*t/5*Math.PI)))).concat([[0,0,h[2]-n[2]]]).map((t=>t.add([0,0,-h[2]/2])))}function ico_axis_3(r,o=n,a=s){const[i,c,d]=[r[0].norm(),r[1].norm(),r[2].sub(r[1]).norm()],l=[0,i*(1/e),0],u=[i/2,-i*(e/6),0],h=[-i/2,-i*(e/6),0],p=[0,-i*(2*e/3),0];function fold(t){let[n,s]=[p.uvec().mul(i*(e/2)),u.sub(h).uvec()];const l=[0,-i*(e/6),0].add(n.roro(s,t)),m=l.roro([0,0,1],2/3*Math.PI);t=r[0].angle(r[1]),[n,s]=[l.sub(u).uvec().mul(c),l.cross(u).uvec()];const b=n.roro(s,t),[_,g]=[u.add(b.proj(n)),u.add(b)];[n,s]=[g.sub(_),u.sub(l).uvec()];const f=t=>d-_.add(n.roro(s,t)).sub(m).norm();brackets(f,0,2*Math.PI,o).next().value;t=bisection(f,...brackets(f,0,2*Math.PI,o).next().value,a,o).slice(-1);const y=_.add(n.roro(s,t));return[l,m,y,Math.abs(l[1])-y.sub([0,0,y[2]]).norm()]}const m=Math.PI/180/10;t=0;for(let e=0;e*mfold(t).slice(-1)[0];try{t=bisection(obj,...brackets(obj,t,Math.PI/4,o).next().value,a,o).slice(-1)}catch(w){throw new Error("impossible construction!")}const[b,_,g]=fold(t).slice(0,-1);if(Number.isNaN(b[0]))throw new Error("impossible construction!");const y=[0,0,1];t=2*Math.PI/3;const M=g.roro(y,-t),v=M.sub([0,0,M[2]]).roro(y,Math.PI/3).uvec().mul(l[1]).add([0,0,M[2]+b[2]-l[2]]);let P=[l,u,h,b,_.roro(y,t),_,g,M,M.roro(y,-t),v,v.roro(y,-t),v.roro(y,-2*t)];return P.map((t=>t.add([0,0,(P[0][2]-P.slice(-1)[0][2])/2])))}function ico_axis_2(r,e=n,o=s){const[i,c,d]=[r[0].norm(),r[1].norm(),r[2].sub(r[1]).norm()],l=[i/2,0,0],u=[-i/2,0,0],h=[0,-i*a/2,-(i*a-i)/2],p=[0,i*a/2,-(i*a-i)/2];function fold(t){let a=u.add(h).div(2),[n,s]=[a.sub(l),h.sub(u).uvec()];const i=a.add(n.roro(s,t)),p=i.roro([0,0,1],Math.PI);t=r[0].angle(r[1]),[n,s]=[h.sub(l).uvec().mul(c),h.cross(l).uvec()];const m=n.roro(s,t);a=l.add(m.proj(n));const b=l.add(m);[n,s]=[b.sub(a),l.sub(h).uvec()];const f=t=>d-a.add(n.roro(s,t)).sub(p).norm();t=bisection(f,...brackets(f,0,2*Math.PI,e).next().value,o,e).slice(-1);const _=a.add(n.roro(s,t));return[i,p,_,i.sub([0,0,i[2]]).norm()-_.sub([0,0,_[2]]).norm()]}const m=Math.PI/180/10;t=0;for(let a=0;a*mfold(t).slice(-1)[0];try{t=bisection(obj,...brackets(obj,t,Math.PI/4,e).next().value,o,e).slice(-1)}catch(v){throw new Error("impossible construction!")}const[b,_,g]=fold(t).slice(0,-1);if(Number.isNaN(b[0]))throw new Error("impossible construction!");obj=t=>l.roro([0,0,1],t).add([0,0,g[2]+b[2]]).sub(_).norm()-c;try{t=bisection(obj,...brackets(obj,0,2*Math.PI,e).next().value,o,e).slice(-1)}catch(v){throw new Error("impossible construction!")}const y=l.roro([0,0,1],t).add([0,0,g[2]+b[2]]),M=y.sub([0,0,y[2]]).uvec().roro([0,0,1],Math.PI/2).mul(p[1]).add([0,0,g[2]+b[2]-p[2]]);return coor=[l,u,h,p,b,_,g,g.roro([0,0,1],Math.PI),M,M.roro([0,0,1],Math.PI),y,y.roro([0,0,1],Math.PI)],coor.map((t=>t.add([0,0,(coor[0][2]-coor.slice(-1)[0][2])/2])))}function model_sa_error(t){const r=ck_vectors(calc_tile(t.t,t.R).basis,t.h,t.k,t.H,t.K),e=[[r[3],r[0]],[r[0],r[1]],[r[1],r[2]]],o=["","",ico_axis_2,ico_axis_3,"",ico_axis_5][t.a](r,n,s),a=ico_config(t.a),i=a.t_id.map((t=>parseInt(t[1]))).reduce(((t,r)=>tc[t[1]-1]+=a.t_rep[r]));const d=e.slice(0,i).map(((t,r)=>[...t[0],0].cross([...t[1],0]).norm()/2*c[r])).sum();return(d-a.v_idx.map((t=>t.map((t=>o[t])))).map(((t,r)=>t[1].sub(t[0]).cross(t[2].sub(t[0])).norm()/2*a.t_rep[r])).sum())/d}function lattice_config(t,r,e,o,a,n){const s=calc_tile(n,a),i=ck_vectors(s.basis,t,r,e,o),c=Array.from(tile_grid(i,s.basis)),d=c.map(s.tile),l=c.filter((t=>t.is_vertex)).map((t=>t.coor)).concat([[0,0]]);return d.flat().forEach((t=>{t.data.offset=t.data.mer+(l.some((r=>[t.position.x,t.position.y].sub(r).norm()<=s.radius))?0:3),t.data.centroid=t.segments.map((t=>t.point)).reduce(((t,r)=>t.add(r))).divide(t.segments.length)})),{tile:s,ck:i,lattice:d}}function calc_facets(t){const r=[[3,0],[0,1],[1,2]].map((r=>[t.ck[r[0]],t.ck[r[1]]])).map((t=>new paper.Path({segments:[[0,0],...t],closed:!0,data:{vectors:[[0,0],...t]}}))),e=r.map((r=>new paper.Group({children:t.lattice.flatMap((t=>t.map((t=>{const e=t.intersect(r,{insert:!1});return e.data.has_centroid=r.contains(e.data.centroid),e.data.centroid_on_vertex=r.segments.map((t=>t.point.getDistance(e.data.centroid))).some((t=>t<1e-5)),e})))).filter((t=>t.segments.length>0)),data:r.data})));return r.forEach((t=>t.remove())),e}function draw_net(t){const[r,e,o,a,n,s]=["h","k","H","K","R","t"].map((r=>t[r])),i=lattice_config(r,e,o,a,n,s),c=i.ck;i.lattice.flat().forEach((r=>r.style.fillColor=t["mer_color_"+r.data.offset]+t["mer_alpha_"+r.data.offset]));const d=calc_facets(i);let l;if(5===t.a){const r=new paper.Group(d.slice(0,2)).rotate(-degrees(c[0].angle([1,0]))).scale(-1,1),e=r.clone().rotate(180),o=e.children[1].children.flatMap((t=>t.segments)).map((t=>t.point)).reduce(((t,r)=>t.y{const[a,n]=[r.clone(),e.clone()];return a.position.x+=o*r.children[0].bounds.width,n.position.x+=o*r.children[0].bounds.width,[a,n]})).flatMap((t=>t.children)),position:paper.view.center,style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}}),[r,e].forEach((t=>t.remove()))}else if(3===t.a){const r=c[0].angle([1,0]),e=new paper.Group(d).rotate(-degrees(r)).scale(-1,1),o=d[0].clone().rotate(180);o.position.x+=c[0].norm()/2;const a=[o.bounds.topLeft,o.bounds.topRight,o.bounds.bottomCenter].reduce(((t,r)=>t.add(r))).divide(3),n=o.bounds.topRight.add(new paper.Point([1,0].mul(c[1].norm()).rot(-(Math.PI/3-c[1].angle(c[2]))))),s=o.bounds.topRight.add(new paper.Point([1,0].mul(c[2].norm()).rot(-(Math.PI/3-c[1].angle(c[2])+c[1].angle(c[2]))))),i=new paper.Group([...[1,2,3].map(((t,r)=>e.clone().rotate(120*r,a)))]);i.children.slice(0,-1).forEach((t=>t.children[1].remove()));const u=i.clone().rotate(180);u.bounds.left=Math.min(n.x,s.x),u.bounds.bottom=o.bounds.topRight.y,u.position.y-=u.children[1].children[0].bounds.bottom-n.y,u.children.forEach((t=>i.addChild(t.clone()))),i.position=paper.view.center,l=new paper.Group({children:i.children.flatMap((t=>t.children.flat())),style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}}),[o,e,u].forEach((t=>t.remove()))}else{if(2!==t.a)throw new Error("invalid symmetry mode!");{const r=c[0].angle([1,0]),e=new paper.Group(d).rotate(-degrees(r)).rotate(-60).scale(-1,1),o=e.children[1].clone(),a=e.children[0].bounds.topLeft.subtract(e.children[0].bounds.bottomCenter);o.position=o.position.add(a);const n=new paper.Group([e.clone(),e.children[0].clone().rotate(60,e.children[0].bounds.topRight),o.clone()]),s=n.clone().rotate(180,n.children[0].children[0].bounds.topRight);s.position=s.position.add(a.rotate(240));const i=new paper.Group([n,s]),u=i.children[0].children[2].children.filter((t=>1===t.data.offset)).flatMap((t=>t.segments.map((t=>t.point)))).filter((t=>Math.abs(t.getDistance(i.children[0].children[1].bounds.bottomLeft)-c[1].norm())<1e-5)).reduce(((t,r)=>t.yt.children)).flatMap((t=>t.children));l=new paper.Group({children:[...m.filter(((t,r)=>r%3==0)).flatMap((t=>t.children)),...m.filter(((t,r)=>r%3!=0))],style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}}),[2,5,8,11].forEach((t=>l.children[t].remove())),[e,o,n,s,i,h].forEach((t=>t.remove()))}}return d.forEach((t=>t.remove())),i.lattice.forEach((t=>t.forEach((t=>t.remove())))),l.scale(-1,1)}function draw_capsid(t){const[r,e,o,a,c,d]=["h","k","H","K","R","t"].map((r=>t[r])),l=lattice_config(r,e,o,a,c,d);l.lattice.flat().forEach((r=>r.style.fillColor=t["mer_color_"+r.data.offset]+t["mer_alpha_"+r.data.offset]));const u=calc_facets(l),h=ico_config(t.a),p=["","",ico_axis_2,ico_axis_3,"",ico_axis_5][t.a](l.ck,n,s),m=2*Math.PI/t.a,b=r==o&&e==a?r=>spherize(r,p[0].norm(),t.s):r=>cylinderize(r,p,t.a,t.s),_=camera(...[t.θ,t.ψ,t.φ].map(radians));let g=[];for(let n=0,s=0;nt.concat(1))))),e=[0,1,2].map((t=>p[h.v_idx[n][t]]));for(let o=0;ot.roro([0,0,1],o*m))),n=mmul(T(a),r),i=t.children.map((t=>{const r=t.segments.map((t=>[t.point.x,t.point.y,1])).map((t=>mmul(n,t.T()).flat())).map((t=>b(t))).map((t=>mmul(_,t.concat(1).T()).flat())),e=mmul(_,b(mmul(n,[t.data.centroid.x,t.data.centroid.y,1].T()).flat()).concat(1).T()).flat();return new paper.Path({segments:r.map((t=>t.slice(0,2))),data:Object.assign({},t.data,{id:s,centroid:e,segments_3D:r,normal:r[1].sub(r[0]).cross(r[2].sub(r[0])).uvec()}),closed:!0,style:t.style})}));g=g.concat(new paper.Group({children:i,data:{type:"facet",centroid:mmul(_,b(a.centroid()).concat(1).T()).flat()}}))}}const y=p.map((t=>mmul(_,t.concat(1).T()).flat()));let M=t.penton_fiber_toggle?h.v_con.map((t=>t.map((t=>y[t])).reduce(((t,r)=>t.add(r)),[0,0,0]).uvec())).map(((r,e)=>[y[e],y[e].add(r.mul(t.fiber_length))])):[];M=M.concat(g.flatMap((t=>t.children)).filter((r=>t["mer_toggle_"+r.data.offset]&&r.data.has_centroid)).map((r=>{const e=r.data.centroid;return[e,e.add(r.data.normal.mul(t.fiber_length))]})));let v=[];M.forEach((t=>{let r=0;for(let e of v)t[0].sub(e[0][0]).norm()[t[0][0],t.map((t=>t[1])).reduce(((t,r)=>t.add(r)),[0,0,0]).div(t.length)])).map((t=>new paper.Path.Line({from:t[0],to:t[1],data:{centroid:t[1].mul(2)}})));const P=t.knob_toggle?M.map((r=>new paper.Path.Circle({center:r.segments[1].point,radius:t.knob_size,data:{centroid:r.data.centroid}}))):[];g=g.concat(M).concat(P),g.sort(((t,r)=>t.data.centroid[2]-r.data.centroid[2]));const w=new paper.Group({children:g,position:paper.view.center,style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}});return P.forEach((r=>r.style.fillColor=t.knob_color+t.knob_alpha)),M.forEach((r=>{r.style.strokeColor=t.fiber_color+t.fiber_alpha,r.style.strokeWidth=t.fiber_size})),u.forEach((t=>t.remove())),l.lattice.forEach((t=>t.forEach((t=>t.remove())))),w}Array.prototype.mul=function(t){return this.map((r=>r*t))},Array.prototype.div=function(t){return this.map((r=>r/t))},Array.prototype.add=function(t){return this.map(((r,e)=>r+t[e]))},Array.prototype.sub=function(t){return this.map(((r,e)=>r-t[e]))},Array.prototype.dot=function(t){return this.map(((r,e)=>r*t[e])).reduce(((t,r)=>t+r))},Array.prototype.sum=function(t=0){return this.reduce(((t,r)=>t+r),t)},Array.prototype.centroid=function(){return this.reduce(((t,r)=>t.add(r))).div(this.length)},Array.prototype.cross=function(t){return[this[1]*t[2]-this[2]*t[1],this[2]*t[0]-this[0]*t[2],this[0]*t[1]-this[1]*t[0]]},Array.prototype.rot=function(t){const[r,e]=[Math.cos(t),Math.sin(t)];return mmul([[r,-e],[e,r]],this.T()).flat()},Array.prototype.roro=function(t,r){return this.mul(Math.cos(r)).add(t.cross(this).mul(Math.sin(r))).add(t.mul(t.dot(this)).mul(1-Math.cos(r)))},Array.prototype.norm=function(){return Math.sqrt(this.map((t=>t*t)).sum())},Array.prototype.uvec=function(){return this.div(this.norm())},Array.prototype.angle=function(t){return Math.acos(this.dot(t)/(this.norm()*t.norm()))},Array.prototype.proj=function(t){return t.mul(this.dot(t)/t.dot(t))},Array.prototype.T=function(){return this.map((t=>[t]))},Array.prototype.has=function(t){return this.some((r=>r.length===t.length&&r.every(((r,e)=>r===t[e]))))},"undefined"!=typeof exports&&(module.exports={ico_axis_2:ico_axis_2,ico_axis_3:ico_axis_3,ico_axis_5:ico_axis_5}); \ No newline at end of file +const t="2.2.0",e=Math.sqrt(3),r=Math.sqrt(5),o=(1+r)/2,a=100,n=1e-15,s=1e-5;function mmul(t,e){const[r,o,a]=[t.length,t[0].length,e[0].length];for(var n=new Array(r),s=0;sArray.from({length:t.length},(()=>[])))),r=0;r[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}})],radius:r};else if("trihex"===t)a={basis:[[2*r,0],[r,r*e]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:2},((e,a)=>new paper.Path.RegularPolygon({center:t.coor.add([r,-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(180).rotate(60*a,t.coor)))],radius:2*o};else if("snubhex"===t)a={basis:[[2.5*r,o],[.5*r,3*o]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:6},((e,r)=>new paper.Path.RegularPolygon({center:t.coor.add([0,-o-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(60*r,t.coor))),new paper.Path.RegularPolygon({center:t.coor.add([1.5*r,-1/3*o]),sides:3,radius:2/3*o,data:{mer:2}}),new paper.Path.RegularPolygon({center:t.coor.add([-1.5*r,2/3*o]),sides:3,radius:2/3*o,data:{mer:2}}).rotate(180)],radius:2*o};else if("rhombitrihex"===t)a={basis:[[r+o+.5*r,.5*r+o],[0,2*o+r]],tile:t=>[new paper.Path.RegularPolygon({center:t.coor,sides:6,radius:r,data:{mer:1}}).rotate(30),...Array.from({length:3},((o,a)=>new paper.Path.RegularPolygon({center:t.coor.add([0,r+r*(e/3)]),sides:3,radius:r/e,data:{mer:2}}).rotate(-60*a-30,t.coor))),...Array.from({length:3},((e,a)=>new paper.Path.RegularPolygon({center:t.coor.add([0,-o-.5*r]),sides:4,radius:Math.sqrt(2*r*r)/2,data:{mer:3}}).rotate(60*a,t.coor)))],radius:Math.sqrt(Math.pow(o+r,2)+Math.pow(r/2,2))};else if("dualhex"===t)a={basis:[[1.5*r,o],[0,2*o]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path.RegularPolygon({center:t.coor.add([0,o-r*e/6]),sides:3,radius:r/e,data:{mer:1}}).rotate(60*n,t.coor))),radius:r};else if("dualtrihex"===t)a={basis:[[2*o,0],[o,e*o]],tile:t=>[...Array.from({length:6},((e,a)=>new paper.Path({segments:[[0,0],[.5*o,-.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[o,0],[.5*o,.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)]].map((e=>t.coor.add(e))),closed:!0,data:{mer:1}}).rotate(60*a,t.coor))),...Array.from({length:6},((e,a)=>new paper.Path({segments:[[-.5*o,.5*r+.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[0,.5*r],[o-.5*o,.5*r+.25*r*Math.sin(Math.PI/6)/Math.cos(Math.PI/3)],[0,.5*r+.25*r*Math.sin(Math.PI/6)*2/Math.cos(Math.PI/3)]].map((e=>t.coor.add(e))),closed:!0,data:{mer:2}}).rotate(60*a,t.coor)))],radius:r};else if("dualsnubhex"===t)a={basis:[[2.5*r,o],[.5*r,2*o+r*e/3*2-r*e/6]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path({segments:[[0,0],[0,o+r*e/6],[.5*r,o+r*e/3],[r,o+r*e/6],[r,r*e/3]].map((e=>t.coor.add(e))),closed:!0,data:{mer:1}}).rotate(60*n,t.coor))),radius:o+r*e/3};else{if("dualrhombitrihex"!==t)throw new Error("incorrect tile mode");a={basis:[[1.5*r,o],[0,2*o]],tile:t=>Array.from({length:6},((a,n)=>new paper.Path({segments:[[0,0],[0,o],[.5*r,o],[e/2*o,.5*o]].map((e=>t.coor.add(e))),closed:!0,data:{mer:1}}).rotate(60*n,t.coor))),radius:r}}return a}function*tile_grid(t,e){const r=t.map((t=>mmul(T(inv2(e)),[t,1].flat().T()).flat())).map((t=>t.map(Math.round))),[o,a,n,s]=[...[0,1].map(((t,e)=>Math.min(...r.map((t=>t[e]))))),...[0,1].map(((t,e)=>Math.max(...r.map((t=>t[e])))))];for(let i=o;ie.sub(t)));return t.add(a.cross(n).mul(s.norm()**2).add(s.cross(a).mul(n.norm()**2)).add(n.cross(s).mul(a.norm()**2)).div(2*det3([a,n,s])))}function body_radius(t){return t[6].sub([0,0,t[6][2]]).norm()}function height(t){return t[0][2]-t[19][2]}function body_height(t){return t[4][2]-t[6][2]}function sd_sphere(t,e){return t.norm()-e}function spherize(t,e,r){return t.uvec().mul(Math.abs(sd_sphere(t,e))*r).add(t)}function cylinderize(t,e,r,o){const[a,n]=[body_radius(e),body_height(e)/2];let s,i;if(5===r)s=[0,0,n-a/2],i=e[0][2]+a/2-n;else if(3===r){const[t,r]=[e[0],e[3]];s=triangle_circumcircle_center(t,r,[r[0],-r[1],r[2]]),i=t.sub(s).norm()}else 2===r&&(p1=e[0],s=tetrahedron_circumsphere_center(p1,...[1,4,5].map((t=>e[t]))),i=p1.sub(s).norm());const[c,l,d,u]=[[0,0,s[2]],[0,0,-s[2]],[0,0,n],[0,0,-n]];if(n[t,e[r]])))}function ico_axis_5(t){const[e,o]=[t[0].norm(),t[1].norm()],a=e*Math.sqrt((5+r)/10),n=[0,0,(1+r)*e/(2*Math.sqrt(5+2*r))],s=[-a,0,0].roro([0,0,1],.3*Math.PI),i=s.add([e,0,0]),c=t[0].angle(t[1]),l=i.add([o,0,0].roro([0,1,0],-Math.PI-c)),d=s.add(l.sub(s).proj(i.sub(s))),u=[d[0],-Math.abs(d[1])*Math.sqrt(a*a*d[1]*d[1]-(d[0]*d[1])**2)/(d[1]*d[1]),0];if(Number.isNaN(u[1]))throw new Error("impossible construction!");const p=u.add([0,0,-Math.sqrt(l[2]*l[2]-(d[1]-u[1])**2)]);return[n,s,i].concat([1,2,3].map((t=>i.roro([0,0,1],2*t/5*Math.PI)))).concat([p]).concat([1,2,3,4].map((t=>p.roro([0,0,1],2*t/5*Math.PI)))).concat([[0,0,p[2]-n[2]]]).map((t=>t.add([0,0,-p[2]/2])))}function ico_axis_3(t,r=a,o=n){const[s,i,c]=[t[0].norm(),t[1].norm(),t[2].sub(t[1]).norm()],l=[0,s*(1/e),0],d=[s/2,-s*(e/6),0],u=[-s/2,-s*(e/6),0],p=[0,-s*(2*e/3),0];function fold(a){let[n,l]=[p.uvec().mul(s*(e/2)),d.sub(u).uvec()];const h=[0,-s*(e/6),0].add(n.roro(l,a)),m=h.roro([0,0,1],2/3*Math.PI);a=t[0].angle(t[1]),[n,l]=[h.sub(d).uvec().mul(i),h.cross(d).uvec()];const _=n.roro(l,a),[g,b]=[d.add(_.proj(n)),d.add(_)];[n,l]=[b.sub(g),d.sub(h).uvec()];const f=t=>c-g.add(n.roro(l,t)).sub(m).norm();brackets(f,0,2*Math.PI,r).next().value;a=bisection(f,...brackets(f,0,2*Math.PI,r).next().value,o,r).slice(-1);const y=g.add(n.roro(l,a));return[h,m,y,Math.abs(h[1])-y.sub([0,0,y[2]]).norm()]}const h=Math.PI/180/10;let m=0;for(let e=0;e*hfold(t).slice(-1)[0];try{m=bisection(obj,...brackets(obj,m,Math.PI/4,r).next().value,o,r).slice(-1)}catch(P){throw new Error("impossible construction!")}const[_,g,b]=fold(m).slice(0,-1);if(Number.isNaN(_[0]))throw new Error("impossible construction!");const y=[0,0,1];m=2*Math.PI/3;const M=b.roro(y,-m),w=M.sub([0,0,M[2]]).roro(y,Math.PI/3).uvec().mul(l[1]).add([0,0,M[2]+_[2]-l[2]]);let v=[l,d,u,_,g.roro(y,m),g,b,M,M.roro(y,-m),w,w.roro(y,-m),w.roro(y,-2*m)];return v.map((t=>t.add([0,0,(v[0][2]-v.slice(-1)[0][2])/2])))}function ico_axis_2(t,e=a,r=n){const[s,i,c]=[t[0].norm(),t[1].norm(),t[2].sub(t[1]).norm()],l=[s/2,0,0],d=[-s/2,0,0],u=[0,-s*o/2,-(s*o-s)/2],p=[0,s*o/2,-(s*o-s)/2];function fold(o){let a=d.add(u).div(2),[n,s]=[a.sub(l),u.sub(d).uvec()];const p=a.add(n.roro(s,o)),h=p.roro([0,0,1],Math.PI);o=t[0].angle(t[1]),[n,s]=[u.sub(l).uvec().mul(i),u.cross(l).uvec()];const m=n.roro(s,o);a=l.add(m.proj(n));const _=l.add(m);[n,s]=[_.sub(a),l.sub(u).uvec()];const f=t=>c-a.add(n.roro(s,t)).sub(h).norm();o=bisection(f,...brackets(f,0,2*Math.PI,e).next().value,r,e).slice(-1);const g=a.add(n.roro(s,o));return[p,h,g,p.sub([0,0,p[2]]).norm()-g.sub([0,0,g[2]]).norm()]}const h=Math.PI/180/10;let m=0;for(let o=0;o*hfold(t).slice(-1)[0];try{m=bisection(obj,...brackets(obj,m,Math.PI/4,e).next().value,r,e).slice(-1)}catch(w){throw new Error("impossible construction!")}const[_,g,b]=fold(m).slice(0,-1);if(Number.isNaN(_[0]))throw new Error("impossible construction!");obj=t=>l.roro([0,0,1],t).add([0,0,b[2]+_[2]]).sub(g).norm()-i;try{m=bisection(obj,...brackets(obj,0,2*Math.PI,e).next().value,r,e).slice(-1)}catch(w){throw new Error("impossible construction!")}const y=l.roro([0,0,1],m).add([0,0,b[2]+_[2]]),M=y.sub([0,0,y[2]]).uvec().roro([0,0,1],Math.PI/2).mul(p[1]).add([0,0,b[2]+_[2]-p[2]]);return coor=[l,d,u,p,_,g,b,b.roro([0,0,1],Math.PI),M,M.roro([0,0,1],Math.PI),y,y.roro([0,0,1],Math.PI)],coor.map((t=>t.add([0,0,(coor[0][2]-coor.slice(-1)[0][2])/2])))}function model_sa_error(t){const e=ck_vectors(calc_tile(t.t,t.R).basis,t.h,t.k,t.H,t.K),r=[[e[3],e[0]],[e[0],e[1]],[e[1],e[2]]],o=["","",ico_axis_2,ico_axis_3,"",ico_axis_5][t.a](e,a,n),s=ico_config(t.a),i=s.t_id.map((t=>parseInt(t[1]))).reduce(((t,e)=>tc[t[1]-1]+=s.t_rep[e]));const l=r.slice(0,i).map(((t,e)=>[...t[0],0].cross([...t[1],0]).norm()/2*c[e])).sum();return(l-s.v_idx.map((t=>t.map((t=>o[t])))).map(((t,e)=>t[1].sub(t[0]).cross(t[2].sub(t[0])).norm()/2*s.t_rep[e])).sum())/l}function lattice_config(t,e,r,o,a,n){const s=calc_tile(n,a),i=ck_vectors(s.basis,t,e,r,o),c=Array.from(tile_grid(i,s.basis)),l=c.map(s.tile),d=c.filter((t=>t.is_vertex)).map((t=>t.coor)).concat([[0,0]]);return l.flat().forEach((t=>{t.data.offset=t.data.mer+(d.some((e=>[t.position.x,t.position.y].sub(e).norm()<=s.radius))?0:3),t.data.centroid=t.segments.map((t=>t.point)).reduce(((t,e)=>t.add(e))).divide(t.segments.length)})),{tile:s,ck:i,lattice:l}}function calc_facets(t,e){const r=[[3,0],[0,1],[1,2]].map((e=>[t.ck[e[0]],t.ck[e[1]]])).map((t=>new paper.Path({segments:[[0,0],...t],closed:!0,data:{vectors:[[0,0],...t]}}))),o=r.map((r=>new paper.Group({children:t.lattice.flatMap((t=>t.map((t=>{const o=t.curves.map((t=>[t.segment1.point,t.segment2.point])),a=t.intersect(r,{insert:!1});return a.data.has_centroid=r.contains(a.data.centroid),a.data.centroid_on_vertex=a.segments.findIndex((t=>t.point.getDistance(a.data.centroid)<1e-5))>-1,a.style.fillColor=e["mer_color_"+a.data.offset]+e["mer_alpha_"+a.data.offset],a.data.strokes=a.curves.map((t=>[t.segment1.point,t.segment2.point])).map(((t,e)=>o.some((e=>[0,1].every((r=>e[0].subtract(t[r]).cross(e[1].subtract(t[r]))<1e-5))&&e[0].subtract(e[1]).isCollinear(t[0].subtract(t[1]))))?e:-1)).split(-1).map((t=>(t.push((t[t.length-1]+1)%a.curves.length),t))),a})))).sort(((t,e)=>t.data.offset-e.data.offset)).filter((t=>t.segments.length>0)),data:r.data})));return r.forEach((t=>t.remove())),o}function draw_lattice(t){const[e,r,o,a,n,s]=["h","k","H","K","R","t"].map((e=>t[e])),i=lattice_config(e,r,o,a,n,s);return i.lattice.flat().forEach((e=>e.style.fillColor=t["mer_color_"+e.data.offset]+t["mer_alpha_"+e.data.offset])),new paper.Group(new paper.Group({children:i.lattice.flat(),position:paper.view.center,style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}}))}function draw_facets(t){const[e,r,o,a,n,s]=["h","k","H","K","R","t"].map((e=>t[e])),i=lattice_config(e,r,o,a,n,s),c=new paper.Group({children:calc_facets(i,t),position:paper.view.center});return i.lattice.forEach((t=>t.forEach((t=>t.remove())))),t.facet_toggle?(c.style.strokeColor=t.line_color+t.line_alpha,c.style.strokeWidth=t.line_size,c.style.strokeCap="round",c.style.strokeJoin="round",c):(c.remove(),new paper.Group({children:c.children.map((e=>new paper.Group(e.children.map((e=>{const r=e.segments.map((t=>t.point)),o=new paper.Group(e.data.strokes.map((e=>new paper.Path({segments:e.map((t=>r[t])),closed:!1,style:{strokeColor:t.line_color,strokeWidth:t.line_size}}))));return new paper.Group([e.clone(),o])}))))),style:{strokeCap:"round",strokeJoin:"round"}}))}function draw_net(t){const[e,r,o,a,n,s]=["h","k","H","K","R","t"].map((e=>t[e])),i=lattice_config(e,r,o,a,n,s),c=i.ck,l=calc_facets(i,t);let d;if(5===t.a){const t=new paper.Group(l.slice(0,2)).rotate(-degrees(c[0].angle([1,0]))).scale(-1,1),e=t.clone().rotate(180),r=e.children[1].children.flatMap((t=>t.segments)).map((t=>t.point)).reduce(((t,e)=>t.y{const[a,n]=[t.clone(),e.clone()];return a.position.x+=o*t.children[0].bounds.width,n.position.x+=o*t.children[0].bounds.width,[a,n]})).flatMap((t=>t.children)),position:paper.view.center}),[t,e].forEach((t=>t.remove()))}else if(3===t.a){const t=c[0].angle([1,0]),e=new paper.Group(l).rotate(-degrees(t)).scale(-1,1),r=l[0].clone().rotate(180);r.position.x+=c[0].norm()/2;const o=[r.bounds.topLeft,r.bounds.topRight,r.bounds.bottomCenter].reduce(((t,e)=>t.add(e))).divide(3),a=r.bounds.topRight.add(new paper.Point([1,0].mul(c[1].norm()).rot(-(Math.PI/3-c[1].angle(c[2]))))),n=r.bounds.topRight.add(new paper.Point([1,0].mul(c[2].norm()).rot(-(Math.PI/3-c[1].angle(c[2])+c[1].angle(c[2]))))),s=new paper.Group([...[1,2,3].map(((t,r)=>e.clone().rotate(120*r,o)))]);s.children.slice(0,-1).forEach((t=>t.children[1].remove()));const i=s.clone().rotate(180);i.bounds.left=Math.min(a.x,n.x),i.bounds.bottom=r.bounds.topRight.y,i.position.y-=i.children[1].children[0].bounds.bottom-a.y,i.children.forEach((t=>s.addChild(t.clone()))),s.position=paper.view.center,d=new paper.Group({children:s.children.flatMap((t=>t.children.flat()))}),[r,e,i].forEach((t=>t.remove()))}else{if(2!==t.a)throw new Error("invalid symmetry mode!");{const t=c[0].angle([1,0]),e=new paper.Group(l).rotate(-degrees(t)).rotate(-60).scale(-1,1),r=e.children[1].clone(),o=e.children[0].bounds.topLeft.subtract(e.children[0].bounds.bottomCenter);r.position=r.position.add(o);const a=new paper.Group([e.clone(),e.children[0].clone().rotate(60,e.children[0].bounds.topRight),r.clone()]),n=a.clone().rotate(180,a.children[0].children[0].bounds.topRight);n.position=n.position.add(o.rotate(240));const s=new paper.Group([a,n]),i=s.children[0].children[2].children.filter((t=>1===t.data.offset)).flatMap((t=>t.segments.map((t=>t.point)))).filter((t=>Math.abs(t.getDistance(s.children[0].children[1].bounds.bottomLeft)-c[1].norm())<1e-5)).reduce(((t,e)=>t.yt.children)).flatMap((t=>t.children));d=new paper.Group({children:[...h.filter(((t,e)=>e%3==0)).flatMap((t=>t.children)),...h.filter(((t,e)=>e%3!=0))]}),[2,5,8,11].forEach((t=>d.children[t].remove())),[e,r,a,n,s,u].forEach((t=>t.remove()))}}return d.scale(-1,1),l.forEach((t=>t.remove())),i.lattice.forEach((t=>t.forEach((t=>t.remove())))),t.facet_toggle?(d.style.strokeColor=t.line_color+t.line_alpha,d.style.strokeWidth=t.line_size,d.style.strokeCap="round",d.style.strokeJoin="round",d):(d.remove(),new paper.Group({children:d.children.map((e=>new paper.Group(e.children.map((e=>{const r=e.segments.map((t=>t.point)),o=new paper.Group(e.data.strokes.map((e=>new paper.Path({segments:e.map((t=>r[t])),closed:!1,style:{strokeColor:t.line_color,strokeWidth:t.line_size}}))));return new paper.Group([e.clone(),o])}))))),style:{strokeCap:"round",strokeJoin:"round"}}))}function draw_capsid(t){const[e,r,o,i,c,l]=["h","k","H","K","R","t"].map((e=>t[e])),d=lattice_config(e,r,o,i,c,l),u=calc_facets(d,t),p=ico_config(t.a),h=["","",ico_axis_2,ico_axis_3,"",ico_axis_5][t.a](d.ck,a,n),m=2*Math.PI/t.a,_=e==o&&r==i?e=>spherize(e,h[0].norm(),t.s):e=>cylinderize(e,h,t.a,t.s),g=camera(...[t.θ,t.ψ,t.φ].map(radians));let b=[];for(let a=0,n=0;at.concat(1))))),r=[0,1,2].map((t=>h[p.v_idx[a][t]]));for(let o=0;ot.roro([0,0,1],o*m))),s=mmul(T(a),e),i=t.children.map((t=>{const e=t.segments.map((t=>[t.point.x,t.point.y,1])).map((t=>mmul(s,t.T()).flat())).map((t=>_(t))).map((t=>mmul(g,t.concat(1).T()).flat())),r=mmul(g,_(mmul(s,[t.data.centroid.x,t.data.centroid.y,1].T()).flat()).concat(1).T()).flat();return new paper.Path({segments:e.map((t=>t.slice(0,2))),closed:t.closed,data:Object.assign({},t.data,{id:n,centroid:r,segments_3D:e,normal:e[1].sub(e[0]).cross(e[2].sub(e[0])).uvec()}),style:t.style})}));b=b.concat(new paper.Group({children:i,data:{type:"facet",centroid:mmul(g,_(a.centroid()).concat(1).T()).flat()}}))}}const y=h.map((t=>mmul(g,t.concat(1).T()).flat()));let M=t.penton_fiber_toggle?p.v_con.map((t=>t.map((t=>y[t])).reduce(((t,e)=>t.add(e)),[0,0,0]).uvec())).map(((e,r)=>[y[r],y[r].add(e.mul(t.fiber_length))])):[];M=M.concat(b.flatMap((t=>t.children)).filter((e=>t["mer_toggle_"+e.data.offset]&&e.data.has_centroid)).map((e=>{const r=e.data.centroid;return[r,r.add(e.data.normal.mul(t.fiber_length))]})));let w=[];M.forEach((t=>{let e=0;for(let r of w)t[0].sub(r[0][0]).norm()[t[0][0],t.map((t=>t[1])).reduce(((t,e)=>t.add(e)),[0,0,0]).div(t.length)])).map((t=>new paper.Path.Line({from:t[0],to:t[1],data:{centroid:t[1].mul(2)}})));const v=t.knob_toggle?M.map((e=>new paper.Path.Circle({center:e.segments[1].point,radius:t.knob_size,data:{centroid:e.data.centroid}}))):[];b=b.concat(M).concat(v),b.sort(((t,e)=>t.data.centroid[2]-e.data.centroid[2]));const P=new paper.Group({children:b,position:paper.view.center,style:{strokeWidth:t.line_size,strokeCap:"round",strokeJoin:"round"}});return v.forEach((e=>{e.style.strokeColor=t.line_color+t.line_alpha,e.style.fillColor=t.knob_color+t.knob_alpha})),M.forEach((e=>{e.style.strokeColor=t.fiber_color+t.fiber_alpha,e.style.strokeWidth=t.fiber_size})),u.forEach((t=>t.remove())),d.lattice.forEach((t=>t.forEach((t=>t.remove())))),t.facet_toggle?(P.children.filter((t=>"facet"===t.data.type)).forEach((e=>{e.style.strokeColor=t.line_color+t.line_alpha,e.style.strokeWidth=t.line_size})),P.style.strokeCap="round",P.style.strokeJoin="round",P):(P.remove(),new paper.Group({children:P.children.map((e=>Object.hasOwn(e.data,"type")&&"facet"===e.data.type?new paper.Group(e.children.map((e=>{const r=e.segments.map((t=>t.point)),o=new paper.Group(e.data.strokes.map((e=>new paper.Path({segments:e.map((t=>r[t])),closed:!1,style:{strokeColor:t.line_color+t.line_alpha,strokeWidth:t.line_size}}))));return new paper.Group([e.clone(),o])}))):e)),style:{strokeCap:"round",strokeJoin:"round"}}))}Array.prototype.mul=function(t){return this.map((e=>e*t))},Array.prototype.div=function(t){return this.map((e=>e/t))},Array.prototype.add=function(t){return this.map(((e,r)=>e+t[r]))},Array.prototype.sub=function(t){return this.map(((e,r)=>e-t[r]))},Array.prototype.dot=function(t){return this.map(((e,r)=>e*t[r])).reduce(((t,e)=>t+e))},Array.prototype.sum=function(t=0){return this.reduce(((t,e)=>t+e),t)},Array.prototype.centroid=function(){return this.reduce(((t,e)=>t.add(e))).div(this.length)},Array.prototype.cross=function(t){return[this[1]*t[2]-this[2]*t[1],this[2]*t[0]-this[0]*t[2],this[0]*t[1]-this[1]*t[0]]},Array.prototype.rot=function(t){const[e,r]=[Math.cos(t),Math.sin(t)];return mmul([[e,-r],[r,e]],this.T()).flat()},Array.prototype.roro=function(t,e){return this.mul(Math.cos(e)).add(t.cross(this).mul(Math.sin(e))).add(t.mul(t.dot(this)).mul(1-Math.cos(e)))},Array.prototype.norm=function(){return Math.sqrt(this.map((t=>t*t)).sum())},Array.prototype.uvec=function(){return this.div(this.norm())},Array.prototype.angle=function(t){return Math.acos(this.dot(t)/(this.norm()*t.norm()))},Array.prototype.proj=function(t){return t.mul(this.dot(t)/t.dot(t))},Array.prototype.T=function(){return this.map((t=>[t]))},Array.prototype.distance=function(t){return this.sub(t).norm()},Array.prototype.has=function(t,e=n){return this.some((r=>r.length===t.length&&r.distance(t)t.length))},"undefined"!=typeof exports&&(module.exports={ico_axis_2:ico_axis_2,ico_axis_3:ico_axis_3,ico_axis_5:ico_axis_5}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 231d51b..e2897d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "democapsid", - "version": "2.1.3", + "version": "2.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "democapsid", - "version": "2.1.3", + "version": "2.2.0", "license": "MIT", "dependencies": { "paper": "^0.12.18" diff --git a/package.json b/package.json index 322f1fc..97bba1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democapsid", - "version": "2.1.3", + "version": "2.2.0", "author": "Daniel Antonio Negrón", "license": "MIT", "description": "Render viral capsids in the browser and export SVG.", diff --git a/screenshot.png b/screenshot.png index 8610758..d28a95d 100644 Binary files a/screenshot.png and b/screenshot.png differ