Geometry

Mesh baking needs only the standard library; CAD-solid export needs the cad extra (pip install "longeron[cad]").

Parametric 3D geometry for architecture mixes (spike).

Builds to-scale UAVs from a mix’s catalog attribute values with plain triangle meshes (stdlib math only). Four airframe families are supported: the N-arm multirotor (drone_geometry() – arms every 360 / N degrees, sized from prop diameter + tip clearance; a 3-arm frame grows a tail boom, coaxial=True stacks counter-rotating motor pairs on every arm, and the default is the classic quad), the streamlined teardrop-body quad (teardrop_quad_geometry() – a lathed low-drag bullet stood on end, its long axis normal to the planar rotor quad around it), the cruciform tail-sitter VTOL (winged_vtol_geometry() – a minimal lathed fuselage, two unswept airfoil wing pairs in a + cruciform, and one tractor rotor on each of the four wingtips with every thrust axis parallel to the chords/body axis; baked nose-up in its hover attitude), and the streamlined interceptor (interceptor_geometry() – slender lathed fuselage, thin unswept NACA-0009 wing, cruciform tail, pusher prop). The tailless flying wing (flying_wing_geometry()) is the one family drawn with real stability-and-control geometry: quarter-chord sweep, tip washout, and a parametric reflexed section, straight from its model attributes. Every lifting surface is lofted from a real NACA 4-digit section (naca4_profile()), not a rectangular slab. Motor cylinders come from motor mass (solid-cylinder density heuristic), prop disks from diameter, and the battery box from battery mass (LiPo density + brick proportions). One call turns a configuration into the mesh dict longeron.widgets.viewer3d paints:

{"unit": "m",
 "parts": [{"name", "color", "opacity",
            "vertices": [x, y, z, ...], "faces": [i, j, k, ...]}, ...],
 "bounds": [[xmin, ymin, zmin], [xmax, ymax, zmax]]}

A part may additionally carry a key – a stable model identity (by convention the SysML part usage’s qualified name) stamped by tag_parts() – which the viewer uses for linked selection (longeron.analysis.link); untagged parts fall back to their name.

The same geometry feeds the GEOMETRIC REQUIREMENT CHECKS, which are CAD-NATIVE: camera_occlusion() builds a VIEW CONE solid at a mounted camera (apex at the lens, axis along the boresight, half-angle fieldOfView / 2) and boolean-INTERSECTS it with every other component’s parametric solid – the same solids to_cadquery() builds – reporting intersected volume over cone volume (0.0 is a perfectly clear view), and disc_overlap() boolean-intersects each propeller disc (a thin cylinder solid, stamped analytically by drone_geometry() in split_instances mode) with every other component, reporting the overlap volume (0.0 is no overlap). cadquery (the cad extra) powers the exact booleans; without it both checks fall back to a deterministic stdlib volume quadrature over the mesh triangles that integrates the SAME measures (see occlusion_report() / overlap_report() for the accuracy contract). Both are deterministic and keyed by geometry_checks() to feed the DeepScout program’s installation requirements (examples/deepscout/aircraft.sysml) through the scoreboard’s values= seam in either posture.

House pattern: MESH geometry is baked in Python once per configuration (a millisecond or so – no CAD kernel in the render loop); the front-end never recomputes it. Y is up, +X is forward, one unit is one metre, and every dimension is a real measurement or a documented heuristic, so two mixes render truly to scale side by side (lineup() merges several configurations into one to-scale scene). mission_geometry() dispatches a UavMissions-style mix onto its family builder from the selected airframe’s attributes.

to_cadquery() rebuilds the quad-copter assembly as CAD solids (STEP export, and the solid source of the CAD-native checks above) behind the cad extra – the mesh pipeline here deliberately does not need the ~1 GB OCC kernel.

longeron.analysis.geometry.BayShape

the payload-bay shape vocabulary of the DeepScout Airframe def (examples/deepscout/aircraft.sysml – the bayShape attribute): a "box" slung under a rotor hub, a "hull" bay inside a lathed fuselage, or an "ogive" pod blended at a flying wing’s root

