1. Short answers
Is a single ground-truth OpenSCAD model reasonable?
Yes — at your stage it is a very good choice. OpenSCAD is text (perfect for git and for AI-assisted
editing), fully parametric, and exports STL, which is exactly what your slicer, Capytaine, and
OpenFOAM-family CFD all natively consume. The one weak link is FEA, which strongly prefers
B-rep geometry (STEP). There is a workable bridge (FreeCAD can import .scad and export
STEP), but if FEA becomes central, migrating the same parametric logic to
CadQuery or build123d (Python, code-first, AI-friendly, exports STEP and STL
from one source) is the natural evolution.
How to handle multi-part files? Keep one params.scad (the only file you ever
edit numbers in), a library of part modules, and one assembly.scad that places parts
with named transforms and doubles as the machine-readable assembly specification. Export each part
headlessly with openscad -D PART="..." from a script.
Can a 1:50 plastic model tell you anything about aluminum strength? Yes — but only about stiffness, load paths, and elastic buckling, not about yield/failure. Convenient coincidence: Froude-consistent structural scaling wants Emodel = Efull/50 ≈ 70 GPa/50 ≈ 1.4 GPa, which is nearly exactly PETG/ABS (~1.6–2.3 GPa). The right role for the plastic model is to validate your FEA model; the validated FEA (re-run with aluminum properties) is what predicts strength. Details in §4.
Recommended toolset (all open source): OpenSCAD (now) or CadQuery/build123d (later) → STL for slicer & Capytaine & OpenFOAM; STEP via FreeCAD or CadQuery → Gmsh → CalculiX (PrePoMax or FreeCAD FEM) for FEA; Meshmagick for hydrostatics; OpenFOAM+olaFlow or DualSPHysics for CFD when you actually need it.
2. Structuring a single-source OpenSCAD project
2.1 Repository layout
seastead/
├── params.scad ← THE ONLY FILE WITH NUMBERS (single source of truth)
├── lib/
│ ├── column.scad ← module column(...) { ... } (geometry only, no numbers)
│ ├── pontoon.scad
│ ├── deck.scad
│ └── bracing.scad
├── assembly.scad ← places all parts; PART selector for headless export
├── export.sh ← regenerates every deliverable (run before every test)
├── manifest.csv ← generated: part, stl file, position, rotation (BOM/assembly spec)
└── stl/ ← generated output
2.2 params.scad — every dimension lives here
// ============ params.scad ============
// Convention: author in FULL-SCALE METERS, Z up, waterline at z=0.
$fn = 64; // raise to 96–128 for final export
LAMBDA = 50; // model scale for printing (1:50)
// Columns
N_COLS = 4;
COL_X = 24; // spacing [m]
COL_Y = 18;
COL_D = 3.0;
COL_WALL = 0.050; // 50 mm aluminum
DRAFT = 12;
AIRGAP = 6;
// Pontoon, deck, bracing, ballast, mooring... all here.
scale(1000/LAMBDA) so the slicer receives
millimeters at model scale. Better still: export 3MF instead of STL for printing — 3MF carries
units, so the slicer gets it right automatically. OpenSCAD exports 3MF directly.2.3 assembly.scad — part placement is the assembly spec
include <params.scad>
use <lib/column.scad>
use <lib/deck.scad>
use <lib/pontoon.scad>
PART = "assembly"; // overridden on CLI: -D "PART=\"column\""
PRINT = false; // overridden for 1:50 export: -D "PRINT=true"
// Every part gets a named frame. These transforms are THE assembly definition.
col_positions = [ for (i=[0:N_COLS-1])
[ (i%2)*COL_X - COL_X/2, floor(i/2)*COL_Y - COL_Y/2, -DRAFT ] ];
module place(pos, rot=[0,0,0]) { translate(pos) rotate(rot) children(); }
module whole() {
for (p = col_positions) place(p) column(d=COL_D, wall=COL_WALL,
h=DRAFT+AIRGAP+DECK_T);
place([0,0, AIRGAP]) deck();
place([0,0,-DRAFT]) pontoon_pair();
}
scale(PRINT ? 1000/LAMBDA : 1)
if (PART == "assembly") whole();
else if (PART == "column") column(d=COL_D, wall=COL_WALL, h=DRAFT+AIRGAP+DECK_T);
else if (PART == "deck") deck();
else if (PART == "pontoon") pontoon_pair();
// Bonus: emit the assembly manifest from the same data → always in sync
for (i=[0:N_COLS-1])
echo(str("column_", i, ",stl/column.stl,", col_positions[i], ",[0,0,0]"));
OpenSCAD has no "mates" or constraints — the place() transforms in this file
are the fit-up specification, and the echo() output gives you a CSV
manifest (part name → STL file → translation/rotation) that lets Blender, FreeCAD, trimesh, or a
human reassemble the full structure exactly. If you want smarter, self-locating parts, look at the
BOSL2 library: its attachment system
(attach(), anchors, orientations) is the closest thing OpenSCAD has to CAD mates.
2.4 Headless export of every deliverable
#!/usr/bin/env bash
# export.sh — regenerate everything from the single source
set -e
PARTS="column deck pontoon assembly"
mkdir -p stl
for p in $PARTS; do
# full scale, meters → Capytaine / FreeCAD→STEP→FEA / CFD
openscad -o "stl/${p}_full.stl" -D "PART=\"$p\"" -D "PRINT=false" assembly.scad
# 1:LAMBDA, millimeters → slicer
openscad -o "stl/${p}_1to${LAMBDA}.stl" -D "PART=\"$p\"" -D "PRINT=true" assembly.scad
done
openscad assembly.scad 2> manifest.csv # capture echo() output
openscad --backend=manifold ...). Also: design rules for clean meshes —
avoid coincident/coplanar faces between parts (overlap them slightly), prefer hull()/
minkowski() for fair shapes, and verify "watertight 2-manifold" before exporting
(slicers, Capytaine and snappyHexMesh all demand it). trimesh can check:
python -c "import trimesh; m=trimesh.load('stl/assembly_full.stl'); print(m.is_watertight, m.volume)"
— and this also gives you displacement volume for free.3. The five pipelines
3.1 3D-printed Froude tank model you're here already
Froude scaling (λ = 50, i.e. full-scale dimension ÷ model dimension):
| Quantity | Full : Model | @ λ=50 |
|---|---|---|
| Length, wave height, draft | λ | ×50 |
| Time, wave period | √λ | ×7.07 |
| Velocity | √λ | ×7.07 |
| Acceleration | 1 | ×1 |
| Mass / displacement | λ³ | ×125,000 |
| Force | λ³ | ×125,000 |
| Pressure, stress | λ | ×50 |
| Mass moment of inertia | λ⁵ | ×3.1×10⁸ |
| Mooring stiffness | λ² | ×2,500 |
- Example: a 2.0 m, 10 s design wave → 40 mm, 1.41 s in the tank. A 8,000 t concept → 64 kg model.
- Ballast to the correct displacement, CG, and radii of gyration (scale λ⁵ for inertia) — motion response depends as much on these as on geometry. Build adjustable ballast trays into the printed model.
- FDM prints are rarely watertight: seal with epoxy/XTC-3D, or use resin printing for wet parts.
- Reynolds number does not scale — viscous damping (especially roll damping of columns/spars) will be under-represented. Note it; don't chase it at this stage.
3.2 Capytaine (linear seakeeping, RAOs)
OpenSCAD STL is directly usable. Requirements:
- Meters, outward face normals, watertight-ish surface mesh of the hull. Include the above-water part if you like and call
keep_immersed_part(). - Panel size: ≥ ~6–8 panels per shortest wave period's wavelength; refine once and check convergence.
- RAOs require rigid-body mass, CG and inertia — supply from your ballast layout; Meshmagick (same author ecosystem) computes displacement, waterplane, and hydrostatic stiffness (GM) straight from the same STL.
- Remember it's linear potential flow: excellent for comparing concepts and getting RAOs vs. wave height (linearly) and period; it will underpredict viscous roll damping and says nothing about slamming/green water. Cross-check one case with Nemoh or BEMRosetta (also open source).
import capytaine as cpt, numpy as np
body = cpt.FloatingBody(mesh="stl/assembly_full.stl", name="stead")
body.keep_immersed_part() # clip at z = 0
body.center_of_mass = (0, 0, ZCG) # from params/ballast calc
body.inertia_matrix = body.compute_rigid_body_inertia() # or supply ballast inertia
omegas = np.linspace(0.2, 2.0, 40) # rad/s
probs = [cpt.BEMProblem(body=body, omega=w, wave_direction=0.0,
radiating_dof=d) for w in omegas for d in body.dofs]
ds = cpt.BEMSolver().solve_all(probs)
rao = cpt.post_pro.rao(ds) # response per unit wave amplitude
Visualization of wave size/period effects: animate RAO-driven rigid-body motion of your STL
with matplotlib (GIF/MP4) or vedo/pyvista in 3D; or export GLB via
trimesh (mesh.export('model.glb')) and animate in three.js — convenient since the results
are headed for a website anyway.
3.3 FEA (structural strength)
FEA mesherss want STEP B-rep, not STL. Two workable routes from OpenSCAD:
- FreeCAD bridge: FreeCAD's OpenSCAD workbench executes your .scad (needs OpenSCAD installed) and rebuilds it as B-rep solids, which export to STEP:
Works well for moderately complex models; if a huge CSG tree chokes, convert part-by-part (you already have per-part export) and reassemble in FreeCAD using your manifest.csv.# run: freecadcmd scad2step.py assembly.scad seastead.step import importCSG, Import, sys doc = importCSG.open(sys.argv[-2]) solids = [o for o in doc.Objects if o.TypeId == "Part::Feature"] Import.export(solids, sys.argv[-1]) - Rebuild in CadQuery/build123d (see §6) — cleaner long term.
Loads at concept stage: don't overcomplicate. Use the "design wave" method — pick the worst regular-wave phasing from Capytaine output, apply hydrostatic + wave pressure as a static load case, plus self-weight and mooring tensions. Focus on column–deck and column–pontoon joints (stress concentrations), global bending, and plate buckling. Fatigue and class-rule load cases are the naval architect's job later.
Recommended stack: Gmsh (STEP → tet mesh) → PrePoMax (friendly GUI for CalculiX; Windows) or the FreeCAD FEM workbench (CalculiX/Elmer; cross-platform) → ParaView for pretty pictures. Salome-Meca/Code_Aster is the heavyweight alternative (powerful, steep).
3.5 CFD (waves, in the computer)
CFD is the one pipeline that natively loves STL (snappyHexMesh meshes around STL surfaces), so OpenSCAD output is fine here. Options, all open source:
- OpenFOAM + olaFlow (or waves2Foam): the standard VoF wave-tank setup. Powerful; expect days of case setup and hours–days per run.
- REEF3D: marine-focused, built-in regular/irregular wave generation and moorings; friendlier than raw OpenFOAM for exactly this problem.
- DualSPHysics (SPH, GPU): excellent for slamming, green water, breaking waves; has DesignSPHysics, a FreeCAD-integrated GUI.
4. Scaled structural modeling in plastic (1:50 → aluminum)
Verdict: yes, worth doing — as a stiffness and load-path validation tool for your FEA model, not as a direct strength test. The physics:
Under Froude scaling, forces scale as λ³ and geometry as λ, so stresses scale as λ. For a structurally meaningful model you must also scale stiffness — i.e. match the Cauchy/Froude combined requirement Emodel = Efull/λ. At λ = 50 with aluminum (E ≈ 70 GPa): target E ≈ 1.4 GPa — and PETG/ABS sit at ~1.6–2.3 GPa. This is a lucky near-match. Define the correction factor χ = λ·Emodel/Efull (≈1.4 for PETG):
| Measured on model | Full-scale value | Scales correctly? |
|---|---|---|
| Applied force / pressure | ×λ³ / ×λ | ✔ (apply Froude-scaled loads) |
| Deflection | ×λ·χ (≈ ×71) | ✔ with correction |
| Strain (gauges/DIC) | ×χ (≈ ×1.4) | ✔ with correction |
| Natural frequencies | ÷√(λ·χ) (≈ ÷8.4) | ✔ — and matches Froude time scaling when χ≈1 |
| Elastic buckling modes & loads | scales consistently | ✔ (stiffness-driven) |
| Yield / ultimate failure | ✖ | Model is ~10× "too strong" (see below) |
Why failure doesn't scale: you'd need a plastic with yield ≈ 250 MPa/50 ≈ 5 MPa; real printed plastics are 30–60 MPa and fail by layer delamination. A PETG model carries roughly an order of magnitude more scaled load than the aluminum original before failing. It will never show you the real failure load — but it will show you global deflection shapes, vibration modes, joint flexibility, and elastic buckling, which are the things FEA models usually get wrong.
Rules that make it useful
- Scale every structural dimension, including wall/plate thicknesses (0.05 m → 1.0 mm printed). Where printability forces thicker walls, record it and replicate the cheat in the comparison FEA model.
- Print tensile coupons with the same orientation/infill and measure your actual E; printed parts are anisotropic. Compute χ from measured E, not datasheet E.
- Apply loads as λ³-scaled forces at the correct scaled locations (whiffletree/dead weights), or test in the wave tank so the loads come for free.
- Measure deflections (dial gauges/laser), strains (cheap foil gauges or DIC), and natural frequencies (tap test + phone accelerometer).
- Build the FEA model of the plastic model first and match its measurements; only then trust the aluminum FEA. This is the whole point of the exercise.
- Beware creep: plastics sag under sustained load — take readings promptly, keep temperatures steady.
5. Open-source software map
| Tool | Role in your pipeline | Consumes your OpenSCAD output? | Notes |
|---|---|---|---|
| OpenSCAD (+ BOSL2) | Parametric geometry (current) | — | Text-based, AI-friendly; mesh-only output; use Manifold backend for speed |
| CadQuery / build123d | Parametric geometry (upgrade path) | replaces it | Python code-first CAD on OpenCASCADE; exports STEP + STL + glTF; assemblies with mates; ideal for AI generation |
| FreeCAD | B-rep bridge, FEM frontend, assemblies | ✔ imports .scad (OpenSCAD workbench) | Exports STEP; FEM workbench drives CalculiX/Elmer; scriptable via freecadcmd |
| Capytaine | Linear seakeeping (RAOs) | ✔ STL directly | Pair with Meshmagick for hydrostatics/GM |
| Meshmagick | Hydrostatics from mesh | ✔ STL directly | Displacement, waterplane, stability at concept level |
| Nemoh / BEMRosetta / HAMS | BEM cross-check | ✔ via mesh conversion | Trust-but-verify Capytaine on one case |
| Gmsh | FEA/CFD meshing | ✔ STL (remesh) / STEP (clean) | Also converts between mesh formats |
| CalculiX + PrePoMax | FEA solver + GUI | via STEP/INP | PrePoMax is the least painful open FEA front end |
| Elmer / Code_Aster / FEniCS | FEA alternatives | via Gmsh meshes | Elmer is inside FreeCAD; Code_Aster via Salome-Meca |
| OpenFOAM + olaFlow | VoF wave CFD | ✔ STL natively (snappyHexMesh) | Industrial standard; steep but well documented |
| REEF3D | Marine CFD | ✔ STL | Built-in irregular waves + moorings; very relevant to floating structures |
| DualSPHysics | SPH CFD (slamming, green water) | ✔ STL | GPU; DesignSPHysics GUI lives inside FreeCAD |
| meshio / trimesh / pyvista | Mesh plumbing & mass properties | ✔ STL | Format conversion, watertight checks, volume/inertia, glTF export for web viz |
| ParaView / Blender | Post-processing & presentation | ✔ | Blender ocean modifier + your STL makes great concept videos |
6. Alternative single-source stacks
Option A — Stay OpenSCAD-centric (lowest friction)
Exactly §2 + §3: OpenSCAD → STL for print/Capytaine/CFD; FreeCAD importCSG → STEP →
Gmsh → CalculiX for FEA. Cost: the FEA bridge is the fragile link. Benefit: you keep your current
AI-assisted workflow untouched.
Option B — CadQuery or build123d (recommended evolution)
Same code-first, AI-friendly, git-diffable workflow — but on the OpenCASCADE B-rep kernel, so one Python file produces STEP (FEA/CFD), STL/3MF (printing, Capytaine), and glTF (web viz), plus true assemblies with constraints. Fillets/chamfers are native (no more Minkowski gymnastics). Your OpenSCAD logic ports over concept-by-concept; AI assistants are fluent in CadQuery.
import cadquery as cq
from params import * # same single source of numbers
def column():
return (cq.Workplane("XY").circle(COL_D/2).extrude(COL_H)
.faces(">Z").shell(-COL_WALL)) # thin wall, native B-rep
assy = cq.Assembly(name="seastead")
for i, (x, y) in enumerate(column_positions()):
assy.add(column(), name=f"col_{i}", loc=cq.Location(cq.Vector(x, y, -DRAFT)))
assy.add(deck(), name="deck", loc=cq.Location(cq.Vector(0, 0, AIRGAP)))
assy.save("seastead.step") # → Gmsh → CalculiX, or OpenFOAM
cq.exporters.export(assy.toCompound().scale(1000/50), "seastead_1to50.stl") # → slicer
Option C — FreeCAD-centric (GUI + scripting)
Parametric B-rep modeling, built-in FEM workbench, assembly workbench (v1.0+), Python scripting
headless via freecadcmd, and DesignSPHysics for SPH waves — the most "all-in-one" open
option. Downside: the GUI/topological-naming learning curve; AI help is best via its Python API
rather than the GUI.
7. Gotchas checklist
- STL has no units and no topology — fix a written convention (meters, Z-up, waterline z=0) and enforce it in
params.scadcomments and the export script. - Non-manifold meshes from coincident CSG faces break slicers, snappyHexMesh, and BEM panels — overlap parts slightly; check with trimesh.
- Face normals must point outward for Capytaine; flipped normals give nonsense added mass.
- Panel size vs. wavelength: don't crank
$fnblindly — mesh resolution should follow the shortest wave period you simulate, not the printer's resolution. - OpenSCAD performance: use the Manifold backend and keep
$fnlow while iterating. - Reynolds mismatch at 1:50: viscous/roll damping is under-represented in both tank and BEM; budget a CFD or empirical correction for the final concept.
- Version everything: params.scad + export.sh in git means every STL is reproducible; tag a commit for every tank test.
- Printed model integrity: watertight sealing, measured material E, creep-aware test procedure.
8. What to hand the naval architect later
- STEP assembly + BOM/manifest (from Option A or B pipeline)
- Mass properties report: displacement, CG, inertia tensor, hydrostatics/GM (Meshmagick output)
- RAO plots vs. wave period/direction (Capytaine), plus tank-test data and videos
- Design-wave load cases used in FEA, and the FEA report (with the plastic-model validation evidence from §4)
- A written list of assumptions and known limitations (viscous damping, fatigue untouched, no class rules applied yet)
That package lets the NA start engineering immediately instead of re-discovering your concept — and class-society rules (e.g., ABS/DNV floating-structure guides) get applied at that stage, not now.