Dave Cheng
Published

Aida Field Companion: Voice-and-Vision Maker Tool

A pocket-sized Arduino UNO Q companion for hands-free design notes, translation, reminders and deliberate visual inspection.

AdvancedWork in progress5 days88
Aida Field Companion: Voice-and-Vision Maker Tool

Things used in this project

Hardware components

Arduino UNO Q
Arduino UNO Q
×1

Software apps and online services

Fusion
Autodesk Fusion

Story

Read more

Custom parts and enclosures

Aida Field Companion Fusion 360 dataset

Native Autodesk Fusion .f3d application-stage enclosure dataset. Generated in Autodesk Fusion 2704.1.53; physical fit, tolerances and manufacturability remain to be verified.

Aida Field Companion STEP review copy

Neutral STEP export for mechanical review. It accompanies, but does not replace, the native Autodesk Fusion .f3d dataset.

Schematics

Preliminary bill of materials

Application-stage BOM with 16 line items and planning allowances. Candidate compatibility and prices remain to be verified before manufacture.

Machine-readable preliminary BOM

CSV copy of the application-stage BOM for review and future cost updates.

Fusion concept: component layout

Application-stage Fusion screen capture showing the intended packaging arrangement. Peripheral bodies remain provisional placeholders.

Fusion concept: top view

Orthographic Fusion view of the enclosure face and proposed user-facing features. Openings and feature positions remain provisional.

Fusion concept: front three-quarter view

Application-stage Autodesk Fusion concept screen capture. This is CAD evidence, not a prototype photograph or proof of physical fit.

Aida Field Companion proposed system architecture

Application-stage architecture diagram. Proposed design; not yet hardware-validated.

Code

Reproducible Autodesk Fusion concept generator

Python
Python add-in script used inside Autodesk Fusion to generate and export the application-stage enclosure concept.
"""Generate the preliminary Aida Field Companion design in Autodesk Fusion.

This is a Fusion *script*, not a normal CPython program. Run it from Fusion's
Utilities > Scripts and Add-Ins dialog. It creates a new direct-modelling design
and exports both Aida_Field_Companion_Concept_v0_1.f3d and .step next to this
script.

Only the Arduino UNO Q PCB outline and mounting-hole pattern, plus the published
Adafruit 5206 display envelope, are treated as dimensioned references. Every
unverified electronics body is deliberately named "[PLACEHOLDER]" and must be
replaced or measured before manufacturing.
"""

from __future__ import annotations

import os
import traceback

import adsk.core
import adsk.fusion


MM_PER_CM = 10.0
OUTPUT_BASENAME = "Aida_Field_Companion_Concept_v0_1"

# Enclosure concept parameters, millimetres. These are design decisions rather
# than measured hardware constraints and may be changed before running.
ENCLOSURE_X = 115.0
ENCLOSURE_Y = 75.0
LOWER_HEIGHT = 25.5
LID_THICKNESS = 2.5
WALL = 2.0
FLOOR = 2.2
FIT_CLEARANCE = 0.30

# Arduino UNO Q mechanical drawing (ABX00162 datasheet, revision 24/03/2026).
# Arduino's store page currently rounds the long dimension differently; the
# downloaded official STEP model remains the manufacturing authority.
UNO_X = 68.58
UNO_Y = 53.34
UNO_PCB_Z = 1.60
UNO_X0 = (ENCLOSURE_X - UNO_X) / 2.0
UNO_Y0 = (ENCLOSURE_Y - UNO_Y) / 2.0
# Provisional assembled stack datum. With the conservative 8 mm top-side
# keepout below, this leaves 0.5 mm to the candidate display envelope. The
# received board and official carrier CAD must replace this assumed position.
UNO_BOARD_Z = 10.00
UNO_TOP_KEEPOUT_Z = 8.00
UNO_MOUNT_HOLES = (
    (13.97, 2.54),
    (66.04, 7.62),
    (66.04, 35.56),
    (15.24, 50.80),
)