alias of Literal[‘box’, ‘hull’, ‘ogive’]

longeron.analysis.geometry.GeometryEngine

which implementation the interference/occlusion checks run on: "cad" (exact booleans on the OCC kernel, behind the cad extra), "mesh" (the dependency-free sampled fallback), or "auto" (cad when importable, mesh otherwise – the honest default)

alias of Literal[‘auto’, ‘cad’, ‘mesh’]

longeron.analysis.geometry.airframe_geometry(*, wing_span, wing_area, taper, fuselage_length, motor_count, arm_count, prop_diameter, motor_mass, battery_mass, fc_mass=None, esc_mass=0.014, arm_thickness=None, arm_width=None, sweep_deg=0.0, washout_deg=0.0, wing_section=None, center_section_span=0.0, pod_length=None, pod_diameter=0.05, pod_station=None, fuselage_diameter=None, tail_area=None, tilt_pivot_setback=None, tilt_arm=None, nose_arm=None, tilt_deg=None, bay_shape=None, bay_length=None, bay_width=None, bay_height=None, camera=None)[source]

Family-dispatched geometry from airframe-shell attribute values.

The keyword names mirror the geometry knobs of the DeepScout Airframe def (examples/deepscout/aircraft.sysml) plus the propulsion sizes a mix or a display default supplies. The dispatch ladder picks the builder: no wing and no fuselage -> drone_geometry() (the N-arm multirotor – arm_count sets the frame family, and a station count of twice the arm count stacks the coaxial pairs); no wing but a real fuselage -> teardrop_quad_geometry() (the upended bullet); a wing with no fuselage -> flying_wing_geometry() (the tailless family: the wing IS the fuselage); a single motor station -> interceptor_geometry(); a wing AND a fuselage with a declared tilt-pivot chain (tilt_arm) -> tilt_tri_geometry() (the convertible tilt-rotor tri, rendered at tilt_deg – hover attitude by default, the tail-sitter’s convention); otherwise winged_vtol_geometry() (the cruciform tail-sitter, rendered in hover attitude). arm_thickness/arm_width draw the quad families’ arms at a load-sized tube diameter when given; esc_mass is the drone branch’s 30.5 mm stack heuristic. Two callers feed this ladder: mission_geometry() from a mission-catalog mix, and longeron.analysis.grand.scene_for() from a fleet airframe definition’s own attributes. The tailless S&C and pusher-installation knobs (sweep_deg, washout_deg, wing_section, center_section_span, pod_length, pod_diameter, pod_station) reach flying_wing_geometry() only – the flying wings are the one family whose model declares them; every other loft keeps its zero-sweep planform until the loft framework generalizes – and the tilt-pivot knobs (tilt_pivot_setback, tilt_arm, nose_arm, tilt_deg, with fuselage_diameter and tail_area riding along) reach tilt_tri_geometry() only, the one family whose model declares a pivot chain. The declared payload bay rides along per family (bay_shape and dimensions, the Airframe def’s bayShape vocabulary – BayShape): a "box" reaches the multirotor builder as the slung box, an "ogive" reaches the flying wings as the blended pod, and a "hull" bay needs no knobs – the lathed builders draw their hull bays from the fuselage they already have. camera mounts the mission camera on the multirotor builder (the other builders draw their own camera bodies at fixed stations).

Return type:

dict[str, Any]

longeron.analysis.geometry.architecture_geometry(study, architecture, **overrides)[source]

drone_geometry() for a mix (see architecture_params()).

Return type:

dict[str, Any]

longeron.analysis.geometry.architecture_params(study, architecture)[source]

Geometry inputs from a drone-catalog mix.

Expects the TradeQuad-style variation points motors (attribute mass), props (diameterIn), battery (mass), and esc (mass) – this is demo-grade wiring for the drone catalog, not a generic geometry mapping.

Return type:

dict[str, float | None]

longeron.analysis.geometry.camera_occlusion(mesh, camera=None, *, sensing_range=None, resolution=24, exclude=('camera',), engine='auto')[source]

The airframe volume inside the view cone over the cone volume.

