├── .gitignore ├── README.md ├── components └── Cover.js ├── index.idyll ├── package-lock.json ├── package.json ├── posts └── half-edge │ ├── components │ ├── EdgeFlipConsistencyChecker.js │ ├── HalfEdgeDiagram.js │ ├── HalfEdgeStepper.js │ ├── HalfEdgeTables.js │ ├── HalfEdgeVis.js │ ├── OBJEditor.js │ ├── Raw.js │ ├── TriangleCaption.js │ └── util │ │ ├── Color.js │ │ ├── Mesh.js │ │ ├── OBJLoader.js │ │ └── Vec3.js │ ├── index.idyll │ ├── package.json │ ├── static │ ├── LinuxLibertine │ │ ├── LinuxLibertine-Italic.woff │ │ └── LinuxLibertine-Regular.woff │ └── images │ │ ├── edgeflip.inkscape.svg │ │ ├── edgeflip.svg │ │ ├── triangle.inkscape.svg │ │ └── triangle.svg │ └── styles.css ├── styles.css └── template ├── components └── custom-component.js ├── data └── example-data.json ├── gitignore ├── index.idyll ├── package.json ├── static └── images │ └── quill.svg └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idyll 61 | /docs 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Geometry Processing Algorithms 2 | ============================== 3 | 4 | Source code of the interactive book, “Geometry Processing Algorithms.” 5 | 6 | Made with [Idyll][]. 7 | 8 | Currently, only one article on half-edge data structures is complete. 9 | 10 | 11 | Building 12 | -------- 13 | 14 | Make sure you have [Idyll][] installed. Idyll can be installed with 15 | 16 | npm install -g idyll 17 | 18 | After installing Idyll, `cd` to each directory in the `posts` directory and run 19 | 20 | idyll 21 | 22 | to generate each post. It will also run a local server which will automatically 23 | reload the pages in your browser if you make any changes to the files. You can 24 | also run `idyll` in this directory to generate a nice index page which links to 25 | all the articles (but still requires that you generate each post beforehand). 26 | 27 | To generate final versions of each post, run 28 | 29 | idyll build --minify true 30 | 31 | in each directory in `posts`. 32 | 33 | [Idyll]: https://idyll-lang.org 34 | 35 | 36 | Overview 37 | -------- 38 | 39 | Each article in `posts` has the following structure: 40 | 41 | - `components` contains custom React components that make up the visualizations 42 | - `index.idyll` contains the text content of the article 43 | 44 | `components/util/Mesh.js` and `components/util/Vec3.js` are based on files 45 | provided for Assignment 7 in the UBC course CPSC 424. 46 | -------------------------------------------------------------------------------- /components/Cover.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class Cover extends React.Component { 4 | render() { 5 | const { hasError, idyll, updateProps, 6 | title, author, 7 | ...props } = this.props; 8 | 9 | return ( 10 |
11 |

Geometry Processing Algorithms