# Integration envelopes. The display dimensions come from Adafruit's product
# page; all other values in this section are unverified packaging placeholders.
MEDIA_CARRIER_Z = 4.20
DISPLAY_X = 45.8
DISPLAY_Y = 36.8
DISPLAY_Z = 5.4
DISPLAY_X0 = (ENCLOSURE_X - DISPLAY_X) / 2.0
DISPLAY_Y0 = 9.0
DISPLAY_BOTTOM_Z = LOWER_HEIGHT - DISPLAY_Z
DISPLAY_WINDOW_X = 38.0
DISPLAY_WINDOW_Y = 30.0
DISPLAY_WINDOW_X0 = DISPLAY_X0 + (DISPLAY_X - DISPLAY_WINDOW_X) / 2.0
DISPLAY_WINDOW_Y0 = DISPLAY_Y0 + (DISPLAY_Y - DISPLAY_WINDOW_Y) / 2.0
CAMERA_PCB_X = 24.0
CAMERA_PCB_Y = 24.0
CAMERA_PCB_Z = 2.0
CAMERA_CENTRE_X = 100.5
CAMERA_CENTRE_Y = 60.5
SPEAKER_CENTRE_X = 12.5
SPEAKER_CENTRE_Y = 32.0
SPEAKER_DIAMETER = 20.0
SPEAKER_DEPTH = 5.0
ACTION_BUTTON_X = 102.0
ACTION_BUTTON_Y = 12.0
ACTION_BUTTON_RADIUS = 3.5
SHUTTER_TRACK_X0 = 84.5
SHUTTER_TRACK_Y0 = 54.5
SHUTTER_TRACK_X = 22.5
SHUTTER_TRACK_Y = 12.0
SHUTTER_RECESS_DEPTH = 0.8
SHUTTER_X0 = 85.0
SHUTTER_Y0 = 55.0
SHUTTER_X = 10.0
SHUTTER_Y = 11.0
SHUTTER_Z = 0.6
CONTROL_PCB_X0 = 96.0
CONTROL_PCB_Y0 = 10.0
CONTROL_PCB_Z0 = 4.0
CONTROL_PCB_X = 15.0
CONTROL_PCB_Y = 20.0
CONTROL_PCB_Z = 1.2
CONTROL_PCB_COMPONENT_Z = 3.0

# The upper-right fastener moves inboard to avoid the camera module envelope.
LID_FASTENERS = ((5.0, 5.0), (110.0, 5.0), (5.0, 70.0), (84.0, 70.0))


def cm(mm: float) -> float:
    """Convert millimetres to Fusion API internal centimetres."""

    return mm / MM_PER_CM


def point(x_mm: float, y_mm: float, z_mm: float) -> adsk.core.Point3D:
    return adsk.core.Point3D.create(cm(x_mm), cm(y_mm), cm(z_mm))


def vector(x: float, y: float, z: float) -> adsk.core.Vector3D:
    return adsk.core.Vector3D.create(x, y, z)


def temporary_box(
    manager: adsk.fusion.TemporaryBRepManager,
    x_mm: float,
    y_mm: float,
    z_mm: float,
    size_x_mm: float,
    size_y_mm: float,
    size_z_mm: float,
) -> adsk.fusion.BRepBody:
    centre = point(
        x_mm + size_x_mm / 2.0,
        y_mm + size_y_mm / 2.0,
        z_mm + size_z_mm / 2.0,
    )
    bounds = adsk.core.OrientedBoundingBox3D.create(
        centre,
        vector(1.0, 0.0, 0.0),
        vector(0.0, 1.0, 0.0),
        cm(size_x_mm),
        cm(size_y_mm),
        cm(size_z_mm),
    )
    body = manager.createBox(bounds)
    if not body:
        raise RuntimeError("Fusion failed to create a temporary box")
    return body


def temporary_cylinder(
    manager: adsk.fusion.TemporaryBRepManager,
    x_mm: float,
    y_mm: float,
    z0_mm: float,
    z1_mm: float,
    radius_mm: float,
) -> adsk.fusion.BRepBody:
    body = manager.createCylinderOrCone(
        point(x_mm, y_mm, z0_mm),
        cm(radius_mm),
        point(x_mm, y_mm, z1_mm),
        cm(radius_mm),
    )
    if not body:
        raise RuntimeError("Fusion failed to create a temporary cylinder")
    return body