The scalar measure behind the clearView requirement of the DeepScout program (“occludedFraction”): 0.0 is a perfectly clear view cone, anything positive means some component pokes into it. See occlusion_report() for the cone construction, the engines, and the per-part offender breakdown.

Return type:

float

longeron.analysis.geometry.disc_overlap(mesh, *, resolution=24, engine='auto')[source]

The total propeller-disc overlap volume, in cubic metres.

The scalar measure behind the propClearance requirement of the DeepScout program (“discOverlapVolume”): the sum over every disc of overlap_report()’s per-disc overlap. 0.0 means no disc touches anything; a disc-against-disc overlap counts once per participating disc.

Return type:

float

longeron.analysis.geometry.drone_geometry(*, prop_diameter_in, motor_mass, battery_mass, esc_mass, fc_mass=None, arm_count=4, coaxial=False, arm_thickness=0.005, arm_width=0.013, segments=24, split_instances=False, motor_spacing=None, camera=None, bay_length=None, bay_width=None, bay_height=None)[source]

A to-scale multirotor mesh dict from catalog attribute values.

arm_count sets the frame family: arms radiate every 360 / N degrees (_arm_angles() – the “X” convention, nose clear), a 3-arm frame stretches its single rear arm into the tail boom (_TRI_BOOM_RATIO), and the default 4 reproduces the classic quad byte-for-byte. coaxial stacks a counter-rotating pair on every arm: the upper motor rides the arm top exactly like the flat build, the lower hangs _COAX_DROP beneath it on a drawn standoff post, and its prop disc spins below – two discs per arm, both stamped in split_instances mode.

The frame is derived: adjacent motors sit one prop diameter plus _PROP_CLEARANCE apart (for any N, on the circle that spacing implies), so a 10-inch cruiser genuinely dwarfs a 5-inch racer and a hexa is honestly wider than a quad. motor_spacing overrides that derivation with a FIXED adjacent motor-to-motor distance – a real frame does not grow when a bigger prop is bolted onto it, so a prop-swap what-if passes the stock spacing and lets disc_overlap() judge the result. arm_thickness/arm_width default to the demo heuristics; callers with load-sized arm tubes (see mission_geometry()) pass the sized outer diameter so heavier- loaded designs genuinely look beefier. Parts of one kind merge into a single mesh (one draw call each in the viewer).

split_instances keeps the motor and prop instances as separate parts – motor1 .. motorR and prop1 .. propR for R rotors, the same names and order as the to_cadquery() assembly children (coaxial builds count the uppers first, arm by arm, then the lowers in the same arm order) – so each can carry its own identity key (e.g. an M0 individual id, see tag_parts()) for per-instance linked selection. The geometry is a pure re-partition: concatenating the instance parts reproduces the merged part exactly, and the default (False) output is unchanged. Split mode additionally stamps the analytic propeller discs onto the mesh (mesh["discs"]: centre, normal, radius, thickness, owning part, and the same-station parts an overlap check must ignore) – the input overlap_report() consumes – and the parametric recipe (mesh["cad"]: this function’s own sizing inputs) from which the CAD-native checks rebuild the exact solids via to_cadquery().

camera mounts the mission camera: a mapping with the placement and boresight attribute names of the DeepScout program’s ScoutParts::F450Kit::Camera part (x/y/z metres from the top-plate centre, azimuth/elevation/fieldOfView degrees), typically the slot dict of an instantiated/interpreted camera individual. It adds a violet camera body part (a small box, yawed to the azimuth) and stamps the parameters on mesh["camera"] for camera_occlusion()’s view cone.

The hub owns its structure: a bottom plate closes the plate stack (the battery straps to it at its unchanged station) and one arm-root gusset per arm grows out of the plates (_hub_gussets()) – the drawn twin of the mission sizing’s gussetMass. fc_mass stacks the flight-controller board above the ESC on the 30.5 mm pattern (its own slate fc part, so the FC is selectable). bay_length/bay_width/ bay_height sling the declared payload-bay box under the battery (a fleet airframe’s bayShape "box" – see airframe_geometry()); a coaxial build drops the box below its lower disc plane. All three default to None: no bay part.

