STL Generation 3D converts network data into 3D-printable STL meshes.
Inputs are node positions plus connectivity data, either as CSV files,
NumPy .npy files, or explicit-schema pickle .pkl files. Outputs are
STL files and optional HTML previews.
Clone the repository, create a virtual environment, and install the Python dependencies:
git clone https://github.com/DMREF-networks/STL_generation_3d.git
cd STL_generation_3d
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m unittest discoverOn Windows, activate the environment with:
.\.venv\Scripts\activateThe project is currently checked with Python 3.8.10. It uses
trimesh, manifold3d, shapely, numpy, scipy, matplotlib, and
plotly.
The committed sample configs are meant as examples. For day-to-day use, keep your own configs and generated STL/HTML files in local working folders such as:
local_configs/
local_output/
Those folders are ignored by Git so collaborators can create their own inputs and outputs without mixing them into the shared repository history.
Each network needs:
- an
xyfile containing node positions - an
adjfile containing connectivity - optionally, a
node_diametersfile containing one node diameter per node
The interactive converter pairs files by name:
name_xy.csv
name_adj.csv
or:
name_xy.npy
name_adj.npy
Suffixes after _xy and _adj are preserved, so these are also paired:
network_xy_0.1.npy
network_adj_0.1.npy
Position files may have two columns (x, y) or three columns
(x, y, z). Two-dimensional positions are automatically placed on
z = 0.
Optional node-diameter files may be CSV, NumPy .npy, or pickle .pkl
numeric arrays with shape (N,) or (N, 1), where N is the number of
nodes. Values are absolute node sphere diameters in millimeters after the
network has been scaled to cube_side_length_mm.
Adjacency data can be either:
- a square adjacency matrix, where nonzero entries are edges
- an edge list, commonly
(source, target),(source, target, thickness),(source, target, material), or(source, target, thickness, material)
For .npy edge-list inputs, use edge_list_interpretation in the
saved config to define what extra columns mean. This keeps legacy
(E, 3) edge lists whose third column is edge length separate from new
(E, 3) files whose third column is thickness or material code.
Run the interactive script:
python npyToSTLScript.pyThe script prompts for:
- input type:
csvornpy - input directory
- beam diameter in millimeters
- model side length in millimeters
- whether adjacency values should control beam thickness
- meshing method:
cylindersorplanar
Generated STL and HTML preview files are written to the current working directory.
Run the local browser UI with:
python material_stl_ui.pyThe script opens a browser page with form controls for the common
configuration choices: input files, connectivity format, material source,
beam size, thickness mode, junction policy, node material, and output
folder. Use the Browse and Choose buttons to select local files and
folders through the file explorer, or paste paths directly into the
fields. Keep the Python process running while using the page. The
browser provides the interface, and the local Python process does the
meshing and file writes.
The normal UI workflow is to set the form controls and click
Generate STLs. Each generation also writes a matching JSON config file
into the output folder so the parameters can be reused later. If you
already have a JSON config, use Load Config to populate the form before
generating. The advanced JSON panel is still available for manual edits,
but it is not required for the normal workflow.
For edge-list NumPy files, set Edge list interpretation explicitly.
If the selected interpretation uses material codes, the Edge Material Codes table maps values such as 0, 1, 2, and 3 to material
names. That mapping is saved in the generated config as
edge_material_map.
If the file picker cannot open in your environment, paste the file path into the field instead.
By default, adjacency values are treated as binary connectivity. Every nonzero edge uses the beam diameter entered at the prompt.
If variable thickness is enabled, adjacency values become scale factors:
edge_diameter = beam_diameter * adjacency_value
For edge-list inputs, the default edge thickness is 1.0, meaning one
base beam diameter. The UI exposes this as an explicit edge-list
interpretation instead of a separate scale factor:
legacy:source,target[,length]; extra columns are ignored and every edge uses thickness1.0thickness:source,target,thickness; column 3 is the edge thickness multipliermaterial:source,target,material_code; column 3 is a material name or integer material codethickness_material:source,target,thickness,material_code; column 3 is thickness and column 4 is material name/code
The converter supports two methods:
cylinders: creates a cylinder for each edge and a sphere at each connected node, then merges the geometry with a 3D boolean union. This works for 2D and 3D networks.planar: creates 2D rectangles and discs, unions them with Shapely, then extrudes the result. This is only for flat 2D networks, but it is usually more robust for thin planar networks.
Node junctions are sized per node. By default, each junction sphere or
disc uses the thickest beam touching that node. If a job sets
node_diameters, those file values are used instead. Isolated nodes do not
get junction geometry.
STL does not reliably store material metadata inside a single file. The supported multi-material workflow is to export one STL per material in the same coordinate frame. Slicers can then import the files together and assign each file to a different material or extruder.
Use the config-driven generator for multi-material output:
python config_to_stl.py sample_configs/multimaterial_test.jsonThat sample writes files like:
test_multimaterial_rigid.stl
test_multimaterial_flexible.stl
test_multimaterial_conductive.stl
test_multimaterial_preview.html
Config paths are resolved relative to the JSON config file.
For adjacency matrices, keep the adjacency matrix numeric and add a same-shape material matrix:
{
"output_dir": "../samples_output/material_demo",
"default_material": "rigid",
"geometry": {
"beam_diameter_mm": 3.0,
"cube_side_length_mm": 80,
"variable_thickness": true,
"node_material": "rigid",
"node_material_priority": true,
"node_radius_scale": 1.0,
"boolean_union": true
},
"jobs": [
{
"name": "test_multimaterial",
"positions": "demo_xy.csv",
"adjacency": "demo_adj.csv",
"adjacency_format": "auto",
"material_matrix": "demo_material_matrix.csv"
}
]
}The material matrix must have the same shape as the adjacency matrix.
Cell (i, j) names the material for edge (i, j). Empty, 0, none,
null, or nan cells fall back to default_material.
To use per-node diameters, add a node_diameters file path to the job:
{
"positions": "nodes.npy",
"node_diameters": "node_diameters.npy",
"adjacency": "edges.npy"
}For edge-list inputs, material can be carried directly in the edge file:
source,target,thickness,material
0,1,1.0,rigid
1,2,0.6,flexibleSet "adjacency_format": "edge_list" for that layout. For headerless
NumPy edge lists, also set edge_list_interpretation so the loader does
not have to guess:
{
"jobs": [
{
"name": "coded_edges",
"positions": "nodes.npy",
"adjacency": "edges.npy",
"adjacency_format": "edge_list",
"edge_list_interpretation": "material",
"edge_material_map": {
"0": "material_a",
"1": "material_b",
"2": "support_material"
}
}
]
}edge_material_map records what integer values in the edge list meant
when the config was generated. If a material-code column contains text
material names instead of integers, those names are used directly.
Pickle edge lists can carry explicit column metadata, which avoids guessing
whether a third column means thickness or material. Use a .pkl containing
either a list of edge dictionaries:
[
{"source": 0, "target": 1, "thickness": 1.0, "material": "rigid"},
{"source": 1, "target": 2, "thickness": 0.6, "material": "flexible"},
]or a dictionary with columns and edges:
{
"columns": ["source", "target", "thickness", "material"],
"edges": [
[0, 1, 1.0, "rigid"],
[1, 2, 0.6, "flexible"],
],
}Edge-list column handling is intentionally explicit when
edge_list_interpretation is present:
edge_list_interpretation |
Columns | Meaning |
|---|---|---|
legacy |
source,target[,length] |
default thickness, default_material; column 3 is ignored |
thickness |
source,target,thickness |
column 3 controls beam diameter |
material |
source,target,material_code |
default thickness plus per-edge material |
thickness_material |
source,target,thickness,material_code |
per-edge thickness and per-edge material |
If edge_list_interpretation is omitted, the loader keeps the older
heuristic behavior for compatibility: numeric third columns are treated
as thickness only when variable thickness is enabled, and text third
columns are treated as material.
Node junctions are controlled by geometry.node_material and
geometry.junction_policy:
- If
geometry.node_materialis set, every node junction sphere is written to that material, regardless of the incident edge materials. By default those node spheres also reserve their physical volume: beam-material STLs are cut away at the node spheres so a slicer cannot overwrite the node material with beam material. Set"node_material_priority": falseonly if you intentionally want the older overlapping-material output. geometry.node_radius_scalecontrols the radius of generated junction spheres relative to the thickest incident beam radius. The default is1.0; values like1.25or1.35make the node material visibly protrude around beam junctions. If the job has anode_diametersfile, those absolute diameters are used instead of this automatic scale rule.separate: same-material nodes stay with that material; mixed nodes are written tomixed_junction_material.dominant: mixed nodes go to the material with the largest total incident edge weight.
If geometry.node_material is omitted, node junctions still have a
defined default behavior: nodes touching only one material are written
with that edge material, and mixed-material nodes follow
junction_policy. junction_policy can be "separate" or
"dominant". For a single shared node material, set
geometry.node_material to an existing material name, such as the same
name as default_material. To use a separate node material, add that
material to the config first and then set geometry.node_material to its
name.
This demo is intended for teaching the full workflow from edge list and node positions to multi-material STL files.
python examples/random_material_edge_list_demo.py
python config_to_stl.py sample_configs/random_edge_material_demo/edge_material_demo.jsonThe demo writes:
sample_configs/random_edge_material_demo/edge_material_demo_xy.csv
sample_configs/random_edge_material_demo/edge_material_demo_edges.csv
sample_configs/random_edge_material_demo/edge_material_demo_node_materials.csv
sample_configs/random_edge_material_demo/edge_material_demo.json
The edge list has columns:
source,target,thickness,materialEach edge is randomly assigned to one of two edge materials. All nodes
are assigned to the same node material through geometry.node_material.
By default, the demo tries to use the neighboring sr_huppi_project
centerline-network generator if it is available at
/home/james/sr_huppi_project; otherwise it falls back to a small
self-contained synthetic network. To force one behavior:
python examples/random_material_edge_list_demo.py --source sr --sr-repo /path/to/sr_huppi_project
python examples/random_material_edge_list_demo.py --source syntheticThe repository includes a deterministic Voronoi example with about 150 network nodes. Its edges are randomly split between two materials, and all node junctions use one shared node material.
Generate or refresh the demo files with:
python examples/voronoi_random_material_demo.pyGenerate the STL files with:
python config_to_stl.py sample_configs/voronoi_random_material_demo/voronoi_material_demo.jsonThe committed demo has 152 graph nodes and 226 edges. The edge material split is recorded directly in:
sample_configs/voronoi_random_material_demo/voronoi_material_demo_edges.csv
The HuPPI periodic demo uses the neighboring HuPPI-Network-Analysis
repository to generate one perturbed-lattice point pattern with disorder
strength a = 0.5, then builds four periodic networks clipped at the box
boundaries:
- Gabriel
- Delaunay
- Delaunay-centroidal
- Voronoi
Generate the full demo with:
python examples/huppi_periodic_network_stl_demo.pyThe generated input/config files are contained in:
sample_configs/huppi_periodic_a05_demo/
The STL and HTML preview outputs are written to:
samples_output/huppi_periodic_a05_demo/
Each network folder contains three variants:
uniform_thickness.json: two-column edge list, one default beam thickness, one materialvariable_thickness.json: random edge thickness weights from0.5to2.0; the base beam diameter is scaled so the mean beam diameter is about3 mmtwo_materials.json: random beam materialsbeam_material_aandbeam_material_b, plus separatenode_materialjunction STLs whose volume is subtracted from the beam STLs. These configs setnode_radius_scaleto1.35so the node material is visible in the HTML previews.
The committed demo inputs use a 12 x 12 underlying point pattern
(144 points), seed 20260528, an 80 mm max coordinate span, and an
average beam diameter of about 3 mm. The generated graph sizes are
listed in sample_configs/huppi_periodic_a05_demo/manifest.json.
Open this file directly in a browser:
material_config_builder.html
The builder creates and downloads JSON config files and shows the command to run. A plain local HTML file cannot execute Python meshing code or write STL files directly, so STL generation still happens through:
python config_to_stl.py material_config.jsonGenerated Python caches and older byproducts can be removed with:
./clean.shOn Windows:
clean.batRun the lightweight regression tests with:
python -m unittest discover