Files
prosperon/line2d.cm
2026-01-21 09:05:02 -06:00

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') {
push(pts, {x: p.x + pos.x, y: p.y + pos.y})
} else {
push(pts, {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
push(cumulative, 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
}
push(normals, 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)
push(verts, {
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)
push(verts, {
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
push(indices, base + 0)
push(indices, base + 1)
push(indices, base + 2)
// Second triangle
push(indices, base + 1)
push(indices, base + 3)
push(indices, base + 2)
}
// Handle closed path
if (closed && length(pts) > 2) {
var last = (length(pts) - 1) * 2
push(indices, last + 0)
push(indices, last + 1)
push(indices, 0)
push(indices, last + 1)
push(indices, 1)
push(indices, 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
push(verts, {
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)
push(verts, {
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++) {
push(indices, base_idx)
push(indices, base_idx + 1 + i)
push(indices, 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
push(verts, {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})
push(verts, {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})
push(verts, {x: ex + n.x * w, y: ey + n.y * w, u: u, v: v_offset, r: 1, g: 1, b: 1, a: 1})
push(verts, {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})
push(indices, base_idx + 0)
push(indices, base_idx + 1)
push(indices, base_idx + 2)
push(indices, base_idx + 1)
push(indices, base_idx + 3)
push(indices, 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