Return type:

dict[str, Any]

longeron.analysis.geometry.flying_wing_geometry(*, wing_span, wing_area, taper, motor_count, prop_diameter, motor_mass, battery_mass, fc_mass=None, sweep_deg=0.0, washout_deg=0.0, section='0015', center_section_span=0.0, pod_length=None, pod_diameter=0.05, pod_station=None, bay_length=None, bay_diameter=None, segments=24)[source]

A to-scale tailless flying wing: swept panels, trailing-edge pushers.

The wing IS the airframe (the DeepScout FlyingWings convention: fuselageLength 0 marks the family): straight-tapered panels lofted from section (a NACA 4-digit code or a "reflexed"-prefixed one, see _lift_surface()), the quarter-chord line raked aft by sweep_deg and the tips twisted down by washout_deg; a winglet rides each swept tip, raked with the wing. With center_section_span > 0 the planform is CRANKED – the standard flying-wing pusher installation: the trailing edge runs FLAT across the center section (level with the crank station’s trailing edge) so a root pusher disc tucks behind it, and only the outer panels carry the swept trailing edge (and the washout: a cranked center bay flies untwisted).

Every motor station hangs a pusher pod on the local trailing edge: a nacelle of pod_diameter running pod_length straight aft, the motor can flush at its tail and the prop disc just behind it. pod_station places the stations: the model’s declared podStation (metres from the centerline, mirrored per side) – the tip-prop twin flows its wingtip stations through here exactly the way sweep flows. None keeps the display default, a symmetric row across the middle third of the span (which is the root twin’s declared station). pod_length None derives the clearance length locally – base housing + disc clearance + the trailing edge’s aft rise across the disc’s own span extent – so the disc plane clears the wing for ANY prop; the model-driven callers pass the declared podLength (derived the same way in examples/deepscout/flyingwing.sysml for the branch’s reference disc). The planform knobs are the model’s own declared attributes (sweepDeg / washoutDeg / centerSectionSpan / the reflexed 15% bay section), so the drawn planform IS the declared one.

The payload bay is a blended OGIVE pod on the root’s belly (bay_length x bay_diameter, the model’s bayLength / bayWidth; None sizes a display pod from the battery brick): a body of revolution whose crown rides up inside the root section, round nose just ahead of the leading edge, fine tail well clear of the pusher discs. The battery brick, the flight-controller board, and the mission camera ride INSIDE it at their true stations – their own selectable parts – replacing the naked battery sleeve this family used to poke through its own skin. One stand-in remains, named so nobody reads more than the mesh knows: the reflexed camber line is a simple parametric S-camber, not a catalog airfoil, and the panels loft straight (C0 at the crank) – the blended center body this family really flies is a job for the loft framework’s lofted wing body.

Return type:

dict[str, Any]

longeron.analysis.geometry.geometry_checks(mesh, *, sensing_range=None, resolution=24, engine='auto')[source]

Both geometric requirement measures, keyed for the scoreboard.

Returns {"occludedFraction": ..., "discOverlapVolume": ...} – exactly the free names the installation requirements of the DeepScout program measure, so the result feeds scoreboard(model, values=geometry_checks(mesh)) directly (the lightest honest wiring: the measures are computed kernel-side from the same parametric geometry the 3D viewer paints, then injected as evaluation-frame bindings; nothing is baked into the model file). Needs a mesh built with drone_geometry(split_instances=True, camera=...). Both measures read 0.0 for a clean installation in BOTH engines (see occlusion_report() for the engine contract).

Return type:

dict[str, float]

longeron.analysis.geometry.interceptor_geometry(*, body_length, wing_span, wing_area, taper, prop_diameter, motor_mass, battery_mass, fc_mass=None, segments=24)[source]

A to-scale streamlined interceptor: slender body, pusher prop.

The fuselage is a lathed low-drag body just wide enough for the battery brick; the wing is a thin unswept NACA-0009 loft (chord = wing_area / wing_span, straight quarter-chord) at mid-body, the cruciform tail fins carry the same section, and the single catalog prop pushes at the stern. The battery bay is drawn as an indigo sleeve around the fuselage at its true length and position (the brick rides inside the body).