def subtract(
    manager: adsk.fusion.TemporaryBRepManager,
    target: adsk.fusion.BRepBody,
    tool: adsk.fusion.BRepBody,
) -> None:
    ok = manager.booleanOperation(
        target, tool, adsk.fusion.BooleanTypes.DifferenceBooleanType
    )
    if not ok:
        raise RuntimeError("Fusion failed to complete a Boolean subtraction")


def new_component(
    root: adsk.fusion.Component,
    name: str,
    status: str,
) -> adsk.fusion.Component:
    occurrence = root.occurrences.addNewComponent(adsk.core.Matrix3D.create())
    component = occurrence.component
    component.name = name
    component.attributes.add("AidaAU2027", "geometryStatus", status)
    return component


def persist_body(
    component: adsk.fusion.Component,
    transient: adsk.fusion.BRepBody,
    name: str,
    opacity: float | None = None,
) -> adsk.fusion.BRepBody:
    body = component.bRepBodies.add(transient)
    if not body:
        raise RuntimeError(f"Fusion failed to persist body: {name}")
    body.name = name
    if opacity is not None:
        try:
            body.opacity = opacity
        except Exception:
            # Opacity is presentation-only and must never prevent generation.
            pass
    return body


def add_ring(
    component: adsk.fusion.Component,
    manager: adsk.fusion.TemporaryBRepManager,
    x_mm: float,
    y_mm: float,
    z0_mm: float,
    z1_mm: float,
    outer_radius_mm: float,
    inner_radius_mm: float,
    name: str,
) -> adsk.fusion.BRepBody:
    ring = temporary_cylinder(
        manager, x_mm, y_mm, z0_mm, z1_mm, outer_radius_mm
    )
    bore = temporary_cylinder(
        manager, x_mm, y_mm, z0_mm - 0.2, z1_mm + 0.2, inner_radius_mm
    )
    subtract(manager, ring, bore)
    return persist_body(component, ring, name)


def build_lower_shell(
    root: adsk.fusion.Component,
    manager: adsk.fusion.TemporaryBRepManager,
) -> None:
    component = new_component(root, "01 Lower enclosure", "CONCEPT GEOMETRY")
    shell = temporary_box(
        manager, 0.0, 0.0, 0.0, ENCLOSURE_X, ENCLOSURE_Y, LOWER_HEIGHT
    )
    cavity = temporary_box(
        manager,
        WALL,
        WALL,
        FLOOR,
        ENCLOSURE_X - 2.0 * WALL,
        ENCLOSURE_Y - 2.0 * WALL,
        LOWER_HEIGHT + 2.0,
    )
    subtract(manager, shell, cavity)

    # Placeholder USB-C service aperture. Its position must be aligned to the
    # official UNO Q STEP model before manufacture.
    usb_c_cut = temporary_box(
        manager,
        ENCLOSURE_X - WALL - 0.5,
        30.5,
        6.0,
        WALL + 1.5,
        14.0,
        4.5,
    )
    subtract(manager, shell, usb_c_cut)
    persist_body(component, shell, "Lower shell - USB-C aperture provisional")

    # Four lid fastener towers, sized for provisional M2.5 inserts.
    for index, (x_mm, y_mm) in enumerate(
        LID_FASTENERS, start=1
    ):
        add_ring(
            component,
            manager,
            x_mm,
            y_mm,
            FLOOR,
            LOWER_HEIGHT - 1.0,
            3.5,
            1.30,
            f"Lid insert tower {index} - M2.5 provisional",
        )

    # Two-part stack supports stop at either face of the placeholder carrier
    # PCB instead of incorrectly passing solid geometry through it. Locations
    # follow the standard UNO mounting pattern in the UNO Q mechanical drawing.
    for index, (hole_x, hole_y) in enumerate(UNO_MOUNT_HOLES, start=1):
        add_ring(
            component,
            manager,
            UNO_X0 + hole_x,
            UNO_Y0 + hole_y,
            FLOOR,
            MEDIA_CARRIER_Z,
            3.0,
            1.40,
            f"Carrier support {index} - M2.5 clearance provisional",
        )
        add_ring(
            component,
            manager,
            UNO_X0 + hole_x,
            UNO_Y0 + hole_y,
            MEDIA_CARRIER_Z + 1.6,
            UNO_BOARD_Z,
            3.0,
            1.40,
            f"Carrier-to-UNO spacer {index} - M2.5 clearance provisional",
        )


