Module:CineMol/model
-- This is a port of CineMol to lua
-- CineMol https://github.com/moltools/CineMol was written by David Meijer, Marnix H. Medema & Justin J. J. van der Hooft and is MIT licensed
-- Please consider any edits made to this page as dual licensed MIT & CC-BY-SA 4.0
local p = {}
local geometry = require( 'Module:CineMol/geometry' )
local style = require( 'Module:CineMol/style' )
local cinemolsvg = require( 'Module:CineMol/svg' )
local calculate_convex_hull = require( 'Module:CineMol/fitting' ).calculate_convex_hull
local Cylinder = geometry.Cylinder
local Line3D = geometry.Line3D
local Point2D = geometry.Point2D
local Point3D = geometry.Point3D
local Sphere = geometry.Sphere
local cylinder_intersects_with_cylinder = geometry.cylinder_intersects_with_cylinder
local get_points_on_surface_cylinder = geometry.get_points_on_surface_cylinder
local get_points_on_surface_sphere = geometry.get_points_on_surface_sphere
local point_is_inside_cylinder = geometry.point_is_inside_cylinder
local point_is_inside_sphere = geometry.point_is_inside_sphere
local sphere_intersects_with_cylinder = geometry.sphere_intersects_with_cylinder
local sphere_intersects_with_sphere = geometry.sphere_intersects_with_sphere
local checkType = geometry.checkType
local Cartoon = style.Cartoon
local Color = style.Color
local Depiction = style.Depiction
local Fill = style.Fill
local FillStyle = style.FillStyle
local Glossy = style.Glossy
local LinearGradient = style.LinearGradient
local RadialGradient = style.RadialGradient
local Solid = style.Solid
local Wire = style.Wire
local Line2D = cinemolsvg.Line2D
local Polygon2D = cinemolsvg.Polygon2D
local Svg = cinemolsvg.Svg
local ViewBox = cinemolsvg.ViewBox
-- End of imports
-- Apply focal length to a 3D point.
function p.apply_focal_length(point, focal_length)
checkType('apply_focal_length', 1, point, 'Point3D')
checkType('apply_focal_length', 2, focal_length, 'number')
local x,y,z = point.x, point.y, point.z
local x_proj, y_proj
if (focal_length - z) > 0 then
x_proj = (focal_length * x) / (focal_length - z)
y_proj = (focal_length * y) / (focal_length - z)
else
-- Handle the case when z = -focal_length to avoid division by zero
x_proj, y_proj = x, y
end
return Point2D(x_proj, y_proj)
end
function p.ModelSphere(geometry, depiction)
checkType( 'ModelSphere', 1, geometry, 'Sphere' )
checkType( 'ModelSphere', 2, depiction, 'Depiction' )
return {
_TYPE = 'ModelSphere',
geometry = geometry,
depiction = depiction
}
end
function p.ModelCylinder(geometry, depiction)
checkType( 'ModelCylinder', 1, geometry, 'Cylinder' )
checkType( 'ModelCylinder', 2, depiction, 'Depiction' )
return {
_TYPE = 'ModelCylinder',
geometry = geometry,
depiction = depiction
}
end
function p.ModelWire(geometry, color, width, opacity)
checkType( 'ModelWire', 1, geometry, 'Line3D' )
checkType( 'ModelWire', 2, color, 'Color' )
checkType( 'ModelWire', 3, width, 'number' )
checkType( 'ModelWire', 4, opacity, 'number' )
return {
_TYPE = 'ModelWire',
geometry = geometry,
color = color,
width = width,
opacity = opacity
}
end
-- ==============
-- Create visible 2D polygon from node geometry
-- ==============
-- Get the vertices of the polygon that represents the visible part of the node
function p.get_node_polygon_vertices( this, others, resolution, focal_length )
-- Seems like this should be a method of Model base class, but original doesn't do that, so following that.
checkType( 'get_node_polygon_verticies', 2, others, 'table' )
checkType( 'get_node_polygon_verticies', 3, resolution, 'number' )
local points
if this._TYPE == 'ModelSphere' then
points = get_points_on_surface_sphere( this.geometry, resolution, resolution, true )
elseif this._TYPE == 'ModelCylinder' then
points = get_points_on_surface_cylinder(this.geometry, math.floor(resolution / 2.0))
else
-- If node is not a sphere or cylinder (i.e., unsupported geometries), return empty list.
return {}
end
local visible_points = {}
for i, point in ipairs(points) do
local loopBreak = false
for j, node in ipairs(others) do
if node._TYPE == 'ModelSphere' and point_is_inside_sphere(node.geometry, point) then
loopBreak = true
break
elseif node._TYPE == 'ModelCylinder' and point_is_inside_cylinder(node.geometry, point) then
loopBreak = true
break
end
end
if not loopBreak then
local x, y, z = point.x, point.y, point.z
local am_point
if focal_length ~= nil then
am_point = p.apply_focal_length(Point3D(x, y, z), focal_length)
else
am_point = Point2D(x, y)
end
visible_points[#visible_points+1] = am_point
end
end
-- If no visible points, return empty list.
if #visible_points == 0 then
return {}
end
-- Calculate convex hull of visible points.
local inds = calculate_convex_hull(visible_points)
local verts = {}
for i,ind in ipairs( inds ) do
verts[#verts+1] = visible_points[ind]
end
return verts
end
-- =============
-- Draw scene
-- =============
-- Filter, rotate, and scale nodes based on what to include in the scene.
function p.prepare_nodes_for_intersecting( nodes_to_sort, include_spheres, include_cylinders, include_wires, rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis, scale )
rotation_over_x_axis = rotation_over_x_axis == nil and 0 or rotation_over_x_axis
rotation_over_y_axis = rotation_over_y_axis == nil and 0 or rotation_over_y_axis
rotation_over_z_axis = rotation_over_z_axis == nil and 0 or rotation_over_z_axis
checkType( 'prepare_nodes_for_intersecting', 1, nodes_to_sort, 'table' )
checkType( 'prepare_nodes_for_intersecting', 5, rotation_over_x_axis, 'number' )
checkType( 'prepare_nodes_for_intersecting', 6, rotation_over_y_axis, 'number' )
checkType( 'prepare_nodes_for_intersecting', 7, rotation_over_z_axis, 'number' )
local nodes = {}
for _, node in ipairs( nodes_to_sort ) do
if node._TYPE == 'ModelSphere' and include_spheres then
node.geometry.center = node.geometry.center:rotate(
rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis
)
if scale ~= nil then
node.geometry.radius = node.geometry.radius * scale
node.geometry.center = Point3D(
node.geometry.center.x * scale,
node.geometry.center.y * scale,
node.geometry.center.z * scale
)
if node.depiction.name == 'Cartoon' then
node.depiction.outline_width = node.depiction.outline_width * scale
end
end
nodes[#nodes+1] = node
elseif node._TYPE == 'ModelCylinder' and include_cylinders then
node.geometry.start = node.geometry.start:rotate(
rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis
)
node.geometry.endp = node.geometry.endp:rotate(
rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis
)
if scale ~= nil then
node.geometry.radius = node.geometry.radius * scale
node.geometry.start = Point3D(
node.geometry.start.x * scale,
node.geometry.start.y * scale,
node.geometry.start.z * scale
)
node.geometry.endp = Point3D(
node.geometry.endp.x * scale,
node.geometry.endp.y * scale,
node.geometry.endp.z * scale
)
if node.depiction.name == 'Cartoon' then
node.depiction.outline_width = node.depiction.outline_width * scale
end
end
nodes[#nodes+1] = node
elseif node._TYPE == 'ModelWire' and include_wires then
node.geometry.start = node.geometry.start:rotate(
rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis
)
node.geometry.endp = node.geometry.endp:rotate(
rotation_over_x_axis, rotation_over_y_axis, rotation_over_z_axis
)
if scale ~= nil then
node.geometry.start = Point3D(
node.geometry.start.x * scale,
node.geometry.start.y * scale,
node.geometry.start.z * scale
)
node.geometry.endp = Point3D(
node.geometry.endp.x * scale,
node.geometry.endp.y * scale,
node.geometry.endp.z * scale
)
node.width = node.width * scale
end
nodes[#nodes+1] = node
end
end
return nodes
end
-- Create a fill for a node.
function p.create_fill( node, reference )
checkType( 'create_fill', 1, node, 'table' )
checkType( 'create_fill', 2, reference, 'string' )
local fill = nil
if node._TYPE == 'ModelWire' then
local stroke_color = node.color
local stroke_width = node.width
local opacity = node.opacity
local style = Wire( stroke_color, stroke_width, opacity )
fill = Fill(reference, style)
elseif node.depiction.name == 'Cartoon' then
local fill_color = node.depiction.fill_color
local stroke_color = node.depiction.outline_color
local stroke_width = node.depiction.outline_width
local opacity = node.depiction.opacity
local style = Solid(fill_color, stroke_color, stroke_width, opacity)
fill = Fill(reference, style)
-- Glossy style is different for spheres and cylinders.
elseif (
node.depiction.name == 'Glossy'
and node._TYPE == 'ModelSphere'
and node.geometry._TYPE == 'Sphere'
) then
if node.geometry._TYPE ~= 'Sphere' then
error("Node geometry of ModelSphere must be a sphere.")
end
local x, y = node.geometry.center.x, node.geometry.center.y
local fill_color = node.depiction.fill_color
local center = Point2D(x, y)
local radius = node.geometry.radius
local opacity = node.depiction.opacity
local style = RadialGradient(fill_color, center, radius, opacity)
fill = Fill(reference, style)
elseif node.depiction.name == 'Glossy' and node._TYPE == 'ModelCylinder' then
if node.geometry._TYPE ~= 'Cylinder' then
error("Node geometry of ModelCylinder must be a cylinder.")
end
local start_x, start_y = node.geometry.start.x, node.geometry.start.y
local end_x, end_y = node.geometry.endp.x, node.geometry.endp.y
local fill_color = node.depiction.fill_color
local start_center = Point2D(start_x, start_y)
local end_center = Point2D(end_x, end_y)
local radius = node.geometry.radius
local opacity = node.depiction.opacity
local style = LinearGradient(fill_color, start_center, end_center, opacity)
fill = Fill(reference, style)
end
return fill
end
-- Calculate which of the previous nodes intersect with the current node.
function p.calculate_intersecting_nodes(
node,
other_nodes,
calculate_sphere_sphere_intersections,
calculate_sphere_cylinder_intersections,
calculate_cylinder_sphere_intersections,
calculate_cylinder_cylinder_intersections,
filter_nodes_for_intersecting
)
checkType( 'calculate_intersecting_nodes', 1, node, 'table' )
checkType( 'calculate_intersecting_nodes', 2, other_nodes, 'table' )
local previous_nodes = {}
-- Wireframe is drawn as a line and has no intersections with other nodes.
if node._TYPE ~= 'ModelWire' then
for _, prev_node in ipairs(other_nodes) do
if not filter_nodes_for_intersecting then
previous_nodes[#previous_nodes+1] = prev_node
elseif (
node._TYPE == 'ModelSphere'
and prev_node._TYPE == 'ModelSphere'
and calculate_sphere_sphere_intersections
) then
if sphere_intersects_with_sphere(node.geometry, prev_node.geometry) then
previous_nodes[#previous_nodes+1] = prev_node
end
elseif (
node._TYPE == 'ModelSphere' and prev_node._TYPE == 'ModelCylinder'
) and calculate_sphere_cylinder_intersections then
if sphere_intersects_with_cylinder(node.geometry, prev_node.geometry) then
previous_nodes[#previous_nodes+1] = prev_node
end
elseif (
node._TYPE == 'ModelCylinder' and prev_node._TYPE == 'ModelSphere'
) and calculate_cylinder_sphere_intersections then
if sphere_intersects_with_cylinder(prev_node.geometry, node.geometry) then
previous_nodes[#previous_nodes+1] = prev_node
end
elseif (
node._TYPE == 'ModelCylinder'
and prev_node._TYPE == 'ModelCylinder'
and calculate_cylinder_cylinder_intersections
) then
if cylinder_intersects_with_cylinder(node.geometry, prev_node.geometry) then
previous_nodes[#previous_nodes+1] = prev_node
end
end
end
end
return previous_nodes
end
local function first_n(table, n)
local res = {}
for i,v in ipairs(table) do
if i >= n then
break
end
res[#res+1] = v
end
return res
end
function p.Scene( nodes )
nodes = nodes == nil and {} or nodes
checkType( 'Scene', 1, nodes, 'table' )
local obj = {
_TYPE = 'Scene',
nodes = nodes
}
function obj:add_node( node )
checkType( 'add_node', 1, self, 'Scene' )
self.nodes[#self.nodes+1] = node
end
function obj:calculate_view_box( points, margin )
checkType( 'Scene:calculate_view_box', 1, self, 'Scene' )
checkType( 'Scene:calculate_view_box', 2, points, 'table' )
checkType( 'Scene:calculate_view_box', 3, margin, 'number' )
if #points == 0 then
-- original is missing return, presumably a bug.
return ViewBox(0, 0, 0, 0)
end
local min_x, min_y, max_x, max_y = 1/0, 1/0, -1/0, -1/0
for _, point in ipairs(points) do
min_x = math.min(min_x, point.x)
min_y = math.min(min_y, point.y)
max_x = math.max(max_x, point.x)
max_y = math.max(max_y, point.y)
end
min_x = min_x - margin
min_y = min_y - margin
max_x = max_x + margin
max_y = max_y + margin
local width = max_x - min_x
local height = max_y - min_y
return ViewBox(min_x, min_y, width, height)
end
-- Draw the scene.
function obj:draw(
resolution,
window, -- window might not work due to how we make SVGs. Instead set img attribute on svg
view_box,
rotation_over_x_axis,
rotation_over_y_axis,
rotation_over_z_axis,
include_spheres,
include_cylinders,
include_wires,
calculate_sphere_sphere_intersections,
calculate_sphere_cylinder_intersections,
calculate_cylinder_sphere_intersections,
calculate_cylinder_cylinder_intersections,
filter_nodes_for_intersecting,
scale,
focal_length
)
checkType( 'Scene:draw', 1, self, 'Scene' )
checkType( 'Scene:draw', 2, resolution, 'number' )
rotation_over_x_axis = rotation_over_x_axis == nil and 0 or rotation_over_x_axis
rotation_over_y_axis = rotation_over_y_axis == nil and 0 or rotation_over_y_axis
rotation_over_z_axis = rotation_over_z_axis == nil and 0 or rotation_over_z_axis
include_spheres = include_spheres == nil and true or include_spheres
include_cylinders = include_cylinders == nil and true or include_cylinders
include_wires = include_wires == nil and true or include_wires
calculate_sphere_sphere_intersections = calculate_sphere_sphere_intersections == nil and true or calculate_sphere_sphere_intersections
calculate_sphere_cylinder_intersections = calculate_sphere_cylinder_intersections == nil and true or calculate_sphere_cylinder_intersections
calculate_cylinder_sphere_intersections = calculate_cylinder_sphere_intersections == nil and true or calculate_cylinder_sphere_intersections
calculate_cylinder_cylinder_intersections = calculate_cylinder_cylinder_intersections == nil and true or calculate_cylinder_cylinder_intersections
scale = scale == nil and 1.0 or scale
checkType( 'Scene:draw', 16, scale, 'number' )
-- Filter geometries.
local nodes = p.prepare_nodes_for_intersecting(
self.nodes,
include_spheres,
include_cylinders,
include_wires,
rotation_over_x_axis,
rotation_over_y_axis,
rotation_over_z_axis,
scale
)
-- Get sorting values for nodes. We sort on z-coordinate as we always look at the
-- scene from the z-axis, towards the origin.
-- We also use node number to make it a stable sort to emulate the ordering of the python project.
local sorting_values = {}
for i, node in ipairs(nodes) do
local midpoint_z
if node._TYPE == 'ModelSphere' then
sorting_values[#sorting_values+1] = { node.geometry.center.z, i, node }
elseif node._TYPE == 'ModelCylinder' then
local cylinder_start = node.geometry.start
local cylinder_end = node.geometry.endp
midpoint_z = (cylinder_start.z + cylinder_end.z) / 2
sorting_values[#sorting_values+1] = { midpoint_z, i, node }
elseif node._TYPE == 'ModelWire' then
local wire_start = node.geometry.start
local wire_end = node.geometry.endp
midpoint_z = (wire_start.z + wire_end.z) / 2
sorting_values[#sorting_values+1] = { midpoint_z, i, node }
end
end
-- Sort nodes by sorting values.
table.sort( sorting_values, function( a, b )
if a[1] == b[1] then
return a[2] < b[2]
end
return a[1] < b[1]
end )
local nodes = {}
for _,v in ipairs( sorting_values ) do
nodes[#nodes+1] = v[3]
end
-- Keep track of reference points for determining viewbox later on.
local ref_points = {}
-- Calculate 2D shape and fill for each node.
local objects = {}
local fills = {}
-- Loop over nodes and create shapes and fills to populate the SVG.
for i, node in ipairs(nodes) do
-- Create reference tag for node to connect shape to style.
-- subtract 1 from i to make it match the python version for easier testing.
local reference = "node-" .. (i-1)
-- Calculate which of the previously drawn nodes intersect with the current node.
local previous_nodes = p.calculate_intersecting_nodes(
node,
first_n(nodes, i),
calculate_sphere_sphere_intersections,
calculate_sphere_cylinder_intersections,
calculate_cylinder_sphere_intersections,
calculate_cylinder_cylinder_intersections,
filter_nodes_for_intersecting
)
-- Calculate line for wire.
if node._TYPE == 'ModelWire' then
local start, endp
if focal_length ~= nil then
start = p.apply_focal_length(node.geometry.start, focal_length)
endp = p.apply_focal_length(node.geometry.endp, focal_length)
else
start = Point2D(node.geometry.start.x, node.geometry.start.y)
endp = Point2D(node.geometry.endp.x, node.geometry.endp.y)
end
local line = Line2D(reference, start, endp)
if view_box == nil then
ref_points[#ref_points+1] = start
ref_points[#ref_points+1] = endp
end
objects[#objects+1] = line
-- Otherwise, calculate polygon for visible part of node.
else
local points = p.get_node_polygon_vertices(node, previous_nodes, resolution, focal_length)
local polygon = Polygon2D(reference, points)
if view_box == nil then
for _,pt in ipairs( points ) do
ref_points[#ref_points+1] = pt
end
end
objects[#objects+1] = polygon
end
-- Create style.
local fill = p.create_fill(node, reference)
if fill ~= nil then
fills[#fills+1] = fill
end
-- padding = len(str(len(nodes)))
-- logger.info(f" Drawing node {i + 1:>{padding}} of {len(nodes)}")
end
-- Calculate view box.
if view_box == nil then
view_box = self:calculate_view_box(ref_points, 5.0)
end
-- view_box, window, background_color, fills, objects
local svg = Svg(view_box, window, nil, fills, objects)
return svg
end
return obj
end
return p
Content Disclaimer
Informasi ini disarikan dari Wikipedia dan disajikan kembali untuk tujuan edukasi. Konten tersedia di bawah lisensi CC BY-SA 3.0. Kami tidak bertanggung jawab atas ketidakakuratan data yang bersumber dari kontribusi publik tersebut.
- The information displayed on this website is sourced in part or in whole from Wikipedia and has been adapted for the purpose of restating it. We strive to provide accurate and relevant information, however:
- There is no guarantee of absolute accuracy. Wikipedia is an open, collaborative project that can be edited by anyone, so information is subject to change.
- It is not intended to constitute professional advice. The content displayed is for informational and educational purposes only. For important decisions (e.g., medical, legal, or financial), please consult a professional.
- Content copyright. Wikipedia is licensed under the Creative Commons Attribution-ShareAlike License (CC BY-SA). This means that content may be reused with appropriate attribution and shared under a similar license.
- Responsible use. Any risk arising from the use of information from this website is entirely the responsibility of the user.