Return type:

dict[str, Any]

longeron.analysis.geometry.lineup(meshes, *, gap=0.25, labels=None)[source]

Merge mesh dicts into one to-scale scene on a shared ground plane.

Each mesh keeps its parts (names prefixed by its label so a scene can carry several configurations); everything sits on a shared ground plane (ymin aligned) with gap metres between neighbouring cells. Up to three meshes pack side by side along X at their true widths; larger lineups fold into the adaptive grid of _grid_shape() (rows along Z, row-major from the front, uniform cells sized by the largest footprint) so four configurations read as 2x2, six as 2x3, eight as 2x4. With labels the scene carries a labels list ({text, anchor}) that longeron.widgets.viewer3d.mesh_viewer() renders as a billboard caption above each cell.

Return type:

dict[str, Any]

longeron.analysis.geometry.mission_geometry(study, architecture, **overrides)[source]

Family-dispatched geometry for a mission-catalog mix.

The selected airframe’s attributes feed airframe_geometry(), whose dispatch ladder picks the family builder. When the mix’s metrics carry the load-sized armOuterDiameter (the assembly’s structural sizing), the quad families draw their arms at that diameter – a sprint-motor aluminum build genuinely looks beefier than a carbon eco build.

Return type:

dict[str, Any]

longeron.analysis.geometry.mission_params(study, architecture)[source]

Geometry inputs from a mission-catalog mix.

Expects variation points airframe (attributes wingSpan, wingArea, taper, fuselageLength, motorCount, armCount), motors (mass), props (diameter), and battery (mass) – the convention of the DeepScout mission catalog (examples/deepscout/missions.sysml). The tailless S&C knobs (sweepDeg, washoutDeg, wingSection) ride along when the selected airframe declares them and default to the unswept legacy planform when it does not.

Return type:

dict[str, Any]

longeron.analysis.geometry.naca4_profile(code='2412', points=24)[source]

A closed NACA 4-digit section as chord-normalized (x, y) pairs.

Cosine-spaced stations, closed trailing edge (the -0.1036 thickness coefficient), ordered TE -> upper surface -> LE -> lower surface -> TE, i.e. counter-clockwise in the chord plane. points is the total vertex count of the closed polygon (~24 is plenty for a mesh loft).

Return type:

list[tuple[float, float]]

longeron.analysis.geometry.occlusion_report(mesh, camera=None, *, sensing_range=None, resolution=24, exclude=('camera',), engine='auto')[source]

How much of the camera’s view cone the airframe fills, and what.

The check is CAD-NATIVE: a VIEW CONE solid – apex at the camera position, axis along the azimuth/elevation boresight, half-angle fieldOfView / 2, reaching sensing_range metres (default: the airframe bounding-box diagonal, long enough to sweep past the whole craft) – is boolean-intersected with every other component’s parametric solid, the same solids to_cadquery() builds. A perfectly clear view intersects nothing. Returns:

{"engine": "cad" | "mesh",
 "occludedFraction": intersected volume / cone volume,
 "occludedVolume": ...,             # m^3, union of all offenders
 "coneVolume": ..., "sensingRange": ...,
 "obstructions": {part: m^3, ...}}  # offenders, largest first

engine picks the implementation. "cad" (exact booleans) needs the cad extra and a mesh built by drone_geometry(split_instances=True) (which stamps the parametric recipe the solids are rebuilt from); "mesh" is a stdlib fallback that integrates the SAME measure by deterministic volume quadrature – ray-parity point-in-solid tests over an exact cell decomposition of the cone – against the mesh triangles. The default "auto" uses CAD when both prerequisites hold. The quadrature is exact for a clear cone (every weight counted is a genuine interior point) but can MISS features thinner than a grid cell (resolution axial slabs); treat its nonzero readings as real and its zeros as “nothing grid-cell-sized”. CAD per-part volumes are each exact (parts that interpenetrate each other are counted once per part); the mesh engine attributes each cell to the first part (mesh order) containing it.

