var film2d = use('film2d') var math = use('math/radians') var line_proto = { type: 'mesh2d', set_pos: function(x, y) { this.pos.x = x this.pos.y = y this._dirty = true return this }, set_points: function(points) { this.points = points this._dirty = true this._rebuild() return this }, set_point: function(i, x, y) { if (i >= 0 && i < this.points.length) { this.points[i].x = x this.points[i].y = y this._dirty = true this._rebuild() } return this }, set_width: function(w) { this.width = w this._dirty = true this._rebuild() return this }, set_widths: function(widths) { this.widths = widths this._dirty = true this._rebuild() return this }, _rebuild: function() { var result = build_polyline_mesh(this) this.verts = result.verts this.indices = result.indices this._cumulative_lengths = result.cumulative_lengths this._total_length = result.total_length }, destroy: function() { film2d.unregister(this._id) } } function build_polyline_mesh(line) { var points = line.points || [] if (points.length < 2) return {verts: [], indices: [], cumulative_lengths: [], total_length: 0} var width = line.width || 10 var widths = line.widths var closed = line.closed || false var join = line.join || 'miter' var cap = line.cap || 'butt' var miter_limit = line.miter_limit || 4 var uv_mode = line.uv ? (line.uv.mode || 'repeat') : 'repeat' var u_per_unit = line.uv ? (line.uv.u_per_unit || (1 / 16)) : (1 / 16) var u_offset = line.uv ? (line.uv.u_offset || 0) : 0 var v_scale = line.uv ? (line.uv.v_scale || 1) : 1 var v_offset = line.uv ? (line.uv.v_offset || 0) : 0 var pos = line.pos || {x: 0, y: 0} var points_space = line.points_space || 'world' // Transform points if in local space var pts = [] for (var i = 0; i < points.length; i++) { var p = points[i] if (points_space == 'local') { pts.push({x: p.x + pos.x, y: p.y + pos.y}) } else { pts.push({x: p.x, y: p.y}) } } // Calculate cumulative distances var cumulative = [0] for (var i = 1; i < pts.length; i++) { var dx = pts[i].x - pts[i-1].x var dy = pts[i].y - pts[i-1].y cumulative.push(cumulative[i-1] + math.sqrt(dx*dx + dy*dy)) } var total_length = cumulative[cumulative.length - 1] // Build triangle strip mesh var verts = [] var indices = [] // Get width at point i function get_width(i) { if (widths && widths.length > i) return widths[i] return width } // Get U coordinate at point i function get_u(i) { if (uv_mode == 'stretch') { return total_length > 0 ? cumulative[i] / total_length : 0 } else if (uv_mode == 'per_segment') { if (i == 0) return 0 var seg_idx = i - 1 var seg_len = cumulative[i] - cumulative[i-1] return 1 // Each segment ends at u=1 } else { // repeat (default) return cumulative[i] * u_per_unit + u_offset } } // Calculate normals at each point var normals = [] for (var i = 0; i < pts.length; i++) { var prev = i > 0 ? pts[i-1] : (closed ? pts[pts.length-1] : null) var curr = pts[i] var next = i < pts.length-1 ? pts[i+1] : (closed ? pts[0] : null) var n = {x: 0, y: 0} if (prev && next) { // Middle point - average normals var d1x = curr.x - prev.x var d1y = curr.y - prev.y var d2x = next.x - curr.x var d2y = next.y - curr.y var len1 = math.sqrt(d1x*d1x + d1y*d1y) var len2 = math.sqrt(d2x*d2x + d2y*d2y) if (len1 > 0.0001) { d1x /= len1; d1y /= len1 } if (len2 > 0.0001) { d2x /= len2; d2y /= len2 } // Normals (perpendicular) var n1x = -d1y, n1y = d1x var n2x = -d2y, n2y = d2x // Average n.x = n1x + n2x n.y = n1y + n2y var nlen = math.sqrt(n.x*n.x + n.y*n.y) if (nlen > 0.0001) { n.x /= nlen; n.y /= nlen } // Miter correction var dot = n1x * n.x + n1y * n.y if (dot > 0.0001) { var miter_scale = 1 / dot if (miter_scale > miter_limit) miter_scale = miter_limit n.x *= miter_scale n.y *= miter_scale } } else if (next) { // Start point var dx = next.x - curr.x var dy = next.y - curr.y var len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } else if (prev) { // End point var dx = curr.x - prev.x var dy = curr.y - prev.y var len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } n.x = -dy n.y = dx } normals.push(n) } // Generate vertices (2 per point - left and right of line) for (var i = 0; i < pts.length; i++) { var p = pts[i] var n = normals[i] var w = get_width(i) * 0.5 var u = get_u(i) // Left vertex (v=0) verts.push({ x: p.x + n.x * w, y: p.y + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1 }) // Right vertex (v=1) verts.push({ x: p.x - n.x * w, y: p.y - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) } // Generate indices (triangle strip as triangles) for (var i = 0; i < pts.length - 1; i++) { var base = i * 2 // First triangle indices.push(base + 0) indices.push(base + 1) indices.push(base + 2) // Second triangle indices.push(base + 1) indices.push(base + 3) indices.push(base + 2) } // Handle closed path if (closed && pts.length > 2) { var last = (pts.length - 1) * 2 indices.push(last + 0) indices.push(last + 1) indices.push(0) indices.push(last + 1) indices.push(1) indices.push(0) } // Add round caps if requested if (!closed && cap == 'round') { add_round_cap(verts, indices, pts[0], normals[0], get_width(0), get_u(0), v_offset, v_scale, true) add_round_cap(verts, indices, pts[pts.length-1], normals[pts.length-1], get_width(pts.length-1), get_u(pts.length-1), v_offset, v_scale, false) } else if (!closed && cap == 'square') { add_square_cap(verts, indices, pts[0], normals[0], get_width(0), get_u(0), v_offset, v_scale, true, pts[1]) add_square_cap(verts, indices, pts[pts.length-1], normals[pts.length-1], get_width(pts.length-1), get_u(pts.length-1), v_offset, v_scale, false, pts[pts.length-2]) } return { verts: verts, indices: indices, cumulative_lengths: cumulative, total_length: total_length } } function add_round_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_start) { var w = width * 0.5 var segments = 8 var base_idx = verts.length // Direction along the line var dx = is_start ? -n.y : n.y var dy = is_start ? n.x : -n.x // Center vertex verts.push({ x: p.x, y: p.y, u: u, v: 0.5 * v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) // Arc vertices var start_angle = is_start ? math.arc_tangent(n.y, n.x) : math.arc_tangent(-n.y, -n.x) for (var i = 0; i <= segments; i++) { var angle = start_angle + (i / segments) * 3.14159 var cx = math.cosine(angle) var cy = math.sine(angle) verts.push({ x: p.x + cx * w, y: p.y + cy * w, u: u, v: (0.5 + cy * 0.5) * v_scale + v_offset, r: 1, g: 1, b: 1, a: 1 }) } // Fan triangles for (var i = 0; i < segments; i++) { indices.push(base_idx) indices.push(base_idx + 1 + i) indices.push(base_idx + 2 + i) } } function add_square_cap(verts, indices, p, n, width, u, v_offset, v_scale, is_start, adjacent) { var w = width * 0.5 var base_idx = verts.length // Direction along the line (away from adjacent point) var dx = p.x - adjacent.x var dy = p.y - adjacent.y var len = math.sqrt(dx*dx + dy*dy) if (len > 0.0001) { dx /= len; dy /= len } // Extend by half width var ext = w var ex = p.x + dx * ext var ey = p.y + dy * ext // Four corners of the cap verts.push({x: p.x + n.x * w, y: p.y + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1}) verts.push({x: p.x - n.x * w, y: p.y - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1}) verts.push({x: ex + n.x * w, y: ey + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1}) verts.push({x: ex - n.x * w, y: ey - n.y * w, u: u, v: v_scale + v_offset, r: 1, g: 1, b: 1, a: 1}) indices.push(base_idx + 0) indices.push(base_idx + 1) indices.push(base_idx + 2) indices.push(base_idx + 1) indices.push(base_idx + 3) indices.push(base_idx + 2) } var defaults = { type: 'mesh2d', // routing plane: 'default', layer: 0, groups: [], visible: true, // transform pos: {x: 0, y: 0}, points_space: 'world', // geometry points: [], closed: false, // thickness width: 10, widths: null, // join/cap join: 'miter', cap: 'butt', miter_limit: 4, // material image: null, tint: {r: 1, g: 1, b: 1, a: 1}, opacity: 1, blend: 'alpha', filter: 'linear', // UV behavior uv: { space: 'world', mode: 'repeat', u_per_unit: 1 / 16, u_offset: 0, v_scale: 1, v_offset: 0, rotate: 0 }, // dashes dash_len: 0, gap_len: 0, dash_offset: 0 } function make_line(props) { var data = {} for (var k in defaults) { var v = defaults[k] if (is_object(v) && !is_array(v)) { data[k] = {} for (var kk in v) data[k][kk] = v[kk] } else { data[k] = v } } // Apply user props (deep merge for objects) for (var k in props) { var v = props[k] if (is_object(v) && !is_array(v) && is_object(data[k])) { for (var kk in v) data[k][kk] = v[kk] } else { data[k] = v } } // Ensure groups is array if (!data.groups) data.groups = [] if (is_text(data.groups)) data.groups = [data.groups] var line = meme(line_proto, data) line._rebuild() film2d.register(line) return line } var line2d = { polyline: function(props) { return make_line(props) }, line: function(x1, y1, x2, y2, props) { var p = props || {} p.points = [{x: x1, y: y1}, {x: x2, y: y2}] return make_line(p) } } return line2d