def build_lid(
    root: adsk.fusion.Component,
    manager: adsk.fusion.TemporaryBRepManager,
) -> None:
    component = new_component(root, "02 Front lid", "CONCEPT GEOMETRY")
    lid = temporary_box(
        manager,
        0.0,
        0.0,
        LOWER_HEIGHT,
        ENCLOSURE_X,
        ENCLOSURE_Y,
        LID_THICKNESS,
    )

    # Screen, camera, microphone, mute slider, user button and speaker openings.
    display_cut = temporary_box(
        manager,
        DISPLAY_WINDOW_X0,
        DISPLAY_WINDOW_Y0,
        LOWER_HEIGHT - 0.5,
        DISPLAY_WINDOW_X,
        DISPLAY_WINDOW_Y,
        LID_THICKNESS + 1.0,
    )
    subtract(manager, lid, display_cut)

    aperture_cylinders = [
        (CAMERA_CENTRE_X, CAMERA_CENTRE_Y, 5.0, "camera"),
        (11.0, 63.0, 1.2, "microphone"),
        (ACTION_BUTTON_X, ACTION_BUTTON_Y, 4.0, "user button"),
    ]
    for x_mm, y_mm, radius_mm, _label in aperture_cylinders:
        cut = temporary_cylinder(
            manager,
            x_mm,
            y_mm,
            LOWER_HEIGHT - 0.5,
            LOWER_HEIGHT + LID_THICKNESS + 0.5,
            radius_mm,
        )
        subtract(manager, lid, cut)

    mute_cut = temporary_box(
        manager,
        23.0,
        61.0,
        LOWER_HEIGHT - 0.5,
        11.0,
        5.0,
        LID_THICKNESS + 1.0,
    )
    subtract(manager, lid, mute_cut)

    # Shallow conceptual track for a manually operated camera shutter. The
    # separate plate is shown parked open; rail/latch details remain unverified.
    shutter_track = temporary_box(
        manager,
        SHUTTER_TRACK_X0,
        SHUTTER_TRACK_Y0,
        LOWER_HEIGHT + LID_THICKNESS - SHUTTER_RECESS_DEPTH,
        SHUTTER_TRACK_X,
        SHUTTER_TRACK_Y,
        SHUTTER_RECESS_DEPTH + 0.2,
    )
    subtract(manager, lid, shutter_track)

    # A bounded grille over the provisional 20 mm speaker location.
    for x_mm in (8.5, 12.5, 16.5):
        for y_mm in (24.0, 28.0, 32.0, 36.0, 40.0):
            grille_hole = temporary_cylinder(
                manager,
                x_mm,
                y_mm,
                LOWER_HEIGHT - 0.5,
                LOWER_HEIGHT + LID_THICKNESS + 0.5,
                1.0,
            )
            subtract(manager, lid, grille_hole)

    for x_mm, y_mm in LID_FASTENERS:
        screw_hole = temporary_cylinder(
            manager,
            x_mm,
            y_mm,
            LOWER_HEIGHT - 0.5,
            LOWER_HEIGHT + LID_THICKNESS + 0.5,
            1.4,
        )
        subtract(manager, lid, screw_hole)

    persist_body(component, lid, "Front lid - all apertures provisional")

    # Downward locating rim with 0.30 mm nominal clearance per side.
    rim_outer = temporary_box(
        manager,
        WALL + FIT_CLEARANCE,
        WALL + FIT_CLEARANCE,
        LOWER_HEIGHT - 1.8,
        ENCLOSURE_X - 2.0 * (WALL + FIT_CLEARANCE),
        ENCLOSURE_Y - 2.0 * (WALL + FIT_CLEARANCE),
        1.8,
    )
    rim_inner = temporary_box(
        manager,
        WALL + FIT_CLEARANCE + 1.2,
        WALL + FIT_CLEARANCE + 1.2,
        LOWER_HEIGHT - 2.0,
        ENCLOSURE_X - 2.0 * (WALL + FIT_CLEARANCE + 1.2),
        ENCLOSURE_Y - 2.0 * (WALL + FIT_CLEARANCE + 1.2),
        2.2,
    )
    subtract(manager, rim_outer, rim_inner)

    # The locating rim is deliberately relieved around shell towers and the
    # speaker/camera envelopes. A continuous ring would collide with those
    # parts and prevent the lid from seating.
    for x_mm, y_mm in LID_FASTENERS:
        tower_clearance = temporary_cylinder(
            manager,
            x_mm,
            y_mm,
            LOWER_HEIGHT - 2.0,
            LOWER_HEIGHT + 0.2,
            3.8,
        )
        subtract(manager, rim_outer, tower_clearance)

    speaker_clearance = temporary_cylinder(
        manager,
        SPEAKER_CENTRE_X,
        SPEAKER_CENTRE_Y,
        LOWER_HEIGHT - 2.0,
        LOWER_HEIGHT + 0.2,
        SPEAKER_DIAMETER / 2.0 + 0.3,
    )
    subtract(manager, rim_outer, speaker_clearance)

    camera_clearance = temporary_box(
        manager,
        CAMERA_CENTRE_X - CAMERA_PCB_X / 2.0 - 0.3,
        CAMERA_CENTRE_Y - CAMERA_PCB_Y / 2.0 - 0.3,
        LOWER_HEIGHT - 2.0,
        CAMERA_PCB_X + 0.6,
        CAMERA_PCB_Y + 0.6,
        2.2,
    )
    subtract(manager, rim_outer, camera_clearance)
    persist_body(component, rim_outer, "Lid locating rim - fit coupon required")