12 |
Jerry Yin and Jeffrey Goh
13 |
14 | ); 15 | } 16 | } 17 | 18 | module.exports = Cover; 19 | -------------------------------------------------------------------------------- /index.idyll: -------------------------------------------------------------------------------- 1 | [meta 2 | title: "Geometry Processing Algorithms" 3 | description: "Interactive explainers for geometry processing algorithms" /] 4 | 5 | [Cover 6 | title: "Geometry Processing Algorithms" 7 | author: "Jerry Yin and Jeffrey Goh" /] 8 | 9 | 10 | ## Contents 11 | 12 | NOTE: Articles need to be compiled before these links will work 13 | 14 | * [Half-edge data structures](./half-edge/) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geom-vis", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "idyll": { 6 | "theme": "none", 7 | "layout": "none", 8 | "css": "styles.css", 9 | "authorView": false, 10 | "output": "docs/" 11 | }, 12 | "dependencies": { 13 | "d3": "^4.0.0", 14 | "idyll": "^4.4.0", 15 | "idyll-d3-component": "^2.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /posts/half-edge/components/EdgeFlipConsistencyChecker.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const OBJLoader = require('./util/OBJLoader'); 3 | import {HalfEdgeDiagram} from './HalfEdgeDiagram'; 4 | import {HalfEdgeTables} from './HalfEdgeTables'; 5 | 6 | const stageZeroDefaultHighlight = {type: "edge", id: 3}; 7 | 8 | function flipEdge(mesh, e, stage) { 9 | const e5 = e.getPrev(); 10 | const e4 = e.getNext(); 11 | const twin = e.getTwin(); 12 | const e1 = twin.getPrev(); 13 | const e0 = twin.getNext(); 14 | if (stage >= 1) { 15 | for (const he of [e0, e1, e4, e5]) { 16 | he.getOrigin().setHalfEdge(he); 17 | } 18 | e1.getFace().setHalfEdge(e1); 19 | e5.getFace().setHalfEdge(e5); 20 | } 21 | if (stage >= 2) { 22 | e.setNext(e5); 23 | e.setPrev(e0); 24 | e.setOrigin(e1.getOrigin()); 25 | e.setFace(e5.getFace()); 26 | twin.setNext(e1); 27 | twin.setPrev(e4); 28 | twin.setOrigin(e5.getOrigin()); 29 | twin.setFace(e1.getFace()); 30 | } 31 | if (stage >= 3) { 32 | e0.setNext(e); 33 | e1.setNext(e4); 34 | e4.setNext(twin); 35 | e5.setNext(e0); 36 | e0.setPrev(e5); 37 | e1.setPrev(twin); 38 | e4.setPrev(e1); 39 | e5.setPrev(e); 40 | } 41 | } 42 | 43 | class EdgeFlipConsistencyChecker extends React.Component { 44 | constructor(props) { 45 | super(props); 46 | 47 | this.handleHoverChange = this.handleHoverChange.bind(this); 48 | 49 | // Stage 0 50 | const mesh = OBJLoader.parse(` 51 | v 0.0 1.0 0.0 52 | v 1.0 1.0 0.0 53 | v 0.0 0.0 0.0 54 | v 1.0 0.0 0.0 55 | f 1 3 4 56 | f 1 4 2 57 | `); 58 | let drawMesh = mesh.copy(); // Mesh to draw 59 | 60 | // Partial edge flip 61 | flipEdge(mesh, mesh.edges[3], props.stage); 62 | // Stage 2 is inconsistent and can't be drawn so we draw the closest 63 | // thing 64 | flipEdge(drawMesh, drawMesh.edges[3], 65 | (props.stage === 2 ? 3 : props.stage)); 66 | 67 | this.state = { 68 | mesh: mesh, 69 | drawMesh: drawMesh, 70 | hover: (props.stage === 0 ? stageZeroDefaultHighlight : null) 71 | }; 72 | } 73 | 74 | initialize(node, props) {} 75 | 76 | handleHoverChange(new_hover) { 77 | if (this.props.stage === 0 && new_hover === null) { 78 | // Special case: we want to highlight the input half-edge when 79 | // nothing else is highlighted 80 | this.setState({hover: stageZeroDefaultHighlight}); 81 | } else { 82 | this.setState({hover: new_hover}); 83 | } 84 | } 85 | 86 | render() { 87 | const { hasError, idyll, updateProps, ...props } = this.props; 88 | return ( 89 |
90 | 93 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | module.exports = EdgeFlipConsistencyChecker; 104 | -------------------------------------------------------------------------------- /posts/half-edge/components/HalfEdgeDiagram.js: -------------------------------------------------------------------------------- 1 | const React = require("react"); 2 | const d3 = require("d3"); 3 | const D3Component = require("idyll-d3-component"); 4 | import {Vec3} from "./util/Vec3"; 5 | import {Palette} from "./util/Color"; 6 | 7 | const animDuration = 500; 8 | const margin = {top: 27, right: 27, bottom: 27, left: 27}; 9 | const canvasWidth = 600; 10 | const canvasHeight = 0.666667 * canvasWidth; 11 | const width = canvasWidth - margin.left - margin.right; 12 | const height = canvasHeight - margin.top - margin.bottom; 13 | const subShift = "3px"; 14 | 15 | function half_edge_class(e) { 16 | return (e.getFace() === undefined ? "boundary edge" : "interior edge"); 17 | } 18 | 19 | export class HalfEdgeDiagram extends D3Component { 20 | 21 | initialize(node, props) { 22 | if (typeof props.mesh === "string") { 23 | console.error(props.mesh); 24 | return; 25 | } 26 | 27 | const vertices = this.props.mesh.vertices; 28 | const edges = this.props.mesh.edges; 29 | const faces = this.props.mesh.faces; 30 | 31 | let svg = (this.svg = d3.select(node).append('svg')); 32 | svg = svg 33 | .attr("viewBox", `0 0 ${canvasWidth} ${canvasHeight}`) 34 | .attr("class", "half-edge-diagram"); 35 | 36 | // Define arrow-heads 37 | svg 38 | .append("svg:defs") 39 | .append("svg:marker") 40 | .attr("id", "head_red") 41 | .attr("orient", "auto") 42 | .attr("markerWidth", "30") 43 | .attr("markerHeight", "30") 44 | .attr("refX", "7") 45 | .attr("refY", "4") 46 | .append("path") 47 | .attr("d", "M 0 0 8 4.25 0 4.25") 48 | .style("fill", Palette.boundary); 49 | svg 50 | .append("svg:defs") 51 | .append("svg:marker") 52 | .attr("id", "head_blue") 53 | .attr("orient", "auto") 54 | .attr("markerWidth", "30") 55 | .attr("markerHeight", "30") 56 | .attr("refX", "7") 57 | .attr("refY", "4") 58 | .append("path") 59 | .attr("d", "M 0 0 8 4.25 0 4.25") 60 | .style("fill", Palette.interior); 61 | svg 62 | .append("svg:defs") 63 | .append("svg:marker") 64 | .attr("id", "head_orange") 65 | .attr("orient", "auto") 66 | .attr("markerWidth", "30") 67 | .attr("markerHeight", "30") 68 | .attr("refX", "7") 69 | .attr("refY", "4") 70 | .append("path") 71 | .attr("d", "M 0 0 8 4.25 0 4.25") 72 | .style("fill", Palette.hover); 73 | svg = svg 74 | .append("g") 75 | .attr("transform", `translate(${margin.left}, ${margin.top})`); 76 | 77 | this.x = d3.scaleLinear().range([0, width]); 78 | this.y = d3.scaleLinear().range([height, 0]); 79 | 80 | this.update(props, undefined); 81 | } 82 | 83 | update(props, oldProps) { 84 | if (typeof props.mesh === "string") { 85 | const svg = this.svg.select("g"); 86 | svg.selectAll("*").remove(); 87 | svg.append("text") 88 | .attr("x", 0) 89 | .attr("y", 0) 90 | .attr("class", "error") 91 | .text(props.mesh); 92 | return; 93 | } 94 | 95 | const vertices = props.mesh.vertices; 96 | const edges = props.mesh.edges; 97 | const faces = props.mesh.faces; 98 | 99 | // Try to do "equal axis" axes -- i.e. 10 pixels in horizontal direction 100 | // corresponds to the same distance in vertex coordinate space as 10 101 | // pixels in vertical direction 102 | let x_extent = d3.extent(vertices, (d) => d.getPosition().x()); 103 | let y_extent = d3.extent(vertices, (d) => d.getPosition().y()); 104 | const norm_x_range = 105 | (x_extent[1] - x_extent[0]) * (canvasHeight / canvasWidth); 106 | if (norm_x_range > y_extent[1] - y_extent[0]) { 107 | const y_centre = 0.5 * (y_extent[0] + y_extent[1]); 108 | y_extent = [ 109 | y_centre - 0.5 * norm_x_range, 110 | y_centre + 0.5 * norm_x_range, 111 | ]; 112 | } else { 113 | const x_centre = 0.5 * (x_extent[0] + x_extent[1]); 114 | const norm_y_range = 115 | (y_extent[1] - y_extent[0]) * (canvasWidth / canvasHeight); 116 | x_extent = [ 117 | x_centre - 0.5 * norm_y_range, 118 | x_centre + 0.5 * norm_y_range, 119 | ]; 120 | } 121 | this.figure_scale = x_extent[1] - x_extent[0]; 122 | this.x.domain(x_extent); 123 | this.y.domain(y_extent); 124 | 125 | const svg = this.svg.select("g"); 126 | svg.selectAll(".error").remove(); // clear error message 127 | 128 | const vertex = svg.selectAll(".vertex").data(vertices); 129 | vertex.exit().remove(); 130 | const vertex_enter = 131 | vertex.enter() 132 | .append("g") 133 | .attr("class", "vertex") 134 | .attr("id", (v) => "vertex" + v.getId()) 135 | .on("mouseover", (v, i) => { 136 | props.onHoverChange({type: "vertex", id: i}); 137 | }) 138 | .on("mouseout", (v, i) => { 139 | props.onHoverChange(null); 140 | }); 141 | vertex_enter 142 | .append("circle") 143 | .attr("r", 14) 144 | .attr("cx", (v) => this.x(v.getPosition().x())) 145 | .attr("cy", (v) => this.y(v.getPosition().y())); 146 | vertex_enter 147 | .append("text") 148 | .attr("x", (v) => this.x(v.getPosition().x())) 149 | .attr("y", (v) => this.y(v.getPosition().y())) 150 | .html((v) => `v${v.getId() + 1}`) 151 | .attr("class", "vertex-label"); 152 | const vertex_merge = vertex.merge(vertex_enter); 153 | vertex_merge 154 | .selectAll("circle") 155 | .transition().duration(animDuration) 156 | // TODO: Bizarre, but v is a copy of the old value for some reason 157 | .attr("cx", (v) => this.x(vertices[v.getId()].getPosition().x())) 158 | .attr("cy", (v) => this.y(vertices[v.getId()].getPosition().y())); 159 | vertex_merge 160 | .selectAll("text") 161 | .transition().duration(animDuration) 162 | .attr("x", (v) => this.x(vertices[v.getId()].getPosition().x())) 163 | .attr("y", (v) => this.y(vertices[v.getId()].getPosition().y())); 164 | 165 | const edge = svg.selectAll(".edge").data(edges); 166 | edge.exit().remove(); 167 | const edge_enter = 168 | edge.enter() 169 | .append("g") 170 | .attr("class", (e) => half_edge_class(e)) 171 | .attr("id", (e) => "edge" + e.getId()) 172 | .on("mouseover", (e, i) => { 173 | props.onHoverChange({type: "edge", id: i}); 174 | }) 175 | .on("mouseout", (e, i) => { 176 | props.onHoverChange(null); 177 | }); 178 | edge_enter 179 | .append("line") 180 | .attr("x1", (e) => this.x(this.getArrowStartX(e))) 181 | .attr("y1", (e) => this.y(this.getArrowStartY(e))) 182 | .attr("x2", (e) => this.x(this.getArrowEndX(e))) 183 | .attr("y2", (e) => this.y(this.getArrowEndY(e))); 184 | edge_enter 185 | .append("text") 186 | .attr("x", (e) => this.x(this.getArrowMiddleX(e) + this.getArrow(e)[3].x())) 187 | .attr("y", (e) => this.y(this.getArrowMiddleY(e) + this.getArrow(e)[3].y())) 188 | .html((e) => `e${e.getId()}`); 189 | const edge_merge = edge.merge(edge_enter) 190 | .attr("class", (e) => half_edge_class(e)); 191 | edge_merge 192 | .selectAll("line") 193 | .transition().duration(animDuration) 194 | .attr("x1", (e) => this.x(this.getArrowStartX(edges[e.getId()]))) 195 | .attr("y1", (e) => this.y(this.getArrowStartY(edges[e.getId()]))) 196 | .attr("x2", (e) => this.x(this.getArrowEndX(edges[e.getId()]))) 197 | .attr("y2", (e) => this.y(this.getArrowEndY(edges[e.getId()]))); 198 | edge_merge 199 | .selectAll("text") 200 | .transition().duration(animDuration) 201 | .attr("x", (e) => 202 | (this.x(this.getArrowMiddleX(edges[e.getId()]) 203 | + this.getArrow(edges[e.getId()])[3].x()))) 204 | .attr("y", (e) => 205 | (this.y(this.getArrowMiddleY(edges[e.getId()]) 206 | + this.getArrow(edges[e.getId()])[3].y()))); 207 | 208 | const face = svg.selectAll(".face").data(faces); 209 | face.exit().remove(); 210 | const face_enter = 211 | face.enter() 212 | .append("text") 213 | .attr("x", (f) => this.x(this.getArrow(f.getHalfEdge())[2].x())) 214 | .attr("y", (f) => this.y(this.getArrow(f.getHalfEdge())[2].y())) 215 | .html((f) => `f${f.getId()}`) 216 | .attr("class", "face") 217 | .attr("id", (f) => `face${f.getId()}`) 218 | .on("mouseover", (f, i) => { 219 | props.onHoverChange({type: "face", id: i}); 220 | }) 221 | .on("mouseout", (f, i) => { 222 | props.onHoverChange(null); 223 | }); 224 | face.merge(face_enter) 225 | .transition().duration(animDuration) 226 | .attr("x", (f) => this.x(this.getArrow(f.getHalfEdge())[2].x())) 227 | .attr("y", (f) => this.y(this.getArrow(f.getHalfEdge())[2].y())); 228 | 229 | svg.selectAll(".hover").classed("hover", false); 230 | if (props.hover) { 231 | if (props.hover.type === "vertex" || props.hover.type === "edge") { 232 | svg.select(`#${props.hover.type}${props.hover.id}`) 233 | .classed("hover", true); 234 | } else if (props.hover.type === "face") { 235 | svg.select(`#face${props.hover.id}`).classed("hover", true); 236 | let it = faces[props.hover.id].getHalfEdge(); 237 | const start_it = it; 238 | do { 239 | svg.select(`#edge${it.getId()}`).classed("hover", true); 240 | it = it.getNext(); 241 | } while (it !== start_it); 242 | } 243 | } 244 | } 245 | 246 | getArrow(e) { 247 | let start = e.getOrigin().getPosition(); 248 | start.setZ(0); // ignore Z 249 | let end = e.getTwin().getOrigin().getPosition(); 250 | end.setZ(0); 251 | const direction = end.subtract(start).normalized(); 252 | 253 | // Compute a normal vector, to do offset 254 | let normal = new Vec3(direction.y(), -direction.x(), 0); 255 | let startIt = e; 256 | if (e.getFace() === undefined) { 257 | startIt = e.getTwin(); 258 | } 259 | let it = startIt; 260 | const faceVerts = []; 261 | do { 262 | faceVerts.push(it.getOrigin().getPosition()); 263 | it = it.getNext(); 264 | } while (it !== startIt); 265 | let faceCentroid = new Vec3(0, 0, 0); 266 | for (let pos of faceVerts) { 267 | faceCentroid = faceCentroid.add(pos); 268 | } 269 | faceCentroid = faceCentroid.multiply(1 / faceVerts.length); 270 | const midPoint = start.add(end).multiply(0.5); 271 | let toCentroid = faceCentroid.subtract(midPoint); 272 | if (normal.dot(toCentroid) < 0) { 273 | normal = normal.multiply(-1); 274 | } 275 | if (e.getFace() === undefined) { 276 | normal = normal.multiply(-1); 277 | } 278 | 279 | // Offset a little bit by normal vector 280 | const offset = normal.multiply(0.01 * this.figure_scale); 281 | start = start.add(offset); 282 | end = end.add(offset); 283 | 284 | const padding = direction.multiply(0.04 * this.figure_scale); 285 | const edgeLabelOffset = offset.multiply(2.75); 286 | return [start.add(padding), end.subtract(padding), 287 | faceCentroid, edgeLabelOffset]; 288 | 289 | } 290 | 291 | getArrowStartX(e) { 292 | return this.getArrow(e)[0].x(); 293 | } 294 | getArrowStartY(e) { 295 | return this.getArrow(e)[0].y(); 296 | } 297 | getArrowEndX(e) { 298 | return this.getArrow(e)[1].x(); 299 | } 300 | getArrowEndY(e) { 301 | return this.getArrow(e)[1].y(); 302 | } 303 | 304 | getArrowMiddleX(e) { 305 | return (this.getArrow(e)[0].x() + this.getArrow(e)[1].x()) / 2 306 | } 307 | 308 | getArrowMiddleY(e) { 309 | return (this.getArrow(e)[0].y() + this.getArrow(e)[1].y()) / 2 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /posts/half-edge/components/HalfEdgeStepper.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const OBJLoader = require('./util/OBJLoader'); 3 | import {HalfEdgeDiagram} from './HalfEdgeDiagram'; 4 | 5 | function startingState(type) { 6 | if (type === "ccw_face") { 7 | const mesh = OBJLoader.parse(` 8 | v 1.0 4.0 0.0 9 | v 3.0 4.0 0.0 10 | v 0.0 2.0 0.0 11 | v 4.0 2.0 0.0 12 | v 1.0 0.0 0.0 13 | v 3.0 0.0 0.0 14 | f 1 3 5 6 4 2`); 15 | const hover = {type: "edge", id: 5}; 16 | return {mesh: mesh, hover: hover}; 17 | } else { 18 | const mesh = OBJLoader.parse(` 19 | v 1.0 4.0 0.0 20 | v 3.0 4.0 0.0 21 | v 0.0 2.0 0.0 22 | v 2.0 2.0 0.0 23 | v 4.0 2.0 0.0 24 | v 1.0 0.0 0.0 25 | v 3.0 0.0 0.0 26 | f 1 3 6 4 27 | f 1 4 2 28 | f 2 4 5 29 | f 4 7 5`); 30 | const hover = {type: "edge", id: 8}; 31 | return {mesh: mesh, hover: hover}; 32 | } 33 | } 34 | 35 | function buttonText(type) { 36 | if (type === "ccw_face") { 37 | return "he = he.next"; 38 | } else if (type === "ccw_vertex") { 39 | return "he = he.prev.twin"; 40 | } else if (type === "cw_vertex") { 41 | return "he = he.twin.next"; 42 | } else { 43 | return "???"; 44 | } 45 | } 46 | 47 | class HalfEdgeStepper extends React.Component { 48 | constructor(props) { 49 | super(props); 50 | 51 | this.state = startingState(props.type); 52 | // Since the user might hit the button before the animation is complete, 53 | // we need to maintain a queue of things to do 54 | this.actions = []; 55 | } 56 | 57 | initialize(node, props) {} 58 | 59 | step() { 60 | // There is technically a race condition here... 61 | const doActionsAfterQueueing = (this.actions.length === 0); 62 | 63 | if (this.props.type === "ccw_face") { 64 | this.actions.push(() => { 65 | let he = this.state.mesh.edges[this.state.hover.id]; 66 | he = he.getNext(); 67 | this.setState({hover: {type: "edge", id: he.getId()}}); 68 | }); 69 | } else if (this.props.type === "ccw_vertex") { 70 | this.actions.push(() => { 71 | let he = this.state.mesh.edges[this.state.hover.id]; 72 | he = he.getPrev(); 73 | this.setState({hover: {type: "edge", id: he.getId()}}); 74 | }); 75 | this.actions.push(() => { 76 | let he = this.state.mesh.edges[this.state.hover.id]; 77 | he = he.getTwin(); 78 | this.setState({hover: {type: "edge", id: he.getId()}}); 79 | }); 80 | } else if (this.props.type === "cw_vertex") { 81 | this.actions.push(() => { 82 | let he = this.state.mesh.edges[this.state.hover.id]; 83 | he = he.getTwin(); 84 | this.setState({hover: {type: "edge", id: he.getId()}}); 85 | }); 86 | this.actions.push(() => { 87 | let he = this.state.mesh.edges[this.state.hover.id]; 88 | he = he.getNext(); 89 | this.setState({hover: {type: "edge", id: he.getId()}}); 90 | }); 91 | } 92 | 93 | if (doActionsAfterQueueing) { 94 | this.animate_step(); 95 | } 96 | } 97 | 98 | animate_step() { 99 | if (this.actions.length > 0) { 100 | const todo = this.actions.shift(); 101 | todo(); 102 | if (this.actions.length > 0) { 103 | // Queue next action 104 | setTimeout(this.animate_step.bind(this), 450); 105 | } 106 | } 107 | } 108 | 109 | randomize() { 110 | this.actions = []; 111 | this.setState({hover: { 112 | type: "edge", 113 | id: Math.floor(Math.random() * this.state.mesh.edges.length) 114 | }}); 115 | } 116 | 117 | render() { 118 | const { hasError, idyll, updateProps, ...props } = this.props; 119 | return ( 120 |
121 | {}} /> 124 | 127 | {(this.props.randomize 128 | ? (  129 | 132 | ) 133 | : undefined)} 134 |
135 | ); 136 | } 137 | } 138 | 139 | module.exports = HalfEdgeStepper; 140 | -------------------------------------------------------------------------------- /posts/half-edge/components/HalfEdgeTables.js: -------------------------------------------------------------------------------- 1 | import { throws } from 'assert'; 2 | import { Vertex } from './util/Mesh'; 3 | 4 | const React = require('react'); 5 | 6 | function get_vertex_class_name(props, vertex_id) { 7 | return ((props.hover && props.hover.type === "vertex" 8 | && vertex_id === props.hover.id) 9 | ? "hover" : ""); 10 | } 11 | 12 | function get_edge_class_name(props, edge) { 13 | var edgeType = edge.getFace() !== undefined ? 'interior' : 'boundary'; 14 | 15 | if (props.hover && props.hover.type === "edge" 16 | && edge.getId() === props.hover.id) { 17 | return "hover " + edgeType; 18 | } else { 19 | return edgeType; 20 | } 21 | 22 | } 23 | 24 | function get_face_class_name(props, face_id) { 25 | return ((props.hover && props.hover.type === "face" 26 | && props.hover.id === face_id) 27 | ? "hover" : ""); 28 | } 29 | 30 | function VertexTable(props) { 31 | const rows = props.mesh.vertices.map((v) => { 32 | const p = v.getPosition(); 33 | const id = v.getId(); 34 | const coordinate = ({p.x()}, {p.y()}, {p.z()}); 35 | const vertex = v{id + 1}; 36 | const edge = 37 | (v.getHalfEdge() !== undefined 38 | ? e{v.getHalfEdge().getId()} 39 | : ); 40 | const edge_id = 41 | (v.getHalfEdge() !== undefined 42 | ? v.getHalfEdge().getId() 43 | : undefined); 44 | const vertex_class_name = get_vertex_class_name(props, v.getId()); 45 | let edge_class_name = (v.getHalfEdge() !== undefined ? get_edge_class_name(props, v.getHalfEdge()) : ""); 46 | if (props.stage === 1) { 47 | edge_class_name += " changed"; 48 | } 49 | 50 | const on_edge = 51 | (edge_id !== undefined 52 | ? props.onChange.bind(props, {type: "edge", id: edge_id}) 53 | : undefined); 54 | 55 | return ( 56 | 57 | {vertex} 60 | {coordinate} 61 | {edge} 64 | 65 | ); 66 | }); 67 | 68 | return ( 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {rows} 78 |
VertexCoordinateIncident edge
79 | ); 80 | } 81 | 82 | function FaceTable(props) { 83 | const rows = props.mesh.faces.map((f) => { 84 | const id = f.getId(); 85 | const face = f{id}; 86 | // Note faces, unlike vertices, are guaranteed to have a half-edge 87 | const edge = 88 | (e{f.getHalfEdge().getId()}); 89 | const face_class_name = get_face_class_name(props, id); 90 | let edge_class_name = 91 | (f.getHalfEdge() !== undefined 92 | ? get_edge_class_name(props, f.getHalfEdge()) : ""); 93 | if (props.stage === 1) { 94 | edge_class_name += " changed"; 95 | } 96 | 97 | return ( 98 | 99 | {face} 102 | {edge} 105 | 106 | ); 107 | }); 108 | 109 | return ( 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | {rows} 118 |
FaceHalf-edge
119 | ); 120 | } 121 | 122 | function HalfEdgeTable(props) { 123 | const rows = props.mesh.edges.map((e) => { 124 | const id = e.getId(); 125 | const edge_text = e{id}; 126 | const origin_text = v{e.getOrigin().getId() + 1} 127 | const twin_text = e{e.getTwin().getId()}; 128 | const face_text = 129 | (e.getFace() !== undefined 130 | ? f{e.getFace().getId()} 131 | : ); 132 | const next_text = e{e.getNext().getId()}; 133 | const prev_text = e{e.getPrev().getId()}; 134 | 135 | const edge_class_name = get_edge_class_name(props, e); 136 | let origin_class_name = 137 | get_vertex_class_name(props, e.getOrigin().getId()); 138 | let twin_class_name = get_edge_class_name(props, e.getTwin()); 139 | let face_class_name = 140 | get_face_class_name(props, (e.getFace() !== undefined 141 | ? e.getFace().getId() 142 | : undefined)); 143 | let next_class_name = get_edge_class_name(props, e.getNext()); 144 | let prev_class_name = get_edge_class_name(props, e.getPrev()); 145 | if (props.stage === 2 && (id === 2 || id === 3)) { 146 | origin_class_name += " changed"; 147 | face_class_name += " changed"; 148 | next_class_name += " changed"; 149 | prev_class_name += " changed"; 150 | } else if (props.stage === 3 && id < 6 && id !== 2 && id !== 3) { 151 | next_class_name += " changed"; 152 | prev_class_name += " changed"; 153 | } 154 | if (props.check) { 155 | // no check for face, origin, since the reverse map is arbitrary 156 | if (e !== e.getTwin().getTwin()) 157 | twin_class_name += " inconsistent"; 158 | if (e !== e.getNext().getPrev()) 159 | next_class_name += " inconsistent"; 160 | if (e !== e.getPrev().getNext()) 161 | prev_class_name += " inconsistent"; 162 | } 163 | 164 | const on_face = 165 | (e.getFace() !== undefined 166 | ? props.onChange.bind(props, {type: "face", id: e.getFace().getId()}) 167 | : undefined); 168 | 169 | return ( 170 | 171 | {edge_text} 174 | {origin_text} 177 | {twin_text} 180 | {face_text} 183 | {next_text} 186 | {prev_text} 189 | 190 | ); 191 | }); 192 | return ( 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | {rows} 205 |
Half-edgeOriginTwinIncident faceNextPrev
206 | ); 207 | } 208 | 209 | export class HalfEdgeTables extends React.Component { 210 | constructor(props) { 211 | super(props); 212 | this.onChange = this.onChange.bind(this); 213 | this.onChangeOut = this.onChangeOut.bind(this); 214 | } 215 | 216 | onChange(h) { 217 | this.props.onHoverChange(h); 218 | } 219 | 220 | onChangeOut() { 221 | this.props.onHoverChange(null); 222 | } 223 | 224 | pinVis() { 225 | if (document.getElementById("pin").checked) { 226 | document.querySelector(".pin-container").classList.add("pinned"); 227 | } else { 228 | document.querySelector(".pin-container").classList.remove("pinned"); 229 | } 230 | } 231 | 232 | render() { 233 | const { hasError, idyll, updateProps, ...props } = this.props; 234 | if (typeof this.props.mesh === 'string') { 235 | return ( 236 |
237 |

Records

238 |
239 | ); 240 | } 241 | return ( 242 |
243 |

Records

244 | {props.allow_pinning ? 245 | (
246 | 248 | 249 |
) : undefined} 250 |
251 | 257 |
258 |
259 | 265 |
266 |
267 | 273 |
274 |
275 | ); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /posts/half-edge/components/HalfEdgeVis.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const OBJLoader = require('./util/OBJLoader'); 3 | import {OBJEditor} from './OBJEditor'; 4 | import {HalfEdgeDiagram} from './HalfEdgeDiagram'; 5 | import {HalfEdgeTables} from './HalfEdgeTables'; 6 | 7 | class HalfEdgeVis extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.handleOBJChange = this.handleOBJChange.bind(this); 12 | this.handleHoverChange = this.handleHoverChange.bind(this); 13 | 14 | this.state = { 15 | obj: `# Enter your mesh definition in OBJ format below... 16 | v 1.0 4.0 0.0 17 | v 3.0 4.0 0.0 18 | v 0.0 2.0 0.0 19 | v 2.0 2.0 0.0 20 | v 4.0 2.0 0.0 21 | v 1.0 0.0 0.0 22 | v 3.0 0.0 0.0 23 | f 1 3 4 24 | f 1 4 2 25 | f 2 4 5 26 | f 3 6 4 27 | f 4 6 7 28 | f 4 7 5 29 | `, 30 | mesh: null, 31 | hover: null, 32 | }; 33 | 34 | this.reloadMesh(this.state.obj); 35 | } 36 | 37 | initialize(node, props) {} 38 | 39 | handleOBJChange(text) { 40 | this.setState({obj: text}); 41 | 42 | this.reloadMesh(text); 43 | } 44 | 45 | handleHoverChange(new_hover) { 46 | this.setState({hover: new_hover}); 47 | } 48 | 49 | render() { 50 | const { hasError, idyll, updateProps, ...props } = this.props; 51 | return ( 52 |
53 |
54 | 56 | 59 |
60 | 64 |
65 | ); 66 | } 67 | 68 | reloadMesh(obj_text) { 69 | const mesh = OBJLoader.parse(obj_text); 70 | if (this.state.mesh === null) { 71 | this.state.mesh = mesh; 72 | } else { 73 | this.setState({mesh: mesh}); 74 | } 75 | } 76 | } 77 | 78 | module.exports = HalfEdgeVis; 79 | -------------------------------------------------------------------------------- /posts/half-edge/components/OBJEditor.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | export class OBJEditor extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.onChange = this.onChange.bind(this); 8 | } 9 | 10 | onChange(e) { 11 | this.props.onOBJChange(e.target.value); 12 | } 13 | 14 | render() { 15 | const { hasError, idyll, updateProps, ...props } = this.props; 16 | return ( 17 | 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /posts/half-edge/components/Raw.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * Sometimes, the most straightforward thing to do is to write raw HTML, 5 | * especially if tight control is needed or to work around the many bizarre 6 | * quirks in Idyll's parser. This component helps achieve that. 7 | * 8 | * Usage: 9 | * 10 | * [Raw] 11 | * ``` 12 | *
Look, raw HTML
! 13 | * ``` 14 | * [/Raw] 15 | * 16 | * [Raw]`Raw span.`[/Raw] 17 | * 18 | * Note that we wrap the HTML in backticks so that Idyll's parser doesn't kick 19 | * in and mess with the markup. 20 | */ 21 | class Raw extends React.PureComponent { 22 | render() { 23 | let inner; 24 | if (this.props.children[0].type === 'pre') { 25 | // unwrap
, then unwrap 
26 |             inner = this.props.children[0].props.children[0].props.children[0];
27 |         } else {
28 |             // unwrap 
29 |             inner = this.props.children[0].props.children[0];
30 |         }
31 |         return (
32 |             
33 |         );
34 |     }
35 | }
36 | 
37 | Raw._idyll = {
38 |     name: "Raw",
39 |     tagType: "open"
40 | }
41 | 
42 | export default Raw;
43 | 


--------------------------------------------------------------------------------
/posts/half-edge/components/TriangleCaption.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | 
 3 | function hover(id) {
 4 |     document.querySelector(`.triangle-fig [id^='${id}']`)
 5 |         .setAttribute("class", "hover");
 6 | }
 7 | 
 8 | function clear(id) {
 9 |     document.querySelector(`.triangle-fig [id^='${id}']`)
10 |         .setAttribute("class", "");
11 | }
12 | 
13 | class TriangleCaption extends React.PureComponent {
14 |     render() {
15 |         return (
16 |             
17 |                 Visualization of a half-edge h, along with its{' '}
18 |                  hover("twin")}
20 |                       onMouseOut={() => clear("twin")}>
21 |                     twin
22 |                 ,{' '}
23 |                  hover("next")}
25 |                       onMouseOut={() => clear("next")}>
26 |                     next
27 |                 , and{' '}
28 |                  hover("prev")}
30 |                       onMouseOut={() => clear("prev")}>
31 |                     previous
32 |                  half-edges.{' '}
33 |                 h also stores references to its{' '}
34 |                  hover("origin")}
36 |                       onMouseOut={() => clear("origin")}>
37 |                     origin vertex
38 |                  and{' '}
39 |                  hover("incident-face")}
41 |                       onMouseOut={() => clear("incident-face")}>
42 |                     incident face
43 |                 .
44 |             
45 |         );
46 |     }
47 | }
48 | 
49 | TriangleCaption._idyll = {
50 |     name: "TriangleCaption",
51 |     tagType: "closed"
52 | }
53 | 
54 | export default TriangleCaption;
55 | 


--------------------------------------------------------------------------------
/posts/half-edge/components/util/Color.js:
--------------------------------------------------------------------------------
1 | // Must sync these with the colours in styles.css
2 | export const Palette = {
3 |     boundary: "#c70a2d",
4 |     interior: "#0a85c7",
5 |     hover: "#ed9907",
6 | };
7 | 


--------------------------------------------------------------------------------
/posts/half-edge/components/util/Mesh.js:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Half-edge data structure.
  3 |  *
  4 |  * Based on the code from UBC CPSC 424's Assignment 7.
  5 |  */
  6 | 
  7 | "use strict";
  8 | 
  9 | import {Vec3} from "./Vec3.js";
 10 | 
 11 | const assert = require('assert');
 12 | 
 13 | export class Vertex {
 14 |     constructor(x, y, z, idx) {
 15 |         this.position = new Vec3(x, y, z);
 16 |         this.id = idx;
 17 |     }
 18 | 
 19 |     getId() { return this.id; }
 20 | 
 21 |     getPosition() { return this.position; }
 22 | 
 23 |     setPosition(new_x, new_y, new_z) {
 24 |         this.position.value[0] = new_x;
 25 |         this.position.value[1] = new_y;
 26 |         this.position.value[2] = new_z;
 27 |     }
 28 | 
 29 |     getHalfEdge() { return this.he; }
 30 | 
 31 |     setHalfEdge(e) { this.he = e; }
 32 | 
 33 |     copy() {
 34 |         return new Vertex(this.position.value[0], this.position.value[1],
 35 |                           this.position.value[2], this.id);
 36 |     }
 37 | }
 38 | 
 39 | export class HalfEdge {
 40 |     constructor(idx) {
 41 |         this.id = idx;
 42 |     }
 43 | 
 44 |     getId() { return this.id; }
 45 |     getOrigin() { return this.origin; }
 46 |     getTwin() { return this.twin; }
 47 |     getPrev() { return this.prev; }
 48 |     getNext() { return this.next; }
 49 |     getFace() { return this.face; }
 50 | 
 51 |     setOrigin(v) { this.origin = v; }
 52 |     setTwin(e) { this.twin = e; }
 53 |     setPrev(e) { this.prev = e; }
 54 |     setNext(e) { this.next = e; }
 55 |     setFace(f) { this.face = f; }
 56 | 
 57 |     copy() {
 58 |         return new HalfEdge(this.id);
 59 |     }
 60 | }
 61 | 
 62 | export class Face {
 63 |     constructor(idx) {
 64 |         this.id = idx;
 65 |     }
 66 | 
 67 |     getId() { return this.id; }
 68 | 
 69 |     getHalfEdge() { return this.he; }
 70 | 
 71 |     setHalfEdge(e) { this.he = e; }
 72 | 
 73 |     copy() {
 74 |         return new Face(this.id);
 75 |     }
 76 | }
 77 | 
 78 | /**
 79 |  * Half-edge data structure.
 80 |  */
 81 | export class Mesh {
 82 |     constructor () {
 83 |         this.vertices = [];
 84 |         this.edges = [];
 85 |         this.faces = [];
 86 |         this.normals = [];
 87 |         this.edgeMap = new Map();
 88 |     }
 89 | 
 90 |     buildMesh(verts, normals, faces) {
 91 |         this.clear();
 92 | 
 93 |         // Add vertices and vertex normals
 94 |         for (let i = 0; i < verts.length; i++) {
 95 |             this.addVertexPos(verts[i][0], verts[i][1], verts[i][2], i);
 96 | 
 97 |             if (normals.length > 0) {
 98 |                 let n = new Vec3(normals[i][0], normals[i][1], normals[i][2]);
 99 |                 this.normals.push(n);
100 |             }
101 |         }
102 | 
103 |         // Add faces
104 |         for (const f of faces) {
105 |             this.addFaceByVerts(f.map(i => this.vertices[i]));
106 |         }
107 | 
108 |         // Fix boundary half-edges
109 |         for (let i = 0, len = this.edges.length; i < len; ++i) {
110 |             const he = this.edges[i];
111 |             if (he.getTwin() === undefined) {
112 |                 this.addEdge(he.getNext().getOrigin(), he.getOrigin());
113 |             }
114 |         }
115 |         for (const he of this.edges) {
116 |             if (he.getFace() === undefined) {
117 |                 // Boundary half-edges will be missing next and prev info
118 |                 let next = he.getTwin();
119 |                 do {
120 |                     next = next.getPrev().getTwin();
121 |                 } while (next.getFace() !== undefined);
122 |                 he.setNext(next);
123 |                 next.setPrev(he);
124 |             }
125 |         }
126 | 
127 |         this.edgeMap.clear();
128 |     }
129 | 
130 |     clear() {
131 |         this.vertices = [];
132 |         this.edges = [];
133 |         this.faces = [];
134 |         this.normals = [];
135 |         this.edgeMap.clear();
136 |     }
137 | 
138 |     addVertexPos(x, y, z, i) {
139 |         var v = new Vertex(x, y, z, i);
140 |         this.vertices.push(v);
141 |         return this.vertices[this.vertices.length - 1];
142 |     }
143 | 
144 |     addFaceByVerts(verts) {
145 |         const createOrFail = (v1, v2) => {
146 |             if (this.findEdge(v1, v2) !== undefined) {
147 |                 throw Error(`Duplicate half edge between v${v1.getId()} and v${v2.getId()}`);
148 |             }
149 |             return this.addEdge(v1, v2);
150 |         };
151 | 
152 |         const edges = [];
153 |         for (let i = 1; i < verts.length; ++i) {
154 |             edges.push(createOrFail(verts[i -1], verts[i]));
155 |         }
156 |         edges.push(createOrFail(verts[verts.length - 1], verts[0]));
157 | 
158 |         return this._addFaceByHalfEdges(edges);
159 |     }
160 | 
161 |     _addFaceByHalfEdges(edges) {
162 |         // Add the face to the mesh
163 |         const f = this.addFace();
164 | 
165 |         // Initialize face-edge relationship
166 |         f.setHalfEdge(edges[0]);
167 | 
168 |         // Initialize edge-face relationship
169 |         for (const e of edges) {
170 |             e.setFace(f);
171 |         }
172 | 
173 |         // Connect edge cycle around face
174 |         const len = edges.length;
175 |         for (let i = 0; i < len; ++i) {
176 |             edges[i].setNext(edges[(i + 1) % len]);
177 |             edges[i].setPrev(edges[(i - 1 + len) % len]);
178 |         }
179 | 
180 |         return f;
181 |     }
182 | 
183 |     addFace() {
184 |         var f = new Face(this.faces.length);
185 |         this.faces.push(f);
186 |         return f;
187 |     }
188 | 
189 |     addHalfEdge() {
190 |         var he = new HalfEdge(this.edges.length);
191 |         this.edges.push(he);
192 |         return he;
193 |     }
194 | 
195 |     addEdge(v1, v2) {
196 |         var he = this.addHalfEdge();
197 | 
198 |         var key = String(v1.getId()) + "," + String(v2.getId());
199 |         this.edgeMap.set(key, he);
200 | 
201 |         // Associate edge with its origin vertex
202 |         he.setOrigin(v1);
203 |         if (v1.getHalfEdge() === undefined) {
204 |             v1.setHalfEdge(he);
205 |         }
206 | 
207 |         // Associate edge with its twin, if it exists
208 |         var t_he = this.findEdge(v2, v1);
209 |         if (t_he !== undefined) {
210 |             he.setTwin(t_he);
211 |             t_he.setTwin(he);
212 |         }
213 | 
214 |         return he;
215 |     }
216 | 
217 |     findEdge(v1, v2) {
218 |         const key = String(v1.getId()) + "," + String(v2.getId());
219 |         return this.edgeMap.get(key);
220 |     }
221 | 
222 |     getBoundingBox() {
223 |         if (this.vertices.length == 0) return;
224 | 
225 |         var min = this.vertices[0].getPosition().copy();
226 |         var max = this.vertices[0].getPosition().copy();
227 | 
228 |         for (var i = 0; i < this.vertices.length; i++) {
229 |             for (var j = 0; j < 3; j++) {
230 |                 var pos = this.vertices[i].getPosition();
231 | 
232 |                 if (min.value[j] > pos.value[j]) {
233 |                     min.value[j] = pos.value[j];
234 |                 }
235 |                 if (max.value[j] < pos.value[j]) {
236 |                     max.value[j] = pos.value[j];
237 |                 }
238 |             }
239 |         }
240 | 
241 |         return [min, max];
242 |     }
243 | 
244 |     copy() {
245 |         const other = new Mesh();
246 |         // Start by copying everything except for references, which are circular
247 |         for (const v of this.vertices) {
248 |             other.vertices.push(v.copy());
249 |         }
250 |         for (const f of this.faces) {
251 |             other.faces.push(f.copy());
252 |         }
253 |         for (const e of this.edges) {
254 |             other.edges.push(e.copy());
255 |         }
256 |         for (const n of this.normals) {
257 |             other.normals.push(n.copy());
258 |         }
259 | 
260 |         // Update references
261 |         for (const v of this.vertices) {
262 |             const i = v.getId();
263 |             other.vertices[i].setHalfEdge(other.edges[v.getHalfEdge().getId()]);
264 |         }
265 |         for (const f of this.faces) {
266 |             const i = f.getId();
267 |             other.faces[i].setHalfEdge(other.edges[f.getHalfEdge().getId()]);
268 |         }
269 |         for (const e of this.edges) {
270 |             const he = other.edges[e.getId()];
271 |             he.setOrigin(other.vertices[e.getOrigin().getId()]);
272 |             he.setTwin(other.edges[e.getTwin().getId()]);
273 |             if (e.getFace() !== undefined)
274 |                 he.setFace(other.faces[e.getFace().getId()]);
275 |             he.setNext(other.edges[e.getNext().getId()]);
276 |             he.setPrev(other.edges[e.getPrev().getId()]);
277 |         }
278 |         this.edgeMap.forEach((e, key) => {
279 |             other.edgeMap[key] = other.edges[e.getId()];
280 |         });
281 | 
282 |         return other;
283 |     }
284 | 
285 |     checkConsistency() {
286 |         for (const he of this.edges) {
287 |             if (he !== he.getTwin().getTwin()) {
288 |                 console.error("he inconsistent twin");
289 |             }
290 |             if (he.getFace() !== he.getNext().getFace()) {
291 |                 console.error("next face was inconsistent");
292 |             }
293 |             if (he.getFace() !== he.getPrev().getFace()) {
294 |                 console.error("prev face was inconsistent");
295 |             }
296 |             if (he !== he.getPrev().getNext()) {
297 |                 console.error("he inconsistent next");
298 |             }
299 |             if (he !== he.getNext().getPrev()) {
300 |                 console.error("he inconsistent prev");
301 |             }
302 |         }
303 |         for (const v of this.vertices) {
304 |             if (v.getHalfEdge() !== undefined && v !== v.getHalfEdge().getOrigin()) {
305 |                 console.error("v inconsistent he");
306 |             }
307 |         }
308 |         for (const f of this.faces) {
309 |             if (f !== f.getHalfEdge().getFace()) {
310 |                 console.error("f inconsistent he");
311 |             }
312 |         }
313 |     }
314 | }
315 | 


--------------------------------------------------------------------------------
/posts/half-edge/components/util/OBJLoader.js:
--------------------------------------------------------------------------------
  1 | "use strict;"
  2 | 
  3 | import {Mesh} from "./Mesh.js";
  4 | 
  5 | /**
  6 |  * Parse an OBJ file.
  7 |  *
  8 |  * @param   string       str - The contents of an OBJ file
  9 |  * @return  Mesh|string  The mesh constructed from OBJ file, or a string
 10 |  *                       describing an error if an error occurred.
 11 |  */
 12 | export function parse(str) {
 13 |     const vertices = [];
 14 |     const normals = [];
 15 |     const faces = [];
 16 | 
 17 |     const lines = str.trim().split("\n");
 18 |     let state = 0;
 19 |     for (let i = 0; i < lines.length; ++i) {
 20 |         const line = lines[i];
 21 |         const tokens = line.trim().split(/\s+/g);
 22 |         if (tokens.length === 0) {
 23 |             continue;
 24 |         }
 25 |         switch (tokens[0]) {
 26 |             case '#':
 27 |                 // comment
 28 |                 break;
 29 |             case 'v': {
 30 |                 if (state > 0) {
 31 |                     return `l. ${i+1}: Found vertex at unexpected place in file`;
 32 |                 }
 33 |                 if (tokens.length !== 4) {
 34 |                     return `l. ${i+1}: Expected three components per vertex, got ${tokens.length}`;
 35 |                 }
 36 |                 const maybeVec = parseFloats([tokens[1], tokens[2], tokens[3]]);
 37 |                 if (typeof maybeVec === "string") {
 38 |                     return `l. ${i+1}: ${maybeVec}`;
 39 |                 }
 40 |                 vertices.push(maybeVec);
 41 |                 break;
 42 |             }
 43 |             case 'vt':
 44 |                 if (state > 1) {
 45 |                     return `l. ${i+1}: Found texture coordinate at unexpected place in file`;
 46 |                 }
 47 |                 state = 1;
 48 |                 // ignore
 49 |                 break;
 50 |             case 'vn': {
 51 |                 if (state > 2) {
 52 |                     return `l. ${i+1}: Found normal at unexpected place in file`;
 53 |                 }
 54 |                 state = 2;
 55 |                 if (tokens.length !== 4) {
 56 |                     return `l. ${i+1}: Expected three components per normal, got ${tokens.length}`;
 57 |                 }
 58 |                 const maybeVec = parseFloats([tokens[1], tokens[2], tokens[3]]);
 59 |                 if (typeof maybeVec === "string") {
 60 |                     return `l. ${i+1}: ${maybeVec}`;
 61 |                 }
 62 |                 normals.push(maybeVec);
 63 |                 break;
 64 |             }
 65 |             case 'f':
 66 |                 if (state > 3) {
 67 |                     return `l. ${i+1}: Found face at unexpected place in file`;
 68 |                 }
 69 |                 state = 3;
 70 |                 if (tokens.length < 4) {
 71 |                     return `l. ${i+1}: Each face must have at least three vertices`;
 72 |                 }
 73 |                 const face = [];
 74 |                 for (let j = 1; j < tokens.length; ++j) {
 75 |                     face.push(tokens[j].split("/")[0]);
 76 |                 }
 77 | 
 78 |                 for (let j = 0; j < face.length; ++j) {
 79 |                     const index = Number(face[j]);
 80 |                     if (Number.isNaN(index) || !Number.isInteger(index)) {
 81 |                         return `l. ${i+1}: Invalid face index '${face[j]}'`;
 82 |                     }
 83 |                     face[j] = (index >= 0 ? index - 1 : vertices.length + index);
 84 |                     if (face[j] < 0 || face[j] >= vertices.length) {
 85 |                         return `l. ${i+1}: Face index ${face[j]+1} out of bounds`;
 86 |                     }
 87 |                 }
 88 |                 faces.push(face);
 89 |                 break;
 90 |             default:
 91 |                 break;
 92 |         }
 93 |     }
 94 | 
 95 |     const mesh = new Mesh();
 96 |     try {
 97 |         mesh.buildMesh(vertices, normals, faces);
 98 |     } catch (e) {
 99 |         return e.message;
100 |     }
101 |     return mesh;
102 | }
103 | 
104 | function parseFloats(tokens) {
105 |     const values = [];
106 |     for (const t of tokens) {
107 |         const f = Number(t);
108 |         if (Number.isNaN(f)) {
109 |             return `Failed to parse token '${t}' as a number`;
110 |         }
111 |         values.push(f);
112 |     }
113 |     return values;
114 | }
115 | 


--------------------------------------------------------------------------------
/posts/half-edge/components/util/Vec3.js:
--------------------------------------------------------------------------------
  1 | "use strict";
  2 | 
  3 | /**
  4 |  * A 3D vector of floating point values.
  5 |  */
  6 | export function Vec3(dx, dy, dz) {
  7 |     // Components
  8 |     this.value = new Float32Array(3);
  9 | 
 10 |     if (arguments.length >= 1) this.value[0] = dx;
 11 |     if (arguments.length >= 2) this.value[1] = dy;
 12 |     if (arguments.length >= 3) this.value[2] = dz;
 13 | 
 14 |     this.x = function() { return this.value[0]; }
 15 |     this.y = function() { return this.value[1]; }
 16 |     this.z = function() { return this.value[2]; }
 17 | 
 18 |     /**
 19 |      * Return a deep copy of this vector.
 20 |      */
 21 |     this.copy = function() {
 22 |         return new Vec3(this.value[0], this.value[1], this.value[2]);
 23 |     };
 24 | 
 25 |     /**
 26 |      * Set the vector's components.
 27 |      */
 28 |     this.set = function(new_x, new_y, new_z) {
 29 |         this.value[0] = new_x;
 30 |         this.value[1] = new_y;
 31 |         this.value[2] = new_z;
 32 |     };
 33 | 
 34 |     this.setX = function(new_x) {
 35 |         this.value[0] = new_x;
 36 |     };
 37 |     this.setY = function(new_y) {
 38 |         this.value[1] = new_y;
 39 |     };
 40 |     this.setZ = function(new_z) {
 41 |         this.value[2] = new_z;
 42 |     };
 43 | 
 44 |     /**
 45 |      * Return the Euclidean norm of this vector.
 46 |      */
 47 |     this.norm = function() {
 48 |         return Math.sqrt(this.value[0] * this.value[0] +
 49 |                          this.value[1] * this.value[1] +
 50 |                          this.value[2] * this.value[2]);
 51 |     };
 52 | 
 53 |     /**
 54 |      * Return a unit-length vector which points in the same direction.
 55 |      */
 56 |     this.normalized = function() {
 57 |         let length = this.norm();
 58 |         if (Math.abs(length) < 0.0000001) {
 59 |             return this.copy();
 60 |         }
 61 | 
 62 |         let factor = 1.0 / length;
 63 |         let new_x = this.value[0] * factor;
 64 |         let new_y = this.value[1] * factor;
 65 |         let new_z = this.value[2] * factor;
 66 |         return new Vec3(new_x, new_y, new_z);
 67 |     };
 68 | 
 69 |     /**
 70 |      * Return the result of adding  v` to `this`.
 71 |      */
 72 |     this.add = function(v) {
 73 |         var new_x = this.value[0] + v.value[0];
 74 |         var new_y = this.value[1] + v.value[1];
 75 |         var new_z = this.value[2] + v.value[2];
 76 |         return new Vec3(new_x, new_y, new_z);
 77 |     };
 78 | 
 79 |     /**
 80 |      * Return the result of subtracting `v` from `this`.
 81 |      */
 82 |     this.subtract = function(v) {
 83 |         var new_x = this.value[0] - v.value[0];
 84 |         var new_y = this.value[1] - v.value[1];
 85 |         var new_z = this.value[2] - v.value[2];
 86 |         return new Vec3(new_x, new_y, new_z);
 87 |     };
 88 | 
 89 |     /**
 90 |      * Return `this` scaled by scalar `s`.
 91 |      */
 92 |     this.multiply = function(s) {
 93 |         var new_x = this.value[0] * s;
 94 |         var new_y = this.value[1] * s;
 95 |         var new_z = this.value[2] * s;
 96 |         return new Vec3(new_x, new_y, new_z);
 97 |     };
 98 | 
 99 |     /**
100 |      * Return the dot product of `this`` and `other`.
101 |      */
102 |     this.dot = function(other) {
103 |         return (this.value[0] * other.value[0] +
104 |                 this.value[1] * other.value[1] +
105 |                 this.value[2] * other.value[2]);
106 |     }
107 | 
108 |     /**
109 |      * Return true if and only if `this` is mathematically equivalent to
110 |      * `other`.`
111 |      */
112 |     this.equals = function(other) {
113 |         return (this.value[0] == other.value[0] &&
114 |                 this.value[1] == other.value[1] &&
115 |                 this.value[2] == other.value[2] );
116 |     };
117 | }
118 | 


--------------------------------------------------------------------------------
/posts/half-edge/index.idyll:
--------------------------------------------------------------------------------
  1 | [meta title: "Half-Edge Data Structures" /]
  2 | 
  3 | # Half-Edge Data Structures
  4 | 
  5 | [Raw]
  6 | ```
  7 | 
8 | Jerry Yin and Jeffrey Goh
9 | Dec. 10, 2019 10 |
11 | ``` 12 | [/Raw] 13 | 14 | [hr /] 15 | 16 | We can represent discrete surfaces as polygon meshes. Polygon meshes can be 17 | thought of as graphs (which have vertices and edges between vertices) plus a 18 | list of _faces_, where a face is a cycle of edges. 19 | 20 | Below, we specify a mesh as a list of vertices and a list of faces, where each 21 | face is specified as a cycle of vertices. The edges of the mesh are 22 | implied—edges connect adjacent vertices of a face. 23 | 24 | [Equation display:true className:fullWidth] 25 | \begin{aligned} 26 | v_1 &= (1,4) \qquad 27 | v_2 = (3,4) \qquad 28 | v_3 = (0,2) \qquad 29 | v_4 = (2, 2) \\ 30 | v_5 &= (4, 2) \qquad 31 | v_6 = (1, 0) \qquad 32 | v_7 = (3, 0) 33 | \end{aligned} 34 | [/Equation] 35 | [Equation display:true] 36 | V = \{v_1, v_2, v_3, v_4, v_5, v_6, v_7\} 37 | [/Equation] 38 | [Equation display:true] 39 | F = \{(v_1, v_3, v_4), (v_1, v_4, v_2), (v_2, v_4, v_5), 40 | (v_3, v_6, v_4), (v_4, v_6, v_7), (v_4, v_7, v_5)\} 41 | [/Equation] 42 | 43 | The face-list representation is popular for on-disk storage due to its lack of 44 | redundancy, however it is difficult to write algorithms that operate directly on 45 | such a representation. For example, to determine whether or not [Equation]v_6[/Equation] and [Equation]v_3[/Equation] are connected, we must 46 | iterate through the face list until we find (or fail to find) the edge we are 47 | looking for. 48 | 49 | [aside] 50 | [SVG src:"static/images/triangle.svg" className:"triangle-fig"/] 51 | 52 | [TriangleCaption /] 53 | [/aside] 54 | 55 | A popular data structure which can answer such queries in constant time is the _half-edge data structure_. In a half-edge data structure, we explicitly store the edges of the mesh by representing each edge with a pair of directed _half-edge twins_, with each of the two half-edges twins pointing in opposite directions. A half-edge stores a reference to its twin, as well as references to the previous and next half-edges along the same face or hole. A vertex stores its position and a reference to an arbitrary half-edge that originates from that vertex, and a face stores an arbitrary half-edge belonging to that face. A half-edge data structure stores arrays of vertex, face, and half-edge records. 56 | 57 | For representing boundary edges (edges adjacent to a hole), we have two options. 58 | We can either represent boundary edges with a single half-edge whose twin 59 | pointer is null, or we can represent boundary edges as a pair of half-edges, 60 | with the half-edge adjacent to the hole having a null face pointer. It turns 61 | out the latter design choice results in much simpler code, since we will soon 62 | see that getting a half-edge's twin is a far more common operation than getting 63 | a half-edge's face, and being able to simply assume that we have a non-null twin 64 | results in far fewer special cases. 65 | 66 | [Raw] 67 | ``` 68 | 69 | ``` 70 | [/Raw] 71 | 72 | Below, we show the half-edge diagram and records table for a more complex mesh. 73 | The mesh vertices and connectivity can be edited in the editor. 74 | 75 | [HalfEdgeVis /] 76 | 77 | 78 | ## Iterating around a face 79 | 80 | Sometimes we need to traverse a face to get all of its vertices or half-edges. 81 | For example, if we wish to compute the centroid of a face, we must find the 82 | positions of the vertices of that face. 83 | 84 | In code, given the face `f`, this will look something like this: 85 | 86 | ``` 87 | start_he = f.halfedge 88 | he = start_he 89 | do { 90 | # do something useful 91 | 92 | he = he.next 93 | } while he != start_he 94 | ``` 95 | 96 | Note that we use a do-while loop instead of a while loop, since we want to check 97 | the condition at the end of the loop iteration. At the start of the first 98 | iteration, `he == start_he`, so if we checked the condition at the start of the 99 | loop, our loop wouldn't run for any iterations. 100 | 101 | [HalfEdgeStepper type:"ccw_face" /] 102 | 103 | To traverse the face in the opposite direction, one can simply replace `he.next` 104 | with `he.prev`. 105 | 106 | ## Iterating around a vertex 107 | 108 | In the last section, we described how to construct a face iterator. Another 109 | useful iterator is the vertex ring iterator. Often, we want to iterate around 110 | the _vertex ring_ (also known as a _vertex umbrella_) around a vertex. More 111 | specifically, we want to iterate through all the half-edges with a given vertex 112 | as its origin. 113 | 114 | In the next two sections we will assume a counter-clockwise winding order for 115 | the faces (which is the default in OpenGL). 116 | 117 | ### Counter-clockwise traversal 118 | 119 | In code, given the vertex `v`, iterating through all the half-edges going out of `v` in counter-clockwise order looks like this: 120 | 121 | ``` 122 | start_he = v.halfedge 123 | he = start_he 124 | do { 125 | # do something useful 126 | 127 | he = he.prev.twin 128 | } while he != start_he 129 | ``` 130 | 131 | [HalfEdgeStepper type:"ccw_vertex" randomize:true /] 132 | 133 | Note that our code still works even if there are boundary half-edges or 134 | non-triangular faces. 135 | 136 | ### Clockwise traversal 137 | 138 | Traversing the vertex ring in clockwise order to very similar to traversing the 139 | ring in counter-clockwise order, except that we replace `he = he.prev.twin` with `he = he.twin.next`. 140 | 141 | [HalfEdgeStepper type:"cw_vertex" randomize:true /] 142 | 143 | 144 | ## Modifying a half-edge data structure 145 | 146 | In the previous section, we discussed how to iterate over a face and over a 147 | vertex ring. Modifying a half-edge data structure is more tricky, because it 148 | can be easy for references to become inconsistent if the records are not 149 | modified properly. 150 | 151 | [aside] 152 | [SVG src:"static/images/edgeflip.svg" className:"edgeflip-fig"/] 153 | 154 | Illustration of the [Raw]`EdgeFlip`[/Raw] algorithm. 155 | [/aside] 156 | 157 | As an exercise, we will walk through how to implement the [Raw]`EdgeFlip`[/Raw] algorithm, which, given a half-edge in the middle of two triangle faces, flips the orientation of the half-edge and its twin. 158 | 159 | We will show the records table at each step of the algorithm. 160 | 161 | We begin with our input half-edge highlighted (either *e[sub]3[/sub]* or *e[sub]2[/sub]* in the below mesh, but let's say *e[sub]3[/sub]*). 162 | 163 | [EdgeFlipConsistencyChecker stage:0 /] 164 | 165 | We first get references to all affected half-edges, since traversing the mesh 166 | whilst it is in an inconsistent state will be difficult. 167 | 168 | ``` 169 | def FlipEdge(HalfEdge e): 170 | e5 = e.prev 171 | e4 = e.next 172 | twin = e.twin 173 | e1 = twin.prev 174 | e0 = twin.next 175 | ``` 176 | 177 | Next, we make sure there's no face or vertex references to `e` or `twin` 178 | (*e[sub]3[/sub]* and *e[sub]2[/sub]* in the diagram), which will we recycle in 179 | the process of performing the edge flip. 180 | 181 | ``` 182 | for he in {e0, e1, e4, e5}: 183 | he.origin.halfedge = &he 184 | e1.face.halfedge = &e1 185 | e5.face.halfedge = &e5 186 | ``` 187 | 188 | These operations are safe to do since the choice of representative half-edge is 189 | arbitrary; the mesh is still in a consistent state. The affected cells are 190 | coloured light blue, although not all cells change to values different from 191 | their old values. 192 | 193 | [EdgeFlipConsistencyChecker stage:1 /] 194 | 195 | Next we recycle `e` and `twin`. We will (arbitrarily) have `e` be the top 196 | diagonal half-edge in the diagram, and `twin` be its twin. We can fill in the 197 | members of `e` and `twin` according to the below diagram. After this, our data 198 | structure will become inconsistent. We outline inconsistent cells in red. 199 | 200 | ``` 201 | e.next = &e5 202 | e.prev = &e0 203 | e.origin = e1.origin 204 | e.face = e5.face 205 | twin.next = &e1 206 | twin.prev = &e4 207 | twin.origin = e5.origin 208 | twin.face = e1.face 209 | ``` 210 | 211 | [EdgeFlipConsistencyChecker stage:2 check:true /] 212 | 213 | We update affected `next` and `prev` references. Again, we can reference the 214 | diagram to fill in these values. 215 | 216 | ``` 217 | e0.next = &e 218 | e1.next = &e4 219 | e4.next = &twin 220 | e5.next = &e0 221 | e0.prev = &e5 222 | e1.prev = &twin 223 | e4.prev = &e1 224 | e5.prev = &e 225 | ``` 226 | 227 | [EdgeFlipConsistencyChecker stage:3 check:true /] 228 | -------------------------------------------------------------------------------- /posts/half-edge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geom-vis-half-edge-article", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "idyll": { 6 | "theme": "none", 7 | "layout": "none", 8 | "css": "styles.css", 9 | "authorView": false, 10 | "output": "../../docs/half-edge/", 11 | "components": [ 12 | "../../components/", 13 | "components" 14 | ] 15 | }, 16 | "dependencies": { 17 | "idyll": "^4.0.0", 18 | "idyll-d3-component": "^2.0.0", 19 | "d3": "^4.0.0" 20 | }, 21 | "devDependencies": { 22 | "gh-pages": "^0.12.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /posts/half-edge/static/LinuxLibertine/LinuxLibertine-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjmiah/interactive-geometry/8efb435f75c641d7c298089aab2b9654d2e7db44/posts/half-edge/static/LinuxLibertine/LinuxLibertine-Italic.woff -------------------------------------------------------------------------------- /posts/half-edge/static/LinuxLibertine/LinuxLibertine-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enjmiah/interactive-geometry/8efb435f75c641d7c298089aab2b9654d2e7db44/posts/half-edge/static/LinuxLibertine/LinuxLibertine-Regular.woff -------------------------------------------------------------------------------- /posts/half-edge/static/images/edgeflip.inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 35 | 36 | 44 | 50 | 51 | 59 | 65 | 66 | 74 | 80 | 81 | 89 | 95 | 96 | 97 | 122 | 124 | 125 | 127 | image/svg+xml 128 | 130 | 131 | 132 | 133 | 134 | 139 | 143 | 148 | 154 | 155 | 160 | 166 | 170 | 175 | 181 | 182 | 185 | 190 | 196 | 197 | 200 | 205 | 211 | 212 | 216 | 221 | 227 | 228 | 235 | 241 | 248 | 255 | 259 | 264 | 270 | 271 | 276 | 282 | 288 | 294 | 298 | 304 | 310 | 311 | 316 | 320 | 325 | 331 | 332 | 337 | 343 | 347 | 352 | 358 | 359 | 362 | 367 | 373 | 374 | 377 | 382 | 388 | 389 | 393 | 398 | 404 | 405 | 412 | 418 | 425 | 432 | 436 | 441 | 447 | 448 | 453 | 459 | 463 | 469 | 475 | 476 | 480 | 486 | 492 | 493 | 494 | 495 | -------------------------------------------------------------------------------- /posts/half-edge/static/images/edgeflip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /posts/half-edge/static/images/triangle.inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 70 | 75 | 81 | 82 | 84 | 89 | 95 | 96 | 100 | 105 | 111 | 112 | 115 | 120 | 126 | 127 | 130 | 135 | 141 | 142 | 146 | 151 | 157 | 158 | 165 | 171 | 178 | h 189 | 190 | 191 | -------------------------------------------------------------------------------- /posts/half-edge/static/images/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | t 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | n 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | p 35 | 36 | 37 | 38 | 39 | 40 | o 41 | 42 | h 43 | 44 | 45 | -------------------------------------------------------------------------------- /posts/half-edge/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Linux Libertine'; 3 | src: url('LinuxLibertine/LinuxLibertine-Regular.woff') format('woff'); 4 | font-display: swap; 5 | } 6 | 7 | @font-face { 8 | font-family: 'Linux Libertine'; 9 | src: url('LinuxLibertine/LinuxLibertine-Italic.woff') format('woff'); 10 | font-style: italic; 11 | font-display: swap; 12 | } 13 | 14 | :root { 15 | /* Must sync these with the colours in util/Color.js */ 16 | --boundary-color: #c70a2d; 17 | --interior-color: #0a85c7; 18 | --hover-color: #ed9907; 19 | --face-label-color: #18ab88; 20 | 21 | --anim-duration: 0.11s; 22 | } 23 | 24 | * { box-sizing: border-box; } 25 | 26 | body { 27 | margin: 0; 28 | padding: 0; 29 | color: #333; 30 | background-color: #f9f9f6; 31 | hyphens: auto; 32 | } 33 | 34 | h1, h2, h3, .idyll-text-container .katex-display, .idyll-text-container p, 35 | .idyll-text-container > img, .idyll-text-container > ol, 36 | .idyll-text-container > ul, .idyll-text-container > pre, 37 | .half-edge-stepper { 38 | max-width: 550px; 39 | } 40 | 41 | .idyll-root { 42 | margin: 80px auto; 43 | padding: 0; 44 | max-width: 900px; 45 | 46 | font-family: "Linux Libertine", "Linux Libertine O", "KaTeX_Main", serif; 47 | font-size: 1.1rem; 48 | line-height: 1.5; 49 | } 50 | 51 | hr { margin: 2em 0; } 52 | 53 | h1 { 54 | margin: 0; 55 | font-weight: normal; 56 | font-style: italic; 57 | font-size: 2.4em; 58 | } 59 | 60 | .subtitle { 61 | margin: 0; 62 | } 63 | 64 | h2, h3 { 65 | margin: 1em 0em 0.5em; 66 | } 67 | 68 | h2 { 69 | font-size: 1.5em; 70 | font-style: italic; 71 | font-weight: normal; 72 | } 73 | 74 | h3 { 75 | font-size: 1.12em; 76 | font-weight: bold; 77 | } 78 | 79 | code { 80 | letter-spacing: -0.03em; 81 | font-stretch: condensed; 82 | hyphens: none; 83 | } 84 | 85 | pre { 86 | padding-left: 2em; 87 | line-height: 120%; 88 | font-size: 0.95em; 89 | } 90 | 91 | .aside-container, aside { 92 | box-sizing: content-box; 93 | float: right; 94 | clear: right; 95 | font-size: 0.88em; 96 | width: 300px; 97 | margin: 0 0 15px 15px; 98 | position: relative; 99 | } 100 | 101 | svg { 102 | width: 100%; 103 | height: auto; 104 | } 105 | 106 | span.katex { font-size: 1em; } 107 | 108 | em > sup, em > sub { 109 | font-style: normal; 110 | } 111 | 112 | .sc { 113 | font-variant: small-caps; 114 | letter-spacing: 0.03em; 115 | margin-right: -0.03em; 116 | } 117 | 118 | a:link, a:visited, a:active, a:hover { color: currentColor; } 119 | 120 | .can-hover { 121 | text-decoration: underline; 122 | text-decoration-style: dashed; 123 | text-decoration-color: #3c94ea; 124 | color: #0e4781; 125 | border-radius: 2px; 126 | cursor: help; 127 | } 128 | 129 | .can-hover:hover { 130 | background-color: #ffbb6688; 131 | } 132 | 133 | ::-moz-selection { background: #bef; } 134 | ::selection { background: #bef; } 135 | 136 | /* Triangle figure */ 137 | 138 | .triangle-fig { 139 | font-size: 0.75em; 140 | font-style: italic; 141 | fill: currentColor; 142 | } 143 | 144 | .triangle-fig text, .triangle-fig circle { 145 | -webkit-transition: fill var(--anim-duration) ease-out; 146 | -moz-transition: fill var(--anim-duration) ease-out; 147 | -o-transition: fill var(--anim-duration) ease-out; 148 | transition: fill var(--anim-duration) ease-out; 149 | } 150 | 151 | .triangle-fig .hover { 152 | fill: #e68000; 153 | } 154 | 155 | .triangle-fig [id^='incident-face'] { 156 | opacity: 0.1; 157 | -webkit-transition: opacity var(--anim-duration) ease-out; 158 | -moz-transition: opacity var(--anim-duration) ease-out; 159 | -o-transition: opacity var(--anim-duration) ease-out; 160 | transition: opacity var(--anim-duration) ease-out; 161 | } 162 | 163 | .triangle-fig .hover[id^='incident-face'] { 164 | opacity: 0.5; 165 | } 166 | 167 | .triangle-fig .line { 168 | stroke: currentColor; 169 | -webkit-transition: stroke var(--anim-duration) ease-out; 170 | -moz-transition: stroke var(--anim-duration) ease-out; 171 | -o-transition: stroke var(--anim-duration) ease-out; 172 | transition: stroke var(--anim-duration) ease-out; 173 | } 174 | 175 | .triangle-fig .hover .line { 176 | stroke: #e68000; 177 | } 178 | 179 | /* Main vis */ 180 | 181 | .half-edge-vis > .pin-container, .consistency-checker { 182 | display: grid; 183 | margin: 32px 0; 184 | width: 100%; 185 | grid-template-columns: repeat(2, 1fr); 186 | grid-gap: 16px; 187 | align-items: center; 188 | } 189 | 190 | .half-edge-vis, .consistency-checker .half-edge-tables { 191 | font-size: 0.84em; 192 | } 193 | 194 | .half-edge-stepper { 195 | margin: 32px 0; 196 | text-align: center; 197 | } 198 | 199 | .pinned { 200 | position: sticky; 201 | top: 0; 202 | left: 0; 203 | margin: 0; 204 | background-color: #f9f9f6; 205 | } 206 | 207 | .half-edge-tables { 208 | width: 100%; 209 | display: grid; 210 | grid-template-columns: 1.618fr 1fr; 211 | grid-gap: 16px; 212 | } 213 | 214 | .consistency-checker .half-edge-tables { 215 | grid-column: 2; 216 | } 217 | 218 | div.vertices, div.faces { 219 | grid-row: 2; 220 | } 221 | 222 | div.half-edges { 223 | grid-column: 1 / 3; 224 | } 225 | 226 | textarea, code { 227 | font-family: "Roboto Mono", Monaco, monospace; 228 | font-size: 0.83em; 229 | } 230 | 231 | textarea { 232 | width: 100%; 233 | height: 100%; 234 | resize: none; 235 | border: 1px solid currentColor; 236 | padding: 10px; 237 | } 238 | 239 | table { 240 | width: 100%; 241 | 242 | border-collapse: collapse; 243 | } 244 | 245 | thead { 246 | font-weight: bold; 247 | } 248 | 249 | th { 250 | background-color: rgba(0, 0, 0, 0.02); 251 | } 252 | 253 | td, th { 254 | border: 1px solid #ccc; 255 | padding: 1px 8px; 256 | text-align: center; 257 | } 258 | 259 | h4 { 260 | margin: 0; 261 | font-size: 1.25em; 262 | font-variant: small-caps; 263 | text-transform: lowercase; 264 | letter-spacing: 0.15em; 265 | } 266 | 267 | .pin-checkbox-container { 268 | text-align: right; 269 | } 270 | 271 | .error { 272 | color: #f30; 273 | fill: currentColor; 274 | } 275 | 276 | sub, sup { 277 | font-size: 0.75em; 278 | } 279 | 280 | td { 281 | -webkit-transition: background-color var(--anim-duration) ease-out; 282 | -moz-transition: background-color var(--anim-duration) ease-out; 283 | -o-transition: background-color var(--anim-duration) ease-out; 284 | transition: background-color var(--anim-duration) ease-out; 285 | } 286 | 287 | td.changed { 288 | background-color: rgba(7, 214, 237, 0.1); 289 | } 290 | 291 | td.inconsistent { 292 | border: 3px solid red; 293 | } 294 | 295 | td.hover { 296 | background-color: rgba(237, 153, 7, 0.5); 297 | } 298 | 299 | td.boundary { 300 | color: var(--boundary-color); 301 | } 302 | 303 | td.interior { 304 | color: var(--interior-color); 305 | } 306 | 307 | .half-edge-diagram text { 308 | cursor: default; 309 | /* set label anchors to centre of text box */ 310 | text-anchor: middle; 311 | dominant-baseline: middle; 312 | font-style: italic; 313 | font-size: 1.3em; 314 | } 315 | 316 | .half-edge-diagram .error { 317 | text-anchor: initial; 318 | dominant-baseline: initial; 319 | } 320 | 321 | .interior.edge, .boundary.edge { 322 | stroke-width: 2; 323 | font-style: italic; 324 | -webkit-transition: stroke var(--anim-duration) ease-out; 325 | -moz-transition: stroke var(--anim-duration) ease-out; 326 | -o-transition: stroke var(--anim-duration) ease-out; 327 | transition: stroke var(--anim-duration) ease-out; 328 | } 329 | 330 | .interior.edge { 331 | stroke: var(--interior-color); 332 | marker-end: url(#head_blue); 333 | } 334 | 335 | .boundary.edge { 336 | stroke: var(--boundary-color); 337 | marker-end: url(#head_red); 338 | } 339 | 340 | .edge.hover { 341 | stroke: var(--hover-color); 342 | marker-end: url(#head_orange); 343 | } 344 | 345 | .boundary.edge text, .interior.edge text { 346 | stroke: none; 347 | -webkit-transition: fill var(--anim-duration) ease-out; 348 | -moz-transition: fill var(--anim-duration) ease-out; 349 | -o-transition: fill var(--anim-duration) ease-out; 350 | transition: fill var(--anim-duration) ease-out; 351 | } 352 | 353 | .boundary.edge text { 354 | fill: var(--boundary-color); 355 | } 356 | 357 | .interior.edge text { 358 | fill: var(--interior-color); 359 | } 360 | 361 | .edge.hover text { 362 | fill: var(--hover-color); 363 | } 364 | 365 | .vertex { 366 | cursor: default; 367 | fill: #444; 368 | -webkit-transition: fill var(--anim-duration) ease-out; 369 | -moz-transition: fill var(--anim-duration) ease-out; 370 | -o-transition: fill var(--anim-duration) ease-out; 371 | transition: fill var(--anim-duration) ease-out; 372 | } 373 | 374 | .vertex-label { 375 | fill: #fff; 376 | font-style: italic; 377 | } 378 | 379 | /* subscript */ 380 | .vertex-label tspan, .face tspan, .edge tspan { 381 | font-size: 0.75em; 382 | font-style: normal; 383 | } 384 | 385 | .vertex.hover { 386 | fill: var(--hover-color); 387 | } 388 | 389 | .face { 390 | fill: var(--face-label-color); 391 | font-style: italic; 392 | } 393 | 394 | .face.hover { 395 | fill: var(--hover-color); 396 | } 397 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | * { box-sizing: border-box; } 2 | 3 | body { 4 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | background-color: #f4f4f4; 6 | } 7 | 8 | .idyll-text-container { 9 | margin: 0 auto; 10 | width: 500px; 11 | } 12 | 13 | header.cover { 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: flex-end; 17 | margin: 64px auto; 18 | width: 250px; 19 | height: 340px; 20 | padding: 26px 18px; 21 | 22 | background: linear-gradient(to right, 23 | rgba(255, 255, 255, 0), 24 | rgba(255, 255, 255, 0) 1%, 25 | rgba(255, 255, 255, 0.07) 5%, 26 | rgba(0, 0, 0, 0.07) 7%, 27 | rgba(0, 0, 0, 0) 11%), 28 | /* linear-gradient(rgba(53, 50, 50, 0), 29 | rgba(53, 50, 50, 0) 59%, 30 | rgba(53, 50, 50, 1) 59%, 31 | rgba(53, 50, 50, 1)), 32 | */ #eb5e33; 33 | box-shadow: 4px 4px 32px rgba(0, 0, 0, 0.4); 34 | border-radius: 2px 6px 6px 2px; 35 | color: #eaeaea; 36 | line-height: 0.835; 37 | text-align: right; 38 | } 39 | 40 | h1 { margin: 10px 0; } 41 | 42 | header .author { font-size: 0.92em; } 43 | -------------------------------------------------------------------------------- /template/components/custom-component.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | class CustomComponent extends React.Component { 4 | render() { 5 | const { hasError, idyll, updateProps, ...props } = this.props; 6 | return ( 7 |
8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | } 24 | 25 | module.exports = CustomComponent; 26 | -------------------------------------------------------------------------------- /template/data/example-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "x": 0, 4 | "y": 0 5 | }, 6 | { 7 | "x": 1, 8 | "y": 1 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /template/gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idyll 61 | build 62 | -------------------------------------------------------------------------------- /template/index.idyll: -------------------------------------------------------------------------------- 1 | [meta title:"{{title}}" description:"Short description of your project" /] 2 | 3 | [Header 4 | fullWidth:true 5 | title:"{{title}}" 6 | subtitle:"Welcome to Idyll. Open index.idyll to start writing" 7 | author:"Your Name Here" 8 | authorLink:"https://idyll-lang.org" 9 | date:`(new Date()).toDateString()` 10 | background:"#222222" 11 | color:"#ffffff" 12 | /] 13 | 14 | ## Introduction 15 | 16 | This is an Idyll post. It is generated via 17 | the file `index.idyll`. To compile this post using 18 | idyll, run the command `idyll` inside of this directory. 19 | 20 | 21 | Idyll posts are designed to support interaction and 22 | data-driven graphics. 23 | 24 | [var name:"state" value:0 /] 25 | [CustomD3Component className:"d3-component" state:state /] 26 | [button onClick:`state++`] 27 | Click Me. 28 | [/button] 29 | 30 | Configuration can be done via the `idyll` field in `package.json`. 31 | 32 | ## Markup 33 | 34 | Idyll is based on Markdown. 35 | 36 | You can use familiar syntax 37 | to create **bold** (`**bold**` ) and *italic* (``*italic*` ) styles, 38 | 39 | * lists 40 | * of 41 | * items, 42 | 43 | ``` 44 | * lists 45 | * of 46 | * items, 47 | ``` 48 | 49 | 1. and numbered 50 | 2. lists 51 | 3. of items, 52 | 53 | 54 | ``` 55 | 1. and numbered 56 | 2. lists 57 | 3. of items, 58 | ``` 59 | 60 | in addition to [hyperlinks](https://idyll-lang.org) and images: 61 | 62 | ![quill](static/images/quill.svg) 63 | 64 | ``` 65 | ![quill](static/images/quill.svg) 66 | ``` 67 | 68 | ## Components 69 | 70 | Components can be embedded using a bracket syntax: 71 | 72 | ``` 73 | [Range /] 74 | ``` 75 | 76 | and can contain nested content: 77 | 78 | ``` 79 | [Equation]e = mc^{2}[/Equation] 80 | ``` 81 | 82 | Components accept properties: 83 | 84 | ``` 85 | [Range value:x min:0 max:1 /] 86 | ``` 87 | 88 | that can be bound to variables to achieve interactivity (more in next section). 89 | 90 | 91 | A variety of components are included by default. See [all the available components](https://idyll-lang.org/docs/components/). You can also use any html tag, for example: `[div] A div! [/div]`. 92 | 93 | To create your own, add it to the `components/` folder. There are examples of how to use Idyll with React and D3 based components already included. 94 | 95 | 96 | 97 | ## Interactivity 98 | 99 | Here is how you can instantiate a variable and bind it to a component: 100 | 101 | [var name:"exampleVar" value:5 /] 102 | 103 | [Range min:0 max:10 value:exampleVar /] 104 | [Display value:exampleVar /] 105 | 106 | ``` 107 | [var name:"exampleVar" value:5 /] 108 | 109 | [Range min:0 max:10 value:exampleVar /] 110 | [Display value:exampleVar /] 111 | ``` 112 | 113 | ## Learn More 114 | 115 | To learn more see the documentation at https://idyll-lang.org/docs/, 116 | join our [chatroom](https://gitter.im/idyll-lang/Lobby), or see the project on [GitHub](https://github.com/idyll-lang/idyll). 117 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idyll-basic-template", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "idyll": { 6 | "theme": "default", 7 | "layout": "centered", 8 | "css": "styles.css", 9 | "authorView": false, 10 | "output": "../../docs/[slug]/", 11 | "components": [ 12 | "../../components/", 13 | "components" 14 | ] 15 | }, 16 | "dependencies": { 17 | "idyll": "^4.0.0", 18 | "idyll-d3-component": "^2.0.0", 19 | "d3": "^4.0.0" 20 | }, 21 | "devDependencies": { 22 | "gh-pages": "^0.12.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /template/static/images/quill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo 5 | Created with Sketch. 6 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /template/styles.css: -------------------------------------------------------------------------------- 1 | .d3-component svg { 2 | background: #ddd; 3 | max-height: 300px; 4 | } 5 | 6 | @media all and (max-width: 1000px) { 7 | .d3-component svg { 8 | max-height: 200px; 9 | } 10 | } 11 | --------------------------------------------------------------------------------