UNPKG

tangram

Version:
654 lines (552 loc) 22 kB
// Geometry building functions import Vector from '../utils/vector'; import Geo from '../utils/geo'; import {outsideTile, isCoordOutsideTile} from './common'; const zero_vec2 = [0, 0]; // Build tessellated triangles for a polyline const CAP_TYPE = { butt: 0, square: 1, round: 2 }; const JOIN_TYPE = { miter: 0, bevel: 1, round: 2 }; const DEFAULT_MITER_LIMIT = 3; const MIN_FAN_WIDTH = 5; // Width of line in tile units to place 1 triangle per fan const TEXCOORD_NORMALIZE = 65535; // Scaling factor for UV attribute values // Scaling factor to add precision to line texture V coordinate packed as normalized short const V_SCALE_ADJUST = Geo.tile_scale; const zero_v = [0, 0], one_v = [1, 0], mid_v = [0.5, 0]; // reusable instances, updated with V coordinate export function buildPolylines ( lines, style, vertex_data, vertex_template, vindex, closed_polygon, remove_tile_edges, tile_edge_tolerance) { var cap_type = style.cap ? CAP_TYPE[style.cap] : CAP_TYPE.butt; var join_type = style.join ? JOIN_TYPE[style.join] : JOIN_TYPE.miter; // Configure miter limit if (join_type === JOIN_TYPE.miter) { const miter_limit = style.miter_limit || DEFAULT_MITER_LIMIT; // default miter limit var miter_len_sq = miter_limit * miter_limit; } // Texture Variables var v_scale; if (vindex.a_texcoord) { v_scale = 1 / (style.texcoord_width * V_SCALE_ADJUST); // scales line texture as a ratio of the line's width } // Values that are constant for each line and are passed to helper functions var context = { closed_polygon, remove_tile_edges, tile_edge_tolerance, miter_len_sq, join_type, cap_type, vertex_data, vertex_template, half_width: style.width / 2, extrude_index: vindex.a_extrude, offset_index: vindex.a_offset, v_scale, texcoord_index: vindex.a_texcoord, texcoord_width: style.texcoord_width, offset: style.offset, geom_count: 0 }; // Process lines for (let i = 0; i < lines.length; i++) { buildPolyline(lines[i], context); } // Process extra lines (which are created above if lines need to be mutated for easier processing) if (context.extra_lines) { for (let i = 0; i < context.extra_lines.length; i++) { buildPolyline(context.extra_lines[i], context); } } return context.geom_count; } function buildPolyline(line, context){ // Skip if line is not valid if (line.length < 2) { return; } var coordCurr, coordNext, normPrev, normNext; var {join_type, cap_type, closed_polygon, remove_tile_edges, tile_edge_tolerance, v_scale, miter_len_sq} = context; var has_texcoord = (context.texcoord_index != null); var v = 0; // Texture v-coordinate // Loop backwards through line to a tile boundary if found // since you need to draw lines that are only partially inside the tile, // so we start at the first index where it is safe to loop through to the last index within the tile if (closed_polygon && join_type === JOIN_TYPE.miter) { var boundaryIndex = getTileBoundaryIndex(line); if (boundaryIndex !== 0) { // create new line that is a cyclic permutation of the original var permutedLine = permuteLine(line, boundaryIndex); context.extra_lines = context.extra_lines || []; context.extra_lines.push(permutedLine); return; } } var index_start = 0; var index_end = line.length - 1; var ignored_indices_count = 0; // FIRST POINT // loop through beginning points if duplicates coordCurr = line[index_start]; coordNext = line[index_start + 1]; while (Vector.isEqual(coordCurr, coordNext)) { index_start++; coordCurr = coordNext; coordNext = line[index_start + 1]; ignored_indices_count++; if (index_start === line.length - 1) { return; } } // loop through ending points to check for duplicates while (Vector.isEqual(line[index_end], line[index_end - 1])) { index_end--; ignored_indices_count++; if (index_end === 0) { return; } } if (line.length < 2 + ignored_indices_count) { return; } normNext = Vector.normalize(Vector.perp(coordCurr, coordNext)); // Skip tile boundary lines and append a new line if needed if (remove_tile_edges && outsideTile(coordCurr, coordNext, tile_edge_tolerance)) { var nonBoundarySegment = getNextNonBoundarySegment(line, index_start, tile_edge_tolerance); if (nonBoundarySegment) { context.extra_lines = context.extra_lines || []; context.extra_lines.push(nonBoundarySegment); } return; } if (closed_polygon){ // Begin the polygon with a join (connecting the first and last segments) normPrev = Vector.normalize(Vector.perp(line[index_end - 1], coordCurr)); startPolygon(coordCurr, normPrev, normNext, join_type, context); } else { // If line begins at edge, don't add a cap if (!isCoordOutsideTile(coordCurr)) { addCap(coordCurr, v, normNext, cap_type, true, context); if (has_texcoord && cap_type !== CAP_TYPE.butt) { v += 0.5 * v_scale * context.texcoord_width; } } // Add first pair of points for the line strip addVertex(coordCurr, normNext, normNext, 1, v, context, 1); addVertex(coordCurr, normNext, normNext, 0, v, context, -1); } // INTERMEDIARY POINTS if (has_texcoord) { v += v_scale * Vector.length(Vector.sub(coordNext, coordCurr)); } for (var i = index_start + 1; i < index_end; i++) { var currIndex = i; var nextIndex = i + 1; coordCurr = line[currIndex]; coordNext = line[nextIndex]; // Skip redundant vertices if (Vector.isEqual(coordCurr, coordNext)) { continue; } // Remove tile boundaries if (remove_tile_edges && outsideTile(coordCurr, coordNext, tile_edge_tolerance)) { addVertex(coordCurr, normNext, normNext, 1, v, context, 1); addVertex(coordCurr, normNext, normNext, 0, v, context, -1); indexPairs(1, context); var nonBoundaryLines = getNextNonBoundarySegment(line, currIndex + 1, tile_edge_tolerance); if (nonBoundaryLines) { context.extra_lines = context.extra_lines || []; context.extra_lines.push(nonBoundaryLines); } return; } normPrev = normNext; normNext = Vector.normalize(Vector.perp(coordCurr, coordNext)); // Add join if (join_type === JOIN_TYPE.miter) { addMiter(v, coordCurr, normPrev, normNext, miter_len_sq, false, context); } else { addJoin(join_type, v, coordCurr, normPrev, normNext, false, context); } if (has_texcoord) { v += v_scale * Vector.length(Vector.sub(coordNext, coordCurr)); } } // LAST POINT coordCurr = coordNext; normPrev = normNext; if (closed_polygon) { // Close the polygon with a miter joint or butt cap if on a tile boundary normNext = Vector.normalize(Vector.perp(coordCurr, line[1])); endPolygon(coordCurr, normPrev, normNext, join_type, v, context); } else { // Finish the line strip addVertex(coordCurr, normPrev, normNext, 1, v, context, 1); addVertex(coordCurr, normPrev, normNext, 0, v, context, -1); indexPairs(1, context); // If line ends at edge, don't add a cap if (!isCoordOutsideTile(coordCurr)) { addCap(coordCurr, v, normPrev, cap_type, false, context); } } } function getTileBoundaryIndex(line){ if (isCoordOutsideTile(line[0])) { return 0; } for (var backIndex = 0; backIndex < line.length; backIndex++) { var coordCurr = line[line.length - 1 - backIndex]; if (isCoordOutsideTile(coordCurr)) { return line.length - 1 - backIndex; } } return 0; } // Iterate through line from startIndex to find a segment not on a tile boundary, if any. function getNextNonBoundarySegment (line, startIndex, tolerance) { var endIndex = startIndex; while (line[endIndex + 1] && outsideTile(line[endIndex], line[endIndex + 1], tolerance)) { endIndex++; } // If there is a line segment remaining that is within the tile, push it to the lines array return (line.length - endIndex >= 2) ? line.slice(endIndex) : false; } // Begin a polygon with a join connecting to the last segment (if valid join-type specified) function startPolygon(coordCurr, normPrev, normNext, join_type, context){ // If polygon starts on a tile boundary, don't add a join if (join_type === undefined || isCoordOutsideTile(coordCurr)) { addVertex(coordCurr, normNext, normNext, 1, 0, context, 1); addVertex(coordCurr, normNext, normNext, 0, 0, context, -1); } else { // If polygon starts within a tile, add a join var v = 0; if (join_type === JOIN_TYPE.miter) { addMiter(v, coordCurr, normPrev, normNext, context.miter_len_sq, true, context); } else { addJoin(join_type, v, coordCurr, normPrev, normNext, true, context); } } } // End a polygon appropriately function endPolygon(coordCurr, normPrev, normNext, join_type, v, context) { // If polygon ends on a tile boundary, don't add a join if (isCoordOutsideTile(coordCurr)) { addVertex(coordCurr, normPrev, normPrev, 1, v, context, 1); addVertex(coordCurr, normPrev, normPrev, 0, v, context, -1); indexPairs(1, context); } else { // If polygon ends within a tile, add Miter or no joint (join added on startPolygon) var miterVec = createMiterVec(normPrev, normNext); if (join_type === JOIN_TYPE.miter && Vector.lengthSq(miterVec) > context.miter_len_sq) { join_type = JOIN_TYPE.bevel; // switch to bevel } if (join_type === JOIN_TYPE.miter) { addVertex(coordCurr, miterVec, normPrev, 1, v, context, 1); addVertex(coordCurr, miterVec, normPrev, 0, v, context, -1); indexPairs(1, context); } else { addVertex(coordCurr, normPrev, normPrev, 1, v, context, 1); addVertex(coordCurr, normPrev, normPrev, 0, v, context, -1); indexPairs(1, context); } } } function createMiterVec(normPrev, normNext) { var miterVec = Vector.normalize(Vector.add(normPrev, normNext)); var scale = 2 / (1 + Math.abs(Vector.dot(normPrev, miterVec))); return Vector.mult(miterVec, scale * scale); } // Add a miter vector or a join if the miter is too sharp function addMiter (v, coordCurr, normPrev, normNext, miter_len_sq, isBeginning, context) { var miterVec = createMiterVec(normPrev, normNext); // Miter limit: if miter join is too sharp, convert to bevel instead if (Vector.lengthSq(miterVec) > miter_len_sq) { addJoin(JOIN_TYPE.bevel, v, coordCurr, normPrev, normNext, isBeginning, context); } else { addVertex(coordCurr, miterVec, miterVec, 1, v, context, 1); addVertex(coordCurr, miterVec, miterVec, 0, v, context, -1); if (!isBeginning) { indexPairs(1, context); } } } // Add a bevel or round join function addJoin(join_type, v, coordCurr, normPrev, normNext, isBeginning, context) { var miterVec = createMiterVec(normPrev, normNext); var isClockwise = (normNext[0] * normPrev[1] - normNext[1] * normPrev[0] > 0); if (context.texcoord_index != null) { zero_v[1] = v; one_v[1] = v; } if (isClockwise){ addVertex(coordCurr, miterVec, miterVec, 1, v, context, 1); addVertex(coordCurr, normPrev, miterVec, 0, v, context, -1); if (!isBeginning) { indexPairs(1, context); } addFan(coordCurr, // extrusion vector of first vertex Vector.neg(normPrev), // controls extrude distance of pivot vertex miterVec, // extrusion vector of last vertex Vector.neg(normNext), // line normal (unused here) miterVec, // uv coordinates zero_v, one_v, zero_v, false, (join_type === JOIN_TYPE.bevel), context ); addVertex(coordCurr, miterVec, miterVec, 1, v, context, 1); addVertex(coordCurr, normNext, miterVec, 0, v, context, -1); } else { addVertex(coordCurr, normPrev, miterVec, 1, v, context, 1); addVertex(coordCurr, miterVec, miterVec, 0, v, context, -1); if (!isBeginning) { indexPairs(1, context); } addFan(coordCurr, // extrusion vector of first vertex normPrev, // extrusion vector of pivot vertex Vector.neg(miterVec), // extrusion vector of last vertex normNext, // line normal for offset miterVec, // uv coordinates one_v, zero_v, one_v, false, (join_type === JOIN_TYPE.bevel), context ); addVertex(coordCurr, normNext, miterVec, 1, v, context, 1); addVertex(coordCurr, miterVec, miterVec, 0, v, context, -1); } } // Add indices to vertex_elements function indexPairs(num_pairs, context){ var vertex_elements = context.vertex_data.vertex_elements; var num_vertices = context.vertex_data.vertex_count; var offset = num_vertices - 2 * num_pairs - 2; for (var i = 0; i < num_pairs; i++){ vertex_elements.push(offset + 2 * i + 2); vertex_elements.push(offset + 2 * i + 1); vertex_elements.push(offset + 2 * i + 0); vertex_elements.push(offset + 2 * i + 2); vertex_elements.push(offset + 2 * i + 3); vertex_elements.push(offset + 2 * i + 1); context.geom_count += 2; } } function addVertex(position, extrude, normal, u, v, context, flip) { var vertex_template = context.vertex_template; var vertex_data = context.vertex_data; // set vertex position vertex_template[0] = position[0]; vertex_template[1] = position[1]; // set line extrusion vector let len = context.half_width * flip; vertex_template[context.extrude_index + 0] = extrude[0] * len; vertex_template[context.extrude_index + 1] = extrude[1] * len; // set line offset vector if (context.offset) { vertex_template[context.offset_index + 0] = normal[0] * context.offset; vertex_template[context.offset_index + 1] = normal[1] * context.offset; } // set UVs if (context.texcoord_index != null) { vertex_template[context.texcoord_index + 0] = u * TEXCOORD_NORMALIZE; vertex_template[context.texcoord_index + 1] = v * TEXCOORD_NORMALIZE; } vertex_data.addVertex(vertex_template); } // Tesselate a fan geometry between points A ----- B // using their normals from a center p \ . . / // and interpolating their UVs \ p / // \./ // C var uvCurr = [0, 0]; function addFan (coord, eA, eC, eB, normal, uvA, uvC, uvB, isCap, isBevel, context) { // eA = extrusion vector of first outer vertex // eC = extrusion vector of inner vertex // eA, eC, eB = extrusion vectors // normal = line normal for calculating cap offsets // coord = center point p - vertex connecting two line segments var cross = eA[0] * eB[1] - eA[1] * eB[0]; var dot = Vector.dot(eA, eB); var angle = Math.atan2(cross, dot); while (angle >= Math.PI) { angle -= 2*Math.PI; } if (isBevel) { numTriangles = 1; } else { // vary number of triangles in fan with angle (based on MIN_FAN_WIDTH) var numTriangles = trianglesPerArc(angle, context.half_width); if (numTriangles < 1) { return; } } var pivotIndex = context.vertex_data.vertex_count; var vertex_elements = context.vertex_data.vertex_elements; if (angle < 0) { // cw addVertex(coord, eC, normal, uvC[0], uvC[1], context, 1); addVertex(coord, eA, normal, uvA[0], uvA[1], context, 1); } else { // ccw addVertex(coord, eC, normal, uvC[0], uvC[1], context, 1); addVertex(coord, eA, normal, uvA[0], uvA[1], context, 1); } var blade = eA; var has_texcoord = (context.texcoord_index != null); if (has_texcoord) { if (isCap){ var affine_uvCurr = Vector.sub(uvA, uvC); } else { uvCurr = Vector.copy(uvA); var uv_delta = Vector.div(Vector.sub(uvB, uvA), numTriangles); } } var angle_step = angle / numTriangles; let flip = ((angle < 0) ? -1 : 1); // if angle < 0, is cw - set 'flip' flag // add outside vertices in reverse order depending on sign of angle let v1, v2; if (cross > 0) { v1 = 2; v2 = 1; } else { v1 = 1; v2 = 2; } for (var i = 0; i < numTriangles; i++) { if (i === 0 && angle < 0) { // if ccw, flip the extrusion vector so offsets work properly blade = Vector.neg(blade); } blade = Vector.rot(blade, angle_step); if (has_texcoord) { if (isCap){ // UV textures go "through" the cap affine_uvCurr = Vector.rot(affine_uvCurr, angle_step); uvCurr[0] = affine_uvCurr[0] + uvC[0]; uvCurr[1] = affine_uvCurr[1] * context.texcoord_width * context.v_scale + uvC[1]; // scale the v-coordinate } else { // UV textures go "around" the join uvCurr = Vector.add(uvCurr, uv_delta); } } addVertex(coord, blade, normal, uvCurr[0], uvCurr[1], context, flip); vertex_elements.push(pivotIndex + i + v1); vertex_elements.push(pivotIndex); vertex_elements.push(pivotIndex + i + v2); } } // Function to add the vertices needed for line caps, // because to re-use the buffers they need to be at the end function addCap (coord, v, normal, type, isBeginning, context) { var neg_normal = Vector.neg(normal); var has_texcoord = (context.texcoord_index != null); switch (type){ case CAP_TYPE.square: var tangent; // first vertex on the lineString if (isBeginning){ tangent = [normal[1], -normal[0]]; addVertex(coord, Vector.add(normal, tangent), normal, 1, v, context, 1); addVertex(coord, Vector.add(neg_normal, tangent), normal, 0, v, context, 1); if (has_texcoord) { // Add length of square cap to texture coordinate v += 0.5 * context.texcoord_width * context.v_scale; } addVertex(coord, normal, normal, 1, v, context, 1); addVertex(coord, neg_normal, normal, 0, v, context, 1); } // last vertex on the lineString else { tangent = [-normal[1], normal[0]]; addVertex(coord, normal, normal, 1, v, context, 1); addVertex(coord, neg_normal, normal, 0, v, context, 1); if (has_texcoord) { // Add length of square cap to texture coordinate v += 0.5 * context.texcoord_width * context.v_scale; } addVertex(coord, Vector.add(normal, tangent), normal, 1, v, context, 1); addVertex(coord, Vector.add(neg_normal, tangent), normal, 0, v, context, 1); } indexPairs(1, context); break; case CAP_TYPE.round: // default for end cap, beginning cap will overwrite below (this way we're always passing a non-null value, // even if texture coords are disabled) var uvA = zero_v, uvB = one_v, uvC = mid_v; var nA, nB; // first vertex on the lineString if (isBeginning) { nA = normal; nB = neg_normal; if (has_texcoord){ v += 0.5 * context.texcoord_width * context.v_scale; uvA = one_v, uvB = zero_v, uvC = mid_v; // update cap UV order } } // last vertex on the lineString - flip the direction of the cap else { nA = neg_normal; nB = normal; } if (has_texcoord) { zero_v[1] = v, one_v[1] = v, mid_v[1] = v; // update cap UV values } addFan(coord, nA, zero_vec2, nB, // extrusion normal normal, // line normal, for offsets uvA, uvC, uvB, // texture coords (ignored if disabled) true, false, context ); break; case CAP_TYPE.butt: return; } } // Calculate number of triangles for a fan given an angle and line width function trianglesPerArc (angle, width) { if (angle < 0) { angle = -angle; } var numTriangles = (width > 2 * MIN_FAN_WIDTH) ? Math.log2(width / MIN_FAN_WIDTH) : 1; return Math.ceil(angle / Math.PI * numTriangles); } // Cyclically permute closed line starting at an index function permuteLine(line, startIndex){ var newLine = []; for (let i = 0; i < line.length; i++){ var index = (i + startIndex) % line.length; // skip the first (repeated) index if (index !== 0) { newLine.push(line[index]); } } newLine.push(newLine[0]); return newLine; }