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 |
495 |
--------------------------------------------------------------------------------
/posts/half-edge/static/images/edgeflip.svg:
--------------------------------------------------------------------------------
1 |
2 |
90 |
--------------------------------------------------------------------------------
/posts/half-edge/static/images/triangle.inkscape.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
191 |
--------------------------------------------------------------------------------
/posts/half-edge/static/images/triangle.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 | 
63 |
64 | ```
65 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------