camera defaults to the parameters stamped on mesh["camera"] by drone_geometry(); pass an explicit mapping (the Camera part’s attribute names) for what-ifs – e.g. the same camera yawed azimuth=180 to look back through the airframe. exclude names mesh parts the cone may legitimately contain (the camera’s own body, whose centre is the cone apex). Deterministic in both engines: equal inputs give equal fractions.

Return type:

dict[str, Any]

longeron.analysis.geometry.overlap_report(mesh, *, resolution=24, engine='auto')[source]

Per-disc overlap: how much of each propeller disc is inside what.

The CAD-native reading of “propeller discs shall not overlap other components”: each disc is a thin cylinder solid (the assembly’s own prop cylinders), boolean-intersected with every other component’s solid. Consumes the analytic discs drone_geometry() stamps on mesh["discs"] in split_instances mode (a disc knows which same-station parts – its own prop and motor can – to ignore). Returns one row per disc, ordered as stamped:

{"disc": "prop1", "engine": "cad", "overlap": 0.0,   # m^3
 "parts": {offender: m^3, ...}}                      # largest first

overlap is the volume of the disc’s intersection with the UNION of the non-excluded components – exactly 0.0 when the disc is clear. Engines as in occlusion_report(): "cad" computes exact booleans on the parametric solids (needs the cad extra + the stamped recipe); "mesh" estimates the same volumes by deterministic mid-plane quadrature over the stamped disc (exact-zero when clear, may miss sub-cell slivers); "auto" prefers CAD.

Return type:

list[dict[str, Any]]

longeron.analysis.geometry.tag_parts(mesh, mapping, *, strict=True)[source]

Stamp model identities onto mesh parts (linked-selection plumbing).