def build_uno_q_reference(
    root: adsk.fusion.Component,
    manager: adsk.fusion.TemporaryBRepManager,
) -> None:
    component = new_component(
        root,
        "10 Arduino UNO Q 4GB [DATASHEET ENVELOPE]",
        "PCB OUTLINE AND HOLE PATTERN VERIFIED; COMPONENT HEIGHTS UNVERIFIED",
    )
    pcb = temporary_box(
        manager, UNO_X0, UNO_Y0, UNO_BOARD_Z, UNO_X, UNO_Y, UNO_PCB_Z
    )
    for hole_x, hole_y in UNO_MOUNT_HOLES:
        drill = temporary_cylinder(
            manager,
            UNO_X0 + hole_x,
            UNO_Y0 + hole_y,
            UNO_BOARD_Z - 0.2,
            UNO_BOARD_Z + UNO_PCB_Z + 0.2,
            1.60,
        )
        subtract(manager, pcb, drill)
    persist_body(component, pcb, "UNO Q PCB datum envelope", 0.65)

    top_keepout = temporary_box(
        manager,
        UNO_X0,
        UNO_Y0,
        UNO_BOARD_Z + UNO_PCB_Z,
        UNO_X,
        UNO_Y,
        UNO_TOP_KEEPOUT_Z,
    )
    persist_body(
        component,
        top_keepout,
        "UNO Q top-side keepout [PLACEHOLDER - replace with official STEP]",
        0.16,
    )


