399 lines
9.7 KiB
Plaintext
399 lines
9.7 KiB
Plaintext
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 < length(this.points)) {
|
|
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 (length(points) < 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 < length(points); 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 < length(pts); 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[length(cumulative) - 1]
|
|
|
|
// Build triangle strip mesh
|
|
var verts = []
|
|
var indices = []
|
|
|
|
// Get width at point i
|
|
function get_width(i) {
|
|
if (widths && length(widths) > 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 < length(pts); i++) {
|
|
var prev = i > 0 ? pts[i-1] : (closed ? pts[length(pts)-1] : null)
|
|
var curr = pts[i]
|
|
var next = i < length(pts)-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 < length(pts); 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 < length(pts) - 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 && length(pts) > 2) {
|
|
var last = (length(pts) - 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[length(pts)-1], normals[length(pts)-1], get_width(length(pts)-1), get_u(length(pts)-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[length(pts)-1], normals[length(pts)-1], get_width(length(pts)-1), get_u(length(pts)-1), v_offset, v_scale, false, pts[length(pts)-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 = length(verts)
|
|
|
|
// 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 = length(verts)
|
|
|
|
// 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 = object(defaults, props)
|
|
|
|
// 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
|