Returns a copy of mesh whose parts named in mapping carry a key – by convention the qualified name of the model part the component renders, or, for per-instance parts (see drone_geometry()’s split_instances), the M0 individual id from longeron.m0.interpret() (Rotorcraft::QuadCopter#0. motors#2), whose dotted path derives the owning usage for linked selection (longeron.analysis.link.individual_qname()). Several mesh parts may share one key, and parts not named keep no key and fall back to their name as their identity in longeron.widgets.viewer3d. Vertex and face arrays are shared with the input, not copied.

With strict (the default) every mapping entry must name a part, so typos fail loudly; pass strict=False to reuse one mapping across airframe families with different part sets. lineup() carries keys through unchanged (its label prefixes only rename), so tag each configuration before merging and a selection lights up in every cell.

Return type:

dict[str, Any]

longeron.analysis.geometry.teardrop_quad_geometry(*, fuselage_length, prop_diameter, motor_mass, battery_mass, fc_mass=None, arm_thickness=0.005, arm_width=0.013, segments=24)[source]

A to-scale streamlined teardrop-body quad (wingless dash bird).

The shell is a body of revolution lathed from the NACA-0025 half-thickness curve and stood on end: its long axis is NORMAL to the rotor plane – the bullet pierces the disk plane blunt-nose-up, fine tail down – so in a dash (the whole quad pitched over) the body flies point-first with minimal frontal area. Four arms radiate horizontally from the widest station to the lift motors, keeping the four prop disks a planar quad around the body (surface normals +Y, parallel to the body axis) with genuine radial clearance to the hull. The battery is drawn as an indigo sleeve at its true position inside the shell.

Return type:

dict[str, Any]

longeron.analysis.geometry.tilt_tri_geometry(*, wing_span, wing_area, taper, fuselage_length, prop_diameter, motor_mass, battery_mass, fc_mass=None, fuselage_diameter=None, tail_area=None, sweep_deg=0.0, section='2412', pod_diameter=0.05, tilt_pivot_setback=0.04, tilt_arm=0.09, nose_arm=0.1, nose_pivot_setback=0.0, tilt_deg=90.0, segments=24)[source]

A to-scale tilt-rotor tri-wing at a commanded tilt angle.

The convertible of the fleet (the DeepScout TiltRotors branch): a lathed fuselage, one swept tapered wing lofted from section, a conventional tail (tailplane pair + fin, NACA 0009, sized from tail_area), and THREE pivoting motor stations – a pusher pod on each wingtip trailing edge (Arc B’s tip-pusher installation) and a tractor at the nose. tilt_deg is the conversion state and a first-class scene parameter: 0 is cruise (tip discs vertical behind the tips, nose disc vertical ahead of the apex), 90 is hover (the tip pods swing DOWN so their discs hang level under the tips, the nose unit swings UP so its disc rides level over the nose), and any angle between renders the conversion mid-arc, so the interference gate can sample the whole sweep.

The pivots are the model’s own declared chain: each tip pivot sits tilt_pivot_setback aft of the LOCAL trailing edge (inside the pod, never inside the wing) with its disc tilt_arm further aft; the nose pivot sits nose_pivot_setback aft of the fuselage apex (0 in the declared geometry) with its disc nose_arm ahead. Pivot-outside-the-surface is the conversion-clearance guarantee – every disc point stays at least its arm’s length from its pivot through the sweep – and the setback knobs exist precisely so a test can BREAK the guarantee (bury a pivot) and prove the interference oracle catches it. The battery brick, the flight-controller board, and the mission camera ride the hull at their true stations as selectable parts; fuselage_diameter draws the hull at the model’s declared width (None falls back to the battery-brick heuristic the other lathed builders use).

Return type:

dict[str, Any]

longeron.analysis.geometry.to_cadquery(*, prop_diameter_in, motor_mass, battery_mass, esc_mass, fc_mass=None, arm_count=4, coaxial=False, arm_thickness=0.005, arm_width=0.013, motor_spacing=None, camera=None, bay_length=None, bay_width=None, bay_height=None)[source]

The same parametric assembly as cadquery solids (cad extra).

Returns a cadquery.Assembly with one named, colored child per part – ready for assembly.export("drone.step") or downstream CAD – built from the same sizing inputs as drone_geometry() (arm_count/coaxial pick the frame family and the coax stacking, motor_spacing fixes the adjacent motor-to-motor distance for prop-swap what-ifs, camera mounts the mission camera body and takes the Camera part’s attribute names). The child names and order match drone_geometry’s split_instances parts (coax lowers follow the uppers). These exact solids are what the CAD-native geometric checks (occlusion_report() / overlap_report()) boolean-intersect. Kept separate from the mesh pipeline so the viewer never depends on the OCC kernel.

Return type:

Any

longeron.analysis.geometry.view_cone(camera, *, length)[source]

The camera’s view cone as a cadquery solid (cad extra).

Apex at the camera position, axis along the azimuth/elevation boresight, half-angle fieldOfView / 2, truncated length metres from the apex (the sensing range under test). This is the solid the CAD-native occlusion check boolean-intersects with the airframe: any non-empty intersection is an obstruction. camera uses the Camera part’s attribute names (see occlusion_report()).

Return type:

Any

longeron.analysis.geometry.winged_vtol_geometry(*, wing_span, wing_area, taper, fuselage_length, prop_diameter, motor_mass, battery_mass, fc_mass=None, segments=24)[source]

A to-scale cruciform tail-sitter VTOL, baked in its hover attitude.

The craft is assembled nose-along-+X (the cruise frame) and then stood on its tail (+X -> +Y), so the scene reads as hover: a minimal slender lathed fuselage; two unswept airfoil-lofted wing pairs in a + cruciform – the main NACA-2412 pair spans wing_span with chord = wing_area / wing_span tapering about a straight quarter-chord, the secondary NACA-0009 pair is _SECONDARY_SPAN_RATIO of that span at the same chords – and one tractor rotor on each of the four wingtips, every thrust axis PARALLEL to the wing chords and the body axis. In hover the vehicle hangs nose-up on its four (now horizontal) disks; for cruise the whole craft pitches over and flies wing-borne. No booms and no separate tail: the cruciform panels are the tail. The main-pair tips carry the catalog props, the secondary tips _SECONDARY_PROP_RATIO-scaled ones (that ratio is baked into the catalog’s diskAreaFactor). Fuselage radius follows the battery brick, drawn as an indigo sleeve at its true station.

Return type:

dict[str, Any]