def build_integration_placeholders(
    root: adsk.fusion.Component,
    manager: adsk.fusion.TemporaryBRepManager,
) -> None:
    carrier = new_component(
        root,
        "11 UNO Media Carrier [PLACEHOLDER]",
        "UNVERIFIED ENVELOPE - IMPORT OFFICIAL CAD",
    )
    carrier_body = temporary_box(
        manager, UNO_X0, UNO_Y0, MEDIA_CARRIER_Z, UNO_X, UNO_Y, 1.6
    )
    for hole_x, hole_y in UNO_MOUNT_HOLES:
        drill = temporary_cylinder(
            manager,
            UNO_X0 + hole_x,
            UNO_Y0 + hole_y,
            MEDIA_CARRIER_Z - 0.2,
            MEDIA_CARRIER_Z + 1.8,
            1.60,
        )
        subtract(manager, carrier_body, drill)
    persist_body(carrier, carrier_body, "Carrier PCB envelope [PLACEHOLDER]", 0.35)

    carrier_keepout = temporary_box(
        manager,
        UNO_X0,
        UNO_Y0,
        MEDIA_CARRIER_Z + 1.6,
        UNO_X,
        UNO_Y,
        UNO_BOARD_Z - (MEDIA_CARRIER_Z + 1.6) - 2.0,
    )
    for hole_x, hole_y in UNO_MOUNT_HOLES:
        clearance = temporary_cylinder(
            manager,
            UNO_X0 + hole_x,
            UNO_Y0 + hole_y,
            MEDIA_CARRIER_Z + 1.4,
            UNO_BOARD_Z - 1.8,
            3.2,
        )
        subtract(manager, carrier_keepout, clearance)
    persist_body(
        carrier,
        carrier_keepout,
        "Carrier connector/component keepout [PLACEHOLDER]",
        0.14,
    )

    display = new_component(
        root,
        "12 Adafruit 5206 1.69in SPI display [CANDIDATE]",
        "OFFICIAL DIMENSION ENVELOPE; UNO Q COMPATIBILITY AND PERFORMANCE UNVERIFIED",
    )
    display_body = temporary_box(
        manager,
        DISPLAY_X0,
        DISPLAY_Y0,
        DISPLAY_BOTTOM_Z,
        DISPLAY_X,
        DISPLAY_Y,
        DISPLAY_Z,
    )
    persist_body(display, display_body, "Adafruit 5206 dimension envelope", 0.28)

    camera = new_component(
        root,
        "13 MIPI-CSI camera [PLACEHOLDER]",
        "UNVERIFIED - SELECT MODULE AND MEASURE",
    )
    camera_body = temporary_box(
        manager,
        CAMERA_CENTRE_X - CAMERA_PCB_X / 2.0,
        CAMERA_CENTRE_Y - CAMERA_PCB_Y / 2.0,
        LOWER_HEIGHT - CAMERA_PCB_Z,
        CAMERA_PCB_X,
        CAMERA_PCB_Y,
        CAMERA_PCB_Z,
    )
    persist_body(camera, camera_body, "Camera PCB envelope [PLACEHOLDER]", 0.32)
    lens = temporary_cylinder(
        manager,
        CAMERA_CENTRE_X,
        CAMERA_CENTRE_Y,
        LOWER_HEIGHT - 4.5,
        LOWER_HEIGHT,
        4.0,
    )
    persist_body(camera, lens, "Camera lens envelope [PLACEHOLDER]", 0.45)

    shutter = new_component(
        root,
        "14 Camera privacy shutter [CONCEPT - OPEN]",
        "PROVISIONAL OPEN-POSITION GEOMETRY - RAIL, LATCH AND CLEARANCE UNVERIFIED",
    )
    shutter_plate = temporary_box(
        manager,
        SHUTTER_X0,
        SHUTTER_Y0,
        LOWER_HEIGHT + LID_THICKNESS - SHUTTER_RECESS_DEPTH + 0.1,
        SHUTTER_X,
        SHUTTER_Y,
        SHUTTER_Z,
    )
    persist_body(
        shutter,
        shutter_plate,
        "Manual camera shutter plate - parked open [CONCEPT]",
        0.75,
    )

    audio = new_component(
        root,
        "15 Speaker and audio [PLACEHOLDER]",
        "UNVERIFIED - SELECT SPEAKER AND AMPLIFIER",
    )
    speaker = temporary_cylinder(
        manager,
        SPEAKER_CENTRE_X,
        SPEAKER_CENTRE_Y,
        LOWER_HEIGHT - SPEAKER_DEPTH,
        LOWER_HEIGHT,
        SPEAKER_DIAMETER / 2.0,
    )
    persist_body(audio, speaker, "20 mm speaker envelope [PLACEHOLDER]", 0.30)

    controls = new_component(
        root,
        "16 Aida low-speed control-display PCB [PLACEHOLDER]",
        "PROVISIONAL ENVELOPE - SCHEMATIC, LAYOUT, CONNECTORS AND FIT UNVERIFIED",
    )
    control_pcb = temporary_box(
        manager,
        CONTROL_PCB_X0,
        CONTROL_PCB_Y0,
        CONTROL_PCB_Z0,
        CONTROL_PCB_X,
        CONTROL_PCB_Y,
        CONTROL_PCB_Z,
    )
    persist_body(
        controls,
        control_pcb,
        "Aida low-speed control-display PCB envelope [PLACEHOLDER]",
        0.38,
    )
    control_keepout = temporary_box(
        manager,
        CONTROL_PCB_X0,
        CONTROL_PCB_Y0,
        CONTROL_PCB_Z0 + CONTROL_PCB_Z,
        CONTROL_PCB_X,
        CONTROL_PCB_Y,
        CONTROL_PCB_COMPONENT_Z,
    )
    persist_body(
        controls,
        control_keepout,
        "Control PCB component and connector keepout [PLACEHOLDER]",
        0.14,
    )

    action_button = new_component(
        root,
        "17 Off-board multifunction action-stop button [PLACEHOLDER]",
        "UNVERIFIED PANEL BUTTON - WIRED HARNESS AND RETENTION NOT MODELLED",
    )
    action_button_body = temporary_cylinder(
        manager,
        ACTION_BUTTON_X,
        ACTION_BUTTON_Y,
        LOWER_HEIGHT - 3.5,
        LOWER_HEIGHT + LID_THICKNESS,
        ACTION_BUTTON_RADIUS,
    )
    persist_body(
        action_button,
        action_button_body,
        "Panel action-stop button envelope [PLACEHOLDER]",
        0.42,
    )


def add_design_metadata(design: adsk.fusion.Design) -> None:
    root = design.rootComponent
    # Fusion 2704 makes a design's root-component name read-only.  Preserve the
    # intended human-readable title as metadata; the exported archive filename
    # supplies the document name when the .f3d is opened again.
    root.attributes.add(
        "AidaAU2027",
        "designTitle",
        "Aida Field Companion - AU 2027 concept v0.1",
    )
    root.attributes.add("AidaAU2027", "modelVersion", "0.1")
    root.attributes.add(
        "AidaAU2027",
        "manufacturingStatus",
        "CONCEPT ONLY - NOT RELEASED FOR MANUFACTURE",
    )
    root.attributes.add(
        "AidaAU2027",
        "verifiedReference",
        "Arduino UNO Q ABX00162 datasheet mechanical drawing rev 24/03/2026",
    )


def export_design(design: adsk.fusion.Design) -> tuple[str, str]:
    output_directory = os.path.dirname(os.path.realpath(__file__))
    f3d_path = os.path.join(output_directory, OUTPUT_BASENAME + ".f3d")
    step_path = os.path.join(output_directory, OUTPUT_BASENAME + ".step")
    exporter = design.exportManager

    archive_options = exporter.createFusionArchiveExportOptions(f3d_path)
    if not archive_options or not exporter.execute(archive_options):
        raise RuntimeError("Fusion did not export the .f3d archive")

    step_options = exporter.createSTEPExportOptions(step_path, design.rootComponent)
    if not step_options or not exporter.execute(step_options):
        raise RuntimeError("Fusion did not export the STEP file")
    return f3d_path, step_path


def run(_context) -> None:
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
        design = adsk.fusion.Design.cast(app.activeProduct)
        if not design:
            raise RuntimeError("The active product is not a Fusion design")
        design.designType = adsk.fusion.DesignTypes.DirectDesignType

        add_design_metadata(design)
        root = design.rootComponent
        manager = adsk.fusion.TemporaryBRepManager.get()
        if not manager:
            raise RuntimeError("Fusion TemporaryBRepManager is unavailable")

        build_lower_shell(root, manager)
        build_lid(root, manager)
        build_uno_q_reference(root, manager)
        build_integration_placeholders(root, manager)
        f3d_path, step_path = export_design(design)

        ui.messageBox(
            "Aida Field Companion concept generated.\n\n"
            f"Fusion archive:\n{f3d_path}\n\n"
            f"STEP exchange file:\n{step_path}\n\n"
            "This is concept geometry. Replace every [PLACEHOLDER] component "
            "and complete the checks in DESIGN_BRIEF.md before manufacture.",
            "Aida AU 2027",
        )
    except Exception:
        if ui:
            ui.messageBox(
                "Generation failed:\n\n" + traceback.format_exc(),
                "Aida AU 2027",
            )
        raise

Credits

Dave Cheng
1 project • 1 follower

Comments