diff --git a/models/README.md b/models/README.md deleted file mode 100644 index fcb4c35f..00000000 --- a/models/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Demo Models - -Curated model fixtures and generator assets for text-to-cad workflows. - -This tree is intended to be committed with Git LFS for large CAD, mesh, robot, -and G-code artifacts. Source generators and concise documentation remain normal -text files. - -## Directory Map - -- [benchmarks/](benchmarks/README.md): build123d benchmark generators plus STEP - and mesh outputs. -- [fun/](fun/README.md): standalone generated CAD examples and printable - outputs. -- [gcode/](gcode/README.md): small slicer input/output fixtures. -- [implicits/](implicits/README.md): browser-native implicit CAD examples. -- [mechanisms/](mechanisms/README.md): flattened mechanism STEP demos and - generated render sidecars. -- [robots/](robots/README.md): URDF/SRDF robot fixtures, meshes, printable - outputs, and selected robot STEP sources. -- [simple/](simple/README.md): compact build123d generators and STEP outputs - for basic parts. - -The larger `mechbench/` and `mechbench2/` external datasets are intentionally -not included in this committed fixture tree. - -## Git LFS Fetching - -Repository LFS config excludes `models/**` from default LFS fetches so ordinary -checkout and publish jobs can avoid downloading every model blob. Fetch the -model artifacts explicitly when you need local bytes: - -```bash -git lfs pull --include="models/**" --exclude="" -``` - -## Cleanup Policy - -- Keep canonical sources (`*.py`, `*.implicit.js`, `*.urdf`, `*.srdf`, and docs) - readable in normal Git. -- Keep durable generated fixtures (`*.step`, `*.stl`, `*.3mf`, `*.glb`, - `*.gcode`, and `*.dxf`) in Git LFS. -- Do not commit supplementary media or sidecar metadata such as `*.png`, - `*.mp4`, `*.gif`, or `*.json` unless a future workflow defines them as a - required model artifact. -- Do not commit local runtime debris such as `.DS_Store`, `__pycache__/`, - `.cache/`, logs, or one-off timestamped review snapshots. -- Put temporary scratch artifacts under ignored local paths, not in this tree. diff --git a/models/benchmarks/.benchmark_01_rectangular_calibration_block.step.glb b/models/benchmarks/.benchmark_01_rectangular_calibration_block.step.glb deleted file mode 100644 index 3deaa917..00000000 --- a/models/benchmarks/.benchmark_01_rectangular_calibration_block.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:04218fad3d5a8fd55851850f00a6b5c09ba93ce13474d27dca497c94023b8564 -size 216928 diff --git a/models/benchmarks/.benchmark_02_circular_flange.step.glb b/models/benchmarks/.benchmark_02_circular_flange.step.glb deleted file mode 100644 index 0f440a7a..00000000 --- a/models/benchmarks/.benchmark_02_circular_flange.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:17eea42429ae7b2cd37ec05f94d88f028078a0aaa5a0cf8977e7c1c491f43a5e -size 915736 diff --git a/models/benchmarks/.benchmark_03_l_bracket.step.glb b/models/benchmarks/.benchmark_03_l_bracket.step.glb deleted file mode 100644 index f468ab70..00000000 --- a/models/benchmarks/.benchmark_03_l_bracket.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f561aa1bbd5ef059de1b2ac17ed886daf220f0bb87f56999e672ea422d15184a -size 256348 diff --git a/models/benchmarks/.benchmark_04_stepped_shaft_keyway.step.glb b/models/benchmarks/.benchmark_04_stepped_shaft_keyway.step.glb deleted file mode 100644 index ca7ce61f..00000000 --- a/models/benchmarks/.benchmark_04_stepped_shaft_keyway.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92930c56e5074e1d6ba54e8059517a98edd178d05f5be495f9a8f6aad4e699f4 -size 212884 diff --git a/models/benchmarks/.benchmark_05_open_top_electronics_enclosure.step.glb b/models/benchmarks/.benchmark_05_open_top_electronics_enclosure.step.glb deleted file mode 100644 index 5fbe77e5..00000000 --- a/models/benchmarks/.benchmark_05_open_top_electronics_enclosure.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ffda86900f9b4c4a10b3b4e4425ea2b40578f97e5f1a3d7f87509d9c15ed7f5 -size 451472 diff --git a/models/benchmarks/.benchmark_06_clevis_bracket_lightening_cutouts.step.glb b/models/benchmarks/.benchmark_06_clevis_bracket_lightening_cutouts.step.glb deleted file mode 100644 index 4e316250..00000000 --- a/models/benchmarks/.benchmark_06_clevis_bracket_lightening_cutouts.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8bf8925a5816ca1a211a455867b54b47894b118cee6548cab79a4e3f355b7b38 -size 617556 diff --git a/models/benchmarks/.benchmark_07_radial_engine_cylinder.step.glb b/models/benchmarks/.benchmark_07_radial_engine_cylinder.step.glb deleted file mode 100644 index 362e4566..00000000 --- a/models/benchmarks/.benchmark_07_radial_engine_cylinder.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6fdb5d82e79d73412a7c7dd795379b4bbe83690c2d25ab2ecc82c1058f6d66f -size 8589660 diff --git a/models/benchmarks/.benchmark_08_centrifugal_impeller.step.glb b/models/benchmarks/.benchmark_08_centrifugal_impeller.step.glb deleted file mode 100644 index c03f2d68..00000000 --- a/models/benchmarks/.benchmark_08_centrifugal_impeller.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4bc1f3f0b7dee2bca143d46e52e031c428f1a690007d05c8382c971b457371d -size 1301648 diff --git a/models/benchmarks/.benchmark_09_spiral_staircase.step.glb b/models/benchmarks/.benchmark_09_spiral_staircase.step.glb deleted file mode 100644 index 9ed74639..00000000 --- a/models/benchmarks/.benchmark_09_spiral_staircase.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dcca689dc15228fc29da875a779fc46c4e6a67155890411f17a27a8563bf018f -size 2849852 diff --git a/models/benchmarks/.benchmark_10_planetary_gear_stage.step.glb b/models/benchmarks/.benchmark_10_planetary_gear_stage.step.glb deleted file mode 100644 index a0ad69a6..00000000 --- a/models/benchmarks/.benchmark_10_planetary_gear_stage.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e2d1853a1d8ba3ccf75b24728626f46014a5aa0e7798f19c9675ed78a8a435b -size 2007188 diff --git a/models/benchmarks/README.md b/models/benchmarks/README.md deleted file mode 100644 index df0ae732..00000000 --- a/models/benchmarks/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Benchmark Models - -Benchmark CAD fixtures for repeatable geometry generation, import/export, and -viewer behavior checks. Each benchmark is intentionally more structured than the -small examples in `../simple/` and is meant to exercise a distinct modeling -surface. - -## Contents - -- `benchmark_*.py`: build123d generator source for each benchmark. -- `benchmark_*.step`: canonical generated STEP output for each benchmark. -- `.benchmark_*.step.glb`: generated render/selector sidecar paired with the - STEP file. -- `benchmark_common.py`: shared helper functions for benchmark generators. -- `validate_benchmark.py`: local benchmark validation helper. - -## Coverage - -The benchmark set covers calibration blocks, flanges, brackets, shafts, -enclosures, clevises, engine-like fins, impellers, spiral stairs, and planetary -gear stages. - -Keep benchmark examples here when they are intended as regression fixtures or -represent a richer version of a simpler shape. diff --git a/models/benchmarks/benchmark_01_rectangular_calibration_block.py b/models/benchmarks/benchmark_01_rectangular_calibration_block.py deleted file mode 100644 index a0badb4a..00000000 --- a/models/benchmarks/benchmark_01_rectangular_calibration_block.py +++ /dev/null @@ -1,31 +0,0 @@ -from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode - -from benchmark_common import line_edges_at_z, safe_chamfer - - -LENGTH = 100.0 -WIDTH = 60.0 -HEIGHT = 20.0 -HOLE_DIAMETER = 8.0 -HOLE_LOCATIONS = ((-35.0, -20.0), (-35.0, 20.0), (35.0, -20.0), (35.0, 20.0)) -TOP_CHAMFER = 2.0 - - -def gen_step(): - """Return the rectangular calibration block benchmark model in millimeters.""" - with BuildPart() as block: - Box(LENGTH, WIDTH, HEIGHT, align=(Align.CENTER, Align.CENTER, Align.MIN)) - - for x_pos, y_pos in HOLE_LOCATIONS: - with Locations(Location((x_pos, y_pos, -1.0))): - Cylinder( - radius=HOLE_DIAMETER / 2.0, - height=HEIGHT + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = block.part - part = safe_chamfer(part, line_edges_at_z(part, HEIGHT), length=TOP_CHAMFER) - part.label = "benchmark_01_rectangular_calibration_block" - return part diff --git a/models/benchmarks/benchmark_01_rectangular_calibration_block.step b/models/benchmarks/benchmark_01_rectangular_calibration_block.step deleted file mode 100644 index 1573635b..00000000 --- a/models/benchmarks/benchmark_01_rectangular_calibration_block.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4622482394af6dd284eafc95dc89858e87ae62e798d078f0c5e3c75adcfaa5a -size 43342 diff --git a/models/benchmarks/benchmark_02_circular_flange.py b/models/benchmarks/benchmark_02_circular_flange.py deleted file mode 100644 index 5cb8ec76..00000000 --- a/models/benchmarks/benchmark_02_circular_flange.py +++ /dev/null @@ -1,50 +0,0 @@ -from math import tau - -from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode - -from benchmark_common import circular_edges, polar_point, safe_fillet - - -OUTER_DIAMETER = 80.0 -THICKNESS = 10.0 -CENTRAL_BORE_DIAMETER = 30.0 -BOLT_HOLE_COUNT = 6 -BOLT_HOLE_DIAMETER = 6.0 -BOLT_CIRCLE_DIAMETER = 60.0 -OUTER_FILLET = 1.5 - - -def gen_step(): - """Return the circular flange benchmark model in millimeters.""" - with BuildPart() as flange: - Cylinder( - radius=OUTER_DIAMETER / 2.0, - height=THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - with Locations(Location((0.0, 0.0, -1.0))): - Cylinder( - radius=CENTRAL_BORE_DIAMETER / 2.0, - height=THICKNESS + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - for index in range(BOLT_HOLE_COUNT): - x_pos, y_pos = polar_point(BOLT_CIRCLE_DIAMETER / 2.0, tau * index / BOLT_HOLE_COUNT) - with Locations(Location((x_pos, y_pos, -1.0))): - Cylinder( - radius=BOLT_HOLE_DIAMETER / 2.0, - height=THICKNESS + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = flange.part - edges = [] - edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=0.0)) - edges.extend(circular_edges(part, radius=OUTER_DIAMETER / 2.0, axis="z", coordinate=THICKNESS)) - part = safe_fillet(part, edges, radius=OUTER_FILLET) - part.label = "benchmark_02_circular_flange_bolt_pattern" - return part diff --git a/models/benchmarks/benchmark_02_circular_flange.step b/models/benchmarks/benchmark_02_circular_flange.step deleted file mode 100644 index 8522520f..00000000 --- a/models/benchmarks/benchmark_02_circular_flange.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c563d4fe61b1be01099872467570e4cbb1595e082a2606c17e4a98e677f6731f -size 40097 diff --git a/models/benchmarks/benchmark_03_l_bracket.py b/models/benchmarks/benchmark_03_l_bracket.py deleted file mode 100644 index 4c39dd0f..00000000 --- a/models/benchmarks/benchmark_03_l_bracket.py +++ /dev/null @@ -1,113 +0,0 @@ -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - add, - extrude, -) - -from benchmark_common import safe_fillet - - -BASE_LENGTH = 80.0 -BASE_WIDTH = 50.0 -BASE_THICKNESS = 8.0 - -BACK_LENGTH = 80.0 -BACK_THICKNESS = 8.0 -BACK_HEIGHT = 50.0 -BACK_CENTER_Y = BASE_WIDTH / 2.0 - BACK_THICKNESS / 2.0 - -HOLE_DIAMETER = 6.0 -BASE_HOLE_LOCATIONS = ((-25.0, -10.0), (25.0, -10.0)) -BACK_HOLE_LOCATIONS = ((-25.0, 30.0), (25.0, 30.0)) - -GUSSET_X_THICKNESS = 8.0 -GUSSET_X_LOCATIONS = (-20.0, 20.0) -GUSSET_DEPTH = 30.0 -GUSSET_HEIGHT = 30.0 -TRANSITION_FILLET = 2.0 - - -def _make_gusset(x_center: float): - back_front_y = BACK_CENTER_Y - BACK_THICKNESS / 2.0 - front_y = back_front_y - GUSSET_DEPTH - top_z = BASE_THICKNESS + GUSSET_HEIGHT - - with BuildPart() as gusset: - with BuildSketch(Plane.YZ): - Polygon( - [ - (back_front_y, BASE_THICKNESS), - (front_y, BASE_THICKNESS), - (back_front_y, top_z), - ], - align=None, - ) - extrude(amount=GUSSET_X_THICKNESS) - - part = gusset.part.moved(Location((x_center - GUSSET_X_THICKNESS / 2.0, 0.0, 0.0))) - part.label = f"gusset_x_{x_center:.0f}" - return part - - -def _transition_edges(part): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - if str(edge.geom_type).endswith("LINE"): - if abs(bbox.min.Z - BASE_THICKNESS) < 0.05 and abs(bbox.max.Z - BASE_THICKNESS) < 0.05: - center_y = bbox.center().Y - if BACK_CENTER_Y - BACK_THICKNESS / 2.0 - 0.05 <= center_y <= BASE_WIDTH / 2.0 + 0.05: - if bbox.size.X > 20.0: - edges.append(edge) - return edges - - -def gen_step(): - """Return the L-bracket benchmark model in millimeters.""" - with BuildPart() as bracket: - Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN)) - - with Locations(Location((0.0, BACK_CENTER_Y, BASE_THICKNESS))): - Box( - BACK_LENGTH, - BACK_THICKNESS, - BACK_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - for x_pos, y_pos in BASE_HOLE_LOCATIONS: - with Locations(Location((x_pos, y_pos, -1.0))): - Cylinder( - radius=HOLE_DIAMETER / 2.0, - height=BASE_THICKNESS + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - for x_pos, z_pos in BACK_HOLE_LOCATIONS: - with Locations(Location((x_pos, BACK_CENTER_Y, z_pos))): - Cylinder( - radius=HOLE_DIAMETER / 2.0, - height=BACK_THICKNESS + 2.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - - for x_center in GUSSET_X_LOCATIONS: - add(_make_gusset(x_center), mode=Mode.ADD) - - part = bracket.part - part = safe_fillet(part, _transition_edges(part), radius=TRANSITION_FILLET) - part.label = "benchmark_03_l_bracket_gussets_two_hole_directions" - return part diff --git a/models/benchmarks/benchmark_03_l_bracket.step b/models/benchmarks/benchmark_03_l_bracket.step deleted file mode 100644 index 3697378d..00000000 --- a/models/benchmarks/benchmark_03_l_bracket.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aeaebdd1637249b0f2566f12501945f3b06c28e41d4e4cb3eef5f61f5c614d4c -size 91399 diff --git a/models/benchmarks/benchmark_04_stepped_shaft_keyway.py b/models/benchmarks/benchmark_04_stepped_shaft_keyway.py deleted file mode 100644 index 99c73371..00000000 --- a/models/benchmarks/benchmark_04_stepped_shaft_keyway.py +++ /dev/null @@ -1,53 +0,0 @@ -from build123d import Align, Box, BuildPart, Cylinder, Location, Locations, Mode - -from benchmark_common import circular_edges, safe_chamfer - - -TOTAL_LENGTH = 120.0 -LEFT_LENGTH = 30.0 -MIDDLE_LENGTH = 60.0 -RIGHT_LENGTH = 30.0 -SMALL_DIAMETER = 20.0 -MIDDLE_DIAMETER = 30.0 -END_CHAMFER = 1.0 - -KEYWAY_X_START = 40.0 -KEYWAY_X_END = 80.0 -KEYWAY_WIDTH = 6.0 -KEYWAY_DEPTH = 3.0 - - -def _add_shaft_section(x_start: float, x_end: float, diameter: float): - with Locations(Location(((x_start + x_end) / 2.0, 0.0, 0.0))): - Cylinder( - radius=diameter / 2.0, - height=x_end - x_start, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.ADD, - ) - - -def gen_step(): - """Return the stepped shaft with keyway benchmark model in millimeters.""" - with BuildPart() as shaft: - _add_shaft_section(0.0, LEFT_LENGTH, SMALL_DIAMETER) - _add_shaft_section(LEFT_LENGTH, LEFT_LENGTH + MIDDLE_LENGTH, MIDDLE_DIAMETER) - _add_shaft_section(LEFT_LENGTH + MIDDLE_LENGTH, TOTAL_LENGTH, SMALL_DIAMETER) - - with Locations(Location(((KEYWAY_X_START + KEYWAY_X_END) / 2.0, 0.0, MIDDLE_DIAMETER / 2.0 - KEYWAY_DEPTH))): - Box( - KEYWAY_X_END - KEYWAY_X_START, - KEYWAY_WIDTH, - KEYWAY_DEPTH + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = shaft.part - end_edges = [] - end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=0.0)) - end_edges.extend(circular_edges(part, radius=SMALL_DIAMETER / 2.0, axis="x", coordinate=TOTAL_LENGTH)) - part = safe_chamfer(part, end_edges, length=END_CHAMFER) - part.label = "benchmark_04_stepped_shaft_keyway" - return part diff --git a/models/benchmarks/benchmark_04_stepped_shaft_keyway.step b/models/benchmarks/benchmark_04_stepped_shaft_keyway.step deleted file mode 100644 index 93f0e7c3..00000000 --- a/models/benchmarks/benchmark_04_stepped_shaft_keyway.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25cd8bbe036ccf22355a0f574cbecf86b4ae0589380e5bba8db06b4e84ece723 -size 37096 diff --git a/models/benchmarks/benchmark_05_open_top_electronics_enclosure.py b/models/benchmarks/benchmark_05_open_top_electronics_enclosure.py deleted file mode 100644 index 85d72587..00000000 --- a/models/benchmarks/benchmark_05_open_top_electronics_enclosure.py +++ /dev/null @@ -1,75 +0,0 @@ -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Cylinder, - Location, - Locations, - Mode, - Plane, - RectangleRounded, - extrude, -) - - -OUTER_LENGTH = 100.0 -OUTER_WIDTH = 70.0 -OUTER_HEIGHT = 30.0 -WALL_THICKNESS = 3.0 -FLOOR_THICKNESS = 3.0 -OUTER_CORNER_RADIUS = 2.0 - -STANDOFF_DIAMETER = 10.0 -STANDOFF_HEIGHT = 12.0 -STANDOFF_HOLE_DIAMETER = 3.0 -STANDOFF_HOLE_DEPTH = 8.0 -STANDOFF_LOCATIONS = ((-35.0, -25.0), (-35.0, 25.0), (35.0, -25.0), (35.0, 25.0)) - - -def gen_step(): - """Return the open-top electronics enclosure benchmark model in millimeters.""" - inner_length = OUTER_LENGTH - 2.0 * WALL_THICKNESS - inner_width = OUTER_WIDTH - 2.0 * WALL_THICKNESS - cavity_height = OUTER_HEIGHT - FLOOR_THICKNESS - standoff_top_z = FLOOR_THICKNESS + STANDOFF_HEIGHT - - with BuildPart() as enclosure: - with BuildSketch(Plane.XY): - RectangleRounded( - OUTER_LENGTH, - OUTER_WIDTH, - OUTER_CORNER_RADIUS, - align=(Align.CENTER, Align.CENTER), - ) - extrude(amount=OUTER_HEIGHT) - - with Locations(Location((0.0, 0.0, FLOOR_THICKNESS))): - Box( - inner_length, - inner_width, - cavity_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - for x_pos, y_pos in STANDOFF_LOCATIONS: - with Locations(Location((x_pos, y_pos, FLOOR_THICKNESS))): - Cylinder( - radius=STANDOFF_DIAMETER / 2.0, - height=STANDOFF_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - with Locations(Location((x_pos, y_pos, standoff_top_z))): - Cylinder( - radius=STANDOFF_HOLE_DIAMETER / 2.0, - height=STANDOFF_HOLE_DEPTH, - align=(Align.CENTER, Align.CENTER, Align.MAX), - mode=Mode.SUBTRACT, - ) - - part = enclosure.part - part.label = "benchmark_05_open_top_electronics_enclosure_bosses" - return part diff --git a/models/benchmarks/benchmark_05_open_top_electronics_enclosure.step b/models/benchmarks/benchmark_05_open_top_electronics_enclosure.step deleted file mode 100644 index 39746d0c..00000000 --- a/models/benchmarks/benchmark_05_open_top_electronics_enclosure.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:10af95cd9aae287d046f6ee3db77700b5bb020a4ca954fb75d6212882b2371bd -size 78896 diff --git a/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.py b/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.py deleted file mode 100644 index e40f9e68..00000000 --- a/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.py +++ /dev/null @@ -1,164 +0,0 @@ -from math import cos, radians, sin - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - add, - extrude, -) - -from benchmark_common import rounded_triangle_points, safe_fillet - - -BASE_LENGTH = 120.0 -BASE_WIDTH = 60.0 -BASE_THICKNESS = 10.0 -BASE_FILLET = 3.0 - -LUG_X_EXTENT = 36.0 -LUG_RADIUS = 18.0 -LUG_HEIGHT_ABOVE_BASE = 42.0 -LUG_THICKNESS_Y = 18.0 -LUG_GAP_Y = 16.0 -LUG_FILLET = 2.0 - -CLEVIS_HOLE_DIAMETER = 14.0 -CLEVIS_HOLE_CENTER_Z = 34.0 - -BASE_HOLE_DIAMETER = 7.0 -BASE_HOLE_LOCATIONS = ((-45.0, -20.0), (-45.0, 20.0), (45.0, -20.0), (45.0, 20.0)) - -CUTOUT_CORNER_RADIUS = 3.0 -RIB_THICKNESS_X = 6.0 - - -def _lug_profile_points() -> list[tuple[float, float]]: - half_x = LUG_X_EXTENT / 2.0 - arc_center_z = BASE_THICKNESS + LUG_HEIGHT_ABOVE_BASE - LUG_RADIUS - points = [(-half_x, BASE_THICKNESS), (half_x, BASE_THICKNESS), (half_x, arc_center_z)] - - for step in range(1, 12): - angle = radians(180.0 * step / 12.0) - points.append((LUG_RADIUS * cos(angle), arc_center_z + LUG_RADIUS * sin(angle))) - - points.append((-half_x, arc_center_z)) - return points - - -def _make_lug(y_min: float, label: str): - with BuildPart() as lug: - with BuildSketch(Plane.XZ): - Polygon(_lug_profile_points(), align=None) - extrude(amount=LUG_THICKNESS_Y) - - part = lug.part.moved(Location((0.0, y_min, 0.0))) - part.label = label - return part - - -def _make_rib(sign: float): - outer_y = sign * (LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y) - foot_y = sign * (BASE_WIDTH / 2.0) - top_z = BASE_THICKNESS + 28.0 - - with BuildPart() as rib: - with BuildSketch(Plane.YZ): - Polygon( - [ - (outer_y, BASE_THICKNESS), - (foot_y, BASE_THICKNESS), - (outer_y, top_z), - ], - align=None, - ) - extrude(amount=RIB_THICKNESS_X) - - part = rib.part.moved(Location((-RIB_THICKNESS_X / 2.0, 0.0, 0.0))) - part.label = "positive_y_diagonal_rib" if sign > 0.0 else "negative_y_diagonal_rib" - return part - - -def _outer_base_edges(part): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - on_outer_x = abs(abs(center.X) - BASE_LENGTH / 2.0) < 0.05 - on_outer_y = abs(abs(center.Y) - BASE_WIDTH / 2.0) < 0.05 - on_top_or_bottom = abs(center.Z - 0.0) < 0.05 or abs(center.Z - BASE_THICKNESS) < 0.05 - if (on_outer_x or on_outer_y) and on_top_or_bottom: - edges.append(edge) - return edges - - -def _lug_base_edges(part): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - if abs(center.Z - BASE_THICKNESS) > 0.05: - continue - if abs(abs(center.Y) - LUG_GAP_Y / 2.0) < 0.05 and bbox.size.X > LUG_X_EXTENT - 1.0: - edges.append(edge) - return edges - - -def _subtract_rounded_triangle(vertices: list[tuple[float, float]]): - with BuildSketch(Plane.XY) as cutout: - Polygon( - rounded_triangle_points(vertices, radius=CUTOUT_CORNER_RADIUS, segments=5), - align=None, - ) - extrude(cutout.sketch, amount=BASE_THICKNESS + 2.0, mode=Mode.SUBTRACT) - - -def gen_step(): - """Return the clevis bracket benchmark model in millimeters.""" - positive_lug_y_min = LUG_GAP_Y / 2.0 + LUG_THICKNESS_Y - negative_lug_y_min = -LUG_GAP_Y / 2.0 - - with BuildPart() as bracket: - Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS, align=(Align.CENTER, Align.CENTER, Align.MIN)) - - base = safe_fillet(bracket.part, _outer_base_edges(bracket.part), radius=BASE_FILLET) - - with BuildPart() as bracket: - add(base, mode=Mode.ADD) - add(_make_lug(positive_lug_y_min, "positive_y_clevis_lug"), mode=Mode.ADD) - add(_make_lug(negative_lug_y_min, "negative_y_clevis_lug"), mode=Mode.ADD) - add(_make_rib(1.0), mode=Mode.ADD) - add(_make_rib(-1.0), mode=Mode.ADD) - - with Locations(Location((0.0, 0.0, CLEVIS_HOLE_CENTER_Z))): - Cylinder( - radius=CLEVIS_HOLE_DIAMETER / 2.0, - height=BASE_WIDTH + 8.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - - for x_pos, y_pos in BASE_HOLE_LOCATIONS: - with Locations(Location((x_pos, y_pos, -1.0))): - Cylinder( - radius=BASE_HOLE_DIAMETER / 2.0, - height=BASE_THICKNESS + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - _subtract_rounded_triangle([(-54.0, -13.0), (-25.0, 0.0), (-54.0, 13.0)]) - _subtract_rounded_triangle([(54.0, -13.0), (25.0, 0.0), (54.0, 13.0)]) - - part = bracket.part - part = safe_fillet(part, _lug_base_edges(part), radius=LUG_FILLET) - part.label = "benchmark_06_aerospace_clevis_bracket_lightening_cutouts" - return part diff --git a/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.step b/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.step deleted file mode 100644 index dc4426c6..00000000 --- a/models/benchmarks/benchmark_06_clevis_bracket_lightening_cutouts.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9bfd3b90305115f0a1d67330b179dcb7e76fc998fb421837e907e5b8e9dbbf72 -size 380740 diff --git a/models/benchmarks/benchmark_07_radial_engine_cylinder.py b/models/benchmarks/benchmark_07_radial_engine_cylinder.py deleted file mode 100644 index b381df51..00000000 --- a/models/benchmarks/benchmark_07_radial_engine_cylinder.py +++ /dev/null @@ -1,122 +0,0 @@ -from math import cos, radians, sin, tau - -from build123d import Align, BuildPart, Cylinder, Location, Locations, Mode - -from benchmark_common import circular_edges, polar_point, safe_fillet - - -BARREL_DIAMETER = 36.0 -BARREL_HEIGHT = 70.0 - -FIN_COUNT = 12 -FIN_DIAMETER = 62.0 -FIN_THICKNESS = 2.0 -FIN_Z_VALUES = tuple(10.0 + 5.0 * index for index in range(FIN_COUNT)) - -BASE_FLANGE_DIAMETER = 70.0 -BASE_FLANGE_THICKNESS = 8.0 -MOUNT_HOLE_COUNT = 6 -MOUNT_HOLE_DIAMETER = 5.0 -MOUNT_BOLT_CIRCLE_DIAMETER = 56.0 - -TOP_CAP_DIAMETER = 44.0 -TOP_CAP_BOTTOM_Z = 70.0 -TOP_CAP_HEIGHT = 8.0 - -BOSS_DIAMETER = 12.0 -BOSS_LENGTH = 24.0 -BOSS_ANGLE_DEGREES = 35.0 -BOSS_BORE_DIAMETER = 5.0 - -EDGE_FILLET = 0.95 - - -def _boss_center() -> tuple[float, float, float]: - angle = radians(BOSS_ANGLE_DEGREES) - start = (TOP_CAP_DIAMETER / 2.0 - 1.0, 0.0, TOP_CAP_BOTTOM_Z + TOP_CAP_HEIGHT / 2.0) - direction = (cos(angle), 0.0, sin(angle)) - return ( - start[0] + direction[0] * BOSS_LENGTH / 2.0, - 0.0, - start[2] + direction[2] * BOSS_LENGTH / 2.0, - ) - - -def _edge_fillets(part): - edges = [] - for z_pos in FIN_Z_VALUES: - edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos)) - edges.extend(circular_edges(part, radius=FIN_DIAMETER / 2.0, axis="z", coordinate=z_pos + FIN_THICKNESS)) - edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=0.0)) - edges.extend(circular_edges(part, radius=BASE_FLANGE_DIAMETER / 2.0, axis="z", coordinate=BASE_FLANGE_THICKNESS)) - return edges - - -def gen_step(): - """Return the radial-engine-style cylinder benchmark model in millimeters.""" - boss_rotation_y = 90.0 - BOSS_ANGLE_DEGREES - boss_center = _boss_center() - - with BuildPart() as cylinder: - Cylinder( - radius=BARREL_DIAMETER / 2.0, - height=BARREL_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - Cylinder( - radius=BASE_FLANGE_DIAMETER / 2.0, - height=BASE_FLANGE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - for z_pos in FIN_Z_VALUES: - with Locations(Location((0.0, 0.0, z_pos))): - Cylinder( - radius=FIN_DIAMETER / 2.0, - height=FIN_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - with Locations(Location((0.0, 0.0, TOP_CAP_BOTTOM_Z))): - Cylinder( - radius=TOP_CAP_DIAMETER / 2.0, - height=TOP_CAP_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - with Locations(Location(boss_center)): - Cylinder( - radius=BOSS_DIAMETER / 2.0, - height=BOSS_LENGTH, - rotation=(0.0, boss_rotation_y, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.ADD, - ) - - for index in range(MOUNT_HOLE_COUNT): - x_pos, y_pos = polar_point(MOUNT_BOLT_CIRCLE_DIAMETER / 2.0, tau * index / MOUNT_HOLE_COUNT) - with Locations(Location((x_pos, y_pos, -1.0))): - Cylinder( - radius=MOUNT_HOLE_DIAMETER / 2.0, - height=BASE_FLANGE_THICKNESS + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - with Locations(Location(boss_center)): - Cylinder( - radius=BOSS_BORE_DIAMETER / 2.0, - height=BOSS_LENGTH + 6.0, - rotation=(0.0, boss_rotation_y, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - - part = cylinder.part - part = safe_fillet(part, _edge_fillets(part), radius=EDGE_FILLET) - part.label = "benchmark_07_radial_engine_cylinder_cooling_fins" - return part diff --git a/models/benchmarks/benchmark_07_radial_engine_cylinder.step b/models/benchmarks/benchmark_07_radial_engine_cylinder.step deleted file mode 100644 index 7a32c81f..00000000 --- a/models/benchmarks/benchmark_07_radial_engine_cylinder.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9dde14a7f17decbaf8ea59e2daf7ecb8153772042920decf16b0d14d871cc9a7 -size 275923 diff --git a/models/benchmarks/benchmark_08_centrifugal_impeller.py b/models/benchmarks/benchmark_08_centrifugal_impeller.py deleted file mode 100644 index 3515cad8..00000000 --- a/models/benchmarks/benchmark_08_centrifugal_impeller.py +++ /dev/null @@ -1,152 +0,0 @@ -from math import asin, cos, hypot, radians, sin, tau - -from build123d import ( - Align, - BuildPart, - BuildSketch, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - add, - extrude, -) - -from benchmark_common import circular_edges, polar_point, safe_fillet - - -BACKPLATE_DIAMETER = 90.0 -BACKPLATE_THICKNESS = 6.0 -BACKPLATE_FILLET = 1.5 -HUB_DIAMETER = 26.0 -HUB_HEIGHT = 22.0 -BORE_DIAMETER = 8.0 - -BLADE_COUNT = 12 -BLADE_ROOT_RADIUS = 18.0 -BLADE_TIP_RADIUS = 43.0 -BLADE_THICKNESS = 3.0 -BLADE_HEIGHT = 16.0 -BLADE_BACKWARD_SWEEP = radians(45.0) -BLADE_SEGMENTS = 5 -BLADE_ROOT_FILLET = 1.0 - - -def _clamp_radius(point: tuple[float, float]) -> tuple[float, float]: - radius = hypot(point[0], point[1]) - if radius < BLADE_ROOT_RADIUS: - scale = BLADE_ROOT_RADIUS / radius - return (point[0] * scale, point[1] * scale) - if radius > BLADE_TIP_RADIUS: - scale = BLADE_TIP_RADIUS / radius - return (point[0] * scale, point[1] * scale) - return point - - -def _blade_center(base_angle: float, t: float) -> tuple[float, float]: - radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t - angle = base_angle - BLADE_BACKWARD_SWEEP * t - return polar_point(radius, angle) - - -def _blade_normal(base_angle: float, t: float) -> tuple[float, float]: - radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t - dr_dt = BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS - dtheta_dt = -BLADE_BACKWARD_SWEEP - angle = base_angle + dtheta_dt * t - dx_dt = dr_dt * cos(angle) - radius * sin(angle) * dtheta_dt - dy_dt = dr_dt * sin(angle) + radius * cos(angle) * dtheta_dt - length = hypot(dx_dt, dy_dt) - return (-dy_dt / length, dx_dt / length) - - -def _blade_end_points(base_angle: float, t: float) -> tuple[tuple[float, float], tuple[float, float]]: - radius = BLADE_ROOT_RADIUS if t == 0.0 else BLADE_TIP_RADIUS - angle = base_angle - BLADE_BACKWARD_SWEEP * t - half_angle = asin((BLADE_THICKNESS / 2.0) / radius) - return (polar_point(radius, angle + half_angle), polar_point(radius, angle - half_angle)) - - -def _blade_outline(base_angle: float) -> list[tuple[float, float]]: - left_side = [] - right_side = [] - half_thickness = BLADE_THICKNESS / 2.0 - - for index in range(BLADE_SEGMENTS + 1): - t = index / BLADE_SEGMENTS - if index == 0 or index == BLADE_SEGMENTS: - left, right = _blade_end_points(base_angle, t) - else: - center = _blade_center(base_angle, t) - normal = _blade_normal(base_angle, t) - left = _clamp_radius((center[0] + normal[0] * half_thickness, center[1] + normal[1] * half_thickness)) - right = _clamp_radius((center[0] - normal[0] * half_thickness, center[1] - normal[1] * half_thickness)) - left_side.append(left) - right_side.append(right) - - return left_side + list(reversed(right_side)) - - -def _make_blade(index: int): - base_angle = tau * index / BLADE_COUNT - with BuildPart() as blade: - with BuildSketch(Plane.XY): - Polygon(_blade_outline(base_angle), align=None) - extrude(amount=BLADE_HEIGHT) - - part = blade.part.moved(Location((0.0, 0.0, BACKPLATE_THICKNESS))) - part.label = f"backward_curved_blade_{index + 1:02d}" - return part - - -def _blade_root_edges(part): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - if abs(center.Z - BACKPLATE_THICKNESS) > 0.05: - continue - radial_distance = hypot(center.X, center.Y) - if BLADE_ROOT_RADIUS - 3.0 <= radial_distance <= 30.0: - if str(edge.geom_type).endswith("LINE"): - edges.append(edge) - return edges - - -def gen_step(): - """Return the centrifugal impeller benchmark model in millimeters.""" - total_height = BACKPLATE_THICKNESS + HUB_HEIGHT - - with BuildPart() as impeller: - Cylinder( - radius=BACKPLATE_DIAMETER / 2.0, - height=BACKPLATE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - for blade_index in range(BLADE_COUNT): - add(_make_blade(blade_index), mode=Mode.ADD) - with Locations(Location((0.0, 0.0, BACKPLATE_THICKNESS))): - Cylinder( - radius=HUB_DIAMETER / 2.0, - height=HUB_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - with Locations(Location((0.0, 0.0, -1.0))): - Cylinder( - radius=BORE_DIAMETER / 2.0, - height=total_height + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = impeller.part - backplate_edges = [] - backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=0.0)) - backplate_edges.extend(circular_edges(part, radius=BACKPLATE_DIAMETER / 2.0, axis="z", coordinate=BACKPLATE_THICKNESS)) - part = safe_fillet(part, backplate_edges, radius=BACKPLATE_FILLET) - part = safe_fillet(part, _blade_root_edges(part), radius=BLADE_ROOT_FILLET) - part.label = "benchmark_08_centrifugal_impeller_backward_curved_blades" - return part diff --git a/models/benchmarks/benchmark_08_centrifugal_impeller.step b/models/benchmarks/benchmark_08_centrifugal_impeller.step deleted file mode 100644 index 82ffc714..00000000 --- a/models/benchmarks/benchmark_08_centrifugal_impeller.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:63e50d63813686b90a75e72c905487599e6b12940851abeb037805dae6222b14 -size 909453 diff --git a/models/benchmarks/benchmark_09_spiral_staircase.py b/models/benchmarks/benchmark_09_spiral_staircase.py deleted file mode 100644 index f9106e0f..00000000 --- a/models/benchmarks/benchmark_09_spiral_staircase.py +++ /dev/null @@ -1,149 +0,0 @@ -from math import cos, radians, sin - -from build123d import ( - Align, - BuildLine, - BuildPart, - BuildSketch, - CenterArc, - Circle, - Color, - Compound, - Cylinder, - Edge, - Line, - Location, - Plane, - extrude, - make_face, - sweep, -) - - -BASE_DIAMETER = 90.0 -BASE_THICKNESS = 5.0 -COLUMN_DIAMETER = 14.0 -COLUMN_HEIGHT = 140.0 - -TREAD_COUNT = 20 -TREAD_THICKNESS = 4.0 -TREAD_INNER_RADIUS = 10.0 -TREAD_OUTER_RADIUS = 62.0 -TREAD_SWEEP_DEGREES = 24.0 -TREAD_FIRST_BOTTOM_Z = 4.0 -TREAD_RISE = 6.0 -TREAD_ROTATION_DEGREES = 18.0 - -HANDRAIL_RADIUS = 66.0 -HANDRAIL_DIAMETER = 5.0 -HANDRAIL_START_Z = 14.0 -HANDRAIL_END_Z = 130.0 -BALUSTER_DIAMETER = 3.0 - - -def _polar_point(radius: float, angle_degrees: float) -> tuple[float, float]: - angle = radians(angle_degrees) - return (radius * cos(angle), radius * sin(angle)) - - -def _make_base(): - with BuildPart() as base: - Cylinder( - radius=BASE_DIAMETER / 2.0, - height=BASE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - part = base.part - part.label = "base_disk" - part.color = Color(0.42, 0.43, 0.45, 1.0) - return part - - -def _make_column(): - with BuildPart() as column: - Cylinder( - radius=COLUMN_DIAMETER / 2.0, - height=COLUMN_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - part = column.part - part.label = "central_column" - part.color = Color(0.70, 0.72, 0.74, 1.0) - return part - - -def _make_tread_profile(): - half_sweep = TREAD_SWEEP_DEGREES / 2.0 - with BuildPart() as tread: - with BuildSketch(Plane.XY): - with BuildLine(): - CenterArc((0.0, 0.0), TREAD_OUTER_RADIUS, -half_sweep, TREAD_SWEEP_DEGREES) - Line(_polar_point(TREAD_OUTER_RADIUS, half_sweep), _polar_point(TREAD_INNER_RADIUS, half_sweep)) - CenterArc((0.0, 0.0), TREAD_INNER_RADIUS, half_sweep, -TREAD_SWEEP_DEGREES) - Line(_polar_point(TREAD_INNER_RADIUS, -half_sweep), _polar_point(TREAD_OUTER_RADIUS, -half_sweep)) - make_face() - extrude(amount=TREAD_THICKNESS) - return tread.part - - -def _make_tread(index: int): - angle = index * TREAD_ROTATION_DEGREES - bottom_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE - part = _make_tread_profile().moved(Location((0.0, 0.0, bottom_z), (0.0, 0.0, angle))) - part.label = f"helical_tread_{index + 1:02d}" - part.color = Color(0.78, 0.58, 0.35, 1.0) - return part - - -def _handrail_center_z(angle_degrees: float) -> float: - return HANDRAIL_START_Z + (HANDRAIL_END_Z - HANDRAIL_START_Z) * angle_degrees / 360.0 - - -def _make_handrail(): - rail_height = HANDRAIL_END_Z - HANDRAIL_START_Z - path = Edge.make_helix( - pitch=rail_height, - height=rail_height, - radius=HANDRAIL_RADIUS, - center=(0.0, 0.0, HANDRAIL_START_Z), - ) - profile_plane = Plane( - origin=(HANDRAIL_RADIUS, 0.0, HANDRAIL_START_Z), - x_dir=(1.0, 0.0, 0.0), - z_dir=path.tangent_at(0.0), - ) - - with BuildPart() as rail: - with BuildSketch(profile_plane): - Circle(HANDRAIL_DIAMETER / 2.0) - sweep(path=path, is_frenet=True) - part = rail.part - part.label = "helical_handrail" - part.color = Color(0.22, 0.28, 0.34, 1.0) - return part - - -def _make_baluster(index: int): - angle = index * TREAD_ROTATION_DEGREES - tread_top_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE + TREAD_THICKNESS - rail_center_z = _handrail_center_z(angle) - x_pos, y_pos = _polar_point(TREAD_OUTER_RADIUS, angle) - with BuildPart() as baluster: - Cylinder( - radius=BALUSTER_DIAMETER / 2.0, - height=rail_center_z - tread_top_z, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - part = baluster.part.moved(Location((x_pos, y_pos, tread_top_z))) - part.label = f"baluster_{index + 1:02d}" - part.color = Color(0.30, 0.33, 0.36, 1.0) - return part - - -def gen_step(): - """Return the miniature spiral staircase benchmark model in millimeters.""" - parts = [_make_base(), _make_column()] - parts.extend(_make_tread(index) for index in range(TREAD_COUNT)) - parts.append(_make_handrail()) - parts.extend(_make_baluster(index) for index in range(TREAD_COUNT)) - return Compound(obj=parts, children=parts, label="benchmark_09_spiral_staircase_helical_handrail") diff --git a/models/benchmarks/benchmark_09_spiral_staircase.step b/models/benchmarks/benchmark_09_spiral_staircase.step deleted file mode 100644 index 6de1656e..00000000 --- a/models/benchmarks/benchmark_09_spiral_staircase.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:452826d6f8ba2b53394b22358f9f6d9e4b548f52762691acb346b98b422d2122 -size 628279 diff --git a/models/benchmarks/benchmark_10_planetary_gear_stage.py b/models/benchmarks/benchmark_10_planetary_gear_stage.py deleted file mode 100644 index cdbd84db..00000000 --- a/models/benchmarks/benchmark_10_planetary_gear_stage.py +++ /dev/null @@ -1,181 +0,0 @@ -from math import tau - -from build123d import ( - Align, - BuildPart, - BuildSketch, - Color, - Compound, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - extrude, -) - -from benchmark_common import polar_point, trapezoid_tooth_profile - - -GEAR_THICKNESS = 8.0 -SUN_TEETH = 24 -SUN_PITCH_DIAMETER = 48.0 -SUN_ROOT_DIAMETER = 42.0 -SUN_OUTSIDE_DIAMETER = 54.0 -SUN_BORE_DIAMETER = 10.0 - -PLANET_COUNT = 3 -PLANET_TEETH = 18 -PLANET_PITCH_DIAMETER = 36.0 -PLANET_ROOT_DIAMETER = 31.0 -PLANET_OUTSIDE_DIAMETER = 41.0 -PLANET_CENTER_RADIUS = 42.0 -PLANET_BORE_DIAMETER = 6.8 - -RING_TEETH = 60 -RING_INTERNAL_PITCH_DIAMETER = 120.0 -RING_INTERNAL_ROOT_DIAMETER = 126.0 -RING_INTERNAL_TIP_DIAMETER = 114.0 -RING_OUTSIDE_DIAMETER = 140.0 - -CARRIER_DIAMETER = 105.0 -CARRIER_BOTTOM_Z = -5.0 -CARRIER_TOP_Z = -1.0 -CARRIER_THICKNESS = CARRIER_TOP_Z - CARRIER_BOTTOM_Z - -PIN_DIAMETER = 6.0 -PIN_HEIGHT = 14.0 -PIN_BOTTOM_Z = -5.0 - - -def _make_external_gear( - *, - label: str, - teeth: int, - root_diameter: float, - outside_diameter: float, - phase: float, - center: tuple[float, float] = (0.0, 0.0), - bore_diameter: float | None = None, - color: Color | None = None, -): - with BuildPart() as gear: - with BuildSketch(Plane.XY): - Polygon( - trapezoid_tooth_profile( - teeth=teeth, - root_radius=root_diameter / 2.0, - tip_radius=outside_diameter / 2.0, - phase=phase, - ), - align=None, - ) - extrude(amount=GEAR_THICKNESS) - - if bore_diameter is not None: - with Locations(Location((0.0, 0.0, -0.5))): - Cylinder( - radius=bore_diameter / 2.0, - height=GEAR_THICKNESS + 1.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = gear.part.moved(Location((center[0], center[1], 0.0))) - part.label = label - part.color = color - return part - - -def _make_internal_ring_gear(): - with BuildPart() as ring: - Cylinder( - radius=RING_OUTSIDE_DIAMETER / 2.0, - height=GEAR_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with BuildSketch(Plane.XY): - Polygon( - trapezoid_tooth_profile( - teeth=RING_TEETH, - root_radius=RING_INTERNAL_ROOT_DIAMETER / 2.0, - tip_radius=RING_INTERNAL_TIP_DIAMETER / 2.0, - phase=-tau / RING_TEETH / 2.0, - root_span_fraction=0.68, - tip_span_fraction=0.34, - ), - align=None, - ) - extrude(amount=GEAR_THICKNESS, mode=Mode.SUBTRACT) - part = ring.part - part.label = "ring_gear_60_internal_teeth" - part.color = Color(0.66, 0.68, 0.70, 1.0) - return part - - -def _make_carrier_plate(): - with BuildPart() as carrier: - with Locations(Location((0.0, 0.0, CARRIER_BOTTOM_Z))): - Cylinder( - radius=CARRIER_DIAMETER / 2.0, - height=CARRIER_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - part = carrier.part - part.label = "carrier_plate" - part.color = Color(0.52, 0.62, 0.58, 1.0) - return part - - -def _make_pin(label: str, center: tuple[float, float]): - with BuildPart() as pin: - with Locations(Location((center[0], center[1], PIN_BOTTOM_Z))): - Cylinder( - radius=PIN_DIAMETER / 2.0, - height=PIN_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - part = pin.part - part.label = label - part.color = Color(0.22, 0.25, 0.29, 1.0) - return part - - -def _planet_center(index: int) -> tuple[float, float]: - return polar_point(PLANET_CENTER_RADIUS, tau * index / PLANET_COUNT) - - -def gen_step(): - """Return the simplified planetary gear benchmark assembly in millimeters.""" - parts = [ - _make_carrier_plate(), - _make_internal_ring_gear(), - _make_external_gear( - label="sun_gear_24_external_teeth", - teeth=SUN_TEETH, - root_diameter=SUN_ROOT_DIAMETER, - outside_diameter=SUN_OUTSIDE_DIAMETER, - phase=-tau / SUN_TEETH / 2.0, - bore_diameter=SUN_BORE_DIAMETER, - color=Color(0.94, 0.60, 0.22, 1.0), - ), - ] - - for index in range(PLANET_COUNT): - center = _planet_center(index) - parts.append( - _make_external_gear( - label=f"planet_gear_{index + 1}_18_external_teeth", - teeth=PLANET_TEETH, - root_diameter=PLANET_ROOT_DIAMETER, - outside_diameter=PLANET_OUTSIDE_DIAMETER, - phase=tau * index / PLANET_COUNT, - center=center, - bore_diameter=PLANET_BORE_DIAMETER, - color=Color(0.24, 0.51, 0.76, 1.0), - ) - ) - parts.append(_make_pin(f"planet_pin_{index + 1}", center)) - - return Compound(obj=parts, children=parts, label="benchmark_10_simplified_planetary_gear_stage") diff --git a/models/benchmarks/benchmark_10_planetary_gear_stage.step b/models/benchmarks/benchmark_10_planetary_gear_stage.step deleted file mode 100644 index ef92a6c0..00000000 --- a/models/benchmarks/benchmark_10_planetary_gear_stage.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00723b6214041720aa70bd85d2f5fc005e3294b49a710a0a51774050100aa532 -size 2269736 diff --git a/models/benchmarks/benchmark_common.py b/models/benchmarks/benchmark_common.py deleted file mode 100644 index 5c2d9548..00000000 --- a/models/benchmarks/benchmark_common.py +++ /dev/null @@ -1,177 +0,0 @@ -from __future__ import annotations - -from math import cos, hypot, sin, tau -from typing import Iterable - -from build123d import Axis, BuildPart, Color, Location, Mode, chamfer, fillet - - -TOLERANCE = 1e-6 - - -def polar_point(radius: float, angle: float) -> tuple[float, float]: - return (radius * cos(angle), radius * sin(angle)) - - -def safe_fillet(part, edges: Iterable, radius: float): - selected = list(edges) - if not selected: - return part - try: - return fillet(selected, radius=radius) - except Exception: - return part - - -def safe_chamfer(part, edges: Iterable, length: float): - selected = list(edges) - if not selected: - return part - try: - return chamfer(selected, length=length) - except Exception: - return part - - -def line_edges_at_z(part, z_value: float, *, tol: float = 0.02): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - if abs(bbox.min.Z - z_value) <= tol and abs(bbox.max.Z - z_value) <= tol: - if str(edge.geom_type).endswith("LINE"): - edges.append(edge) - return edges - - -def circular_edges( - part, - *, - radius: float | None = None, - axis: str | None = None, - coordinate: float | None = None, - tol: float = 0.05, -): - edges = [] - for edge in part.edges(): - try: - edge_radius = edge.radius - edge_center = edge.arc_center - edge_normal = edge.normal() - except Exception: - continue - - if radius is not None and abs(edge_radius - radius) > tol: - continue - - if axis is not None: - components = { - "x": abs(edge_normal.X), - "y": abs(edge_normal.Y), - "z": abs(edge_normal.Z), - } - if components[axis] < 1.0 - 1e-4: - continue - - if coordinate is not None: - coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis or "z"] - if abs(coord - coordinate) > tol: - continue - - edges.append(edge) - - return edges - - -def cylinder_axis_x(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD): - from build123d import Align, Cylinder - - return Cylinder( - radius=radius, - height=length, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=mode, - ).locate(Location(center)) - - -def cylinder_axis_y(radius: float, length: float, center: tuple[float, float, float], *, mode=Mode.ADD): - from build123d import Align, Cylinder - - return Cylinder( - radius=radius, - height=length, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=mode, - ).locate(Location(center)) - - -def rounded_triangle_points( - vertices: list[tuple[float, float]], - *, - radius: float, - segments: int = 5, -) -> list[tuple[float, float]]: - """Approximate a rounded triangular loop with quadratic corner arcs.""" - points: list[tuple[float, float]] = [] - count = len(vertices) - - for index, vertex in enumerate(vertices): - previous = vertices[(index - 1) % count] - next_vertex = vertices[(index + 1) % count] - - to_previous = (previous[0] - vertex[0], previous[1] - vertex[1]) - to_next = (next_vertex[0] - vertex[0], next_vertex[1] - vertex[1]) - previous_length = hypot(*to_previous) - next_length = hypot(*to_next) - offset = min(radius, previous_length * 0.35, next_length * 0.35) - - start = ( - vertex[0] + to_previous[0] / previous_length * offset, - vertex[1] + to_previous[1] / previous_length * offset, - ) - end = ( - vertex[0] + to_next[0] / next_length * offset, - vertex[1] + to_next[1] / next_length * offset, - ) - - for step in range(segments + 1): - t = step / segments - x_pos = (1.0 - t) ** 2 * start[0] + 2.0 * (1.0 - t) * t * vertex[0] + t**2 * end[0] - y_pos = (1.0 - t) ** 2 * start[1] + 2.0 * (1.0 - t) * t * vertex[1] + t**2 * end[1] - points.append((x_pos, y_pos)) - - return points - - -def trapezoid_tooth_profile( - *, - teeth: int, - root_radius: float, - tip_radius: float, - phase: float, - root_span_fraction: float = 0.72, - tip_span_fraction: float = 0.38, -) -> list[tuple[float, float]]: - points: list[tuple[float, float]] = [] - pitch_angle = tau / teeth - - for tooth_index in range(teeth): - center_angle = phase + tooth_index * pitch_angle - points.extend( - ( - polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0), - polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0), - polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0), - polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0), - ) - ) - - return points - - -def label(part, name: str, color: Color | None = None): - part.label = name - if color is not None: - part.color = color - return part diff --git a/models/benchmarks/validate_benchmark.py b/models/benchmarks/validate_benchmark.py deleted file mode 100644 index 5762279a..00000000 --- a/models/benchmarks/validate_benchmark.py +++ /dev/null @@ -1,269 +0,0 @@ -from __future__ import annotations - -from math import atan2, degrees, hypot -from pathlib import Path - -from build123d import import_step - - -ROOT = Path(__file__).resolve().parent -BASIC_TOL = 0.2 -COMPLEX_TOL = 0.5 -ANGLE_TOL = 2.0 - - -class BenchmarkFailure(AssertionError): - pass - - -def close(actual: float, expected: float, tol: float = BASIC_TOL) -> bool: - return abs(actual - expected) <= tol - - -def check(condition: bool, message: str) -> None: - if not condition: - raise BenchmarkFailure(message) - - -def bbox(shape): - box = shape.bounding_box() - return box.min, box.max, ( - box.max.X - box.min.X, - box.max.Y - box.min.Y, - box.max.Z - box.min.Z, - ) - - -def assert_bbox(shape, expected_size, expected_min=None, tol: float = BASIC_TOL): - min_v, max_v, size = bbox(shape) - for actual, expected, axis in zip(size, expected_size, "xyz"): - check(close(actual, expected, tol), f"bbox {axis} size {actual:.3f} != {expected:.3f}") - if expected_min is not None: - for actual, expected, axis in zip((min_v.X, min_v.Y, min_v.Z), expected_min, "xyz"): - check(close(actual, expected, tol), f"bbox min {axis} {actual:.3f} != {expected:.3f}") - - -def axis_name(direction) -> str: - components = {"x": abs(direction.X), "y": abs(direction.Y), "z": abs(direction.Z)} - return max(components, key=components.get) - - -def cyl_faces(shape, *, radius: float | None = None, axis: str | None = None, tol: float = BASIC_TOL): - faces = [] - for face in shape.faces(): - if "CYLINDER" not in str(face.geom_type): - continue - if radius is not None and not close(face.radius, radius, tol): - continue - if axis is not None and axis_name(face.axis_of_rotation.direction) != axis: - continue - faces.append(face) - return faces - - -def torus_count(shape) -> int: - return sum(1 for face in shape.faces() if "TORUS" in str(face.geom_type)) - - -def cone_count(shape) -> int: - return sum(1 for face in shape.faces() if "CONE" in str(face.geom_type)) - - -def cylinder_centers(shape, *, radius: float, axis: str): - centers = [] - for face in cyl_faces(shape, radius=radius, axis=axis): - pos = face.axis_of_rotation.position - centers.append((pos.X, pos.Y, pos.Z)) - return centers - - -def expect_cylinder_at(shape, *, radius: float, axis: str, x=None, y=None, z=None, tol: float = BASIC_TOL): - for center in cylinder_centers(shape, radius=radius, axis=axis): - if x is not None and not close(center[0], x, tol): - continue - if y is not None and not close(center[1], y, tol): - continue - if z is not None and not close(center[2], z, tol): - continue - return - raise BenchmarkFailure(f"missing cylinder radius {radius} axis {axis} at x={x} y={y} z={z}") - - -def plane_faces_at(shape, *, axis: str, coordinate: float, tol: float = BASIC_TOL): - index = "xyz".index(axis) - faces = [] - for face in shape.faces(): - if "PLANE" not in str(face.geom_type): - continue - box = face.bounding_box() - mins = (box.min.X, box.min.Y, box.min.Z) - maxs = (box.max.X, box.max.Y, box.max.Z) - if close(mins[index], coordinate, tol) and close(maxs[index], coordinate, tol): - faces.append(face) - return faces - - -def assert_common(shape, *, solids: int | None = None, min_solids: int | None = None): - check(shape.is_valid, "STEP imports but imported B-rep is invalid") - check(shape.is_manifold, "STEP imports but B-rep is non-manifold") - if solids is not None: - check(len(shape.solids()) == solids, f"solid count {len(shape.solids())} != {solids}") - if min_solids is not None: - check(len(shape.solids()) >= min_solids, f"solid count {len(shape.solids())} < {min_solids}") - - -def validate_01(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (100.0, 60.0, 20.0), (-50.0, -30.0, 0.0)) - for x in (-35.0, 35.0): - for y in (-20.0, 20.0): - expect_cylinder_at(shape, radius=4.0, axis="z", x=x, y=y) - check(len(cyl_faces(shape, radius=4.0, axis="z")) == 4, "expected four 8 mm through holes") - top_faces = plane_faces_at(shape, axis="z", coordinate=20.0) - check(any(close(face.bounding_box().size.X, 96.0) and close(face.bounding_box().size.Y, 56.0) for face in top_faces), "top chamfered face is not inset by 2 mm") - check(cone_count(shape) == 0 and torus_count(shape) == 0, "unexpected hole chamfers/roundovers") - - -def validate_02(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (80.0, 80.0, 10.0), (-40.0, -40.0, 0.0)) - expect_cylinder_at(shape, radius=15.0, axis="z", x=0.0, y=0.0) - bolt_centers = cylinder_centers(shape, radius=3.0, axis="z") - check(len(bolt_centers) == 6, "expected six bolt holes") - angles = sorted(degrees(atan2(y, x)) % 360.0 for x, y, _ in bolt_centers) - for x, y, _ in bolt_centers: - check(close(hypot(x, y), 30.0, COMPLEX_TOL), "bolt hole is off 60 mm bolt circle") - gaps = [(angles[(i + 1) % 6] - angles[i]) % 360.0 for i in range(6)] - check(all(close(gap, 60.0, ANGLE_TOL) for gap in gaps), "bolt holes are not spaced 60 degrees apart") - check(torus_count(shape) >= 2, "missing top/bottom outer edge fillets") - - -def validate_03(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (80.0, 50.0, 58.0), (-40.0, -25.0, 0.0)) - for x in (-25.0, 25.0): - expect_cylinder_at(shape, radius=3.0, axis="z", x=x, y=-10.0) - expect_cylinder_at(shape, radius=3.0, axis="y", x=x, z=30.0) - check(len(cyl_faces(shape, radius=3.0, axis="z")) == 2, "base hole count is not two") - check(len(cyl_faces(shape, radius=3.0, axis="y")) == 2, "back plate hole count is not two") - check(len(shape.faces()) >= 20, "missing gusset/fillet detail") - - -def validate_04(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (120.0, 30.0, 30.0), (0.0, -15.0, -15.0)) - check(len(cyl_faces(shape, radius=10.0, axis="x")) == 2, "expected two 20 mm shaft sections") - check(len(cyl_faces(shape, radius=15.0, axis="x")) == 1, "expected one 30 mm shaft section") - keyway_planes = plane_faces_at(shape, axis="z", coordinate=12.0) - check(any(close(face.bounding_box().size.X, 40.0) and close(face.bounding_box().size.Y, 6.0) for face in keyway_planes), "keyway floor is not 40 x 6 mm at 3 mm depth") - check(cone_count(shape) >= 2, "missing 1 mm end chamfers") - - -def validate_05(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (100.0, 70.0, 30.0), (-50.0, -35.0, 0.0)) - check(any(close(face.bounding_box().size.X, 94.0) and close(face.bounding_box().size.Y, 64.0) for face in plane_faces_at(shape, axis="z", coordinate=3.0)), "inside floor/wall thickness check failed") - for x in (-35.0, 35.0): - for y in (-25.0, 25.0): - expect_cylinder_at(shape, radius=5.0, axis="z", x=x, y=y) - expect_cylinder_at(shape, radius=1.5, axis="z", x=x, y=y) - check(len(cyl_faces(shape, radius=5.0, axis="z")) == 4, "standoff count is not four") - check(len(cyl_faces(shape, radius=1.5, axis="z")) == 4, "blind hole count is not four") - check(len(cyl_faces(shape, radius=2.0, axis="z")) >= 4, "missing 2 mm outside vertical corner fillets") - - -def validate_06(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (120.0, 60.0, 52.0), (-60.0, -30.0, 0.0)) - expect_cylinder_at(shape, radius=7.0, axis="y", x=0.0, z=34.0) - for x in (-45.0, 45.0): - for y in (-20.0, 20.0): - expect_cylinder_at(shape, radius=3.5, axis="z", x=x, y=y) - check(len(cyl_faces(shape, radius=7.0, axis="y")) == 2, "clevis through-hole should pass through two separate lugs") - check(len(cyl_faces(shape, radius=3.5, axis="z")) == 4, "base mounting hole count is not four") - check(len(cyl_faces(shape, radius=2.0, axis="x")) >= 2, "missing lug-to-base radius detail") - check(len(shape.faces()) >= 90, "missing cutout/rib/lug detail") - - -def validate_07(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (79.1, 70.0, 92.7), (-35.0, -35.0, 0.0), tol=COMPLEX_TOL) - check(len(cyl_faces(shape, radius=31.0, axis="z")) == 12, "cooling fin count is not 12") - check(len(cyl_faces(shape, radius=2.5, axis="z")) == 6, "mounting hole count is not six") - check(len(cyl_faces(shape, radius=18.0, axis="z")) >= 1, "missing 36 mm barrel") - check(len(cyl_faces(shape, radius=22.0, axis="z")) == 1, "missing 44 mm top cap") - boss = cyl_faces(shape, radius=6.0) - bore = [face for face in cyl_faces(shape, radius=2.5) if axis_name(face.axis_of_rotation.direction) == "x"] - check(len(boss) == 1 and len(bore) == 1, "missing angled boss or boss bore") - for face in (boss[0], bore[0]): - direction = face.axis_of_rotation.direction - angle = degrees(atan2(abs(direction.Z), abs(direction.X))) - check(close(angle, 35.0, ANGLE_TOL), "boss axis is not 35 degrees upward") - check(torus_count(shape) >= 24, "missing fin/flange edge fillets") - - -def validate_08(shape): - assert_common(shape, solids=1) - assert_bbox(shape, (90.0, 90.0, 28.0), (-45.0, -45.0, 0.0), tol=COMPLEX_TOL) - expect_cylinder_at(shape, radius=4.0, axis="z", x=0.0, y=0.0) - check(len(cyl_faces(shape, radius=13.0, axis="z")) == 1, "missing 26 mm hub") - check(len(shape.faces()) >= 220, "missing repeated curved blade detail") - check(torus_count(shape) >= 2, "missing backplate edge fillets") - - -def validate_09(shape): - assert_common(shape, min_solids=43) - assert_bbox(shape, (137.0, 137.0, 140.0), (-68.5, -68.5, 0.0), tol=COMPLEX_TOL) - check(len(cyl_faces(shape, radius=7.0, axis="z")) == 1, "missing central column") - check(len(cyl_faces(shape, radius=45.0, axis="z")) == 1, "missing base disk") - check(len(cyl_faces(shape, radius=62.0, axis="z")) == 20, "tread outer radius/count failed") - check(len(cyl_faces(shape, radius=10.0, axis="z")) == 20, "tread inner radius/count failed") - check(len(cyl_faces(shape, radius=1.5, axis="z")) == 20, "baluster count failed") - check(len(shape.solids()) == 43, "staircase assembly should have 43 solids") - - -def validate_10(shape): - assert_common(shape, solids=9) - assert_bbox(shape, (140.0, 140.0, 14.0), (-70.0, -70.0, -5.0), tol=COMPLEX_TOL) - check(len(cyl_faces(shape, radius=3.0, axis="z")) == 3, "planet pin count failed") - check(len(cyl_faces(shape, radius=5.0, axis="z")) == 1, "sun bore failed") - check(len(shape.faces()) >= 550 and len(shape.edges()) >= 1600, "gear tooth detail is missing") - - -VALIDATORS = [ - ("1. Rectangular calibration block with four holes", "benchmark_01_rectangular_calibration_block.step", validate_01), - ("2. Circular flange with bolt-hole pattern", "benchmark_02_circular_flange.step", validate_02), - ("3. L-bracket with gussets and two hole directions", "benchmark_03_l_bracket.step", validate_03), - ("4. Stepped shaft with keyway", "benchmark_04_stepped_shaft_keyway.step", validate_04), - ("5. Open-top electronics enclosure with bosses", "benchmark_05_open_top_electronics_enclosure.step", validate_05), - ("6. Aerospace-style clevis bracket with lightening cutouts", "benchmark_06_clevis_bracket_lightening_cutouts.step", validate_06), - ("7. Radial-engine-style cylinder with cooling fins", "benchmark_07_radial_engine_cylinder.step", validate_07), - ("8. Centrifugal impeller with backward-curved blades", "benchmark_08_centrifugal_impeller.step", validate_08), - ("9. Spiral staircase with helical handrail", "benchmark_09_spiral_staircase.step", validate_09), - ("10. Simplified planetary gear stage", "benchmark_10_planetary_gear_stage.step", validate_10), -] - - -def main() -> int: - failures = [] - for title, filename, validator in VALIDATORS: - step_path = ROOT / filename - try: - shape = import_step(step_path) - validator(shape) - except Exception as exc: - failures.append((title, exc)) - print(f"FAIL {title}: {exc}") - else: - print(f"PASS {title}") - - if failures: - print(f"\n{len(failures)} benchmark(s) failed.") - return 1 - - print("\nAll benchmark checks passed.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/models/fun/.centrifugal_impeller.step.glb b/models/fun/.centrifugal_impeller.step.glb deleted file mode 100644 index 614098e0..00000000 --- a/models/fun/.centrifugal_impeller.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe8d7d959847d34f9bd280fd606a70dbf7175ace94436b471037f068b7e3de66 -size 1426100 diff --git a/models/fun/.cutaway_turbofan_engine.step.glb b/models/fun/.cutaway_turbofan_engine.step.glb deleted file mode 100644 index c31f5302..00000000 --- a/models/fun/.cutaway_turbofan_engine.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31f2f670c14e01ff28765d3c1dcbf21cbabf653a6a87473996becd22d4eee1fb -size 7749936 diff --git a/models/fun/.electronics_enclosure_base.step.glb b/models/fun/.electronics_enclosure_base.step.glb deleted file mode 100644 index dfac51e6..00000000 --- a/models/fun/.electronics_enclosure_base.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:55cd500d838169a2a653186597e485df5b549c397702856f1ea661e61422e153 -size 450908 diff --git a/models/fun/.flying_car.step.glb b/models/fun/.flying_car.step.glb deleted file mode 100644 index ed922d49..00000000 --- a/models/fun/.flying_car.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9cce1cec333b6ad8aaa66b6864c579f71f61895a155e831fe0eb15c2167c1d2f -size 8869448 diff --git a/models/fun/.lunar_rover_corner_assembly.step.glb b/models/fun/.lunar_rover_corner_assembly.step.glb deleted file mode 100644 index 3ec44d13..00000000 --- a/models/fun/.lunar_rover_corner_assembly.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb7220bd2b56f149d6c9489a2dc296091570bbe8ce1171242b838437d0899473 -size 6807400 diff --git a/models/fun/.mechanical_iris_aperture.step.glb b/models/fun/.mechanical_iris_aperture.step.glb deleted file mode 100644 index a4b5ee41..00000000 --- a/models/fun/.mechanical_iris_aperture.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bce05c4c02e513d7e1a96ca8401dfa83995a554ec8fd3f1af8a9fa68405dc68a -size 14090228 diff --git a/models/fun/.miniature_spiral_staircase.step.glb b/models/fun/.miniature_spiral_staircase.step.glb deleted file mode 100644 index e815b9d1..00000000 --- a/models/fun/.miniature_spiral_staircase.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e941ba2be0c1d3666d18e15fa76a83fed06188c3839175004524a299d868112 -size 2852472 diff --git a/models/fun/.miniature_spiral_staircase_highres.step.glb b/models/fun/.miniature_spiral_staircase_highres.step.glb deleted file mode 100644 index 0fbabc16..00000000 --- a/models/fun/.miniature_spiral_staircase_highres.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43def8d3e6c4fa9c77a142b1857ab0b7ed907bfdaaaef7948620b0fcbda888cd -size 2667716 diff --git a/models/fun/.pelican_riding_bicycle.step.glb b/models/fun/.pelican_riding_bicycle.step.glb deleted file mode 100644 index ece1111a..00000000 --- a/models/fun/.pelican_riding_bicycle.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e3a9b4954a68c42cad40b6eb987b5a2ac3f9169a0dbf0e0d9dd7b4fc50b753b -size 5796604 diff --git a/models/fun/.photo_coffee_cup.step.glb b/models/fun/.photo_coffee_cup.step.glb deleted file mode 100644 index 5ddf50d1..00000000 --- a/models/fun/.photo_coffee_cup.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2761e2d39e49ae252cda3671f9a7feadabb4c80ee52b21d02f55a7c31c888f5 -size 10137856 diff --git a/models/fun/.planetary_gear_assembly.step.glb b/models/fun/.planetary_gear_assembly.step.glb deleted file mode 100644 index a92ecc4b..00000000 --- a/models/fun/.planetary_gear_assembly.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e5d043540ab7e015b1eb0dbcfecf364f829d834edc13fe6e0339031ae35fac5a -size 1455508 diff --git a/models/fun/.planetary_gear_assembly.step.js b/models/fun/.planetary_gear_assembly.step.js deleted file mode 100644 index 0ea82962..00000000 --- a/models/fun/.planetary_gear_assembly.step.js +++ /dev/null @@ -1,205 +0,0 @@ -const Z_AXIS = [0, 0, 1]; -const ORIGIN = [0, 0, 0]; -const PLANETS = ["planet1", "planet2", "planet3"]; -const PINS = ["pin1", "pin2", "pin3"]; -const SUN_TEETH = 24; -const PLANET_TEETH = 18; -const RING_TEETH = 60; -// Fixed-ring planetary kinematics: carrier follows the sun by Ns / (Ns + Nr). -const CARRIER_RATIO_FIXED_RING = SUN_TEETH / (SUN_TEETH + RING_TEETH); -// 3.5 sun revolutions returns the carrier to 360 deg and each gear to an equivalent tooth phase. -const FULL_MESH_CYCLE_DEG = 1260; - -let orbitGuide = null; - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function radialUnit(feature) { - const center = Array.isArray(feature?.center) ? feature.center : ORIGIN; - const length = Math.hypot(center[0] || 0, center[1] || 0); - if (length <= 1e-6) { - return [1, 0, 0]; - } - return [(center[0] || 0) / length, (center[1] || 0) / length, 0]; -} - -function scaled(vector, scale) { - return vector.map((value) => value * scale); -} - -function createOrbitGuide(THREE) { - const radius = 42; - const points = []; - for (let index = 0; index < 144; index += 1) { - const angle = (index / 144) * Math.PI * 2; - points.push(new THREE.Vector3(Math.cos(angle) * radius, Math.sin(angle) * radius, 4)); - } - const geometry = new THREE.BufferGeometry().setFromPoints(points); - const material = new THREE.LineBasicMaterial({ - color: 0x94a3b8, - transparent: true, - opacity: 0.34, - depthWrite: false - }); - const line = new THREE.LineLoop(geometry, material); - line.name = "planetary-gear-orbit-guide"; - line.renderOrder = 30; - return line; -} - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/fun/planetary_gear_assembly.step" - }, - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - carrier: { ref: "#o1.1.1", label: "Carrier plate", description: "The plate that carries planet pins around the sun axis." }, - ring: { ref: "#o1.2.1", label: "Ring gear", description: "Stationary internal gear, visually faded or hidden by controls." }, - sun: { ref: "#o1.3.1", label: "Sun gear", description: "Central driven gear." }, - planet1: { ref: "#o1.4.1", label: "Planet gear 1" }, - pin1: { ref: "#o1.5.1", label: "Planet pin 1" }, - planet2: { ref: "#o1.6.1", label: "Planet gear 2" }, - pin2: { ref: "#o1.7.1", label: "Planet pin 2" }, - planet3: { ref: "#o1.8.1", label: "Planet gear 3" }, - pin3: { ref: "#o1.9.1", label: "Planet pin 3" } - }, - parameters: { - drive: { type: "number", label: "Drive", description: "Sun gear input angle across one closed mesh cycle.", default: 0, min: 0, max: FULL_MESH_CYCLE_DEG, step: 1, unit: "deg" }, - explode: { type: "number", label: "Explode", description: "Radially separates meshing features for inspection.", default: 0, min: 0, max: 1, step: 0.01 }, - ringVisible: { type: "boolean", label: "Ring gear", default: true }, - orbitGuides: { type: "boolean", label: "Orbit guide", default: false }, - highlightMeshing: { type: "boolean", label: "Mesh highlight", default: false }, - viewMode: { - type: "select", - label: "View", - default: "mesh", - options: [ - { value: "mesh", label: "Mesh study" }, - { value: "cutaway", label: "Cutaway" }, - { value: "carrier", label: "Carrier focus" } - ] - } - }, - animations: { - meshCycle: { - label: "Mesh cycle", - duration: 6, - loop: true, - update({ cycle, set }) { - set("drive", (cycle % 1) * FULL_MESH_CYCLE_DEG); - } - }, - inspectExplode: { - label: "Explode inspect", - duration: 5, - loop: true, - update({ progress, set }) { - set("drive", progress * FULL_MESH_CYCLE_DEG); - set("explode", Math.sin(progress * Math.PI)); - } - } - } - }, - - setup({ THREE, modelGroup, cleanup }) { - if (!THREE || !modelGroup) { - return; - } - orbitGuide = createOrbitGuide(THREE); - modelGroup.add(orbitGuide); - cleanup(() => { - modelGroup.remove(orbitGuide); - orbitGuide.geometry?.dispose?.(); - orbitGuide.material?.dispose?.(); - orbitGuide = null; - }); - }, - - update({ params, features, effects, time }) { - const drive = finite(params.drive); - const explode = finite(params.explode); - const sunAngle = drive; - const carrierAngle = sunAngle * CARRIER_RATIO_FIXED_RING; - const planetSpinRelativeToCarrier = -(SUN_TEETH / PLANET_TEETH) * (sunAngle - carrierAngle); - const lift = explode * 7; - const cutaway = params.viewMode === "cutaway"; - const carrierFocus = params.viewMode === "carrier"; - const highlight = params.highlightMeshing === true && - (!time.playing || (time.progress > 0.45 && time.progress < 0.6)); - - if (orbitGuide) { - orbitGuide.visible = params.orbitGuides !== false; - orbitGuide.rotation.z = (carrierAngle * Math.PI) / 180; - } - - effects.visible("ring", params.ringVisible !== false); - effects.style("ring", { - opacity: cutaway ? 0.22 : 1, - edgeOpacity: cutaway ? 0.38 : 1 - }); - effects.style("sun", { - emissive: highlight ? "#7c2d12" : "", - emissiveIntensity: highlight ? 0.32 : 0 - }); - effects.style(PLANETS, { - emissive: highlight ? "#075985" : "", - emissiveIntensity: highlight ? 0.22 : 0 - }); - effects.style(PINS, { - opacity: 1 - }); - effects.style("carrier", { - opacity: 1, - emissive: carrierFocus ? "#14532d" : "", - emissiveIntensity: carrierFocus ? 0.2 : 0 - }); - - if (highlight) { - effects.highlight(["sun", ...PLANETS], true); - } - - effects.transform("sun", { - transforms: [ - { rotate: { axis: Z_AXIS, origin: features.sun.center, angleDeg: sunAngle } }, - { translate: [0, 0, lift] } - ] - }); - effects.transform("carrier", { - transforms: [ - { rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } }, - { translate: [0, 0, -explode * 4] } - ] - }); - - for (const planetId of PLANETS) { - const radial = radialUnit(features[planetId]); - effects.transform(planetId, { - transforms: [ - { rotate: { axis: Z_AXIS, origin: features[planetId].center, angleDeg: planetSpinRelativeToCarrier } }, - { translate: scaled(radial, explode * 16) }, - { rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } } - ] - }); - } - - for (const pinId of PINS) { - const radial = radialUnit(features[pinId]); - effects.transform(pinId, { - transforms: [ - { translate: scaled(radial, explode * 16) }, - { rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } }, - { translate: [0, 0, -explode * 4] } - ] - }); - } - } -}; diff --git a/models/fun/.robotic_hand_end_effector.step.glb b/models/fun/.robotic_hand_end_effector.step.glb deleted file mode 100644 index 80a8a4b5..00000000 --- a/models/fun/.robotic_hand_end_effector.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d00ad4f2d0b18f3c01a81dececda5d329d180040c6572c0b27515c49d0cc280 -size 17580768 diff --git a/models/fun/.six_axis_industrial_robot_arm.step.glb b/models/fun/.six_axis_industrial_robot_arm.step.glb deleted file mode 100644 index cca7bf84..00000000 --- a/models/fun/.six_axis_industrial_robot_arm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:62386fcc08e20972b115eea0d9d27384d570636fdb4d0eb3308a558d8606e738 -size 14253856 diff --git a/models/fun/.six_blade_open_propeller.step.glb b/models/fun/.six_blade_open_propeller.step.glb deleted file mode 100644 index 64798b91..00000000 --- a/models/fun/.six_blade_open_propeller.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8897a55e441af2ded632895251abf3c33a2f2a67001cd057d93802c010516816 -size 6827056 diff --git a/models/fun/.thick_mobius_strip.step.glb b/models/fun/.thick_mobius_strip.step.glb deleted file mode 100644 index 844d1d5b..00000000 --- a/models/fun/.thick_mobius_strip.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3303ff6c5d0786670cb7da8f8a29c50420ff0c6adef873e7a63628f6aa421cea -size 30854804 diff --git a/models/fun/README.md b/models/fun/README.md deleted file mode 100644 index 61586510..00000000 --- a/models/fun/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Fun Models - -Standalone generated CAD examples used for demos, visual inspection, and -occasional print/export experiments. These models are more expressive than the -benchmark fixtures and are not expected to form a systematic test suite. - -## Contents - -- `*.py`: build123d generator source for authored models. -- `*.step`: canonical generated STEP output. -- `.*.step.glb`: generated render/selector sidecars paired with STEP files. -- `.*.step.js`: optional interaction sidecars paired with STEP files. -- `*.stl`, `*.3mf`, and direct `*.glb`: durable exported/printable artifacts - when the export itself is useful as a fixture. - -Avoid adding screenshots, videos, throwaway slicer profiles, or timestamped -review captures here. diff --git a/models/fun/centrifugal_impeller.py b/models/fun/centrifugal_impeller.py deleted file mode 100644 index 6294a791..00000000 --- a/models/fun/centrifugal_impeller.py +++ /dev/null @@ -1,229 +0,0 @@ -from math import asin, cos, hypot, radians, sin, tau - -from build123d import ( - Align, - BuildPart, - BuildSketch, - Color, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - add, - extrude, - fillet, -) - - -# Units: millimeters. -# Origin: impeller axis centered at X=0, Y=0. -# XY: backplate plane. +Z: vertical impeller axis. -# Assumed rotation direction: counter-clockwise viewed from +Z, so blade tips -# trail clockwise by the specified backward sweep. - -BACKPLATE_DIAMETER = 90.0 -BACKPLATE_RADIUS = BACKPLATE_DIAMETER / 2.0 -BACKPLATE_THICKNESS = 6.0 -BACKPLATE_OUTER_FILLET_RADIUS = 1.5 - -HUB_DIAMETER = 26.0 -HUB_RADIUS = HUB_DIAMETER / 2.0 -HUB_HEIGHT_ABOVE_BACKPLATE = 22.0 - -BORE_DIAMETER = 8.0 -BORE_RADIUS = BORE_DIAMETER / 2.0 - -BLADE_COUNT = 12 -BLADE_ROOT_RADIUS = 18.0 -BLADE_TIP_RADIUS = 43.0 -BLADE_THICKNESS = 3.0 -BLADE_HEIGHT_ABOVE_BACKPLATE = 16.0 -BLADE_BACKWARD_SWEEP_ANGLE = radians(45.0) -BLADE_CURVE_SEGMENTS = 5 -BLADE_ROOT_FILLET_RADIUS = 1.0 -BLADE_ROOT_FILLET_MAX_RADIUS = 30.0 - -MODEL_LABEL = "centrifugal_impeller_12_backward_curved_blades" - - -def _polar_point(radius: float, angle: float) -> tuple[float, float]: - return (radius * cos(angle), radius * sin(angle)) - - -def _clamp_radius(point: tuple[float, float]) -> tuple[float, float]: - x_pos, y_pos = point - radius = hypot(x_pos, y_pos) - - if radius < BLADE_ROOT_RADIUS: - scale = BLADE_ROOT_RADIUS / radius - return (x_pos * scale, y_pos * scale) - if radius > BLADE_TIP_RADIUS: - scale = BLADE_TIP_RADIUS / radius - return (x_pos * scale, y_pos * scale) - return point - - -def _blade_center_point(base_angle: float, t: float) -> tuple[float, float]: - radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t - angle = base_angle - BLADE_BACKWARD_SWEEP_ANGLE * t - return _polar_point(radius, angle) - - -def _blade_normal(base_angle: float, t: float) -> tuple[float, float]: - radius = BLADE_ROOT_RADIUS + (BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS) * t - dr_dt = BLADE_TIP_RADIUS - BLADE_ROOT_RADIUS - dtheta_dt = -BLADE_BACKWARD_SWEEP_ANGLE - angle = base_angle + dtheta_dt * t - - dx_dt = dr_dt * cos(angle) - radius * sin(angle) * dtheta_dt - dy_dt = dr_dt * sin(angle) + radius * cos(angle) * dtheta_dt - length = hypot(dx_dt, dy_dt) - return (-dy_dt / length, dx_dt / length) - - -def _blade_endpoint_points(base_angle: float, t: float) -> tuple[tuple[float, float], tuple[float, float]]: - radius = BLADE_ROOT_RADIUS if t == 0.0 else BLADE_TIP_RADIUS - angle = base_angle - BLADE_BACKWARD_SWEEP_ANGLE * t - half_angle = asin((BLADE_THICKNESS / 2.0) / radius) - return ( - _polar_point(radius, angle + half_angle), - _polar_point(radius, angle - half_angle), - ) - - -def _blade_outline(base_angle: float) -> list[tuple[float, float]]: - left_side: list[tuple[float, float]] = [] - right_side: list[tuple[float, float]] = [] - half_thickness = BLADE_THICKNESS / 2.0 - - for index in range(BLADE_CURVE_SEGMENTS + 1): - t = index / BLADE_CURVE_SEGMENTS - if index == 0 or index == BLADE_CURVE_SEGMENTS: - left_point, right_point = _blade_endpoint_points(base_angle, t) - left_side.append(left_point) - right_side.append(right_point) - continue - - x_pos, y_pos = _blade_center_point(base_angle, t) - nx, ny = _blade_normal(base_angle, t) - left_side.append( - _clamp_radius((x_pos + nx * half_thickness, y_pos + ny * half_thickness)) - ) - right_side.append( - _clamp_radius((x_pos - nx * half_thickness, y_pos - ny * half_thickness)) - ) - - return left_side + list(reversed(right_side)) - - -def _make_blade(index: int): - base_angle = tau * index / BLADE_COUNT - - with BuildPart() as blade: - with BuildSketch(Plane.XY): - Polygon(_blade_outline(base_angle), align=None) - extrude(amount=BLADE_HEIGHT_ABOVE_BACKPLATE) - - part = blade.part.moved(Location((0.0, 0.0, BACKPLATE_THICKNESS))) - part.label = f"blade_{index + 1:02d}" - return part - - -def _make_backplate(): - with BuildPart() as backplate: - Cylinder( - radius=BACKPLATE_RADIUS, - height=BACKPLATE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = backplate.part - outer_edges = [] - outer_edges.extend(_edges_near_z_radius(part, 0.0, BACKPLATE_RADIUS)) - outer_edges.extend(_edges_near_z_radius(part, BACKPLATE_THICKNESS, BACKPLATE_RADIUS)) - part = fillet(outer_edges, radius=BACKPLATE_OUTER_FILLET_RADIUS) - part.label = "filleted_backplate" - return part - - -def _edge_z(edge) -> float: - return edge.bounding_box().center().Z - - -def _edge_xy_radius(edge) -> float: - center = edge.bounding_box().center() - return hypot(center.X, center.Y) - - -def _circle_radius(edge) -> float | None: - try: - return edge.radius - except ValueError: - return None - - -def _edges_near_z_radius(part, z_value: float, radius: float, tolerance: float = 0.02): - edges = [] - for edge in part.edges(): - circle_radius = _circle_radius(edge) - if circle_radius is None: - continue - if abs(_edge_z(edge) - z_value) <= tolerance and abs(circle_radius - radius) <= tolerance: - edges.append(edge) - return edges - - -def _blade_root_edges(part): - min_radius = BLADE_ROOT_RADIUS - BLADE_THICKNESS - edges = [] - - for edge in part.edges(): - if _circle_radius(edge) is not None: - continue - if abs(_edge_z(edge) - BACKPLATE_THICKNESS) > 0.02: - continue - if min_radius <= _edge_xy_radius(edge) <= BLADE_ROOT_FILLET_MAX_RADIUS: - edges.append(edge) - - return edges - - -def gen_step(): - """Return a single solid centrifugal impeller in millimeters.""" - total_height = BACKPLATE_THICKNESS + HUB_HEIGHT_ABOVE_BACKPLATE - backplate = _make_backplate() - - with BuildPart() as impeller: - add(backplate, mode=Mode.ADD) - - with Locations(Location((0.0, 0.0, BACKPLATE_THICKNESS))): - Cylinder( - radius=HUB_RADIUS, - height=HUB_HEIGHT_ABOVE_BACKPLATE, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - - for blade_index in range(BLADE_COUNT): - add(_make_blade(blade_index), mode=Mode.ADD) - - with Locations(Location((0.0, 0.0, -1.0))): - Cylinder( - radius=BORE_RADIUS, - height=total_height + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = impeller.part - - root_edges = _blade_root_edges(part) - root_edges.extend(_edges_near_z_radius(part, BACKPLATE_THICKNESS, HUB_RADIUS)) - if root_edges: - part = fillet(root_edges, radius=BLADE_ROOT_FILLET_RADIUS) - - part.label = MODEL_LABEL - part.color = Color(0.62, 0.66, 0.70, 1.0) - return part diff --git a/models/fun/centrifugal_impeller.step b/models/fun/centrifugal_impeller.step deleted file mode 100644 index 8f73951c..00000000 --- a/models/fun/centrifugal_impeller.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0ed3487ca41eecf81d556b581c5574feeb5c6a021b4b5bad0106759b7ec411fd -size 912708 diff --git a/models/fun/cutaway_turbofan_engine.py b/models/fun/cutaway_turbofan_engine.py deleted file mode 100644 index 4a54ef9c..00000000 --- a/models/fun/cutaway_turbofan_engine.py +++ /dev/null @@ -1,646 +0,0 @@ -from __future__ import annotations - -from math import atan2, cos, degrees, hypot, radians, sin - -from build123d import ( - Align, - Axis, - BuildPart, - BuildSketch, - Color, - Compound, - Cone, - Cylinder, - Face, - Location, - Locations, - Mode, - Plane, - Polygon, - RectangleRounded, - Torus, - Wire, - add, - extrude, - fillet, - loft, -) - - -DISPLAY_NAME = "Cutaway miniature turbofan jet engine" - -# Units: millimeters. -# Origin: front intake center at X=0 on the engine axis. -# Engine axis: +X, centered through Y=0, Z=0. - -MODEL_LABEL = "cutaway_miniature_turbofan_engine" - -CUTAWAY_START_DEG = -5.0 -CUTAWAY_END_DEG = 95.0 -VISIBLE_SECTOR_START_DEG = CUTAWAY_END_DEG -VISIBLE_SECTOR_END_DEG = 360.0 + CUTAWAY_START_DEG - -NACELLE_LENGTH = 180.0 -NACELLE_OUTER_RADIUS = 55.0 -NACELLE_INNER_RADIUS = 48.0 - -BLADE_ROOT_OVERLAP = 1.5 -STATOR_TIP_OVERLAP = 1.0 -STRUT_OVERLAP = 0.8 - -BASE_CENTER_X = 90.0 -BASE_LENGTH = 140.0 -BASE_WIDTH = 70.0 -BASE_THICKNESS = 8.0 -BASE_TOP_Z = -110.0 -BASE_BOTTOM_Z = BASE_TOP_Z - BASE_THICKNESS - - -def _polar_yz(radius: float, angle_rad: float) -> tuple[float, float]: - return (radius * cos(angle_rad), radius * sin(angle_rad)) - - -def _point_yz(x_pos: float, radius: float, angle_rad: float) -> tuple[float, float, float]: - y_pos, z_pos = _polar_yz(radius, angle_rad) - return (x_pos, y_pos, z_pos) - - -def _annular_sector_points( - *, - outer_radius: float, - inner_radius: float, - start_deg: float = VISIBLE_SECTOR_START_DEG, - end_deg: float = VISIBLE_SECTOR_END_DEG, - segments: int = 96, -) -> list[tuple[float, float]]: - outer_points = [] - inner_points = [] - for index in range(segments + 1): - angle = radians(start_deg + (end_deg - start_deg) * index / segments) - outer_points.append(_polar_yz(outer_radius, angle)) - inner_points.append(_polar_yz(inner_radius, angle)) - return outer_points + list(reversed(inner_points)) - - -def _annular_sector_face( - *, - x_pos: float, - outer_radius: float, - inner_radius: float, - start_deg: float = VISIBLE_SECTOR_START_DEG, - end_deg: float = VISIBLE_SECTOR_END_DEG, - segments: int = 72, -) -> Face: - points = [] - for index in range(segments + 1): - angle = radians(start_deg + (end_deg - start_deg) * index / segments) - points.append(_point_yz(x_pos, outer_radius, angle)) - for index in range(segments, -1, -1): - angle = radians(start_deg + (end_deg - start_deg) * index / segments) - points.append(_point_yz(x_pos, inner_radius, angle)) - return Face.make_surface(Wire.make_polygon(points, close=True)) - - -def _yz_angle_degrees(y_pos: float, z_pos: float) -> float: - return degrees(atan2(z_pos, y_pos)) % 360.0 - - -def _angle_delta_deg(a: float, b: float) -> float: - return abs((a - b + 180.0) % 360.0 - 180.0) - - -def _in_cutaway(angle_deg: float) -> bool: - angle = angle_deg % 360.0 - start = CUTAWAY_START_DEG % 360.0 - end = CUTAWAY_END_DEG % 360.0 - if start > end: - return angle >= start or angle <= end - return start <= angle <= end - - -def _safe_fillet(part, edges, radius: float): - if not edges: - return part - try: - return fillet(edges, radius=radius) - except Exception: - return part - - -def _label(part, label: str, color: Color | None = None): - part.label = label - if color is not None: - part.color = color - return part - - -def _make_compound(label: str, parts: list, color: Color | None = None) -> Compound: - compound = Compound(obj=parts, children=parts, label=label) - if color is not None: - compound.color = color - return compound - - -def _make_cylinder_x( - *, - label: str, - x_start: float, - x_end: float, - radius: float, - color: Color, -) -> object: - with BuildPart() as cylinder_part: - with Locations(Location(((x_start + x_end) / 2.0, 0.0, 0.0))): - Cylinder( - radius=radius, - height=x_end - x_start, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - return _label(cylinder_part.part, label, color) - - -def _make_annular_sector( - *, - label: str, - x_start: float, - x_end: float, - outer_radius: float, - inner_radius: float, - color: Color, - fillet_cut_edges: float | None = None, -) -> object: - with BuildPart() as sector: - with BuildSketch(Plane.YZ): - Polygon( - _annular_sector_points( - outer_radius=outer_radius, - inner_radius=inner_radius, - ), - align=None, - ) - extrude(amount=x_end - x_start) - - part = sector.part.moved(Location((x_start, 0.0, 0.0))) - - if fillet_cut_edges is not None: - cut_edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - if bbox.size.X < (x_end - x_start) * 0.85: - continue - angle = _yz_angle_degrees(center.Y, center.Z) - radius = hypot(center.Y, center.Z) - if ( - min(inner_radius, outer_radius) - 1.0 - <= radius - <= max(inner_radius, outer_radius) + 1.0 - and ( - _angle_delta_deg(angle, CUTAWAY_START_DEG) < 1.0 - or _angle_delta_deg(angle, CUTAWAY_END_DEG) < 1.0 - ) - ): - cut_edges.append(edge) - part = _safe_fillet(part, cut_edges, fillet_cut_edges) - - return _label(part, label, color) - - -def _make_tapered_annular_sector( - *, - label: str, - x_start: float, - x_end: float, - outer_start_radius: float, - inner_start_radius: float, - outer_end_radius: float, - inner_end_radius: float, - color: Color, -) -> object: - part = loft( - [ - _annular_sector_face( - x_pos=x_start, - outer_radius=outer_start_radius, - inner_radius=inner_start_radius, - ), - _annular_sector_face( - x_pos=x_end, - outer_radius=outer_end_radius, - inner_radius=inner_end_radius, - ), - ] - ) - return _label(part, label, color) - - -def _make_intake_lip() -> object: - with BuildPart() as lip: - with Locations(Location((5.0, 0.0, 0.0))): - Torus( - major_radius=50.0, - minor_radius=5.0, - major_angle=260.0, - rotation=(0.0, 90.0, 0.0), - ) - part = lip.part.rotate(Axis.X, -175.0) - return _label(part, "rounded_intake_lip_5mm_radius", Color(0.74, 0.77, 0.78, 1.0)) - - -def _make_nacelle_shell() -> Compound: - parts = [ - _make_annular_sector( - label="nacelle_hollow_duct_100deg_cutaway", - x_start=0.0, - x_end=NACELLE_LENGTH, - outer_radius=NACELLE_OUTER_RADIUS, - inner_radius=NACELLE_INNER_RADIUS, - color=Color(0.73, 0.76, 0.78, 1.0), - fillet_cut_edges=2.0, - ), - _make_intake_lip(), - _make_tapered_annular_sector( - label="rear_exhaust_ring_od92_id74", - x_start=160.0, - x_end=180.0, - outer_start_radius=48.0, - inner_start_radius=39.0, - outer_end_radius=46.0, - inner_end_radius=37.0, - color=Color(0.62, 0.65, 0.67, 1.0), - ), - ] - return _make_compound("nacelle_shell", parts) - - -def _normalized(vector: tuple[float, float, float]) -> tuple[float, float, float]: - length = hypot(hypot(vector[0], vector[1]), vector[2]) - return (vector[0] / length, vector[1] / length, vector[2] / length) - - -def _scale(vector: tuple[float, float, float], amount: float) -> tuple[float, float, float]: - return (vector[0] * amount, vector[1] * amount, vector[2] * amount) - - -def _add_vec( - a: tuple[float, float, float], - b: tuple[float, float, float], -) -> tuple[float, float, float]: - return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) - - -def _section_face( - *, - center: tuple[float, float, float], - chord_direction: tuple[float, float, float], - thickness_direction: tuple[float, float, float], - chord: float, - thickness: float, -) -> Face: - chord_half = _scale(chord_direction, chord / 2.0) - thickness_half = _scale(thickness_direction, thickness / 2.0) - points = [ - _add_vec(_add_vec(center, chord_half), thickness_half), - _add_vec(_add_vec(center, _scale(chord_half, -1.0)), thickness_half), - _add_vec(_add_vec(center, _scale(chord_half, -1.0)), _scale(thickness_half, -1.0)), - _add_vec(_add_vec(center, chord_half), _scale(thickness_half, -1.0)), - ] - return Face.make_surface(Wire.make_polygon(points, close=True)) - - -def _make_radial_blade( - *, - label: str, - x_center: float, - base_angle: float, - root_radius: float, - tip_radius: float, - chord: float, - thickness: float, - sweep_deg: float, - twist_root_deg: float, - twist_tip_deg: float, - color: Color, - attachment_radius: float | None = None, - axial_sweep: float = 0.0, -) -> object: - span = tip_radius - root_radius - stations = [] - if attachment_radius is not None and attachment_radius < root_radius: - stations.append((attachment_radius, 0.0, 0.52, 1.08)) - stations.extend( - [ - (root_radius, 0.0, 1.00, 1.00), - (root_radius + span * 0.38, 0.38, 1.04, 0.92), - (root_radius + span * 0.72, 0.72, 0.96, 0.84), - (tip_radius, 1.0, 0.78, 0.68), - ] - ) - - faces = [] - for radius, t, chord_scale, thickness_scale in stations: - angle = base_angle + radians(sweep_deg) * (t**1.25) - twist = radians(twist_root_deg + (twist_tip_deg - twist_root_deg) * t) - tangent = (0.0, -sin(angle), cos(angle)) - x_axis = (1.0, 0.0, 0.0) - chord_direction = _normalized( - ( - cos(twist) * x_axis[0] + sin(twist) * tangent[0], - cos(twist) * x_axis[1] + sin(twist) * tangent[1], - cos(twist) * x_axis[2] + sin(twist) * tangent[2], - ) - ) - thickness_direction = _normalized( - ( - -sin(twist) * x_axis[0] + cos(twist) * tangent[0], - -sin(twist) * x_axis[1] + cos(twist) * tangent[1], - -sin(twist) * x_axis[2] + cos(twist) * tangent[2], - ) - ) - center = ( - x_center + axial_sweep * (t - 0.5), - radius * cos(angle), - radius * sin(angle), - ) - faces.append( - _section_face( - center=center, - chord_direction=chord_direction, - thickness_direction=thickness_direction, - chord=chord * chord_scale, - thickness=thickness * thickness_scale, - ) - ) - - part = loft(faces, ruled=False) - return _label(part, label, color) - - -def _make_fan_rotor() -> Compound: - parts = [ - _make_cylinder_x( - label="fan_hub_d26_l18", - x_start=19.0, - x_end=37.0, - radius=13.0, - color=Color(0.31, 0.38, 0.43, 1.0), - ) - ] - for index in range(24): - parts.append( - _make_radial_blade( - label=f"fan_blade_{index + 1:02d}", - x_center=28.0, - base_angle=radians(index * 360.0 / 24.0), - root_radius=16.0, - tip_radius=45.0, - chord=18.0, - thickness=2.0, - sweep_deg=24.0, - twist_root_deg=-20.0, - twist_tip_deg=15.0, - attachment_radius=13.0 - BLADE_ROOT_OVERLAP, - axial_sweep=3.0, - color=Color(0.77, 0.82, 0.86, 1.0), - ) - ) - return _make_compound("fan_rotor", parts) - - -def _make_compressor_rotor() -> Compound: - parts = [] - stage_centers = [57.0, 69.0, 81.0, 93.0] - for stage_index, x_center in enumerate(stage_centers): - parts.append( - _make_cylinder_x( - label=f"compressor_stage_{stage_index + 1}_disk_d42", - x_start=x_center - 2.0, - x_end=x_center + 2.0, - radius=21.0, - color=Color(0.45, 0.50, 0.54, 1.0), - ) - ) - phase = radians(stage_index * 10.0) - stage_angle_offset = -4.0 if stage_index % 2 else 4.0 - for blade_index in range(18): - parts.append( - _make_radial_blade( - label=f"compressor_s{stage_index + 1}_blade_{blade_index + 1:02d}", - x_center=x_center, - base_angle=phase + radians(blade_index * 360.0 / 18.0), - root_radius=22.0, - tip_radius=34.0, - chord=8.0, - thickness=1.2, - sweep_deg=8.0 + stage_angle_offset, - twist_root_deg=26.0 + stage_angle_offset, - twist_tip_deg=12.0 + stage_angle_offset, - attachment_radius=21.0 - BLADE_ROOT_OVERLAP, - axial_sweep=1.2, - color=Color(0.69, 0.73, 0.76, 1.0), - ) - ) - return _make_compound("compressor_rotor_four_stages", parts) - - -def _make_stator_vanes() -> Compound: - parts = [] - for ring_index, x_center in enumerate([63.0, 75.0, 87.0]): - phase = radians(360.0 / 32.0 + ring_index * 6.0) - for vane_index in range(16): - parts.append( - _make_radial_blade( - label=f"stator_r{ring_index + 1}_vane_{vane_index + 1:02d}", - x_center=x_center, - base_angle=phase + radians(vane_index * 360.0 / 16.0), - root_radius=36.0, - tip_radius=NACELLE_INNER_RADIUS + STATOR_TIP_OVERLAP, - chord=7.5, - thickness=1.0, - sweep_deg=-7.0, - twist_root_deg=-30.0, - twist_tip_deg=-18.0, - axial_sweep=-0.8, - color=Color(0.79, 0.65, 0.37, 1.0), - ) - ) - return _make_compound("stator_vanes_three_rings", parts) - - -def _make_turbine_rotor() -> Compound: - parts = [] - for stage_index, x_center in enumerate([128.0, 145.0]): - parts.append( - _make_cylinder_x( - label=f"turbine_stage_{stage_index + 1}_disk_d46", - x_start=x_center - 2.5, - x_end=x_center + 2.5, - radius=23.0, - color=Color(0.49, 0.45, 0.41, 1.0), - ) - ) - phase = radians(stage_index * 12.0) - for blade_index in range(22): - parts.append( - _make_radial_blade( - label=f"turbine_s{stage_index + 1}_blade_{blade_index + 1:02d}", - x_center=x_center, - base_angle=phase + radians(blade_index * 360.0 / 22.0), - root_radius=24.0, - tip_radius=40.0, - chord=12.0, - thickness=2.0, - sweep_deg=-34.0, - twist_root_deg=-44.0, - twist_tip_deg=-8.0, - attachment_radius=23.0 - BLADE_ROOT_OVERLAP, - axial_sweep=2.2, - color=Color(0.74, 0.59, 0.42, 1.0), - ) - ) - return _make_compound("turbine_rotor_two_stages", parts) - - -def _make_exhaust_cone() -> object: - part = loft( - [ - Face.make_surface( - Wire.make_circle( - 18.0, - plane=Plane((145.0, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0)), - ) - ), - Face.make_surface( - Wire.make_circle( - 7.0, - plane=Plane((178.0, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0)), - ) - ), - ] - ) - return _label(part, "exhaust_cone_taper_d36_to_d14", Color(0.38, 0.40, 0.42, 1.0)) - - -def _make_support_strut(index: int, angle_deg: float, *, trimmed: bool = False) -> object: - angle = radians(angle_deg) - inner_radius = 4.0 - STRUT_OVERLAP - outer_radius = 31.0 if trimmed else NACELLE_INNER_RADIUS + STRUT_OVERLAP - radial_direction = (0.0, cos(angle), sin(angle)) - tangent_direction = (0.0, -sin(angle), cos(angle)) - x_direction = (1.0, 0.0, 0.0) - faces = [] - for radius in (inner_radius, outer_radius): - center = (110.0, radius * radial_direction[1], radius * radial_direction[2]) - faces.append( - _section_face( - center=center, - chord_direction=x_direction, - thickness_direction=tangent_direction, - chord=5.0, - thickness=4.0, - ) - ) - part = loft(faces, ruled=True) - suffix = "_trimmed" if trimmed else "" - return _label(part, f"support_strut_{index}_{int(angle_deg)}deg{suffix}", Color(0.33, 0.36, 0.38, 1.0)) - - -def _make_support_struts() -> Compound: - parts = [] - for index, angle_deg in enumerate([5.0, 95.0, 185.0, 275.0], start=1): - trimmed = _in_cutaway(angle_deg) and _angle_delta_deg(angle_deg, CUTAWAY_END_DEG) > 0.5 - parts.append(_make_support_strut(index, angle_deg, trimmed=trimmed)) - return _make_compound("support_struts_cutaway_trimmed", parts) - - -def _make_mounting_pylon() -> object: - def pylon_face(z_pos: float, x_width: float, y_width: float, x_center: float) -> Face: - points = [ - (x_center - x_width / 2.0, 0.0, z_pos), - (x_center - x_width * 0.18, y_width / 2.0, z_pos), - (x_center + x_width * 0.35, y_width / 2.0, z_pos), - (x_center + x_width / 2.0, 0.0, z_pos), - (x_center + x_width * 0.35, -y_width / 2.0, z_pos), - (x_center - x_width * 0.18, -y_width / 2.0, z_pos), - ] - return Face.make_surface(Wire.make_polygon(points, close=True)) - - part = loft( - [ - pylon_face(BASE_TOP_Z, 22.0, 10.0, 90.0), - pylon_face(-82.0, 19.0, 9.0, 90.0), - pylon_face(-55.0, 18.0, 8.0, 90.0), - ], - ruled=False, - ) - return _label(part, "mounting_pylon_swept_aero_support", Color(0.46, 0.49, 0.50, 1.0)) - - -def _make_display_base() -> object: - with BuildPart() as base: - with BuildSketch(Plane.XY): - RectangleRounded( - BASE_LENGTH, - BASE_WIDTH, - 5.0, - align=(Align.CENTER, Align.CENTER), - ) - extrude(amount=BASE_THICKNESS) - - for x_offset in (-50.0, 50.0): - for y_pos in (-22.0, 22.0): - x_pos = BASE_CENTER_X + x_offset - with Locations(Location((x_pos - BASE_CENTER_X, y_pos, -0.5))): - Cylinder( - radius=3.0, - height=BASE_THICKNESS + 1.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - with Locations(Location((x_pos - BASE_CENTER_X, y_pos, BASE_THICKNESS))): - Cone( - bottom_radius=3.0, - top_radius=5.5, - height=2.6, - align=(Align.CENTER, Align.CENTER, Align.MAX), - mode=Mode.SUBTRACT, - ) - - part = base.part.moved(Location((BASE_CENTER_X, 0.0, BASE_BOTTOM_Z))) - outer_edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - if abs(center.Z - BASE_BOTTOM_Z) > 0.03 and abs(center.Z - BASE_TOP_Z) > 0.03: - continue - if ( - abs(center.X - BASE_CENTER_X) > BASE_LENGTH / 2.0 - 6.0 - or abs(center.Y) > BASE_WIDTH / 2.0 - 6.0 - ): - outer_edges.append(edge) - part = _safe_fillet(part, outer_edges, 3.0) - return _label(part, "display_base_rounded_rectangle_countersunk_holes", Color(0.22, 0.24, 0.25, 1.0)) - - -def gen_step(): - """Return a labeled multi-body cutaway turbofan display model.""" - bodies = [ - _make_nacelle_shell(), - _make_fan_rotor(), - _make_compressor_rotor(), - _make_turbine_rotor(), - _make_cylinder_x( - label="central_shaft_d8_x15_to_x165", - x_start=15.0, - x_end=165.0, - radius=4.0, - color=Color(0.18, 0.20, 0.22, 1.0), - ), - _make_stator_vanes(), - _make_exhaust_cone(), - _make_support_struts(), - _make_mounting_pylon(), - _make_display_base(), - ] - return _make_compound(MODEL_LABEL, bodies) diff --git a/models/fun/cutaway_turbofan_engine.step b/models/fun/cutaway_turbofan_engine.step deleted file mode 100644 index 572476db..00000000 --- a/models/fun/cutaway_turbofan_engine.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cce167a538f3705ee7eaad5d1f9c5ffb143abfb6969d51e2d890a05aa1e5a046 -size 6989731 diff --git a/models/fun/electronics_enclosure_base.py b/models/fun/electronics_enclosure_base.py deleted file mode 100644 index 8b55a69e..00000000 --- a/models/fun/electronics_enclosure_base.py +++ /dev/null @@ -1,79 +0,0 @@ -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Cylinder, - Location, - Locations, - Mode, - Plane, - RectangleRounded, - extrude, -) - - -OUTER_LENGTH = 100.0 -OUTER_WIDTH = 70.0 -OUTER_HEIGHT = 30.0 -WALL_THICKNESS = 3.0 -FLOOR_THICKNESS = 3.0 -OUTER_CORNER_FILLET_RADIUS = 2.0 - -STANDOFF_DIAMETER = 10.0 -STANDOFF_HEIGHT = 12.0 -STANDOFF_HOLE_DIAMETER = 3.0 -STANDOFF_HOLE_DEPTH = 8.0 -STANDOFF_LOCATIONS = ( - (-35.0, -25.0), - (-35.0, 25.0), - (35.0, -25.0), - (35.0, 25.0), -) - - -def gen_step(): - """Return a single solid open-top electronics enclosure base in millimeters.""" - inner_length = OUTER_LENGTH - 2.0 * WALL_THICKNESS - inner_width = OUTER_WIDTH - 2.0 * WALL_THICKNESS - cavity_height = OUTER_HEIGHT - FLOOR_THICKNESS - standoff_top_z = FLOOR_THICKNESS + STANDOFF_HEIGHT - - with BuildPart() as enclosure: - with BuildSketch(Plane.XY): - RectangleRounded( - OUTER_LENGTH, - OUTER_WIDTH, - OUTER_CORNER_FILLET_RADIUS, - align=(Align.CENTER, Align.CENTER), - ) - extrude(amount=OUTER_HEIGHT) - - with Locations(Location((0.0, 0.0, FLOOR_THICKNESS))): - Box( - inner_length, - inner_width, - cavity_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - for x_pos, y_pos in STANDOFF_LOCATIONS: - with Locations(Location((x_pos, y_pos, FLOOR_THICKNESS))): - Cylinder( - radius=STANDOFF_DIAMETER / 2.0, - height=STANDOFF_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.ADD, - ) - with Locations(Location((x_pos, y_pos, standoff_top_z))): - Cylinder( - radius=STANDOFF_HOLE_DIAMETER / 2.0, - height=STANDOFF_HOLE_DEPTH, - align=(Align.CENTER, Align.CENTER, Align.MAX), - mode=Mode.SUBTRACT, - ) - - part = enclosure.part - part.label = "electronics_enclosure_base" - return part diff --git a/models/fun/electronics_enclosure_base.step b/models/fun/electronics_enclosure_base.step deleted file mode 100644 index 59d04dd4..00000000 --- a/models/fun/electronics_enclosure_base.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ab59a0a1fa48601e2e4260557040dc83b1b9d7c09795a7788c1b2c6c54378f9 -size 78773 diff --git a/models/fun/flying_car.py b/models/fun/flying_car.py deleted file mode 100644 index ac2d777b..00000000 --- a/models/fun/flying_car.py +++ /dev/null @@ -1,426 +0,0 @@ -from __future__ import annotations - -import math -from collections.abc import Sequence - -from build123d import ( - Box, - Color, - Compound, - Cylinder, - Face, - Location, - Plane, - Polygon, - Shape, - Torus, - Wire, - BuildPart, - BuildSketch, - chamfer, - extrude, - fillet, - loft, -) -from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder -from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt - - -DISPLAY_NAME = "Roadable VTOL flying car concept" - -# Units: millimeters. -# Origin: center of the road car cabin footprint. -# +X: forward/nose, +Y: driver-left/port side, +Z: up. - -MODEL_LABEL = "roadable_vtol_flying_car_concept" - -COLORS = { - "pearl_body": (0.82, 0.86, 0.84, 1.0), - "graphite": (0.03, 0.035, 0.04, 1.0), - "dark_glass": (0.08, 0.16, 0.24, 0.58), - "warm_lens": (1.0, 0.74, 0.28, 1.0), - "red_lens": (0.95, 0.08, 0.05, 1.0), - "titanium": (0.52, 0.56, 0.58, 1.0), - "brushed_aluminum": (0.72, 0.74, 0.73, 1.0), - "rotor_blue": (0.24, 0.42, 0.56, 1.0), - "safety_orange": (1.0, 0.35, 0.06, 1.0), - "rubber": (0.005, 0.006, 0.007, 1.0), -} - -Vector3 = tuple[float, float, float] - -BODY_STATIONS = [ - (-182.0, 48.0, 24.0, 43.0), - (-146.0, 82.0, 39.0, 42.0), - (-78.0, 102.0, 50.0, 42.0), - (20.0, 104.0, 54.0, 43.0), - (105.0, 82.0, 44.0, 43.0), - (162.0, 42.0, 28.0, 43.0), - (183.0, 10.0, 10.0, 43.0), -] - -CANOPY_STATIONS = [ - (-62.0, 45.0, 10.0, 65.0), - (-22.0, 56.0, 24.0, 72.0), - (32.0, 52.0, 25.0, 72.0), - (74.0, 30.0, 10.0, 65.0), -] - -DUCT_OUTER_RADIUS = 48.0 -DUCT_INNER_RADIUS = 36.0 -DUCT_HEIGHT = 12.0 -DUCT_Z = 58.0 -DUCT_FRONT_X = 112.0 -DUCT_REAR_X = -114.0 -DUCT_CENTER_Y = 112.0 -ROTOR_Z = DUCT_Z + 0.9 - - -def style(shape: Shape, label: str, color_name: str) -> Shape: - shape.label = label - shape.color = Color(*COLORS[color_name]) - return shape - - -def group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound: - compound = Compound(obj=list(children), children=list(children), label=label) - if color_name is not None: - compound.color = Color(*COLORS[color_name]) - return compound - - -def softened(shape: Shape, radius: float) -> Shape: - if radius <= 0.0: - return shape - try: - return fillet(shape.edges(), radius=radius) - except Exception: - try: - return chamfer(shape.edges(), length=radius * 0.35) - except Exception: - return shape - - -def superellipse_section( - x_pos: float, - width: float, - height: float, - z_center: float, - *, - exponent: float = 2.65, - segments: int = 40, -) -> Face: - points: list[Vector3] = [] - half_width = width / 2.0 - half_height = height / 2.0 - for index in range(segments): - angle = 2.0 * math.pi * index / segments - y_pos = math.copysign(abs(math.cos(angle)) ** (2.0 / exponent), math.cos(angle)) * half_width - z_pos = z_center + math.copysign(abs(math.sin(angle)) ** (2.0 / exponent), math.sin(angle)) * half_height - points.append((x_pos, y_pos, z_pos)) - return Face.make_surface(Wire.make_polygon(points, close=True)) - - -def lofted_superellipse(label: str, stations: Sequence[tuple[float, float, float, float]], color_name: str) -> Shape: - faces = [superellipse_section(*station) for station in stations] - return style(softened(loft(faces, ruled=False), 0.75), label, color_name) - - -def cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length, rotation=(0.0, 90.0, 0.0)).moved(Location(center)), label, color_name) - - -def cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length, rotation=(90.0, 0.0, 0.0)).moved(Location(center)), label, color_name) - - -def cylinder_z(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length).moved(Location(center)), label, color_name) - - -def cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape: - axis = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]) - length = math.sqrt(axis[0] ** 2 + axis[1] ** 2 + axis[2] ** 2) - direction = (axis[0] / length, axis[1] / length, axis[2] / length) - wrapped = BRepPrimAPI_MakeCylinder( - gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)), - radius, - length, - ).Shape() - return style(Shape(obj=wrapped), label, color_name) - - -def annular_z(label: str, center: Vector3, outer_radius: float, inner_radius: float, height: float, color_name: str) -> Shape: - outer = Cylinder(outer_radius, height).moved(Location(center)) - inner = Cylinder(inner_radius, height + 3.0).moved(Location(center)) - return style(softened(outer - inner, 0.65), label, color_name) - - -def annular_x(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape: - outer = Cylinder(outer_radius, length, rotation=(0.0, 90.0, 0.0)).moved(Location(center)) - inner = Cylinder(inner_radius, length + 3.0, rotation=(0.0, 90.0, 0.0)).moved(Location(center)) - return style(softened(outer - inner, 0.65), label, color_name) - - -def annular_y(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape: - outer = Cylinder(outer_radius, length, rotation=(90.0, 0.0, 0.0)).moved(Location(center)) - inner = Cylinder(inner_radius, length + 3.0, rotation=(90.0, 0.0, 0.0)).moved(Location(center)) - return style(softened(outer - inner, 0.55), label, color_name) - - -def prism_from_xz(label: str, y_center: float, points_xz: Sequence[tuple[float, float]], thickness: float, color_name: str) -> Shape: - faces = [] - for y_pos in (y_center - thickness / 2.0, y_center + thickness / 2.0): - points = [(x_pos, y_pos, z_pos) for x_pos, z_pos in points_xz] - faces.append(Face.make_surface(Wire.make_polygon(points, close=True))) - return style(softened(loft(faces, ruled=True), 0.45), label, color_name) - - -def prism_from_yz(label: str, x_center: float, points_yz: Sequence[tuple[float, float]], thickness: float, color_name: str) -> Shape: - faces = [] - for x_pos in (x_center - thickness / 2.0, x_center + thickness / 2.0): - points = [(x_pos, y_pos, z_pos) for y_pos, z_pos in points_yz] - faces.append(Face.make_surface(Wire.make_polygon(points, close=True))) - return style(softened(loft(faces, ruled=True), 0.35), label, color_name) - - -def body_shell() -> Compound: - fuselage = lofted_superellipse("one_piece_aero_road_car_fuselage", BODY_STATIONS, "pearl_body") - belly = style( - softened(Box(225.0, 42.0, 8.0).moved(Location((-18.0, 0.0, 18.0))), 1.2), - "flat_structural_battery_spine", - "graphite", - ) - canopy = lofted_superellipse("teardrop_panoramic_canopy", CANOPY_STATIONS, "dark_glass") - - splitter = style( - softened(Box(42.0, 54.0, 3.6).moved(Location((172.0, 0.0, 32.0))), 0.8), - "front_aero_splitter_under_nose", - "graphite", - ) - rear_diffuser = style( - softened(Box(58.0, 68.0, 5.0).moved(Location((-167.0, 0.0, 27.0))), 0.8), - "rear_diffuser_plane", - "graphite", - ) - - lights = [ - cylinder_x("left_warm_headlamp", (184.0, 17.5, 43.5), 4.2, 3.4, "warm_lens"), - cylinder_x("right_warm_headlamp", (184.0, -17.5, 43.5), 4.2, 3.4, "warm_lens"), - cylinder_x("left_rear_marker_lamp", (-184.0, 28.0, 42.0), 3.2, 3.4, "red_lens"), - cylinder_x("right_rear_marker_lamp", (-184.0, -28.0, 42.0), 3.2, 3.4, "red_lens"), - ] - - return group( - "aero_car_body", - [fuselage, belly, canopy, splitter, rear_diffuser, *lights], - "pearl_body", - ) - - -def wing_panel(label: str, side: float) -> Shape: - base_points = [ - (74.0, 43.0), - (96.0, 138.0), - (-92.0, 128.0), - (-66.0, 43.0), - ] - points = [(x_pos, side * y_pos) for x_pos, y_pos in base_points] - if side < 0: - points = list(reversed(points)) - with BuildPart() as wing: - with BuildSketch(Plane.XY): - Polygon(points, align=None) - extrude(amount=5.2) - panel = wing.part.moved(Location((0.0, 0.0, 51.5))) - return style(softened(panel, 0.7), label, "brushed_aluminum") - - -def stabilizers() -> Compound: - fin_profile = [(-174.0, 54.0), (-129.0, 55.0), (-114.0, 108.0), (-166.0, 91.0)] - return group( - "twin_rear_vertical_stabilizers", - [ - prism_from_xz("port_vertical_tail_fin", 36.0, fin_profile, 5.0, "safety_orange"), - prism_from_xz("starboard_vertical_tail_fin", -36.0, fin_profile, 5.0, "safety_orange"), - ], - "safety_orange", - ) - - -def lifting_wings() -> Compound: - center_fairing = style( - softened(Box(120.0, 96.0, 7.0).moved(Location((-2.0, 0.0, 53.0))), 1.0), - "flush_wing_centerbox_between_ducts", - "brushed_aluminum", - ) - return group( - "folding_lifting_wing_set", - [ - center_fairing, - wing_panel("port_deployable_lifting_wing", 1.0), - wing_panel("starboard_deployable_lifting_wing", -1.0), - stabilizers(), - ], - "brushed_aluminum", - ) - - -def rotor_blade_z(label: str, center: Vector3, angle_deg: float, color_name: str) -> Shape: - root_radius = 9.0 - tip_radius = DUCT_INNER_RADIUS - 4.0 - root_width = 9.0 - tip_width = 4.4 - angle = math.radians(angle_deg) - radial = (math.cos(angle), math.sin(angle)) - tangent = (-math.sin(angle), math.cos(angle)) - - corners = [] - for distance, half_width in ( - (root_radius, root_width / 2.0), - (tip_radius, tip_width / 2.0), - (tip_radius, -tip_width / 2.0), - (root_radius, -root_width / 2.0), - ): - corners.append( - ( - center[0] + radial[0] * distance + tangent[0] * half_width, - center[1] + radial[1] * distance + tangent[1] * half_width, - ) - ) - with BuildPart() as blade: - with BuildSketch(Plane.XY): - Polygon(corners, align=None) - extrude(amount=1.8) - return style(blade.part.moved(Location((0.0, 0.0, center[2] - 0.9))), label, color_name) - - -def lift_duct_module(label: str, center: Vector3, front: bool, side: float) -> Compound: - duct_label = "front" if front else "rear" - side_label = "port" if side > 0 else "starboard" - parts: list[Shape] = [ - annular_z(f"{side_label}_{duct_label}_protective_lift_duct_ring", center, DUCT_OUTER_RADIUS, DUCT_INNER_RADIUS, DUCT_HEIGHT, "graphite"), - style( - Torus(DUCT_OUTER_RADIUS - 2.4, 1.3).moved(Location((center[0], center[1], center[2] + DUCT_HEIGHT / 2.0))), - f"{side_label}_{duct_label}_upper_guard_lip", - "titanium", - ), - cylinder_z(f"{side_label}_{duct_label}_rotor_hub", (center[0], center[1], ROTOR_Z), 8.5, 6.0, "titanium"), - ] - - for index in range(6): - parts.append( - rotor_blade_z( - f"{side_label}_{duct_label}_lift_rotor_blade_{index + 1}", - (center[0], center[1], ROTOR_Z + 1.6), - index * 60.0 + (15.0 if front else -15.0), - "rotor_blue", - ) - ) - - for index, angle_deg in enumerate((0.0, 90.0, 180.0, 270.0), start=1): - angle = math.radians(angle_deg) - p1 = (center[0], center[1], center[2]) - p2 = ( - center[0] + math.cos(angle) * (DUCT_INNER_RADIUS - 2.0), - center[1] + math.sin(angle) * (DUCT_INNER_RADIUS - 2.0), - center[2], - ) - parts.append(cylinder_between(f"{side_label}_{duct_label}_duct_cross_strut_{index}", p1, p2, 1.8, "titanium")) - - body_attach = (center[0] + (-31.0 if front else 31.0), side * 50.0, center[2] - 3.0) - parts.append(cylinder_between(f"{side_label}_{duct_label}_faired_duct_boom", body_attach, center, 4.8, "brushed_aluminum")) - return group(label, parts, "graphite") - - -def lift_system() -> Compound: - duct_centers = [ - ("port_front_lift_fan_module", (DUCT_FRONT_X, DUCT_CENTER_Y, DUCT_Z), True, 1.0), - ("starboard_front_lift_fan_module", (DUCT_FRONT_X, -DUCT_CENTER_Y, DUCT_Z), True, -1.0), - ("port_rear_lift_fan_module", (DUCT_REAR_X, DUCT_CENTER_Y, DUCT_Z), False, 1.0), - ("starboard_rear_lift_fan_module", (DUCT_REAR_X, -DUCT_CENTER_Y, DUCT_Z), False, -1.0), - ] - return group( - "quad_tilt_lift_duct_system", - [lift_duct_module(label, center, front, side) for label, center, front, side in duct_centers], - "graphite", - ) - - -def rear_blade_x(label: str, center: Vector3, angle_deg: float) -> Shape: - root_radius = 7.0 - tip_radius = 20.5 - root_width = 6.0 - tip_width = 2.8 - angle = math.radians(angle_deg) - radial = (math.cos(angle), math.sin(angle)) - tangent = (-math.sin(angle), math.cos(angle)) - points: list[tuple[float, float]] = [] - for distance, half_width in ( - (root_radius, root_width / 2.0), - (tip_radius, tip_width / 2.0), - (tip_radius, -tip_width / 2.0), - (root_radius, -root_width / 2.0), - ): - y_pos = center[1] + radial[0] * distance + tangent[0] * half_width - z_pos = center[2] + radial[1] * distance + tangent[1] * half_width - points.append((y_pos, z_pos)) - return prism_from_yz(label, center[0], points, 1.8, "rotor_blue") - - -def rear_thruster() -> Compound: - center = (-201.0, 0.0, 56.0) - parts: list[Shape] = [ - annular_x("rear_cruise_thrust_duct", center, 27.0, 20.5, 28.0, "graphite"), - cylinder_x("rear_cruise_rotor_hub", center, 6.0, 16.0, "titanium"), - cylinder_between("upper_rear_thruster_pylon", (-174.0, 0.0, 61.0), (-190.0, 0.0, 72.0), 3.4, "brushed_aluminum"), - cylinder_between("lower_rear_thruster_pylon", (-174.0, 0.0, 39.0), (-190.0, 0.0, 42.0), 3.0, "brushed_aluminum"), - ] - for index in range(7): - parts.append(rear_blade_x(f"rear_cruise_propulsor_blade_{index + 1}", center, index * 360.0 / 7.0 + 8.0)) - return group("rear_cruise_propulsor", parts, "graphite") - - -def wheel_module(label: str, x_pos: float, side: float) -> Compound: - y_pos = side * 61.0 - wheel_center = (x_pos, y_pos, 20.0) - side_label = "port" if side > 0 else "starboard" - axle_end = (x_pos, side * 45.0, 20.0) - fairing = style( - softened(Box(37.0, 17.0, 14.0).moved(Location((x_pos, side * 52.0, 31.0))), 2.0), - f"{side_label}_{label}_flush_retractable_wheel_fairing", - "pearl_body", - ) - parts = [ - fairing, - cylinder_between(f"{side_label}_{label}_short_axle", axle_end, wheel_center, 3.4, "titanium"), - annular_y(f"{side_label}_{label}_low_profile_tire", wheel_center, 20.0, 11.0, 12.0, "rubber"), - cylinder_y(f"{side_label}_{label}_silver_wheel_hub", wheel_center, 7.0, 15.0, "brushed_aluminum"), - ] - return group(f"{side_label}_{label}_road_wheel_module", parts, "rubber") - - -def road_gear() -> Compound: - return group( - "retractable_road_wheel_set", - [ - wheel_module("front", 108.0, 1.0), - wheel_module("front", 108.0, -1.0), - wheel_module("rear", -105.0, 1.0), - wheel_module("rear", -105.0, -1.0), - ], - "rubber", - ) - - -def gen_step(): - return group( - MODEL_LABEL, - [ - body_shell(), - lifting_wings(), - lift_system(), - rear_thruster(), - road_gear(), - ], - ) diff --git a/models/fun/flying_car.step b/models/fun/flying_car.step deleted file mode 100644 index 3709d4e8..00000000 --- a/models/fun/flying_car.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f27ea5542060a7852811decf363e6d6bf58a751a3f334bc149a92c1e51230464 -size 8324722 diff --git a/models/fun/lunar_rover_corner_assembly.py b/models/fun/lunar_rover_corner_assembly.py deleted file mode 100644 index 5a09535b..00000000 --- a/models/fun/lunar_rover_corner_assembly.py +++ /dev/null @@ -1,552 +0,0 @@ -from __future__ import annotations - -import math -from collections.abc import Iterable, Sequence - -from build123d import * -from build123d import Shape -from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform -from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder -from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf - - -DISPLAY_NAME = "Futuristic lunar rover wheel suspension corner" - -# Coordinate convention: -# - millimeters -# - wheel axis is global Y -# - visible outside wheel face points toward negative Y -# - wheel center is fixed at X=0, Y=0, Z=80 - -WHEEL_CENTER = (0.0, 0.0, 80.0) -BASE_CENTER_X = -20.0 -BASE_TOP_Z = 8.0 - -COLORS = { - "dark_graphite": (0.005, 0.006, 0.007, 1.0), - "matte_black": (0.002, 0.002, 0.002, 1.0), - "satin_titanium": (0.42, 0.43, 0.40, 1.0), - "pale_aluminum": (0.78, 0.80, 0.78, 1.0), - "brushed_steel": (0.50, 0.52, 0.54, 1.0), - "anodized_orange": (1.00, 0.34, 0.08, 1.0), - "spring_orange": (1.00, 0.42, 0.02, 1.0), - "brushed_aluminum": (0.66, 0.68, 0.66, 1.0), - "fastener_steel": (0.18, 0.19, 0.20, 1.0), -} - - -Vector3 = tuple[float, float, float] - - -def v_add(a: Vector3, b: Vector3) -> Vector3: - return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) - - -def v_sub(a: Vector3, b: Vector3) -> Vector3: - return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) - - -def v_mul(a: Vector3, scalar: float) -> Vector3: - return (a[0] * scalar, a[1] * scalar, a[2] * scalar) - - -def v_dot(a: Vector3, b: Vector3) -> float: - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] - - -def v_cross(a: Vector3, b: Vector3) -> Vector3: - return ( - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0], - ) - - -def v_len(a: Vector3) -> float: - return math.sqrt(v_dot(a, a)) - - -def v_norm(a: Vector3) -> Vector3: - length = v_len(a) - if length <= 1e-9: - raise ValueError("zero-length vector") - return (a[0] / length, a[1] / length, a[2] / length) - - -def style(shape: Shape, label: str, color_name: str) -> Shape: - shape.label = label - shape.color = Color(*COLORS[color_name]) - return shape - - -def group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound: - compound = Compound(obj=list(children), children=list(children), label=label) - if color_name is not None: - compound.color = Color(*COLORS[color_name]) - return compound - - -def transformed(shape: Shape, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3) -> Shape: - x_dir = v_norm(axis_x) - y_dir = v_norm(axis_y) - z_dir = v_norm(axis_z) - trsf = gp_Trsf() - trsf.SetValues( - x_dir[0], - y_dir[0], - z_dir[0], - center[0], - x_dir[1], - y_dir[1], - z_dir[1], - center[1], - x_dir[2], - y_dir[2], - z_dir[2], - center[2], - ) - return Shape(obj=BRepBuilderAPI_Transform(shape.wrapped, trsf, True).Shape()) - - -def oriented_box( - label: str, - center: Vector3, - size_x: float, - size_y: float, - size_z: float, - axis_x: Vector3 = (1.0, 0.0, 0.0), - axis_y: Vector3 = (0.0, 1.0, 0.0), - axis_z: Vector3 = (0.0, 0.0, 1.0), - color_name: str = "pale_aluminum", - fillet_radius: float = 0.0, -) -> Shape: - shape = transformed(Box(size_x, size_y, size_z), center, axis_x, axis_y, axis_z) - if fillet_radius > 0.0: - shape = softened(shape, fillet_radius) - return style(shape, label, color_name) - - -def cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape: - axis = v_sub(p2, p1) - length = v_len(axis) - if length <= 1e-6: - return style(Sphere(radius).moved(Location(p1)), label, color_name) - direction = v_norm(axis) - wrapped = BRepPrimAPI_MakeCylinder( - gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)), - radius, - length, - ).Shape() - return style(Shape(obj=wrapped), label, color_name) - - -def cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length, rotation=(0, 90, 0)).moved(Location(center)), label, color_name) - - -def cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length, rotation=(90, 0, 0)).moved(Location(center)), label, color_name) - - -def cylinder_z(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return style(Cylinder(radius, length).moved(Location(center)), label, color_name) - - -def softened(shape: Shape, radius: float) -> Shape: - # Small repeated pads, spokes, tubes, and bolts dominate export time if every - # edge receives a true BREP fillet. Major machined bodies still get rounded - # edges below; small bodies rely on cylindrical profiles and crisp bevel-like - # proportions to keep the STEP robust. - if radius < 0.95: - return shape - try: - return fillet(shape.edges(), radius=radius) - except Exception: - try: - return chamfer(shape.edges(), length=radius * 0.35) - except Exception: - return shape - - -def annular_y(label: str, center: Vector3, outer_radius: float, inner_radius: float, length: float, color_name: str) -> Shape: - outer = Cylinder(outer_radius, length, rotation=(90, 0, 0)).moved(Location(center)) - inner = Cylinder(inner_radius, length + 4.0, rotation=(90, 0, 0)).moved(Location(center)) - return style(softened(outer - inner, 1.2), label, color_name) - - -def wheel_lattice() -> Compound: - center = WHEEL_CENTER - parts: list[Shape] = [ - annular_y("wheel_outer_rim_ring", center, 65.0, 56.0, 42.0, "dark_graphite"), - annular_y("wheel_inner_rim_ring", center, 38.0, 25.0, 40.0, "dark_graphite"), - style(Torus(65.0, 1.8, rotation=(90, 0, 0)).moved(Location((0.0, -21.4, 80.0))), "wheel_outer_bead_visible", "dark_graphite"), - style(Torus(65.0, 1.8, rotation=(90, 0, 0)).moved(Location((0.0, 21.4, 80.0))), "wheel_outer_bead_inner", "dark_graphite"), - ] - - for index in range(16): - theta_inner = (2.0 * math.pi * index) / 16.0 - theta_outer = theta_inner + math.radians(5.0) - p1 = ( - math.cos(theta_inner) * 19.0, - -13.5, - 80.0 + math.sin(theta_inner) * 19.0, - ) - p2 = ( - math.cos(theta_outer) * 56.0, - 13.5, - 80.0 + math.sin(theta_outer) * 56.0, - ) - spoke = cylinder_between(f"wheel_angular_spoke_{index + 1:02d}", p1, p2, 2.8, "dark_graphite") - parts.append(style(softened(spoke, 0.6), spoke.label or f"wheel_angular_spoke_{index + 1:02d}", "dark_graphite")) - - return group("wheel", parts, "dark_graphite") - - -def tread_elements() -> Compound: - parts: list[Shape] = [] - rim_radius = 65.0 - tread_height = 5.0 - # The diagonal chevron bars have tangent-wise corners, so the pad center is - # tucked slightly below the nominal 65 mm rim surface to keep the full tread - # envelope close to the requested 140 mm outside diameter. - radial_center = rim_radius + tread_height / 2.0 - 1.1 - long_size = math.hypot(18.0, 10.0) - - for index in range(32): - theta = (2.0 * math.pi * index) / 32.0 - normal = (math.cos(theta), 0.0, math.sin(theta)) - tangent = (-math.sin(theta), 0.0, math.cos(theta)) - chevron_sign = 1.0 if index % 2 == 0 else -1.0 - for side_name, y_sign in (("outside", -1.0), ("inside", 1.0)): - axis_x = v_norm(v_add(v_mul((0.0, 1.0, 0.0), y_sign * 18.0), v_mul(tangent, chevron_sign * 10.0))) - axis_z = normal - axis_y = v_norm(v_cross(axis_z, axis_x)) - center = v_add( - WHEEL_CENTER, - v_add( - v_mul(normal, radial_center), - v_add(v_mul((0.0, 1.0, 0.0), y_sign * 9.0), v_mul(tangent, chevron_sign * 5.0)), - ), - ) - pad = oriented_box( - f"tread_chevron_{index + 1:02d}_{side_name}", - center, - long_size, - 5.8, - tread_height, - axis_x, - axis_y, - axis_z, - "matte_black", - fillet_radius=0.75, - ) - parts.append(pad) - return group("tire_tread_elements", parts, "matte_black") - - -def hub_and_axle() -> tuple[Shape, Compound]: - hub = cylinder_y("central_hub", WHEEL_CENTER, 17.0, 50.0, "pale_aluminum") - hub = style(softened(hub, 1.2), "central_hub", "pale_aluminum") - - axle_parts: list[Shape] = [ - cylinder_y("axle_stub", (0.0, 8.0, 80.0), 8.0, 78.0, "fastener_steel"), - cylinder_y("visible_axle_cap", (0.0, -29.0, 80.0), 12.0, 6.0, "fastener_steel"), - ] - axle_parts = [style(softened(part, 0.6), part.label or "axle_part", "fastener_steel") for part in axle_parts] - return hub, group("axle_hardware", axle_parts, "fastener_steel") - - -def brake_disc() -> Shape: - disc = Cylinder(41.0, 5.0, rotation=(90, 0, 0)).moved(Location((0.0, 29.0, 80.0))) - for index in range(24): - theta = (2.0 * math.pi * index) / 24.0 - hole_center = ( - math.cos(theta) * 31.0, - 29.0, - 80.0 + math.sin(theta) * 31.0, - ) - disc = disc - Cylinder(2.2, 8.0, rotation=(90, 0, 0)).moved(Location(hole_center)) - disc = softened(disc, 0.4) - return style(disc, "brake_disc_82mm_vented", "brushed_steel") - - -def brake_caliper() -> Compound: - angle = math.radians(42.0) - radial = (math.cos(angle), 0.0, math.sin(angle)) - tangent = (-math.sin(angle), 0.0, math.cos(angle)) - base = v_add(WHEEL_CENTER, v_mul(radial, 45.0)) - parts = [ - oriented_box( - "brake_caliper_outer_half", - v_add(base, (0.0, 20.8, 0.0)), - 30.0, - 7.0, - 17.0, - tangent, - (0.0, 1.0, 0.0), - radial, - "anodized_orange", - fillet_radius=2.0, - ), - oriented_box( - "brake_caliper_inner_half", - v_add(base, (0.0, 37.2, 0.0)), - 30.0, - 7.0, - 17.0, - tangent, - (0.0, 1.0, 0.0), - radial, - "anodized_orange", - fillet_radius=2.0, - ), - oriented_box( - "brake_caliper_bridge", - v_add(WHEEL_CENTER, v_add(v_mul(radial, 53.0), (0.0, 29.0, 0.0))), - 28.0, - 18.0, - 5.5, - tangent, - (0.0, 1.0, 0.0), - radial, - "anodized_orange", - fillet_radius=1.3, - ), - ] - return group("brake_caliper", parts, "anodized_orange") - - -def upright() -> Compound: - parts: list[Shape] = [ - oriented_box("upright_machined_web", (-18.0, 40.0, 80.0), 15.0, 18.0, 72.0, color_name="pale_aluminum", fillet_radius=1.5), - oriented_box("upright_upper_fork_lug", (-10.0, 40.0, 111.0), 30.0, 20.0, 17.0, color_name="pale_aluminum", fillet_radius=1.5), - oriented_box("upright_lower_fork_lug", (-10.0, 40.0, 49.0), 30.0, 20.0, 17.0, color_name="pale_aluminum", fillet_radius=1.5), - annular_y("upright_central_bearing", (0.0, 40.0, 80.0), 19.0, 9.0, 20.0, "pale_aluminum"), - cylinder_y("upright_upper_bushing", (-21.0, 40.0, 111.0), 6.5, 24.0, "satin_titanium"), - cylinder_y("upright_lower_bushing", (-23.0, 40.0, 49.0), 6.5, 24.0, "satin_titanium"), - ] - return group("upright", parts, "pale_aluminum") - - -def bushing(label: str, center: Vector3, length: float = 15.0) -> Shape: - return style(softened(cylinder_y(label, center, 6.2, length, "satin_titanium"), 0.5), label, "satin_titanium") - - -def control_arms() -> tuple[Compound, Compound]: - upper_upright = (-21.0, 40.0, 111.0) - upper_front = (-94.0, -16.0, 118.0) - upper_rear = (-94.0, 52.0, 118.0) - lower_upright = (-23.0, 40.0, 49.0) - lower_front = (-94.0, -20.0, 45.0) - lower_rear = (-94.0, 52.0, 45.0) - - upper_parts = [ - cylinder_between("upper_control_arm_front_tube", upper_front, upper_upright, 3.5, "satin_titanium"), - cylinder_between("upper_control_arm_rear_tube", upper_rear, upper_upright, 3.5, "satin_titanium"), - cylinder_between("upper_control_arm_cross_tube", upper_front, upper_rear, 2.8, "satin_titanium"), - bushing("upper_front_chassis_bushing", upper_front), - bushing("upper_rear_chassis_bushing", upper_rear), - bushing("upper_upright_outer_bushing", upper_upright, 18.0), - ] - upper_parts = [ - style(softened(part, 0.45), part.label or "upper_control_arm_part", "satin_titanium") - if "tube" in (part.label or "") - else part - for part in upper_parts - ] - - lower_parts = [ - cylinder_between("lower_control_arm_front_tube", lower_front, lower_upright, 3.5, "satin_titanium"), - cylinder_between("lower_control_arm_rear_tube", lower_rear, lower_upright, 3.5, "satin_titanium"), - cylinder_between("lower_control_arm_cross_tube", lower_front, lower_rear, 2.8, "satin_titanium"), - bushing("lower_front_chassis_bushing", lower_front), - bushing("lower_rear_chassis_bushing", lower_rear), - bushing("lower_upright_outer_bushing", lower_upright, 18.0), - ] - lower_parts = [ - style(softened(part, 0.45), part.label or "lower_control_arm_part", "satin_titanium") - if "tube" in (part.label or "") - else part - for part in lower_parts - ] - - return ( - group("upper_control_arm", upper_parts, "satin_titanium"), - group("lower_control_arm", lower_parts, "satin_titanium"), - ) - - -def spring_along_axis(label: str, start: Vector3, end: Vector3) -> Compound: - axis = v_sub(end, start) - length = v_len(axis) - axis_z = v_norm(axis) - fallback = (0.0, 0.0, 1.0) - if abs(v_dot(axis_z, fallback)) > 0.92: - fallback = (1.0, 0.0, 0.0) - axis_x = v_norm(v_cross(fallback, axis_z)) - axis_y = v_norm(v_cross(axis_z, axis_x)) - - wire_radius = 1.5 - helix_radius = 10.5 - turns = 8 - segments_per_turn = 8 - total_segments = turns * segments_per_turn - - def helix_point(step: int) -> Vector3: - angle = 2.0 * math.pi * turns * step / total_segments - local = ( - math.cos(angle) * helix_radius, - math.sin(angle) * helix_radius, - length * step / total_segments, - ) - return v_add( - start, - v_add( - v_mul(axis_x, local[0]), - v_add(v_mul(axis_y, local[1]), v_mul(axis_z, local[2])), - ), - ) - - segments: list[Shape] = [] - previous = helix_point(0) - for index in range(1, total_segments + 1): - current = helix_point(index) - segments.append( - cylinder_between( - f"{label}_segment_{index:02d}", - previous, - current, - wire_radius, - "spring_orange", - ) - ) - previous = current - return group(label, segments, "spring_orange") - - -def coilover_shock() -> Compound: - top = (-90.0, -8.0, 118.0) - bottom = (-48.0, -24.0, 58.0) - axis = v_norm(v_sub(top, bottom)) - length = v_len(v_sub(top, bottom)) - damper_start = v_add(bottom, v_mul(axis, 9.0)) - damper_end = v_add(bottom, v_mul(axis, length * 0.62)) - rod_start = v_add(bottom, v_mul(axis, length * 0.52)) - rod_end = v_add(top, v_mul(axis, -7.0)) - spring_start = v_add(bottom, v_mul(axis, 7.0)) - spring_end = v_add(top, v_mul(axis, -7.0)) - - parts = [ - cylinder_between("coilover_damper_body", damper_start, damper_end, 5.0, "brushed_steel"), - cylinder_between("coilover_piston_rod", rod_start, rod_end, 2.5, "fastener_steel"), - spring_along_axis("coilover_helical_spring_8_turns", spring_start, spring_end), - cylinder_y("coilover_top_eyelet", top, 6.0, 14.0, "satin_titanium"), - cylinder_y("coilover_lower_eyelet", bottom, 6.0, 14.0, "satin_titanium"), - ] - return group("coilover_shock", parts, "satin_titanium") - - -def triangular_cutout_tool(label: str, x_center: float, vertices_yz: Sequence[tuple[float, float]]) -> Shape: - plane = Plane(origin=(x_center, 0.0, 0.0), x_dir=(0.0, 1.0, 0.0), z_dir=(1.0, 0.0, 0.0)) - with BuildPart() as tool: - with BuildSketch(plane): - Polygon(*vertices_yz) - extrude(amount=13.0, both=True) - tool.part.label = label - return tool.part - - -def chassis_bulkhead() -> tuple[Shape, Compound]: - plate = Box(8.0, 70.0, 95.0).moved(Location((-95.0, 20.0, 80.0))) - cutouts = [ - triangular_cutout_tool("bulkhead_upper_triangular_cutout", -95.0, [(3.0, 86.0), (37.0, 86.0), (20.0, 117.0)]), - triangular_cutout_tool("bulkhead_lower_triangular_cutout", -95.0, [(3.0, 74.0), (37.0, 74.0), (20.0, 43.0)]), - ] - for tool in cutouts: - plate = plate - tool - for y, z in [(3.0, 86.0), (37.0, 86.0), (20.0, 117.0), (3.0, 74.0), (37.0, 74.0), (20.0, 43.0)]: - plate = plate - cylinder_x("bulkhead_cutout_corner_radius_tool", (-95.0, y, z), 5.0, 13.0, "pale_aluminum") - plate = style(softened(plate, 1.0), "chassis_bulkhead", "pale_aluminum") - - supports = group( - "bulkhead_display_support_blocks", - [ - oriented_box("bulkhead_left_support_block", (-95.0, -8.0, 20.25), 17.0, 12.0, 24.5, color_name="brushed_aluminum", fillet_radius=1.0), - oriented_box("bulkhead_right_support_block", (-95.0, 48.0, 20.25), 17.0, 12.0, 24.5, color_name="brushed_aluminum", fillet_radius=1.0), - ], - "brushed_aluminum", - ) - return plate, supports - - -def countersunk_display_base() -> Shape: - with BuildPart() as part: - with BuildSketch(): - RectangleRounded(170.0, 110.0, 6.0) - extrude(amount=8.0) - base = part.part.moved(Location((BASE_CENTER_X, 0.0, 0.0))) - hole_positions = [ - (BASE_CENTER_X - 70.0, -43.0), - (BASE_CENTER_X + 70.0, -43.0), - (BASE_CENTER_X - 70.0, 43.0), - (BASE_CENTER_X + 70.0, 43.0), - ] - for x, y in hole_positions: - base = base - Cylinder(3.2, 12.0).moved(Location((x, y, 4.0))) - base = base - Cone(3.2, 6.4, 5.0).moved(Location((x, y, 6.3))) - base = softened(base, 1.1) - return style(base, "display_base_rounded_countersunk", "brushed_aluminum") - - -def fasteners() -> Compound: - parts: list[Shape] = [] - for index in range(6): - theta = (2.0 * math.pi * index) / 6.0 - center = (math.cos(theta) * 22.0, -28.5, 80.0 + math.sin(theta) * 22.0) - parts.append(style(softened(cylinder_y(f"hub_bolt_{index + 1:02d}", center, 2.6, 5.0, "fastener_steel"), 0.35), f"hub_bolt_{index + 1:02d}", "fastener_steel")) - - caliper_bolt_centers = [ - v_add(WHEEL_CENTER, (23.0, 17.0, 44.0)), - v_add(WHEEL_CENTER, (43.0, 17.0, 24.0)), - ] - for index, center in enumerate(caliper_bolt_centers, start=1): - parts.append(style(softened(cylinder_y(f"caliper_socket_bolt_{index}", center, 2.2, 4.0, "fastener_steel"), 0.3), f"caliper_socket_bolt_{index}", "fastener_steel")) - - for index, (y, z) in enumerate( - [ - (-16.0, 118.0), - (52.0, 118.0), - (-20.0, 45.0), - (52.0, 45.0), - (-8.0, 34.0), - (48.0, 34.0), - (-8.0, 126.0), - (48.0, 126.0), - ], - start=1, - ): - parts.append(style(softened(cylinder_x(f"bulkhead_bolt_head_{index:02d}", (-89.6, y, z), 2.8, 3.2, "fastener_steel"), 0.25), f"bulkhead_bolt_head_{index:02d}", "fastener_steel")) - return group("fasteners", parts, "fastener_steel") - - -def gen_step(): - upper_arm, lower_arm = control_arms() - chassis_plate, support_blocks = chassis_bulkhead() - hub, axle = hub_and_axle() - children: list[Shape] = [ - countersunk_display_base(), - support_blocks, - wheel_lattice(), - tread_elements(), - hub, - axle, - brake_disc(), - brake_caliper(), - upright(), - upper_arm, - lower_arm, - coilover_shock(), - chassis_plate, - fasteners(), - ] - return group("lunar_rover_wheel_suspension_corner", children) diff --git a/models/fun/lunar_rover_corner_assembly.step b/models/fun/lunar_rover_corner_assembly.step deleted file mode 100644 index 497374c2..00000000 --- a/models/fun/lunar_rover_corner_assembly.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:98de395fc3819f34f086f11c7365aed47bc879147f4a3f73afb42f9916d1d5c7 -size 2939753 diff --git a/models/fun/mechanical_iris_aperture.py b/models/fun/mechanical_iris_aperture.py deleted file mode 100644 index d7e75ff6..00000000 --- a/models/fun/mechanical_iris_aperture.py +++ /dev/null @@ -1,546 +0,0 @@ -from __future__ import annotations - -from math import cos, degrees, radians, sin, tau -from typing import Iterable - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Color, - Compound, - Cone, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - RegularPolygon, - chamfer, - extrude, - fillet, -) - - -# Units: millimeters. -# Origin: center of the aperture on the Z axis. -# XY: ring/blade plane viewed from above. +Z: stack direction upward. - -MODEL_LABEL = "mechanical_iris_aperture_assembly" - -BRUSHED_STEEL = Color(0.64, 0.67, 0.68, 1.0) -STEEL_HIGHLIGHT = Color(0.76, 0.78, 0.78, 1.0) -GRAPHITE = Color(0.0, 0.0, 0.0, 1.0) -BRASS = Color(0.95, 0.68, 0.25, 1.0) -GUNMETAL = Color(0.13, 0.15, 0.17, 1.0) -DARK_GRAY = Color(0.06, 0.065, 0.07, 1.0) -APERTURE_BLACK = Color(0.0, 0.0, 0.0, 1.0) - -BASE_OD = 160.0 -BASE_ID = 52.0 -BASE_THICKNESS = 8.0 -BASE_TOP_Z = BASE_THICKNESS -BASE_MOUNT_HOLE_COUNT = 6 -BASE_MOUNT_HOLE_DIAMETER = 5.0 -BASE_MOUNT_BCD = 130.0 -BASE_COUNTERSINK_TOP_DIAMETER = 10.5 -BASE_COUNTERSINK_DEPTH = 2.4 - -RETAINING_ROOT_OD = 150.0 -RETAINING_TOOTH_TIP_OD = 154.0 -RETAINING_ID = 70.0 -RETAINING_Z_BOTTOM = 8.0 -RETAINING_Z_TOP = 13.0 -RETAINING_HEIGHT = RETAINING_Z_TOP - RETAINING_Z_BOTTOM -RETAINING_TEETH = 120 - -BLADE_COUNT = 12 -BLADE_THICKNESS = 1.2 -BLADE_Z_BOTTOM = 13.05 -BLADE_Z_STAGGER = 0.15 -BLADE_INNER_NOSE_RADIUS = 18.0 -BLADE_NOMINAL_INNER_RADIUS = 24.0 -BLADE_OUTER_RADIUS = 68.0 -BLADE_SPAN_DEGREES = 38.0 -BLADE_PIVOT_RADIUS = 62.0 -BLADE_PIVOT_LOCAL_ANGLE = radians(13.0) -BLADE_PIVOT_HOLE_DIAMETER = 3.0 -BLADE_DRIVE_PIN_RADIUS = 46.0 -BLADE_DRIVE_PIN_LOCAL_ANGLE = radians(-8.0) - -ACTUATING_OD = 138.0 -ACTUATING_ID = 86.0 -ACTUATING_Z_BOTTOM = 14.0 -ACTUATING_Z_TOP = 17.0 -ACTUATING_HEIGHT = ACTUATING_Z_TOP - ACTUATING_Z_BOTTOM -CAM_SLOT_RADIUS = BLADE_DRIVE_PIN_RADIUS -CAM_SLOT_LENGTH = 18.0 -CAM_SLOT_WIDTH = 4.0 - -FRONT_SCREW_COUNT = 6 -FRONT_SCREW_BCD = 120.0 -FRONT_SCREW_HEAD_DIAMETER = 7.0 -FRONT_SCREW_HEAD_HEIGHT = 3.0 - - -def _polar_point(radius: float, angle: float) -> tuple[float, float]: - return (radius * cos(angle), radius * sin(angle)) - - -def _offset_point( - radius: float, - angle: float, - tangent_offset: float, -) -> tuple[float, float]: - radial_x, radial_y = cos(angle), sin(angle) - tangent_x, tangent_y = -sin(angle), cos(angle) - return ( - radius * radial_x + tangent_offset * tangent_x, - radius * radial_y + tangent_offset * tangent_y, - ) - - -def _set_metadata(part, label: str, color: Color): - part.label = label - part.color = color - return part - - -def _safe_chamfer(part, edges: Iterable, length: float): - selected = list(edges) - if not selected: - return part - try: - return chamfer(selected, length=length) - except Exception: - return part - - -def _safe_fillet(part, edges: Iterable, radius: float): - selected = list(edges) - if not selected: - return part - try: - return fillet(selected, radius=radius) - except Exception: - return part - - -def _circular_edges( - part, - *, - radius: float | None = None, - coordinate: float | None = None, - axis: str = "z", - tolerance: float = 0.06, -): - edges = [] - for edge in part.edges(): - try: - edge_radius = edge.radius - edge_center = edge.arc_center - edge_normal = edge.normal() - except Exception: - continue - - if radius is not None and abs(edge_radius - radius) > tolerance: - continue - - components = {"x": abs(edge_normal.X), "y": abs(edge_normal.Y), "z": abs(edge_normal.Z)} - if components[axis] < 1.0 - 1e-4: - continue - - if coordinate is not None: - coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis] - if abs(coord - coordinate) > tolerance: - continue - - edges.append(edge) - - return edges - - -def _edges_at_z(part, z_value: float, *, tolerance: float = 0.04): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - if abs(bbox.min.Z - z_value) <= tolerance and abs(bbox.max.Z - z_value) <= tolerance: - edges.append(edge) - return edges - - -def _tooth_profile( - *, - teeth: int, - root_radius: float, - tip_radius: float, - phase: float = 0.0, - root_span_fraction: float = 0.72, - tip_span_fraction: float = 0.34, -) -> list[tuple[float, float]]: - points: list[tuple[float, float]] = [] - pitch_angle = tau / teeth - - for tooth_index in range(teeth): - center_angle = phase + tooth_index * pitch_angle - points.extend( - ( - _polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0), - _polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0), - ) - ) - - return points - - -def _tick_rectangle_points(angle: float, inner_radius: float, outer_radius: float, width: float): - tangent_x, tangent_y = -sin(angle), cos(angle) - p1 = _polar_point(inner_radius, angle) - p2 = _polar_point(outer_radius, angle) - half_width = width / 2.0 - return [ - (p1[0] - tangent_x * half_width, p1[1] - tangent_y * half_width), - (p2[0] - tangent_x * half_width, p2[1] - tangent_y * half_width), - (p2[0] + tangent_x * half_width, p2[1] + tangent_y * half_width), - (p1[0] + tangent_x * half_width, p1[1] + tangent_y * half_width), - ] - - -def _make_base_ring(): - with BuildPart() as base: - Cylinder( - radius=BASE_OD / 2.0, - height=BASE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - with Locations(Location((0.0, 0.0, -0.75))): - Cylinder( - radius=BASE_ID / 2.0, - height=BASE_THICKNESS + 1.5, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - for index in range(BASE_MOUNT_HOLE_COUNT): - x_pos, y_pos = _polar_point(BASE_MOUNT_BCD / 2.0, tau * index / BASE_MOUNT_HOLE_COUNT) - with Locations(Location((x_pos, y_pos, -0.75))): - Cylinder( - radius=BASE_MOUNT_HOLE_DIAMETER / 2.0, - height=BASE_THICKNESS + 1.5, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - with Locations(Location((x_pos, y_pos, BASE_TOP_Z - BASE_COUNTERSINK_DEPTH))): - Cone( - bottom_radius=BASE_MOUNT_HOLE_DIAMETER / 2.0, - top_radius=BASE_COUNTERSINK_TOP_DIAMETER / 2.0, - height=BASE_COUNTERSINK_DEPTH + 0.2, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - with BuildSketch(Plane.XY.offset(BASE_TOP_Z - 0.22)): - for index in range(72): - angle = tau * index / 72 - major = index % 6 == 0 - inner_radius = 76.0 if major else 77.2 - outer_radius = 79.35 - width = 0.72 if major else 0.44 - Polygon(_tick_rectangle_points(angle, inner_radius, outer_radius, width), align=None) - extrude(amount=0.36, mode=Mode.SUBTRACT) - - part = base.part - chamfer_edges = [] - for radius in (BASE_OD / 2.0, BASE_ID / 2.0): - chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=0.0)) - chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=BASE_THICKNESS)) - part = _safe_chamfer(part, chamfer_edges, 1.0) - return _set_metadata(part, "base_ring_annular_disk_160od_52id_countersunk", BRUSHED_STEEL) - - -def _make_tick_marks() -> list[object]: - ticks = [] - for index in range(72): - angle = tau * index / 72 - major = index % 6 == 0 - inner_radius = 76.0 if major else 77.2 - outer_radius = 79.35 - width = 0.72 if major else 0.44 - with BuildPart() as tick: - with BuildSketch(Plane.XY.offset(BASE_TOP_Z - 0.18)): - Polygon(_tick_rectangle_points(angle, inner_radius, outer_radius, width), align=None) - extrude(amount=0.12) - ticks.append(_set_metadata(tick.part, f"dark_engraved_radial_tick_{index + 1:02d}", DARK_GRAY)) - return ticks - - -def _make_retaining_ring(): - root_radius = RETAINING_ROOT_OD / 2.0 - tip_radius = RETAINING_TOOTH_TIP_OD / 2.0 - - with BuildPart() as retaining: - with BuildSketch(Plane.XY.offset(RETAINING_Z_BOTTOM)): - Polygon( - _tooth_profile( - teeth=RETAINING_TEETH, - root_radius=root_radius, - tip_radius=tip_radius, - phase=-tau / RETAINING_TEETH / 2.0, - root_span_fraction=0.72, - tip_span_fraction=0.34, - ), - align=None, - ) - extrude(amount=RETAINING_HEIGHT) - - with Locations(Location((0.0, 0.0, RETAINING_Z_BOTTOM - 0.5))): - Cylinder( - radius=RETAINING_ID / 2.0, - height=RETAINING_HEIGHT + 1.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = retaining.part - outer_chamfers = [] - outer_chamfers.extend(_edges_at_z(part, RETAINING_Z_BOTTOM)) - outer_chamfers.extend(_edges_at_z(part, RETAINING_Z_TOP)) - part = _safe_chamfer(part, outer_chamfers, 0.45) - return _set_metadata(part, "front_retaining_ring_120_external_teeth_150od_70id", STEEL_HIGHLIGHT) - - -def _blade_outline_points(center_angle: float) -> list[tuple[float, float]]: - local_points: list[tuple[float, float]] = [] - - for step in range(10): - local_angle = radians(-BLADE_SPAN_DEGREES / 2.0) + radians(BLADE_SPAN_DEGREES) * step / 9 - local_points.append((BLADE_OUTER_RADIUS, local_angle)) - - local_points.extend( - ( - (54.0, radians(18.8)), - (36.0, radians(18.2)), - (BLADE_NOMINAL_INNER_RADIUS, radians(18.0)), - (20.0, radians(17.5)), - (BLADE_INNER_NOSE_RADIUS, radians(14.5)), - (BLADE_INNER_NOSE_RADIUS, radians(10.5)), - (BLADE_INNER_NOSE_RADIUS, radians(6.5)), - (BLADE_INNER_NOSE_RADIUS, radians(2.5)), - (BLADE_INNER_NOSE_RADIUS, radians(-1.5)), - (BLADE_INNER_NOSE_RADIUS, radians(-5.5)), - (BLADE_INNER_NOSE_RADIUS, radians(-9.5)), - (BLADE_INNER_NOSE_RADIUS, radians(-13.5)), - (20.0, radians(-16.8)), - (BLADE_NOMINAL_INNER_RADIUS, radians(-18.0)), - (38.0, radians(-18.4)), - (56.0, radians(-18.8)), - ) - ) - - return [_polar_point(radius, center_angle + local_angle) for radius, local_angle in local_points] - - -def _blade_pivot_center(blade_angle: float) -> tuple[float, float]: - return _polar_point(BLADE_PIVOT_RADIUS, blade_angle + BLADE_PIVOT_LOCAL_ANGLE) - - -def _blade_drive_pin_center(blade_angle: float) -> tuple[float, float]: - return _polar_point(BLADE_DRIVE_PIN_RADIUS, blade_angle + BLADE_DRIVE_PIN_LOCAL_ANGLE) - - -def _make_iris_blade(index: int): - blade_angle = tau * index / BLADE_COUNT - z_bottom = BLADE_Z_BOTTOM + index * BLADE_Z_STAGGER - pivot_x, pivot_y = _blade_pivot_center(blade_angle) - - with BuildPart() as blade: - with BuildSketch(Plane.XY.offset(z_bottom)): - Polygon(_blade_outline_points(blade_angle), align=None) - extrude(amount=BLADE_THICKNESS) - - with Locations(Location((pivot_x, pivot_y, z_bottom - 0.2))): - Cylinder( - radius=BLADE_PIVOT_HOLE_DIAMETER / 2.0, - height=BLADE_THICKNESS + 0.4, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = blade.part - part = _safe_chamfer(part, part.edges(), 0.28) - return _set_metadata(part, f"iris_blade_{index + 1:02d}_matte_black_graphite", GRAPHITE) - - -def _curved_slot_points(center_angle: float) -> list[tuple[float, float]]: - half_angle = (CAM_SLOT_LENGTH / CAM_SLOT_RADIUS) / 2.0 - inner_radius = CAM_SLOT_RADIUS - CAM_SLOT_WIDTH / 2.0 - outer_radius = CAM_SLOT_RADIUS + CAM_SLOT_WIDTH / 2.0 - start_angle = center_angle - half_angle - end_angle = center_angle + half_angle - - points = [] - for step in range(8): - angle = start_angle + (end_angle - start_angle) * step / 7 - points.append(_polar_point(outer_radius, angle)) - for step in range(7, -1, -1): - angle = start_angle + (end_angle - start_angle) * step / 7 - points.append(_polar_point(inner_radius, angle)) - return points - - -def _make_actuating_ring(): - with BuildPart() as actuator: - with Locations(Location((0.0, 0.0, ACTUATING_Z_BOTTOM))): - Cylinder( - radius=ACTUATING_OD / 2.0, - height=ACTUATING_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with Locations(Location((0.0, 0.0, ACTUATING_Z_BOTTOM - 0.5))): - Cylinder( - radius=ACTUATING_ID / 2.0, - height=ACTUATING_HEIGHT + 1.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - with BuildSketch(Plane.XY.offset(ACTUATING_Z_BOTTOM - 0.2)): - for index in range(BLADE_COUNT): - blade_angle = tau * index / BLADE_COUNT - slot_angle = blade_angle + BLADE_DRIVE_PIN_LOCAL_ANGLE - Polygon(_curved_slot_points(slot_angle), align=None) - extrude(amount=ACTUATING_HEIGHT + 0.4, mode=Mode.SUBTRACT) - - part = actuator.part - chamfer_edges = [] - for radius in (ACTUATING_OD / 2.0, ACTUATING_ID / 2.0): - chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=ACTUATING_Z_BOTTOM)) - chamfer_edges.extend(_circular_edges(part, radius=radius, coordinate=ACTUATING_Z_TOP)) - part = _safe_chamfer(part, chamfer_edges, 0.8) - return _set_metadata(part, "actuating_ring_12_curved_cam_slots_138od_86id", BRUSHED_STEEL) - - -def _make_pivot_screw(index: int): - blade_angle = tau * index / BLADE_COUNT - x_pos, y_pos = _blade_pivot_center(blade_angle) - shaft_height = ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.2 - head_height = 1.6 - - with BuildPart() as screw: - Cylinder( - radius=1.25, - height=shaft_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with Locations(Location((0.0, 0.0, shaft_height))): - Cylinder( - radius=2.5, - height=head_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with BuildSketch(Plane.XY.offset(shaft_height + head_height - 0.72)): - RegularPolygon(radius=1.35, side_count=6, major_radius=True, rotation=30.0) - extrude(amount=0.82, mode=Mode.SUBTRACT) - - part = screw.part - head_edges = [] - head_edges.extend(_circular_edges(part, radius=2.5, coordinate=shaft_height)) - head_edges.extend(_circular_edges(part, radius=2.5, coordinate=shaft_height + head_height)) - part = _safe_fillet(part, head_edges, 0.35) - part = part.moved(Location((x_pos, y_pos, BLADE_Z_BOTTOM - 0.1))) - return _set_metadata(part, f"pivot_screw_{index + 1:02d}_brass_head_and_shaft", BRASS) - - -def _make_drive_pin(index: int): - blade_angle = tau * index / BLADE_COUNT - x_pos, y_pos = _blade_drive_pin_center(blade_angle) - - with BuildPart() as pin: - Cylinder( - radius=1.3, - height=ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.75, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = pin.part - part = _safe_fillet(part, _circular_edges(part, radius=1.3, coordinate=ACTUATING_Z_TOP - BLADE_Z_BOTTOM + 0.75), 0.45) - part = part.moved(Location((x_pos, y_pos, BLADE_Z_BOTTOM - 0.05))) - return _set_metadata(part, f"drive_pin_{index + 1:02d}_brass_cam_follower", BRASS) - - -def _make_front_socket_screw(index: int): - angle = tau * index / FRONT_SCREW_COUNT + radians(30.0) - x_pos, y_pos = _polar_point(FRONT_SCREW_BCD / 2.0, angle) - shaft_radius = 2.5 - shaft_height = ACTUATING_Z_TOP - RETAINING_Z_BOTTOM + 0.2 - head_radius = FRONT_SCREW_HEAD_DIAMETER / 2.0 - socket_depth = 1.45 - - with BuildPart() as screw: - Cylinder( - radius=shaft_radius, - height=shaft_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with Locations(Location((0.0, 0.0, shaft_height))): - Cylinder( - radius=head_radius, - height=FRONT_SCREW_HEAD_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with BuildSketch(Plane.XY.offset(shaft_height + FRONT_SCREW_HEAD_HEIGHT - socket_depth)): - RegularPolygon(radius=2.05, side_count=6, major_radius=True, rotation=30.0) - extrude(amount=socket_depth + 0.15, mode=Mode.SUBTRACT) - - part = screw.part - head_edges = [] - head_edges.extend(_circular_edges(part, radius=head_radius, coordinate=shaft_height)) - head_edges.extend(_circular_edges(part, radius=head_radius, coordinate=shaft_height + FRONT_SCREW_HEAD_HEIGHT)) - part = _safe_fillet(part, head_edges, 0.5) - part = part.moved(Location((x_pos, y_pos, RETAINING_Z_BOTTOM - 0.1))) - return _set_metadata(part, f"front_socket_head_cap_screw_{index + 1:02d}_gunmetal", GUNMETAL) - - -def _make_aperture_shadow(): - with BuildPart() as shadow: - with Locations(Location((0.0, 0.0, BLADE_Z_BOTTOM - 0.22))): - Cylinder( - radius=18.0, - height=0.12, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - return _set_metadata(shadow.part, "dark_central_aperture_void_36mm_visual_shadow", APERTURE_BLACK) - - -def gen_step(): - """Return a labeled mechanical iris aperture assembly in millimeters.""" - parts = [ - _make_aperture_shadow(), - _make_base_ring(), - *_make_tick_marks(), - _make_retaining_ring(), - ] - - for index in range(BLADE_COUNT): - parts.append(_make_iris_blade(index)) - - parts.append(_make_actuating_ring()) - - for index in range(BLADE_COUNT): - parts.append(_make_drive_pin(index)) - parts.append(_make_pivot_screw(index)) - - for index in range(FRONT_SCREW_COUNT): - parts.append(_make_front_socket_screw(index)) - - assembly = Compound( - obj=parts, - children=parts, - label=MODEL_LABEL, - ) - return assembly diff --git a/models/fun/mechanical_iris_aperture.step b/models/fun/mechanical_iris_aperture.step deleted file mode 100644 index 11f4e451..00000000 --- a/models/fun/mechanical_iris_aperture.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7381db48156166af3ba7c6737fded7365eaf29309b7f8bf2fab637b5e424bba -size 13683014 diff --git a/models/fun/miniature_spiral_staircase.py b/models/fun/miniature_spiral_staircase.py deleted file mode 100644 index 9eede52e..00000000 --- a/models/fun/miniature_spiral_staircase.py +++ /dev/null @@ -1,189 +0,0 @@ -from math import cos, radians, sin - -from build123d import ( - Align, - BuildLine, - BuildPart, - BuildSketch, - CenterArc, - Circle, - Color, - Compound, - Cylinder, - Edge, - Line, - Location, - Plane, - extrude, - make_face, - sweep, -) - - -# Units: millimeters. -# Origin: centered on the staircase axis; base/column bottom starts at Z=0. -# XY: plan view. +Z: staircase rise direction. - -BASE_DIAMETER = 90.0 -BASE_THICKNESS = 5.0 - -COLUMN_DIAMETER = 14.0 -COLUMN_HEIGHT = 140.0 - -TREAD_COUNT = 20 -TREAD_THICKNESS = 4.0 -TREAD_INNER_RADIUS = 10.0 -TREAD_OUTER_RADIUS = 62.0 -TREAD_SWEEP_DEGREES = 24.0 -TREAD_FIRST_BOTTOM_Z = 4.0 -TREAD_RISE = 6.0 -TREAD_ROTATION_DEGREES = 18.0 - -HANDRAIL_RADIUS = 66.0 -HANDRAIL_TUBE_DIAMETER = 5.0 -HANDRAIL_START_Z = 14.0 -HANDRAIL_END_Z = 130.0 - -BALUSTER_DIAMETER = 3.0 - -def _polar_point(radius: float, angle_degrees: float) -> tuple[float, float]: - angle_radians = radians(angle_degrees) - return (radius * cos(angle_radians), radius * sin(angle_radians)) - - -def _make_base(): - with BuildPart() as base: - Cylinder( - radius=BASE_DIAMETER / 2.0, - height=BASE_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = base.part - part.label = "circular_base_disk_diameter_90mm_thickness_5mm" - part.color = Color(0.42, 0.43, 0.45, 1.0) - return part - - -def _make_column(): - with BuildPart() as column: - Cylinder( - radius=COLUMN_DIAMETER / 2.0, - height=COLUMN_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = column.part - part.label = "central_column_diameter_14mm_height_140mm" - part.color = Color(0.70, 0.72, 0.74, 1.0) - return part - - -def _make_tread_profile(): - half_sweep = TREAD_SWEEP_DEGREES / 2.0 - start_angle = -half_sweep - end_angle = half_sweep - - with BuildPart() as tread: - with BuildSketch(Plane.XY): - with BuildLine(): - CenterArc((0.0, 0.0), TREAD_OUTER_RADIUS, start_angle, TREAD_SWEEP_DEGREES) - Line( - _polar_point(TREAD_OUTER_RADIUS, end_angle), - _polar_point(TREAD_INNER_RADIUS, end_angle), - ) - CenterArc((0.0, 0.0), TREAD_INNER_RADIUS, end_angle, -TREAD_SWEEP_DEGREES) - Line( - _polar_point(TREAD_INNER_RADIUS, start_angle), - _polar_point(TREAD_OUTER_RADIUS, start_angle), - ) - make_face() - extrude(amount=TREAD_THICKNESS) - - part = tread.part - part.label = "tread_profile_inner_10mm_outer_62mm_sweep_24deg" - part.color = Color(0.78, 0.58, 0.35, 1.0) - return part - - -def _make_tread(index: int): - angle_degrees = index * TREAD_ROTATION_DEGREES - bottom_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE - tread_profile = _make_tread_profile() - tread = tread_profile.moved(Location((0.0, 0.0, bottom_z), (0.0, 0.0, angle_degrees))) - tread.label = f"tread_{index + 1:02d}_bottom_z_{bottom_z:.0f}mm_angle_{angle_degrees:.0f}deg" - return tread - - -def _handrail_center_z(angle_degrees: float) -> float: - rail_rise = HANDRAIL_END_Z - HANDRAIL_START_Z - return HANDRAIL_START_Z + rail_rise * angle_degrees / 360.0 - - -def _make_handrail(): - rail_height = HANDRAIL_END_Z - HANDRAIL_START_Z - path = Edge.make_helix( - pitch=rail_height, - height=rail_height, - radius=HANDRAIL_RADIUS, - center=(0.0, 0.0, HANDRAIL_START_Z), - ) - start_tangent = path.tangent_at(0.0) - profile_plane = Plane( - origin=(HANDRAIL_RADIUS, 0.0, HANDRAIL_START_Z), - x_dir=(1.0, 0.0, 0.0), - z_dir=start_tangent, - ) - - with BuildPart() as rail: - with BuildSketch(profile_plane): - Circle(HANDRAIL_TUBE_DIAMETER / 2.0) - sweep(path=path, is_frenet=True) - - part = rail.part - part.label = "helical_outer_handrail_radius_66mm_diameter_5mm" - part.color = Color(0.22, 0.28, 0.34, 1.0) - return part - - -def _make_baluster(index: int): - angle_degrees = index * TREAD_ROTATION_DEGREES - tread_top_z = TREAD_FIRST_BOTTOM_Z + index * TREAD_RISE + TREAD_THICKNESS - rail_center_z = _handrail_center_z(angle_degrees) - x_pos, y_pos = _polar_point(TREAD_OUTER_RADIUS, angle_degrees) - height = rail_center_z - tread_top_z - - with BuildPart() as baluster: - Cylinder( - radius=BALUSTER_DIAMETER / 2.0, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = baluster.part.moved(Location((x_pos, y_pos, tread_top_z))) - part.label = f"baluster_{index + 1:02d}_diameter_3mm" - part.color = Color(0.30, 0.33, 0.36, 1.0) - return part - - -def gen_step(): - """Return a labeled miniature spiral staircase STEP compound in millimeters.""" - parts = [ - _make_base(), - _make_column(), - ] - - for index in range(TREAD_COUNT): - parts.append(_make_tread(index)) - - parts.append(_make_handrail()) - - for index in range(TREAD_COUNT): - parts.append(_make_baluster(index)) - - assembly = Compound( - obj=parts, - children=parts, - label="miniature_spiral_staircase", - ) - return assembly diff --git a/models/fun/miniature_spiral_staircase.step b/models/fun/miniature_spiral_staircase.step deleted file mode 100644 index 08af83dd..00000000 --- a/models/fun/miniature_spiral_staircase.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72ce8ee7afd69d11a69a9b95440d6445661f1e514e50d53a38d2a04aa5522384 -size 629787 diff --git a/models/fun/miniature_spiral_staircase_highres.3mf b/models/fun/miniature_spiral_staircase_highres.3mf deleted file mode 100644 index f984356e..00000000 --- a/models/fun/miniature_spiral_staircase_highres.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc12533cf9a596d67d4ecea5db64abea7ee923cad714076b7aeff652ebcadcfc -size 1837721 diff --git a/models/fun/miniature_spiral_staircase_highres.glb b/models/fun/miniature_spiral_staircase_highres.glb deleted file mode 100644 index ef80c959..00000000 --- a/models/fun/miniature_spiral_staircase_highres.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8ec02f41991acebbaeedef0fc091f31001ce4162ce25bf983298f9668535db7 -size 3260204 diff --git a/models/fun/miniature_spiral_staircase_highres.step b/models/fun/miniature_spiral_staircase_highres.step deleted file mode 100644 index 52eef3d5..00000000 --- a/models/fun/miniature_spiral_staircase_highres.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aec51b37f8728d5a9f04fc1b2e3b8a543130fa98cce972fc96098f7c156768f1 -size 626905 diff --git a/models/fun/miniature_spiral_staircase_highres.stl b/models/fun/miniature_spiral_staircase_highres.stl deleted file mode 100644 index 7dcfebba..00000000 --- a/models/fun/miniature_spiral_staircase_highres.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58aa68a62507fee5bc1f826d1ce5120a19737c318d5accf4b1611fcc49071dc5 -size 6360184 diff --git a/models/fun/pelican_riding_bicycle.py b/models/fun/pelican_riding_bicycle.py deleted file mode 100644 index 68172514..00000000 --- a/models/fun/pelican_riding_bicycle.py +++ /dev/null @@ -1,338 +0,0 @@ -from __future__ import annotations - -import math -from collections.abc import Sequence - -from build123d import ( - Box, - Color, - Compound, - Cone, - Cylinder, - Location, - Shape, - Sphere, - Torus, - chamfer, - fillet, - scale, -) -from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform -from OCP.BRepPrimAPI import BRepPrimAPI_MakeCylinder -from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf - - -DISPLAY_NAME = "Pelican riding a bicycle" - -# Units: millimeters. -# Origin: halfway between the wheel hubs on the bicycle center plane. -# X: rear-to-front bicycle direction, Y: wheel axle direction, +Z: up. - -COLORS = { - "rubber_black": (0.003, 0.003, 0.003, 1.0), - "rim_silver": (0.70, 0.72, 0.70, 1.0), - "spoke_steel": (0.82, 0.84, 0.82, 1.0), - "frame_blue": (0.05, 0.28, 0.75, 1.0), - "seat_brown": (0.28, 0.12, 0.05, 1.0), - "pelican_white": (0.96, 0.95, 0.88, 1.0), - "wing_gray": (0.66, 0.68, 0.64, 1.0), - "feather_dark": (0.08, 0.08, 0.075, 1.0), - "bill_yellow": (1.00, 0.78, 0.26, 1.0), - "pouch_orange": (0.95, 0.45, 0.12, 1.0), - "leg_orange": (0.98, 0.50, 0.12, 1.0), - "eye_black": (0.0, 0.0, 0.0, 1.0), -} - -Vector3 = tuple[float, float, float] - - -def _v_add(a: Vector3, b: Vector3) -> Vector3: - return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) - - -def _v_sub(a: Vector3, b: Vector3) -> Vector3: - return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) - - -def _v_mul(a: Vector3, scalar: float) -> Vector3: - return (a[0] * scalar, a[1] * scalar, a[2] * scalar) - - -def _v_dot(a: Vector3, b: Vector3) -> float: - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] - - -def _v_cross(a: Vector3, b: Vector3) -> Vector3: - return ( - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0], - ) - - -def _v_len(a: Vector3) -> float: - return math.sqrt(_v_dot(a, a)) - - -def _v_norm(a: Vector3) -> Vector3: - length = _v_len(a) - if length <= 1e-9: - raise ValueError("zero-length vector") - return (a[0] / length, a[1] / length, a[2] / length) - - -def _style(shape: Shape, label: str, color_name: str) -> Shape: - shape.label = label - shape.color = Color(*COLORS[color_name]) - return shape - - -def _group(label: str, children: Sequence[Shape], color_name: str | None = None) -> Compound: - compound = Compound(obj=list(children), children=list(children), label=label) - if color_name is not None: - compound.color = Color(*COLORS[color_name]) - return compound - - -def _softened(shape: Shape, radius: float) -> Shape: - try: - return fillet(shape.edges(), radius=radius) - except Exception: - try: - return chamfer(shape.edges(), length=radius * 0.45) - except Exception: - return shape - - -def _transformed(shape: Shape, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3) -> Shape: - x_dir = _v_norm(axis_x) - y_dir = _v_norm(axis_y) - z_dir = _v_norm(axis_z) - trsf = gp_Trsf() - trsf.SetValues( - x_dir[0], - y_dir[0], - z_dir[0], - center[0], - x_dir[1], - y_dir[1], - z_dir[1], - center[1], - x_dir[2], - y_dir[2], - z_dir[2], - center[2], - ) - return Shape(obj=BRepBuilderAPI_Transform(shape.wrapped, trsf, True).Shape()) - - -def _oriented_box( - label: str, - center: Vector3, - size_x: float, - size_y: float, - size_z: float, - axis_x: Vector3, - axis_y: Vector3, - axis_z: Vector3, - color_name: str, - *, - radius: float = 0.0, -) -> Shape: - shape = _transformed(Box(size_x, size_y, size_z), center, axis_x, axis_y, axis_z) - if radius > 0.0: - shape = _softened(shape, radius) - return _style(shape, label, color_name) - - -def _cylinder_between(label: str, p1: Vector3, p2: Vector3, radius: float, color_name: str) -> Shape: - axis = _v_sub(p2, p1) - length = _v_len(axis) - if length <= 1e-6: - return _style(Sphere(radius).moved(Location(p1)), label, color_name) - direction = _v_norm(axis) - wrapped = BRepPrimAPI_MakeCylinder( - gp_Ax2(gp_Pnt(*p1), gp_Dir(*direction)), - radius, - length, - ).Shape() - return _style(Shape(obj=wrapped), label, color_name) - - -def _cylinder_y(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return _style(Cylinder(radius, length, rotation=(90, 0, 0)).moved(Location(center)), label, color_name) - - -def _cylinder_x(label: str, center: Vector3, radius: float, length: float, color_name: str) -> Shape: - return _style(Cylinder(radius, length, rotation=(0, 90, 0)).moved(Location(center)), label, color_name) - - -def _ellipsoid(label: str, center: Vector3, radii: Vector3, color_name: str) -> Shape: - shape = scale(Sphere(1.0), by=radii).moved(Location(center)) - return _style(shape, label, color_name) - - -def _flat_cone_x( - label: str, - center: Vector3, - length: float, - base_radius: float, - tip_radius: float, - y_scale: float, - z_scale: float, - color_name: str, -) -> Shape: - cone = Cone(base_radius, tip_radius, length, rotation=(0, 90, 0)) - shape = scale(cone, by=(1.0, y_scale, z_scale)).moved(Location(center)) - return _style(shape, label, color_name) - - -def _make_wheel(label: str, center: Vector3) -> Compound: - cx, cy, cz = center - tire_major_radius = 40.0 - tire_minor_radius = 4.0 - rim_radius = 31.0 - spoke_radius = 0.72 - hub_radius = 4.2 - spoke_count = 20 - - parts: list[Shape] = [ - _style( - Torus(tire_major_radius, tire_minor_radius, rotation=(90, 0, 0)).moved(Location(center)), - f"{label}_continuous_black_tire", - "rubber_black", - ), - _style( - Torus(rim_radius, 1.45, rotation=(90, 0, 0)).moved(Location(center)), - f"{label}_silver_rim", - "rim_silver", - ), - _cylinder_y(f"{label}_hub_barrel", center, hub_radius, 14.0, "rim_silver"), - _cylinder_y(f"{label}_hub_axle_pin", center, 1.8, 24.0, "spoke_steel"), - ] - - for index in range(spoke_count): - theta = index * math.tau / spoke_count - rim_point = (cx + math.cos(theta) * (rim_radius - 1.8), cy, cz + math.sin(theta) * (rim_radius - 1.8)) - opposite_offset = -0.9 if index % 2 == 0 else 0.9 - hub_point = (cx, cy + opposite_offset, cz) - parts.append(_cylinder_between(f"{label}_radial_spoke_{index + 1:02d}", hub_point, rim_point, spoke_radius, "spoke_steel")) - - return _group(label, parts) - - -def _make_bicycle() -> Compound: - rear = (-55.0, 0.0, 44.0) - front = (55.0, 0.0, 44.0) - bottom = (0.0, 0.0, 49.0) - seat_lug = (-15.0, 0.0, 86.0) - head_low = (43.0, 0.0, 66.0) - head_high = (38.0, 0.0, 88.0) - handle_stem = (57.0, 0.0, 99.0) - - parts: list[Shape] = [ - _make_wheel("rear_wheel", rear), - _make_wheel("front_wheel", front), - _cylinder_between("frame_seat_tube", bottom, seat_lug, 2.3, "frame_blue"), - _cylinder_between("frame_top_tube", seat_lug, head_high, 2.2, "frame_blue"), - _cylinder_between("frame_down_tube", bottom, head_low, 2.5, "frame_blue"), - _cylinder_between("frame_head_tube", head_low, head_high, 2.6, "frame_blue"), - _cylinder_between("frame_left_chain_stay", (bottom[0], -4.2, bottom[2]), (rear[0], -4.2, rear[2]), 1.7, "frame_blue"), - _cylinder_between("frame_right_chain_stay", (bottom[0], 4.2, bottom[2]), (rear[0], 4.2, rear[2]), 1.7, "frame_blue"), - _cylinder_between("frame_left_seat_stay", (seat_lug[0], -3.8, seat_lug[2]), (rear[0], -3.8, rear[2]), 1.55, "frame_blue"), - _cylinder_between("frame_right_seat_stay", (seat_lug[0], 3.8, seat_lug[2]), (rear[0], 3.8, rear[2]), 1.55, "frame_blue"), - _cylinder_between("front_left_fork_blade", (head_low[0], -4.0, head_low[2]), (front[0], -4.0, front[2]), 1.7, "frame_blue"), - _cylinder_between("front_right_fork_blade", (head_low[0], 4.0, head_low[2]), (front[0], 4.0, front[2]), 1.7, "frame_blue"), - _cylinder_between("handlebar_stem", head_high, handle_stem, 1.8, "frame_blue"), - _cylinder_y("handlebar_crossbar", (64.0, 0.0, 101.0), 1.6, 38.0, "frame_blue"), - _cylinder_y("left_black_grip", (64.0, -22.5, 101.0), 2.2, 8.0, "rubber_black"), - _cylinder_y("right_black_grip", (64.0, 22.5, 101.0), 2.2, 8.0, "rubber_black"), - _cylinder_between("seat_post", seat_lug, (-17.0, 0.0, 95.0), 1.75, "spoke_steel"), - _style(_softened(Box(24.0, 17.0, 5.0).moved(Location((-20.0, 0.0, 97.0))), 1.2), "brown_saddle", "seat_brown"), - _style(Torus(7.5, 0.9, rotation=(90, 0, 0)).moved(Location(bottom)), "crank_ring", "spoke_steel"), - _cylinder_y("bottom_bracket_axle", bottom, 3.3, 20.0, "rim_silver"), - ] - - pedal_front = (12.5, -11.0, 38.5) - pedal_back = (-12.5, 11.0, 60.5) - parts.extend( - [ - _cylinder_between("front_crank_arm", bottom, pedal_front, 1.0, "spoke_steel"), - _cylinder_between("rear_crank_arm", bottom, pedal_back, 1.0, "spoke_steel"), - _style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_front)), 0.7), "front_pedal_flat", "rubber_black"), - _style(_softened(Box(15.0, 4.0, 2.5).moved(Location(pedal_back)), 0.7), "rear_pedal_flat", "rubber_black"), - ] - ) - - return _group("bicycle", parts, "frame_blue") - - -def _make_pelican() -> Compound: - parts: list[Shape] = [ - _ellipsoid("pelican_round_body", (-12.0, 0.0, 120.0), (24.0, 15.0, 29.0), "pelican_white"), - _ellipsoid("pelican_chest_belly_highlight", (1.0, 0.0, 112.0), (18.0, 13.0, 20.0), "pelican_white"), - _ellipsoid("left_folded_wing", (-22.0, -13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"), - _ellipsoid("right_folded_wing", (-22.0, 13.0, 120.0), (19.0, 3.2, 25.0), "wing_gray"), - _ellipsoid("left_dark_wing_tip", (-33.0, -15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"), - _ellipsoid("right_dark_wing_tip", (-33.0, 15.7, 104.0), (12.0, 2.2, 10.0), "feather_dark"), - _cylinder_between("curved_neck_lower_segment", (-1.0, 0.0, 139.0), (12.0, 0.0, 151.0), 5.0, "pelican_white"), - _cylinder_between("curved_neck_upper_segment", (12.0, 0.0, 151.0), (24.0, 0.0, 157.0), 4.4, "pelican_white"), - _ellipsoid("pelican_head", (28.0, 0.0, 160.0), (10.5, 8.1, 9.5), "pelican_white"), - _flat_cone_x("long_upper_bill", (52.0, 0.0, 160.5), 48.0, 5.2, 1.1, 1.45, 0.55, "bill_yellow"), - _ellipsoid("rounded_lower_bill_pouch", (49.0, 0.0, 151.5), (23.0, 7.0, 7.2), "pouch_orange"), - _cylinder_between("dark_bill_seam_left", (31.0, -7.3, 156.2), (70.0, -2.7, 157.0), 0.45, "feather_dark"), - _cylinder_between("dark_bill_seam_right", (31.0, 7.3, 156.2), (70.0, 2.7, 157.0), 0.45, "feather_dark"), - _ellipsoid("left_eye", (31.5, -7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"), - _ellipsoid("right_eye", (31.5, 7.4, 162.8), (1.45, 0.75, 1.45), "eye_black"), - _ellipsoid("soft_head_crest", (19.0, 0.0, 168.0), (4.0, 4.0, 5.0), "pelican_white"), - ] - - tail_roots = [(-34.0, -4.5, 108.0), (-38.0, 0.0, 110.0), (-34.0, 4.5, 108.0)] - for index, center in enumerate(tail_roots, start=1): - parts.append(_ellipsoid(f"short_tail_feather_{index}", center, (11.0, 2.0, 5.2), "feather_dark")) - - # Bent orange legs and webbed feet make the bird visibly seated on the cycle. - left_hip = (-8.0, -5.7, 96.0) - left_knee = (2.5, -7.5, 74.0) - left_pedal = (12.5, -11.0, 38.5) - right_hip = (-13.0, 5.7, 96.0) - right_knee = (-19.0, 8.4, 74.0) - right_pedal = (-12.5, 11.0, 60.5) - parts.extend( - [ - _cylinder_between("left_upper_leg", left_hip, left_knee, 1.8, "leg_orange"), - _cylinder_between("left_lower_leg", left_knee, left_pedal, 1.6, "leg_orange"), - _cylinder_between("right_upper_leg", right_hip, right_knee, 1.8, "leg_orange"), - _cylinder_between("right_lower_leg", right_knee, right_pedal, 1.6, "leg_orange"), - _ellipsoid("left_webbed_foot_on_forward_pedal", _v_add(left_pedal, (1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"), - _ellipsoid("right_webbed_foot_on_rear_pedal", _v_add(right_pedal, (-1.5, 0.0, 2.0)), (8.5, 3.5, 2.2), "leg_orange"), - ] - ) - - feather_line_specs = [ - ("left_wing_feather_line_1", (-17.0, -16.2, 136.0), (-33.0, -16.2, 112.0)), - ("left_wing_feather_line_2", (-11.0, -16.4, 126.0), (-34.0, -16.4, 101.0)), - ("right_wing_feather_line_1", (-17.0, 16.2, 136.0), (-33.0, 16.2, 112.0)), - ("right_wing_feather_line_2", (-11.0, 16.4, 126.0), (-34.0, 16.4, 101.0)), - ] - for label, p1, p2 in feather_line_specs: - parts.append(_cylinder_between(label, p1, p2, 0.55, "feather_dark")) - - return _group("pelican", parts, "pelican_white") - - -def _make_ground_shadow() -> Compound: - # Thin reference pads give the model a stable visual base without merging - # into the wheels. - front_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((55.0, 0.0, -2.0))), 1.0), "front_wheel_ground_shadow", "rubber_black") - rear_pad = _style(_softened(Box(92.0, 28.0, 1.0).moved(Location((-55.0, 0.0, -2.0))), 1.0), "rear_wheel_ground_shadow", "rubber_black") - return _group("subtle_ground_shadow", [front_pad, rear_pad]) - - -def gen_step() -> Compound: - bicycle = _make_bicycle() - pelican = _make_pelican() - ground = _make_ground_shadow() - assembly = _group("pelican_riding_bicycle", [bicycle, pelican, ground]) - assembly.label = "pelican_riding_bicycle" - return assembly diff --git a/models/fun/pelican_riding_bicycle.step b/models/fun/pelican_riding_bicycle.step deleted file mode 100644 index 09e0da13..00000000 --- a/models/fun/pelican_riding_bicycle.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d0f8704706e35f415ea28801f717ea5d01651bdbe07f55cee1a9f0be0bf885d2 -size 1055246 diff --git a/models/fun/photo_coffee_cup.py b/models/fun/photo_coffee_cup.py deleted file mode 100644 index 7b42d6d7..00000000 --- a/models/fun/photo_coffee_cup.py +++ /dev/null @@ -1,537 +0,0 @@ -from __future__ import annotations - -from math import degrees, cos, sin, sqrt - -from build123d import ( - Align, - BuildPart, - BuildSketch, - Color, - Compound, - Cone, - Cylinder, - Location, - Locations, - Mode, - Plane, - Rectangle, - RectangleRounded, - SlotOverall, - Solid, - Text, - TextAlign, - Torus, - extrude, -) - - -# Units: millimeters. -# Origin: center of the cup base footprint, XY on table plane, +Z upward. - -MODEL_LABEL = "photo_reference_takeaway_coffee_cup" - -PAPER = Color(0.86, 0.78, 0.64, 1.0) -PAPER_EDGE = Color(0.90, 0.82, 0.69, 1.0) -LID_WHITE = Color(0.96, 0.94, 0.90, 1.0) -LID_HIGHLIGHT = Color(1.0, 0.985, 0.955, 1.0) -LID_SHADOW = Color(0.82, 0.80, 0.76, 1.0) -BLUE_LOGO = Color(0.00, 0.62, 0.78, 1.0) -DARK_OPENING = Color(0.08, 0.075, 0.065, 1.0) - -BODY_HEIGHT = 108.0 -BODY_BOTTOM_RADIUS = 30.0 -BODY_TOP_RADIUS = 43.0 -BODY_WALL = 2.2 -BODY_BOTTOM_THICKNESS = 2.8 - -LID_SKIRT_BOTTOM_Z = 103.5 -LID_TOP_Z = 127.0 - -LOGO_LOCAL_BOTTOM_Z = -2.0 -LOGO_LOCAL_TOP_Z = 86.4 -LOGO_WORLD_BOTTOM_Z = 34.0 -LOGO_Z_SCALE = 0.56 -LOGO_X_SCALE = 0.70 -LOGO_WORLD_TOP_Z = LOGO_WORLD_BOTTOM_Z + (LOGO_LOCAL_TOP_Z - LOGO_LOCAL_BOTTOM_Z) * LOGO_Z_SCALE - - -def _set_metadata(part, label: str, color: Color): - part.label = label - part.color = color - return part - - -def _radius_at_z(z_pos: float) -> float: - slope = (BODY_TOP_RADIUS - BODY_BOTTOM_RADIUS) / BODY_HEIGHT - return BODY_BOTTOM_RADIUS + slope * z_pos - - -def _annular_cylinder( - *, - label: str, - outer_radius: float, - inner_radius: float, - height: float, - z_bottom: float, - color: Color, -) -> object: - with BuildPart() as ring: - with Locations(Location((0.0, 0.0, z_bottom))): - Cylinder( - radius=outer_radius, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with Locations(Location((0.0, 0.0, z_bottom - 0.75))): - Cylinder( - radius=inner_radius, - height=height + 1.5, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - return _set_metadata(ring.part, label, color) - - -def _solid_cylinder( - *, - label: str, - radius: float, - height: float, - z_bottom: float, - color: Color, -) -> object: - with BuildPart() as cylinder: - with Locations(Location((0.0, 0.0, z_bottom))): - Cylinder( - radius=radius, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - return _set_metadata(cylinder.part, label, color) - - -def _make_paper_cup_body() -> object: - extra_cut_height = 4.0 - slope = (BODY_TOP_RADIUS - BODY_BOTTOM_RADIUS) / BODY_HEIGHT - inner_bottom_radius = _radius_at_z(BODY_BOTTOM_THICKNESS) - BODY_WALL - inner_top_radius = BODY_TOP_RADIUS - BODY_WALL + slope * extra_cut_height - - with BuildPart() as cup: - Cone( - bottom_radius=BODY_BOTTOM_RADIUS, - top_radius=BODY_TOP_RADIUS, - height=BODY_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - with Locations(Location((0.0, 0.0, BODY_BOTTOM_THICKNESS))): - Cone( - bottom_radius=inner_bottom_radius, - top_radius=inner_top_radius, - height=BODY_HEIGHT - BODY_BOTTOM_THICKNESS + extra_cut_height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - return _set_metadata( - cup.part, - "tapered_beige_paper_cup_open_shell_12oz", - PAPER, - ) - - -def _make_bottom_foot_ring() -> object: - with BuildPart() as foot: - with Locations(Location((0.0, 0.0, 3.2))): - Torus(major_radius=BODY_BOTTOM_RADIUS - 2.0, minor_radius=1.25) - return _set_metadata(foot.part, "subtle_rolled_bottom_paper_foot_ring", PAPER_EDGE) - - -def _make_lid_skirt_parts() -> list[object]: - parts: list[object] = [] - parts.append( - _annular_cylinder( - label="white_plastic_lid_lower_snap_skirt_overhanging_cup", - outer_radius=47.8, - inner_radius=38.8, - height=8.0, - z_bottom=LID_SKIRT_BOTTOM_Z, - color=LID_WHITE, - ) - ) - parts.append( - _annular_cylinder( - label="thin_shadow_groove_between_lid_beads", - outer_radius=46.4, - inner_radius=42.3, - height=1.0, - z_bottom=112.5, - color=LID_SHADOW, - ) - ) - - for label, z_pos, major_radius, minor_radius in [ - ("rounded_lower_snap_lip_bead", 108.8, 45.9, 2.1), - ("rounded_middle_lid_bead", 113.6, 45.2, 1.55), - ("rounded_upper_lid_bead_under_dome", 117.2, 43.4, 1.25), - ]: - with BuildPart() as bead: - with Locations(Location((0.0, 0.0, z_pos))): - Torus(major_radius=major_radius, minor_radius=minor_radius) - parts.append(_set_metadata(bead.part, label, LID_WHITE)) - - return parts - - -def _make_lid_top() -> list[object]: - parts: list[object] = [] - - with BuildPart() as dome: - with Locations(Location((0.0, 0.0, 115.5))): - Cone( - bottom_radius=42.8, - top_radius=39.0, - height=9.4, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - parts.append(_set_metadata(dome.part, "shallow_sloped_lid_dome", LID_WHITE)) - - parts.append( - _solid_cylinder( - label="flat_raised_top_lid_disk", - radius=38.4, - height=2.1, - z_bottom=124.6, - color=LID_HIGHLIGHT, - ) - ) - - with BuildPart() as top_outer_ring: - with Locations(Location((0.0, 0.0, 126.9))): - Torus(major_radius=36.4, minor_radius=0.85) - parts.append(_set_metadata(top_outer_ring.part, "fine_raised_outer_top_emboss_ring", LID_HIGHLIGHT)) - - with BuildPart() as top_inner_ring: - with Locations(Location((0.0, 0.0, 127.15))): - Torus(major_radius=18.0, minor_radius=0.38) - parts.append(_set_metadata(top_inner_ring.part, "faint_central_emboss_circle", LID_HIGHLIGHT)) - - return parts - - -def _make_sip_feature() -> list[object]: - parts: list[object] = [] - top_plane = Plane.XY.offset(LID_TOP_Z) - - with BuildPart() as raised_sip: - with BuildSketch(top_plane): - with Locations((11.0, 18.4)): - SlotOverall(width=31.0, height=12.2, rotation=-9.0) - extrude(amount=1.2) - parts.append(_set_metadata(raised_sip.part, "raised_oval_sipping_platform_on_lid", LID_HIGHLIGHT)) - - with BuildPart() as sip_opening: - with BuildSketch(Plane.XY.offset(LID_TOP_Z + 1.22)): - with Locations((15.2, 20.0)): - SlotOverall(width=16.0, height=4.0, rotation=-9.0) - extrude(amount=0.45) - parts.append(_set_metadata(sip_opening.part, "dark_recessed_sip_slot_opening", DARK_OPENING)) - - with BuildPart() as nose_ridge: - with BuildSketch(Plane.XY.offset(LID_TOP_Z + 0.7)): - with Locations((0.5, 7.5)): - RectangleRounded(width=10.0, height=22.0, radius=2.0, rotation=-8.0) - extrude(amount=0.75) - parts.append(_set_metadata(nose_ridge.part, "low_centered_lid_drink_ridge", LID_HIGHLIGHT)) - - return parts - - -def _make_top_embossing() -> list[object]: - parts: list[object] = [] - emboss_plane = Plane.XY.offset(LID_TOP_Z + 0.18) - - with BuildPart() as caution_text: - with BuildSketch(emboss_plane): - with Locations((-2.5, -10.5)): - Text( - "CAUTION HOT", - font_size=4.0, - text_align=(TextAlign.CENTER, TextAlign.CENTER), - rotation=3.0, - ) - with Locations((-4.0, 6.0)): - Text( - "PLEASE RECYCLE", - font_size=3.0, - text_align=(TextAlign.CENTER, TextAlign.CENTER), - rotation=-4.0, - ) - extrude(amount=0.28) - parts.append(_set_metadata(caution_text.part, "low_raised_lid_embossed_warning_text", LID_HIGHLIGHT)) - - for index, (x_pos, y_pos, width, height, rotation) in enumerate( - [ - (-18.0, 4.5, 10.0, 1.1, 13.0), - (18.0, 4.5, 10.0, 1.1, -13.0), - (-15.0, -18.0, 8.5, 1.0, -16.0), - (15.0, -18.0, 8.5, 1.0, 16.0), - ], - start=1, - ): - with BuildPart() as tick: - with BuildSketch(Plane.XY.offset(LID_TOP_Z + 0.16)): - with Locations((x_pos, y_pos)): - Rectangle(width=width, height=height, rotation=rotation) - extrude(amount=0.24) - parts.append(_set_metadata(tick.part, f"short_emboss_tick_mark_{index:02d}", LID_HIGHLIGHT)) - - return parts - - -def _make_blue_logo() -> list[object]: - bottle_cells: list[object] = [] - z_bottom = LOGO_WORLD_BOTTOM_Z - z_top = LOGO_WORLD_TOP_Z - dz = 0.58 - - z_pos = z_bottom - cell_index = 1 - while z_pos < z_top: - z_next = min(z_pos + dz, z_top) - for interval in _blue_bottle_intervals(z_pos, z_next): - cell = _make_curved_decal_cell( - label=f"blue_bottle_logo_curved_surface_cell_{cell_index:03d}", - x_left_bottom=interval[0], - x_right_bottom=interval[1], - z_bottom=z_pos, - x_left_top=interval[2], - x_right_top=interval[3], - z_top=z_next, - color=BLUE_LOGO, - ) - if cell is not None: - bottle_cells.append(cell) - cell_index += 1 - z_pos = z_next - - mark_cells: list[object] = [] - z_pos = 34.65 - while z_pos < 37.35: - z_next = min(z_pos + 0.36, 37.35) - interval = _registered_mark_interval((z_pos + z_next) / 2.0) - if interval is not None: - cell = _make_curved_decal_cell( - label=f"blue_bottle_registered_mark_surface_cell_{len(mark_cells) + 1:02d}", - x_left_bottom=interval[0], - x_right_bottom=interval[1], - z_bottom=z_pos, - x_left_top=interval[0], - x_right_top=interval[1], - z_top=z_next, - color=BLUE_LOGO, - inner_offset=0.18, - outer_offset=0.72, - ) - if cell is not None: - mark_cells.append(cell) - z_pos = z_next - - logo = Compound( - obj=bottle_cells + mark_cells, - children=bottle_cells + mark_cells, - label="front_blue_bottle_logo_wrapped_on_cup_surface", - ) - logo.color = BLUE_LOGO - return [logo] - - -def _blue_bottle_intervals( - z_bottom: float, - z_top: float, -) -> list[tuple[float, float, float, float]]: - intervals: list[tuple[float, float, float, float]] = [] - bottom_intervals = _logo_outline_intervals_at(z_bottom + 1.0e-4) - top_intervals = _logo_outline_intervals_at(z_top - 1.0e-4) - for bottom_interval, top_interval in zip(bottom_intervals, top_intervals): - intervals.append((bottom_interval[0], bottom_interval[1], top_interval[0], top_interval[1])) - return intervals - - -def _logo_outline_intervals_at(z_pos: float) -> list[tuple[float, float]]: - local_z = (z_pos - LOGO_WORLD_BOTTOM_Z) / LOGO_Z_SCALE + LOGO_LOCAL_BOTTOM_Z - if local_z < LOGO_LOCAL_BOTTOM_Z or local_z > LOGO_LOCAL_TOP_Z: - return [] - - outline = [ - (-4.6, 86.4), - (-3.5, 86.1), - (-2.1, 84.6), - (-0.1, 83.7), - (3.0, 83.2), - (7.4, 83.1), - (8.7, 83.4), - (8.3, 82.2), - (5.9, 81.4), - (1.8, 81.2), - (-2.7, 81.7), - (-5.8, 82.0), - (-6.7, 83.0), - (-6.8, 84.8), - (-6.2, 85.9), - (-3.4, 80.7), - (-3.4, 73.5), - (-3.7, 68.2), - (-4.7, 64.3), - (-6.3, 61.1), - (-8.6, 58.7), - (-11.4, 56.8), - (-13.4, 54.4), - (-14.5, 50.8), - (-15.1, 45.2), - (-15.4, 37.0), - (-15.2, 28.4), - (-14.5, 20.0), - (-13.5, 11.9), - (-12.1, 4.1), - (-10.7, 0.7), - (-8.6, -0.8), - (-4.6, -1.7), - (-0.5, -2.0), - (4.0, -1.8), - (8.1, -0.9), - (10.7, 0.8), - (12.1, 4.5), - (13.5, 11.2), - (14.7, 19.4), - (15.4, 28.1), - (15.7, 37.2), - (15.5, 45.9), - (14.6, 52.0), - (13.0, 56.3), - (10.7, 59.8), - (8.0, 62.2), - (5.4, 63.7), - (2.8, 64.8), - (1.1, 66.5), - (0.3, 69.8), - (0.2, 74.9), - (0.2, 80.4), - ] - xs: list[float] = [] - for index, (x1, z1) in enumerate(outline): - x2, z2 = outline[(index + 1) % len(outline)] - if z1 == z2: - continue - if (z1 <= local_z < z2) or (z2 <= local_z < z1): - t = (local_z - z1) / (z2 - z1) - xs.append(x1 + (x2 - x1) * t) - xs.sort() - return [ - (xs[index] * LOGO_X_SCALE, xs[index + 1] * LOGO_X_SCALE) - for index in range(0, len(xs) - 1, 2) - ] - - -def _registered_mark_interval(z_pos: float) -> tuple[float, float] | None: - center_x = 14.2 - center_z = 36.0 - radius = 1.25 - dy = z_pos - center_z - if abs(dy) > radius: - return None - half_width = radius * sqrt(max(0.0, 1.0 - dy * dy / (radius * radius))) - return (center_x - half_width, center_x + half_width) - - -def _cup_surface_point(arc_x: float, z_pos: float, radial_offset: float) -> tuple[float, float, float]: - radius = _radius_at_z(z_pos) + radial_offset - angle = arc_x / radius - return (radius * sin(angle), -radius * cos(angle), z_pos) - - -def _make_curved_decal_cell( - *, - label: str, - x_left_bottom: float, - x_right_bottom: float, - z_bottom: float, - x_left_top: float, - x_right_top: float, - z_top: float, - color: Color, - inner_offset: float = 0.16, - outer_offset: float = 0.78, -) -> object | None: - x_left = (x_left_bottom + x_left_top) / 2.0 - x_right = (x_right_bottom + x_right_top) / 2.0 - width = x_right - x_left - height = z_top - z_bottom - if width < 0.18 or height <= 0.0: - return None - - z_center = (z_bottom + z_top) / 2.0 - thickness = outer_offset - inner_offset - max_segment_width = 3.2 - segment_count = max(1, int(width / max_segment_width + 0.999)) - segment_parts = [] - - for segment_index in range(segment_count): - segment_left = x_left + width * segment_index / segment_count - segment_right = x_left + width * (segment_index + 1) / segment_count - segment_width = segment_right - segment_left - x_center = (segment_left + segment_right) / 2.0 - segment_parts.append( - _make_surface_decal_box( - label=f"{label}_segment_{segment_index + 1:02d}", - x_center=x_center, - z_center=z_center, - width=segment_width + 0.04, - height=height + 0.035, - thickness=thickness, - radial_offset=inner_offset + thickness / 2.0, - color=color, - ) - ) - - if len(segment_parts) == 1: - return segment_parts[0] - - compound = Compound(obj=segment_parts, children=segment_parts, label=label) - compound.color = color - return compound - - -def _make_surface_decal_box( - *, - label: str, - x_center: float, - z_center: float, - width: float, - height: float, - thickness: float, - radial_offset: float, - color: Color, -) -> object: - radius = _radius_at_z(z_center) + radial_offset - angle = x_center / radius - center = _cup_surface_point(x_center, z_center, radial_offset) - box = Solid.make_box(width, thickness, height) - box = box.moved(Location((-width / 2.0, -thickness / 2.0, -height / 2.0))) - box = box.moved(Location(center, (0.0, 0.0, degrees(angle)))) - return _set_metadata(box, label, color) - - -def gen_step(): - """Return a photo-inspired takeaway coffee cup model in millimeters.""" - parts: list[object] = [ - _make_paper_cup_body(), - _make_bottom_foot_ring(), - ] - parts.extend(_make_lid_skirt_parts()) - parts.extend(_make_lid_top()) - parts.extend(_make_sip_feature()) - parts.extend(_make_top_embossing()) - parts.extend(_make_blue_logo()) - - return Compound(obj=parts, children=parts, label=MODEL_LABEL) diff --git a/models/fun/photo_coffee_cup.step b/models/fun/photo_coffee_cup.step deleted file mode 100644 index e2b9513e..00000000 --- a/models/fun/photo_coffee_cup.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6172f0d0a4903445e4a3a0543314e94b7dd8bc50cfb9db9dcdca7336a4b27609 -size 9305934 diff --git a/models/fun/planetary_gear_assembly.py b/models/fun/planetary_gear_assembly.py deleted file mode 100644 index 4aea4a40..00000000 --- a/models/fun/planetary_gear_assembly.py +++ /dev/null @@ -1,282 +0,0 @@ -from math import cos, pi, sin, tau - -from build123d import ( - Align, - BuildPart, - BuildSketch, - Color, - Compound, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - extrude, -) - - -# Units: millimeters. -# Origin: sun/ring center on the carrier axis. -# XY: gear plane. +Z: gear and pin axes. - -GEAR_THICKNESS = 8.0 -GEAR_BOTTOM_Z = 0.0 - -SUN_TEETH = 24 -SUN_PITCH_DIAMETER = 48.0 -SUN_ROOT_DIAMETER = 42.0 -SUN_OUTSIDE_DIAMETER = 54.0 -SUN_BORE_DIAMETER = 10.0 - -PLANET_TEETH = 18 -PLANET_PITCH_DIAMETER = 36.0 -PLANET_ROOT_DIAMETER = 31.0 -PLANET_OUTSIDE_DIAMETER = 41.0 -PLANET_CENTER_RADIUS = 42.0 -PLANET_COUNT = 3 -PLANET_BORE_DIAMETER = 6.8 - -EXTERNAL_TOOTH_ROOT_SPAN_FRACTION = 0.42 -EXTERNAL_TOOTH_TIP_SPAN_FRACTION = 0.18 - - -def _srgb_channel_to_linear(channel: int) -> float: - value = channel / 255.0 - if value <= 0.04045: - return value / 12.92 - return ((value + 0.055) / 1.055) ** 2.4 - - -def _srgb_color(hex_color: str) -> Color: - value = hex_color.removeprefix("#") - if len(value) != 6: - raise ValueError(f"Expected #rrggbb color, got {hex_color!r}") - return Color( - _srgb_channel_to_linear(int(value[0:2], 16)), - _srgb_channel_to_linear(int(value[2:4], 16)), - _srgb_channel_to_linear(int(value[4:6], 16)), - 1.0, - ) - - -# Match the docs app's dark-mode hero render source colors. -PLANET_COLORS = ( - _srgb_color("#61d4f6"), - _srgb_color("#a6ea90"), - _srgb_color("#ed9ee5"), -) - -RING_TEETH = 60 -RING_INTERNAL_PITCH_DIAMETER = 120.0 -RING_INTERNAL_ROOT_DIAMETER = 126.0 -RING_INTERNAL_TIP_DIAMETER = 115.0 -RING_OUTSIDE_DIAMETER = 140.0 -RING_TOOTH_ROOT_SPAN_FRACTION = 0.68 -RING_TOOTH_TIP_SPAN_FRACTION = 0.34 -RING_COLOR = _srgb_color("#c5adef") -SUN_COLOR = _srgb_color("#fdce76") -CARRIER_COLOR = _srgb_color("#bfcec8") -PIN_COLOR = _srgb_color("#818993") - -CARRIER_DIAMETER = 105.0 -CARRIER_BOTTOM_Z = -5.0 -CARRIER_TOP_Z = -1.0 -CARRIER_THICKNESS = CARRIER_TOP_Z - CARRIER_BOTTOM_Z - -PIN_DIAMETER = 6.0 -PIN_HEIGHT = 14.0 -PIN_BOTTOM_Z = -5.0 -PIN_CARRIER_CLEARANCE_DIAMETER = 6.4 - - -def _polar_point(radius: float, angle: float) -> tuple[float, float]: - return (radius * cos(angle), radius * sin(angle)) - - -def _trapezoid_tooth_profile( - *, - teeth: int, - root_radius: float, - tip_radius: float, - phase: float, - root_span_fraction: float = 0.72, - tip_span_fraction: float = 0.38, -) -> list[tuple[float, float]]: - """Return a closed-profile point loop with straight-sided schematic teeth.""" - pitch_angle = tau / teeth - points: list[tuple[float, float]] = [] - - for tooth_index in range(teeth): - center_angle = phase + tooth_index * pitch_angle - points.extend( - ( - _polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0), - _polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0), - ) - ) - - return points - - -def _make_external_gear( - *, - label: str, - teeth: int, - root_diameter: float, - outside_diameter: float, - phase: float, - center: tuple[float, float] = (0.0, 0.0), - bore_diameter: float | None = None, - color: Color | None = None, -): - with BuildPart() as gear: - with BuildSketch(Plane.XY): - Polygon( - _trapezoid_tooth_profile( - teeth=teeth, - root_radius=root_diameter / 2.0, - tip_radius=outside_diameter / 2.0, - phase=phase, - root_span_fraction=EXTERNAL_TOOTH_ROOT_SPAN_FRACTION, - tip_span_fraction=EXTERNAL_TOOTH_TIP_SPAN_FRACTION, - ), - align=None, - ) - extrude(amount=GEAR_THICKNESS) - - if bore_diameter is not None: - with Locations(Location((0.0, 0.0, -0.5))): - Cylinder( - radius=bore_diameter / 2.0, - height=GEAR_THICKNESS + 1.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = gear.part.moved(Location((center[0], center[1], GEAR_BOTTOM_Z))) - part.label = label - part.color = color - return part - - -def _make_internal_ring_gear(*, label: str, phase: float, color: Color | None = None): - with BuildPart() as ring: - Cylinder( - radius=RING_OUTSIDE_DIAMETER / 2.0, - height=GEAR_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - with BuildSketch(Plane.XY): - Polygon( - _trapezoid_tooth_profile( - teeth=RING_TEETH, - root_radius=RING_INTERNAL_ROOT_DIAMETER / 2.0, - tip_radius=RING_INTERNAL_TIP_DIAMETER / 2.0, - phase=phase, - root_span_fraction=RING_TOOTH_ROOT_SPAN_FRACTION, - tip_span_fraction=RING_TOOTH_TIP_SPAN_FRACTION, - ), - align=None, - ) - extrude(amount=GEAR_THICKNESS, mode=Mode.SUBTRACT) - - part = ring.part - part.label = label - part.color = color - return part - - -def _make_carrier_plate(): - with BuildPart() as carrier: - with Locations(Location((0.0, 0.0, CARRIER_BOTTOM_Z))): - Cylinder( - radius=CARRIER_DIAMETER / 2.0, - height=CARRIER_THICKNESS, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - for index in range(PLANET_COUNT): - center = _polar_point(PLANET_CENTER_RADIUS, tau * index / PLANET_COUNT) - with Locations(Location((center[0], center[1], CARRIER_BOTTOM_Z - 0.1))): - Cylinder( - radius=PIN_CARRIER_CLEARANCE_DIAMETER / 2.0, - height=CARRIER_THICKNESS + 0.2, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = carrier.part - part.label = "carrier_plate" - part.color = CARRIER_COLOR - return part - - -def _make_planet_pin(*, label: str, center: tuple[float, float]): - with BuildPart() as pin: - with Locations(Location((center[0], center[1], PIN_BOTTOM_Z))): - Cylinder( - radius=PIN_DIAMETER / 2.0, - height=PIN_HEIGHT, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - - part = pin.part - part.label = label - part.color = PIN_COLOR - return part - - -def _planet_center(index: int) -> tuple[float, float]: - angle = tau * index / PLANET_COUNT - return _polar_point(PLANET_CENTER_RADIUS, angle) - - -def gen_step(): - """Return a labeled simplified planetary gear assembly in millimeters.""" - sun_pitch_angle = tau / SUN_TEETH - ring_pitch_angle = tau / RING_TEETH - - parts = [ - _make_carrier_plate(), - _make_internal_ring_gear( - label="ring_gear_60_internal_teeth", - phase=-ring_pitch_angle / 2.0, - color=RING_COLOR, - ), - _make_external_gear( - label="sun_gear_24_teeth", - teeth=SUN_TEETH, - root_diameter=SUN_ROOT_DIAMETER, - outside_diameter=SUN_OUTSIDE_DIAMETER, - phase=-sun_pitch_angle / 2.0, - bore_diameter=SUN_BORE_DIAMETER, - color=SUN_COLOR, - ), - ] - - for index in range(PLANET_COUNT): - planet_angle = tau * index / PLANET_COUNT - center = _planet_center(index) - parts.append( - _make_external_gear( - label=f"planet_gear_{index + 1}_18_teeth", - teeth=PLANET_TEETH, - root_diameter=PLANET_ROOT_DIAMETER, - outside_diameter=PLANET_OUTSIDE_DIAMETER, - phase=planet_angle, - center=center, - bore_diameter=PLANET_BORE_DIAMETER, - color=PLANET_COLORS[index], - ) - ) - parts.append(_make_planet_pin(label=f"planet_pin_{index + 1}", center=center)) - - assembly = Compound( - obj=parts, - children=parts, - label="simplified_planetary_gear_assembly", - ) - return assembly diff --git a/models/fun/planetary_gear_assembly.step b/models/fun/planetary_gear_assembly.step deleted file mode 100644 index 8637d89a..00000000 --- a/models/fun/planetary_gear_assembly.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d4cf604e368a3e8249b104a8124001842e6d3015672a601dc7dddc9d0fcb6fbe -size 2271325 diff --git a/models/fun/robotic_hand_end_effector.py b/models/fun/robotic_hand_end_effector.py deleted file mode 100644 index ab191b6d..00000000 --- a/models/fun/robotic_hand_end_effector.py +++ /dev/null @@ -1,597 +0,0 @@ -from math import cos, radians, sin, tau - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Circle, - Color, - Compound, - Cone, - Cylinder, - Edge, - Location, - Locations, - Mode, - Plane, - Polygon, - RectangleRounded, - RegularPolygon, - Torus, - extrude, - fillet, - sweep, -) - - -# Units: millimeters. -# Origin: centered on the palm frame. -# XY: palm plane. +Z: back of hand/up. Fingers extend generally in +Y. - -PALM_WIDTH = 70.0 -PALM_LENGTH = 58.0 -PALM_THICKNESS = 8.0 -PALM_CORNER_RADIUS = 5.0 -PALM_TOP_Z = PALM_THICKNESS - -FINGER_LINK_WIDTH = 8.0 -FINGER_LINK_THICKNESS = 6.0 -FINGER_BASE_Z = PALM_TOP_Z + 4.5 -FINGER_YAW_DEGREES = { - "index": 2.0, - "middle": 0.0, - "ring": -2.0, - "little": -4.0, -} -MAIN_FINGER_BASES = { - "index": (-24.0, 30.0, FINGER_BASE_Z), - "middle": (-8.0, 30.5, FINGER_BASE_Z + 0.5), - "ring": (8.0, 30.0, FINGER_BASE_Z), - "little": (24.0, 29.0, FINGER_BASE_Z - 0.5), -} -MAIN_LINK_LENGTHS = { - "index": (25.0, 20.0, 16.0), - "middle": (25.0, 20.0, 16.0), - "ring": (25.0, 20.0, 16.0), - "little": (22.0, 17.0, 14.0), -} -MAIN_FINGER_FLEX = { - "index": (15.0, 40.0, 60.0), - "middle": (13.0, 37.0, 56.0), - "ring": (16.0, 42.0, 62.0), - "little": (18.0, 45.0, 65.0), -} - -THUMB_BASE = (-42.0, -5.0, FINGER_BASE_Z - 1.0) -THUMB_LINK_LENGTHS = (22.0, 18.0, 14.0) -THUMB_YAWS = (50.0, 36.0, 22.0) -THUMB_FLEX = (10.0, 30.0, 48.0) - -DISPLAY_BASE_BOTTOM_Z = -64.0 -DISPLAY_BASE_THICKNESS = 8.0 -DISPLAY_BASE_TOP_Z = DISPLAY_BASE_BOTTOM_Z + DISPLAY_BASE_THICKNESS -WRIST_CENTER = (0.0, -45.0, -12.0) -WRIST_RING_OUTER_RADIUS = 27.0 -WRIST_RING_INNER_RADIUS = 19.0 - -BRUSHED_ALUMINUM = Color(0.72, 0.74, 0.74, 1.0) -DARK_TITANIUM = Color(0.18, 0.20, 0.22, 1.0) -MATTE_BLACK = Color(0.01, 0.011, 0.012, 1.0) -DARK_GRAPHITE = Color(0.10, 0.105, 0.11, 1.0) -POLISHED_STEEL = Color(0.86, 0.88, 0.88, 1.0) -BLUE_ACCENT = Color(0.05, 0.33, 0.86, 1.0) -AMBER_ACCENT = Color(1.0, 0.55, 0.12, 1.0) - - -def _safe_fillet(edges, radius: float) -> None: - """Apply a cosmetic fillet inside a BuildPart context when topology allows it.""" - try: - fillet(edges, radius) - except Exception: - pass - - -def _set_metadata(part, label: str, color: Color): - part.label = label - part.color = color - return part - - -def _teardrop_points(center: tuple[float, float], width: float, height: float) -> list[tuple[float, float]]: - cx, cy = center - return [ - (cx - width / 2.0, cy - height * 0.40), - (cx + width / 2.0, cy - height * 0.40), - (cx + width * 0.18, cy + height * 0.18), - (cx, cy + height * 0.52), - (cx - width * 0.18, cy + height * 0.18), - ] - - -def _add_teardrop_sketch(center: tuple[float, float], width: float, height: float) -> None: - Polygon(_teardrop_points(center, width, height), align=None) - Circle(width * 0.28, align=Align.CENTER).locate( - Location((center[0], center[1] + height * 0.28, 0.0)) - ) - - -def _make_palm_frame(): - openings = [ - ((-20.0, 0.0), 12.5, 27.0), - ((0.0, 5.0), 13.5, 31.0), - ((20.0, 0.0), 12.5, 27.0), - ] - - with BuildPart() as palm: - with BuildSketch(Plane.XY): - RectangleRounded( - PALM_WIDTH, - PALM_LENGTH, - PALM_CORNER_RADIUS, - align=(Align.CENTER, Align.CENTER), - ) - extrude(amount=PALM_THICKNESS) - _safe_fillet(palm.edges(), 0.8) - - for center, width, height in openings: - with BuildSketch(Plane.XY.offset(-0.8)): - _add_teardrop_sketch(center, width, height) - extrude(amount=PALM_THICKNESS + 1.6, mode=Mode.SUBTRACT) - - return _set_metadata(palm.part, "skeletonized_palm_frame_70x58x8", BRUSHED_ALUMINUM) - - -def _make_palm_ribs(): - ribs = [] - openings = [ - ("left", (-20.0, 0.0), 12.5, 27.0), - ("center", (0.0, 5.0), 13.5, 31.0), - ("right", (20.0, 0.0), 12.5, 27.0), - ] - for name, center, width, height in openings: - with BuildPart() as rib: - with BuildSketch(Plane.XY.offset(PALM_TOP_Z)): - _add_teardrop_sketch(center, width + 6.0, height + 6.0) - extrude(amount=2.0) - - with BuildSketch(Plane.XY.offset(PALM_TOP_Z - 0.4)): - _add_teardrop_sketch(center, width + 0.6, height + 0.6) - extrude(amount=3.0, mode=Mode.SUBTRACT) - _safe_fillet(rib.edges(), 1.5) - - ribs.append(_set_metadata(rib.part, f"raised_palm_rib_{name}_3mm_lip", BRUSHED_ALUMINUM)) - - return ribs - - -def _make_display_base(): - with BuildPart() as base: - with BuildSketch(Plane.XY): - RectangleRounded(110.0, 80.0, 6.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=DISPLAY_BASE_THICKNESS) - _safe_fillet(base.edges(), 3.0) - - for x_pos in (-44.0, 44.0): - for y_pos in (-30.0, 30.0): - with Locations(Location((x_pos, y_pos, -0.6))): - Cylinder( - radius=2.35, - height=DISPLAY_BASE_THICKNESS + 1.2, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - with Locations(Location((x_pos, y_pos, DISPLAY_BASE_THICKNESS - 2.7))): - Cone( - bottom_radius=2.35, - top_radius=5.1, - height=2.9, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - part = base.part.moved(Location((0.0, -5.0, DISPLAY_BASE_BOTTOM_Z))) - return _set_metadata(part, "rounded_display_base_110x80x8_with_countersunk_holes", DARK_TITANIUM) - - -def _make_support_post(): - height = WRIST_CENTER[2] - WRIST_RING_OUTER_RADIUS - DISPLAY_BASE_TOP_Z - with BuildPart() as post: - Cylinder( - radius=6.5, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - _safe_fillet(post.edges(), 0.8) - - part = post.part.moved(Location((0.0, -45.0, DISPLAY_BASE_TOP_Z))) - return _set_metadata(part, "vertical_support_post_to_wrist_gimbal", DARK_TITANIUM) - - -def _make_wrist_gimbal(): - parts = [] - - with BuildPart() as wrist_cylinder: - Cylinder( - radius=21.0, - height=35.0, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - _safe_fillet(wrist_cylinder.edges(), 0.5) - cylinder = wrist_cylinder.part.moved(Location(WRIST_CENTER, (90.0, 0.0, 0.0))) - parts.append(_set_metadata(cylinder, "wrist_short_cylinder_axis_y_od42_length35", DARK_TITANIUM)) - - with BuildPart() as ring: - Cylinder( - radius=WRIST_RING_OUTER_RADIUS, - height=6.0, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - Cylinder( - radius=WRIST_RING_INNER_RADIUS, - height=7.0, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(ring.edges(), 0.5) - roll_ring = ring.part.moved(Location(WRIST_CENTER, (90.0, 0.0, 0.0))) - parts.append(_set_metadata(roll_ring, "perpendicular_wrist_roll_ring_od54_id38_thick6", DARK_TITANIUM)) - - for side, y_offset in (("front", 3.9), ("rear", -3.9)): - for index in range(8): - angle = tau * index / 8.0 - x_pos = WRIST_CENTER[0] + 23.7 * cos(angle) - z_pos = WRIST_CENTER[2] + 23.7 * sin(angle) - with BuildPart() as bolt: - Cylinder(radius=2.15, height=1.5, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(bolt.edges(), 0.5) - part = bolt.part.moved(Location((x_pos, WRIST_CENTER[1] + y_offset, z_pos), (90.0, 0.0, 0.0))) - parts.append( - _set_metadata( - part, - f"wrist_ring_{side}_polished_bolt_head_{index + 1:02d}", - POLISHED_STEEL, - ) - ) - - return parts - - -def _make_servo_housing(label: str, center: tuple[float, float, float], accent_color: Color): - with BuildPart() as servo: - Box(12.0, 18.0, 8.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(servo.edges(), 1.2) - body = servo.part.moved(Location(center)) - body = _set_metadata(body, f"dark_graphite_servo_housing_{label}_12x18x8", DARK_GRAPHITE) - - with BuildPart() as panel: - Box(8.0, 2.8, 0.6, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(panel.edges(), 0.25) - accent = panel.part.moved(Location((center[0], center[1] + 5.5, center[2] + 4.25))) - accent = _set_metadata(accent, f"{label}_servo_blue_amber_accent_panel", accent_color) - - screws = [] - for sx in (-4.2, 4.2): - for sy in (-6.3, 6.3): - screws.append( - _make_screw_head( - f"{label}_servo_screw_{len(screws) + 1:02d}", - (center[0] + sx, center[1] + sy, center[2] + 4.55), - radius=1.05, - height=0.55, - axis="z", - ) - ) - - return [body, accent, *screws] - - -def _make_screw_head(label: str, center: tuple[float, float, float], radius: float, height: float, axis: str): - with BuildPart() as screw: - Cylinder(radius=radius, height=height, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(screw.edges(), 0.35) - rotations = { - "x": (0.0, 90.0, 0.0), - "y": (90.0, 0.0, 0.0), - "z": (0.0, 0.0, 0.0), - } - part = screw.part.moved(Location(center, rotations[axis])) - return _set_metadata(part, f"polished_steel_{label}", POLISHED_STEEL) - - -def _make_palm_fasteners(): - parts = [] - locations = [ - (-29.0, -22.0), - (29.0, -22.0), - (-29.0, 22.0), - (29.0, 22.0), - (-11.0, -23.0), - (11.0, -23.0), - (-11.0, 24.0), - (11.0, 24.0), - ] - for index, (x_pos, y_pos) in enumerate(locations, start=1): - parts.append( - _make_screw_head( - f"palm_frame_socket_screw_{index:02d}", - (x_pos, y_pos, PALM_TOP_Z + 0.45), - radius=1.65, - height=0.9, - axis="z", - ) - ) - return parts - - -def _make_mechanical_link(label: str, length: float, start: tuple[float, float, float], flex: float, yaw: float): - with BuildPart() as link: - with BuildSketch(Plane.XY): - RectangleRounded( - FINGER_LINK_WIDTH, - length, - 2.0, - align=(Align.CENTER, Align.MIN), - ) - extrude(amount=FINGER_LINK_THICKNESS) - - with Locations(Location((0.0, length * 0.50, FINGER_LINK_THICKNESS - 0.55))): - Box( - FINGER_LINK_WIDTH * 0.48, - max(5.0, length * 0.48), - 1.3, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - for x_pos in (-FINGER_LINK_WIDTH / 2.0 - 0.15, FINGER_LINK_WIDTH / 2.0 + 0.15): - with Locations(Location((x_pos, length * 0.52, FINGER_LINK_THICKNESS * 0.52))): - Box( - 1.15, - max(5.0, length * 0.52), - 3.3, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(link.edges(), 1.0) - - part = link.part.moved(Location((0.0, 0.0, -FINGER_LINK_THICKNESS / 2.0))) - part = part.moved(Location(start, (-flex, 0.0, yaw))) - return _set_metadata(part, f"brushed_aluminum_{label}_rounded_recessed_link", BRUSHED_ALUMINUM) - - -def _segment_end(start: tuple[float, float, float], length: float, flex: float, yaw: float) -> tuple[float, float, float]: - edge = Edge.make_line((0.0, 0.0, 0.0), (0.0, length, 0.0)).moved( - Location(start, (-flex, 0.0, yaw)) - ) - candidates = [tuple(vertex) for vertex in edge.vertices()] - return max( - candidates, - key=lambda point: (point[0] - start[0]) ** 2 - + (point[1] - start[1]) ** 2 - + (point[2] - start[2]) ** 2, - ) - - -def _make_hinge_pin(label: str, center: tuple[float, float, float], yaw: float): - parts = [] - with BuildPart() as pin: - Cylinder(radius=4.0, height=12.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(pin.edges(), 0.5) - body = pin.part.moved(Location(center, (0.0, 90.0, yaw))) - parts.append(_set_metadata(body, f"polished_steel_{label}_hinge_pin_d8", POLISHED_STEEL)) - - for side, x_local in (("left", -6.8), ("right", 6.8)): - x_pos = center[0] + x_local * cos(radians(yaw)) - y_pos = center[1] + x_local * sin(radians(yaw)) - with BuildPart() as cap: - Cylinder(radius=5.0, height=1.5, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(cap.edges(), 0.5) - cap_part = cap.part.moved(Location((x_pos, y_pos, center[2]), (0.0, 90.0, yaw))) - parts.append(_set_metadata(cap_part, f"polished_steel_{label}_{side}_circular_end_cap", POLISHED_STEEL)) - - screw_offset = 0.85 if side == "right" else -0.85 - parts.append( - _make_screw_head( - f"{label}_{side}_visible_cap_screw", - ( - center[0] + (x_local + screw_offset) * cos(radians(yaw)), - center[1] + (x_local + screw_offset) * sin(radians(yaw)), - center[2], - ), - radius=1.35, - height=0.65, - axis="x", - ) - ) - - return parts - - -def _make_fingertip_pad(label: str, start: tuple[float, float, float], flex: float, yaw: float): - pad_length = 7.2 - with BuildPart() as pad: - with BuildSketch(Plane.XY): - RectangleRounded(9.5, pad_length, 3.3, align=(Align.CENTER, Align.MIN)) - extrude(amount=7.2) - _safe_fillet(pad.edges(), 1.0) - part = pad.part.moved(Location((0.0, 0.0, -3.6))) - part = part.moved(Location(start, (-flex, 0.0, yaw))) - return _set_metadata(part, f"matte_black_rubber_{label}_rounded_fingertip_pad", MATTE_BLACK) - - -def _make_thumb_saddle(): - with BuildPart() as saddle: - Box(13.0, 20.0, 9.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(saddle.edges(), 1.0) - part = saddle.part.moved(Location((-35.5, -4.0, PALM_TOP_Z + 1.5), (0.0, 0.0, -28.0))) - return _set_metadata(part, "brushed_aluminum_left_side_thumb_mount_saddle", BRUSHED_ALUMINUM) - - -def _make_fingers(): - parts = [] - - for finger_name, base in MAIN_FINGER_BASES.items(): - yaw = FINGER_YAW_DEGREES[finger_name] - lengths = MAIN_LINK_LENGTHS[finger_name] - flex_angles = MAIN_FINGER_FLEX[finger_name] - joint = base - - for joint_index, (length, flex_angle) in enumerate(zip(lengths, flex_angles), start=1): - parts.extend(_make_hinge_pin(f"{finger_name}_joint_{joint_index}", joint, yaw)) - parts.append( - _make_mechanical_link( - f"{finger_name}_phalange_{joint_index}_length_{length:.0f}mm", - length, - joint, - flex_angle, - yaw, - ) - ) - joint = _segment_end(joint, length, flex_angle, yaw) - - parts.append(_make_fingertip_pad(finger_name, joint, flex_angles[-1], yaw)) - - parts.append(_make_thumb_saddle()) - thumb_joint = THUMB_BASE - for joint_index, (length, flex_angle, yaw) in enumerate( - zip(THUMB_LINK_LENGTHS, THUMB_FLEX, THUMB_YAWS), - start=1, - ): - parts.extend(_make_hinge_pin(f"thumb_joint_{joint_index}", thumb_joint, yaw)) - parts.append( - _make_mechanical_link( - f"thumb_phalange_{joint_index}_length_{length:.0f}mm", - length, - thumb_joint, - flex_angle, - yaw, - ) - ) - thumb_joint = _segment_end(thumb_joint, length, flex_angle, yaw) - - parts.append(_make_fingertip_pad("thumb", thumb_joint, THUMB_FLEX[-1], THUMB_YAWS[-1])) - return parts - - -def _tube_profile_plane(start: tuple[float, float, float], next_point: tuple[float, float, float]) -> Plane: - tangent = ( - next_point[0] - start[0], - next_point[1] - start[1], - next_point[2] - start[2], - ) - return Plane(origin=start, x_dir=(1.0, 0.0, 0.0), z_dir=tangent) - - -def _make_tendon_tube(label: str, points: list[tuple[float, float, float]]): - path = Edge.make_spline(points) - with BuildPart() as tube: - with BuildSketch(_tube_profile_plane(points[0], points[1])): - Circle(1.0) - sweep(path=path, is_frenet=True) - _safe_fillet(tube.edges(), 0.2) - return _set_metadata(tube.part, f"matte_black_tendon_tube_{label}_diameter_2mm", MATTE_BLACK) - - -def _make_servos_and_tendons(): - parts = [] - servo_centers = { - "thumb": (-24.0, -15.0, PALM_TOP_Z + 5.0), - "index": (-12.0, -13.0, PALM_TOP_Z + 5.0), - "middle": (0.0, -15.5, PALM_TOP_Z + 5.0), - "ring": (12.0, -13.0, PALM_TOP_Z + 5.0), - "little": (24.0, -15.0, PALM_TOP_Z + 5.0), - } - - for index, (label, center) in enumerate(servo_centers.items()): - parts.extend(_make_servo_housing(label, center, BLUE_ACCENT if index % 2 else AMBER_ACCENT)) - - tendon_targets = { - "thumb": THUMB_BASE, - "index": MAIN_FINGER_BASES["index"], - "middle": MAIN_FINGER_BASES["middle"], - "ring": MAIN_FINGER_BASES["ring"], - "little": MAIN_FINGER_BASES["little"], - } - for label, start_center in servo_centers.items(): - target = tendon_targets[label] - start = (start_center[0], start_center[1] + 9.0, start_center[2] + 3.7) - if label == "thumb": - points = [ - start, - (-36.0, -11.0, PALM_TOP_Z + 14.0), - (-43.5, -3.0, PALM_TOP_Z + 8.0), - (target[0], target[1], target[2] + 1.0), - ] - else: - points = [ - start, - (start[0] * 0.75, 7.0, PALM_TOP_Z + 14.0), - (target[0] * 0.96, 22.0, PALM_TOP_Z + 8.5), - (target[0], target[1] - 1.5, target[2] + 0.6), - ] - parts.append(_make_tendon_tube(label, points)) - - return parts - - -def _make_knuckle_yokes(): - parts = [] - for name, center in MAIN_FINGER_BASES.items(): - with BuildPart() as yoke: - Box(12.0, 7.0, 7.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - Cylinder( - radius=4.25, - height=13.0, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - rotation=(0.0, 90.0, FINGER_YAW_DEGREES[name]), - mode=Mode.SUBTRACT, - ) - _safe_fillet(yoke.edges(), 0.8) - part = yoke.part.moved(Location((center[0], center[1] - 2.5, center[2] - 0.6))) - parts.append(_set_metadata(part, f"brushed_aluminum_{name}_base_knuckle_yoke", BRUSHED_ALUMINUM)) - - return parts - - -def _make_decorative_hex_fasteners(): - parts = [] - for index, (x_pos, y_pos, z_pos) in enumerate( - [ - (-48.0, -36.0, DISPLAY_BASE_TOP_Z + 0.45), - (48.0, -36.0, DISPLAY_BASE_TOP_Z + 0.45), - (-48.0, 26.0, DISPLAY_BASE_TOP_Z + 0.45), - (48.0, 26.0, DISPLAY_BASE_TOP_Z + 0.45), - ], - start=1, - ): - with BuildPart() as fastener: - with BuildSketch(Plane.XY): - RegularPolygon(3.2, 6, rotation=30.0) - extrude(amount=0.9) - _safe_fillet(fastener.edges(), 0.25) - part = fastener.part.moved(Location((x_pos, y_pos, z_pos))) - parts.append(_set_metadata(part, f"polished_steel_display_base_hex_fastener_{index:02d}", POLISHED_STEEL)) - return parts - - -def gen_step(): - """Return a labeled cybernetic robotic hand end-effector STEP assembly.""" - parts = [ - _make_display_base(), - _make_support_post(), - _make_palm_frame(), - *_make_palm_ribs(), - *_make_palm_fasteners(), - *_make_wrist_gimbal(), - *_make_knuckle_yokes(), - *_make_fingers(), - *_make_servos_and_tendons(), - *_make_decorative_hex_fasteners(), - ] - - assembly = Compound( - obj=parts, - children=parts, - label="articulated_cybernetic_robotic_hand_end_effector", - ) - return assembly diff --git a/models/fun/robotic_hand_end_effector.step b/models/fun/robotic_hand_end_effector.step deleted file mode 100644 index f8b8774d..00000000 --- a/models/fun/robotic_hand_end_effector.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2f58a2d464c140f576dfb6f8ec041201653a5ae9a129c3152768f518ff4baae -size 5745283 diff --git a/models/fun/six_axis_industrial_robot_arm.py b/models/fun/six_axis_industrial_robot_arm.py deleted file mode 100644 index 349adc69..00000000 --- a/models/fun/six_axis_industrial_robot_arm.py +++ /dev/null @@ -1,613 +0,0 @@ -from math import atan2, cos, degrees, radians, sin, tau - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Circle, - Color, - Compound, - Cone, - Cylinder, - Edge, - Location, - Locations, - Mode, - Plane, - Polygon, - RectangleRounded, - add, - extrude, - fillet, - sweep, -) - - -# Units: millimeters. -# Origin: axis 1/base yaw center at X=0, Y=0. XY is the floor/display plane, +Z is up. -# Static display pose: raised shoulder, bent elbow, compact 3-axis wrist reaching mostly +X. - -DISPLAY_BASE_LENGTH = 180.0 -DISPLAY_BASE_WIDTH = 140.0 -DISPLAY_BASE_THICKNESS = 12.0 -DISPLAY_BASE_CENTER = (60.0, 0.0, 0.0) - -FIXED_BASE_DIAMETER = 82.0 -FIXED_BASE_BOTTOM_Z = 12.0 -FIXED_BASE_TOP_Z = 42.0 - -TURNTABLE_DIAMETER = 72.0 -TURNTABLE_BOTTOM_Z = 42.0 -TURNTABLE_TOP_Z = 58.0 - -SHOULDER_CENTER = (0.0, 0.0, 90.0) -ELBOW_CENTER = (74.0, 0.0, 142.0) -WRIST_ROLL_CENTER = (154.0, 0.0, 128.0) -WRIST_PITCH_CENTER = (174.0, 0.0, 128.0) -TOOL_ROLL_CENTER = (196.0, 0.0, 128.0) -TOOL_FLANGE_CENTER = (216.0, 0.0, 128.0) - -MATTE_WHITE = Color(0.91, 0.92, 0.90, 1.0) -LIGHT_GRAY = Color(0.73, 0.76, 0.76, 1.0) -DARK_GRAPHITE = Color(0.10, 0.11, 0.12, 1.0) -DEEP_GRAPHITE = Color(0.045, 0.048, 0.052, 1.0) -BRUSHED_STEEL = Color(0.74, 0.76, 0.76, 1.0) -DARK_STEEL = Color(0.24, 0.25, 0.26, 1.0) -BLUE_ACCENT = Color(0.02, 0.30, 0.82, 1.0) -ORANGE_ACCENT = Color(1.0, 0.46, 0.10, 1.0) -MATTE_BLACK = Color(0.005, 0.006, 0.007, 1.0) - - -def _set_metadata(part, label: str, color: Color): - part.label = label - part.color = color - return part - - -def _safe_fillet(edges, radius: float) -> None: - selected = list(edges) - if not selected: - return - try: - fillet(selected, radius=radius) - except Exception: - pass - - -def _axis_rotation(axis: str) -> tuple[float, float, float]: - if axis == "x": - return (0.0, 90.0, 0.0) - if axis == "y": - return (90.0, 0.0, 0.0) - return (0.0, 0.0, 0.0) - - -def _make_axis_cylinder( - label: str, - axis: str, - center: tuple[float, float, float], - radius: float, - length: float, - color: Color, - fillet_radius: float = 0.0, -): - with BuildPart() as cyl: - Cylinder( - radius=radius, - height=length, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - if fillet_radius > 0.0: - _safe_fillet(cyl.edges(), fillet_radius) - part = cyl.part.moved(Location(center, _axis_rotation(axis))) - return _set_metadata(part, label, color) - - -def _make_axis_ring( - label: str, - axis: str, - center: tuple[float, float, float], - outer_radius: float, - inner_radius: float, - thickness: float, - color: Color, -): - with BuildPart() as ring: - Cylinder( - radius=outer_radius, - height=thickness, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - Cylinder( - radius=inner_radius, - height=thickness + 0.4, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(ring.edges(), 0.25) - part = ring.part.moved(Location(center, _axis_rotation(axis))) - return _set_metadata(part, label, color) - - -def _make_screw_head( - label: str, - axis: str, - center: tuple[float, float, float], - radius: float, - height: float, - color: Color = BRUSHED_STEEL, -): - with BuildPart() as screw: - Cylinder( - radius=radius, - height=height, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - with Locations(Location((0.0, 0.0, height / 2.0 - 0.18))): - Cylinder( - radius=radius * 0.43, - height=0.45, - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(screw.edges(), min(0.5, radius * 0.22)) - part = screw.part.moved(Location(center, _axis_rotation(axis))) - return _set_metadata(part, label, color) - - -def _make_rounded_box( - label: str, - size: tuple[float, float, float], - center: tuple[float, float, float], - radius: float, - color: Color, - rotation: tuple[float, float, float] = (0.0, 0.0, 0.0), -): - with BuildPart() as box: - Box(*size, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - if radius > 0.0: - _safe_fillet(box.edges(), radius) - part = box.part.moved(Location(center, rotation)) - return _set_metadata(part, label, color) - - -def _rounded_plate_profile_points( - x_min: float, - x_max: float, - z_min: float, - z_max: float, - radius: float, - steps: int = 8, -) -> list[tuple[float, float]]: - points: list[tuple[float, float]] = [] - centers = ( - (x_max - radius, z_min + radius, -90.0, 0.0), - (x_max - radius, z_max - radius, 0.0, 90.0), - (x_min + radius, z_max - radius, 90.0, 180.0), - (x_min + radius, z_min + radius, 180.0, 270.0), - ) - for cx, cz, start, end in centers: - for step in range(steps + 1): - angle = radians(start + (end - start) * step / steps) - points.append((cx + radius * cos(angle), cz + radius * sin(angle))) - return points - - -def _outer_rect_edges(part, length: float, width: float, z_min: float, z_max: float, tol: float = 0.08): - edges = [] - for edge in part.edges(): - bbox = edge.bounding_box() - center = bbox.center() - on_outer_x = abs(abs(center.X) - length / 2.0) <= tol - on_outer_y = abs(abs(center.Y) - width / 2.0) <= tol - on_top_or_bottom = abs(center.Z - z_min) <= tol or abs(center.Z - z_max) <= tol - if (on_outer_x or on_outer_y) and on_top_or_bottom: - edges.append(edge) - return edges - - -def _make_display_base(): - with BuildPart() as base: - with BuildSketch(Plane.XY): - RectangleRounded( - DISPLAY_BASE_LENGTH, - DISPLAY_BASE_WIDTH, - 8.0, - align=(Align.CENTER, Align.CENTER), - ) - extrude(amount=DISPLAY_BASE_THICKNESS) - - for x_abs in (-20.0, 120.0): - for y_pos in (-50.0, 50.0): - x_pos = x_abs - DISPLAY_BASE_CENTER[0] - with Locations(Location((x_pos, y_pos, -0.6))): - Cylinder( - radius=3.0, - height=DISPLAY_BASE_THICKNESS + 1.2, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - with Locations(Location((x_pos, y_pos, DISPLAY_BASE_THICKNESS - 3.2))): - Cone( - bottom_radius=3.0, - top_radius=6.0, - height=3.4, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - _safe_fillet( - _outer_rect_edges(base.part, DISPLAY_BASE_LENGTH, DISPLAY_BASE_WIDTH, 0.0, DISPLAY_BASE_THICKNESS), - 3.0, - ) - - part = base.part.moved(Location(DISPLAY_BASE_CENTER)) - return _set_metadata(part, "rounded_rectangular_display_base_180x140x12_with_four_countersunk_holes", DARK_GRAPHITE) - - -def _make_fixed_base(): - return _make_axis_cylinder( - "fixed_cylindrical_base_axis1_od82_z12_to_z42", - "z", - (0.0, 0.0, (FIXED_BASE_BOTTOM_Z + FIXED_BASE_TOP_Z) / 2.0), - FIXED_BASE_DIAMETER / 2.0, - FIXED_BASE_TOP_Z - FIXED_BASE_BOTTOM_Z, - LIGHT_GRAY, - fillet_radius=2.0, - ) - - -def _make_turntable(): - return _make_axis_cylinder( - "rotating_base_turntable_axis1_od72_z42_to_z58", - "z", - (0.0, 0.0, (TURNTABLE_BOTTOM_Z + TURNTABLE_TOP_Z) / 2.0), - TURNTABLE_DIAMETER / 2.0, - TURNTABLE_TOP_Z - TURNTABLE_BOTTOM_Z, - DARK_GRAPHITE, - fillet_radius=1.5, - ) - - -def _make_pedestal(): - with BuildPart() as pedestal: - with BuildSketch(Plane.XY): - RectangleRounded(46.0, 58.0, 7.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=43.0) - _safe_fillet(pedestal.edges(), 2.5) - part = pedestal.part.moved(Location((0.0, 0.0, 47.0))) - return _set_metadata(part, "rounded_shoulder_pedestal_46x58x43_to_axis2_center", MATTE_WHITE) - - -def _make_shoulder_lug(label: str, y_center: float): - with BuildPart() as lug: - with Locations(Location((3.0, y_center, 73.5))): - Box(34.0, 12.0, 37.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - with Locations(Location((0.0, y_center, 90.0))): - Cylinder( - radius=22.0, - height=12.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - Cylinder( - radius=13.0, - height=14.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(lug.edges(), 1.6) - return _set_metadata(lug.part, label, MATTE_WHITE) - - -def _make_upper_arm_link(label: str, y_center: float): - shoulder = SHOULDER_CENTER - elbow = ELBOW_CENTER - dx = elbow[0] - shoulder[0] - dz = elbow[2] - shoulder[2] - link_length = (dx * dx + dz * dz) ** 0.5 + 8.0 - link_angle = -degrees(atan2(dz, dx)) - mid = ((shoulder[0] + elbow[0]) / 2.0, y_center, (shoulder[2] + elbow[2]) / 2.0) - - with BuildPart() as link: - with BuildSketch(Plane.XZ.offset(-5.0)): - RectangleRounded(link_length, 18.0, 8.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=10.0) - - with BuildSketch(Plane.XZ.offset(-6.2)): - RectangleRounded(56.0, 7.4, 3.5, align=(Align.CENTER, Align.CENTER)) - extrude(amount=12.4, mode=Mode.SUBTRACT) - _safe_fillet(link.edges(), 1.5) - - part = link.part.moved(Location(mid, (0.0, link_angle, 0.0))) - return _set_metadata(part, label, MATTE_WHITE) - - -def _make_forearm_housing(): - elbow = ELBOW_CENTER - wrist = WRIST_ROLL_CENTER - dx = wrist[0] - elbow[0] - dz = wrist[2] - elbow[2] - length = (dx * dx + dz * dz) ** 0.5 - angle = -degrees(atan2(dz, dx)) - center = ((elbow[0] + wrist[0]) / 2.0, 0.0, (elbow[2] + wrist[2]) / 2.0) - - with BuildPart() as forearm: - Cone( - bottom_radius=16.0, - top_radius=12.0, - height=length, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - _safe_fillet(forearm.edges(), 1.6) - - part = forearm.part.moved(Location(center, (0.0, angle, 0.0))) - return _set_metadata(part, "tapered_rounded_forearm_housing_elbow_to_wrist_axis4", MATTE_WHITE) - - -def _make_forearm_panel(label: str, side: float): - elbow = ELBOW_CENTER - wrist = WRIST_ROLL_CENTER - dx = wrist[0] - elbow[0] - dz = wrist[2] - elbow[2] - angle = -degrees(atan2(dz, dx)) - center = ((elbow[0] + wrist[0]) / 2.0, 0.0, (elbow[2] + wrist[2]) / 2.0) - - with BuildPart() as panel: - with Locations(Location((6.0, side * 15.3, 0.0))): - Box(50.0, 1.2, 10.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - _safe_fillet(panel.edges(), 0.7) - - part = panel.part.moved(Location(center, (0.0, angle, 0.0))) - return _set_metadata(part, label, DEEP_GRAPHITE) - - -def _rotated_y_point( - local: tuple[float, float, float], - center: tuple[float, float, float], - angle_deg: float, -) -> tuple[float, float, float]: - angle = radians(angle_deg) - x_pos = center[0] + local[0] * cos(angle) + local[2] * sin(angle) - y_pos = center[1] + local[1] - z_pos = center[2] - local[0] * sin(angle) + local[2] * cos(angle) - return (x_pos, y_pos, z_pos) - - -def _forearm_panel_screws(parts: list): - elbow = ELBOW_CENTER - wrist = WRIST_ROLL_CENTER - dx = wrist[0] - elbow[0] - dz = wrist[2] - elbow[2] - angle = -degrees(atan2(dz, dx)) - center = ((elbow[0] + wrist[0]) / 2.0, 0.0, (elbow[2] + wrist[2]) / 2.0) - for side_name, side in (("positive_y", 1.0), ("negative_y", -1.0)): - for index, local_x in enumerate((-16.0, 25.0), start=1): - screw_center = _rotated_y_point((local_x, side * 16.1, 0.0), center, angle) - parts.append( - _make_screw_head( - f"forearm_recessed_panel_{side_name}_socket_screw_{index}", - "y", - screw_center, - 1.55, - 1.0, - ) - ) - - -def _add_circular_screws( - parts: list, - prefix: str, - axis: str, - axis_center: tuple[float, float, float], - face_coordinates: tuple[float, ...], - bolt_circle_radius: float, - count: int, - screw_radius: float, - screw_height: float, - start_angle: float = 0.0, -): - for face_index, face_coord in enumerate(face_coordinates, start=1): - for index in range(count): - angle = start_angle + tau * index / count - if axis == "z": - center = ( - axis_center[0] + bolt_circle_radius * cos(angle), - axis_center[1] + bolt_circle_radius * sin(angle), - face_coord, - ) - elif axis == "y": - center = ( - axis_center[0] + bolt_circle_radius * cos(angle), - face_coord, - axis_center[2] + bolt_circle_radius * sin(angle), - ) - else: - center = ( - face_coord, - axis_center[1] + bolt_circle_radius * cos(angle), - axis_center[2] + bolt_circle_radius * sin(angle), - ) - parts.append( - _make_screw_head( - f"{prefix}_face_{face_index}_socket_head_screw_{index + 1:02d}", - axis, - center, - screw_radius, - screw_height, - ) - ) - - -def _make_wrist_yoke_lug(label: str, y_center: float): - with BuildPart() as lug: - with Locations(Location((164.5, y_center, 128.0))): - Box(25.0, 7.0, 17.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - with Locations(Location((174.0, y_center, 128.0))): - Cylinder( - radius=14.5, - height=7.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - Cylinder( - radius=8.0, - height=8.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(lug.edges(), 1.0) - return _set_metadata(lug.part, label, MATTE_WHITE) - - -def _make_tool_flange(): - with BuildPart() as flange: - Cylinder( - radius=19.0, - height=7.0, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - ) - Cylinder( - radius=5.0, - height=9.0, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - for index in range(6): - angle = tau * index / 6.0 - y_pos = 14.0 * cos(angle) - z_pos = 14.0 * sin(angle) - with Locations(Location((0.0, y_pos, z_pos))): - Cylinder( - radius=2.0, - height=9.0, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(flange.edges(), 0.6) - part = flange.part.moved(Location(TOOL_FLANGE_CENTER)) - return _set_metadata(part, "standard_tool_flange_axis6_od38_thick7_with_10mm_bore_and_6x4mm_holes", BRUSHED_STEEL) - - -def _make_cable_guide(): - points = [ - (-15.0, -31.0, 94.0), - (8.0, -31.0, 101.0), - (36.0, -30.0, 119.0), - (66.0, -28.0, 139.0), - (78.0, -27.0, 148.0), - (104.0, -28.0, 142.0), - (134.0, -27.0, 135.0), - (155.0, -22.0, 132.0), - (174.0, -18.0, 130.0), - ] - path = Edge.make_spline(points) - tangent = ( - points[1][0] - points[0][0], - points[1][1] - points[0][1], - points[1][2] - points[0][2], - ) - profile_plane = Plane(origin=points[0], x_dir=(0.0, 1.0, 0.0), z_dir=tangent) - - with BuildPart() as cable: - with BuildSketch(profile_plane): - Circle(2.0) - sweep(path=path, is_frenet=True) - _safe_fillet(cable.edges(), 0.2) - return _set_metadata(cable.part, "matte_black_external_cable_guide_4mm_swept_tube_shoulder_to_wrist", MATTE_BLACK) - - -def _make_cable_clamp(label: str, center: tuple[float, float, float], angle_y: float): - with BuildPart() as clamp: - Box(12.0, 6.0, 5.0, align=(Align.CENTER, Align.CENTER, Align.CENTER)) - Cylinder( - radius=2.2, - height=14.0, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - _safe_fillet(clamp.edges(), 0.6) - part = clamp.part.moved(Location(center, (0.0, angle_y, 0.0))) - return _set_metadata(part, label, DARK_STEEL) - - -def _make_axis_label_ring_set(parts: list): - parts.append(_make_axis_ring("blue_accent_ring_axis1_base_yaw_vertical_z", "z", (0.0, 0.0, 58.9), 38.0, 35.4, 1.6, BLUE_ACCENT)) - parts.append(_make_axis_ring("orange_accent_ring_axis2_shoulder_pitch_y_left", "y", (0.0, -37.1, 90.0), 25.0, 22.0, 1.8, ORANGE_ACCENT)) - parts.append(_make_axis_ring("orange_accent_ring_axis2_shoulder_pitch_y_right", "y", (0.0, 37.1, 90.0), 25.0, 22.0, 1.8, ORANGE_ACCENT)) - parts.append(_make_axis_ring("blue_accent_ring_axis3_elbow_pitch_y_left", "y", (74.0, -34.6, 142.0), 24.0, 21.0, 1.8, BLUE_ACCENT)) - parts.append(_make_axis_ring("blue_accent_ring_axis3_elbow_pitch_y_right", "y", (74.0, 34.6, 142.0), 24.0, 21.0, 1.8, BLUE_ACCENT)) - parts.append(_make_axis_ring("orange_accent_ring_axis4_wrist_roll_x", "x", (154.0, 0.0, 128.0), 17.0, 15.0, 2.0, ORANGE_ACCENT)) - parts.append(_make_axis_ring("blue_accent_ring_axis5_wrist_pitch_y", "y", (174.0, 0.0, 128.0), 16.0, 14.0, 2.0, BLUE_ACCENT)) - parts.append(_make_axis_ring("orange_accent_ring_axis6_tool_roll_x", "x", (196.0, 0.0, 128.0), 14.0, 12.0, 2.0, ORANGE_ACCENT)) - - -def gen_step(): - """Return a labeled six-axis industrial robot arm display assembly in millimeters.""" - parts = [ - _make_display_base(), - _make_fixed_base(), - _make_turntable(), - _make_axis_ring("dark_circular_seam_between_fixed_base_and_turntable_axis1", "z", (0.0, 0.0, 42.2), 41.4, 36.0, 0.8, DEEP_GRAPHITE), - _make_pedestal(), - _make_shoulder_lug("shoulder_yoke_negative_y_lug_with_axis2_bore", -24.0), - _make_shoulder_lug("shoulder_yoke_positive_y_lug_with_axis2_bore", 24.0), - _make_axis_cylinder("dark_graphite_shoulder_motor_housing_axis2_od44_length72", "y", SHOULDER_CENTER, 22.0, 72.0, DARK_GRAPHITE, 1.6), - _make_axis_cylinder("left_shoulder_raised_joint_cap_od48_thick3", "y", (0.0, -37.5, 90.0), 24.0, 3.0, LIGHT_GRAY, 0.6), - _make_axis_cylinder("right_shoulder_raised_joint_cap_od48_thick3", "y", (0.0, 37.5, 90.0), 24.0, 3.0, LIGHT_GRAY, 0.6), - _make_upper_arm_link("upper_arm_negative_y_rounded_side_link_with_oval_lightening_cutout", -18.0), - _make_upper_arm_link("upper_arm_positive_y_rounded_side_link_with_oval_lightening_cutout", 18.0), - _make_axis_cylinder("dark_graphite_elbow_joint_housing_axis3_od42_length66", "y", ELBOW_CENTER, 21.0, 66.0, DARK_GRAPHITE, 1.8), - _make_axis_cylinder("negative_y_elbow_raised_joint_cap_od46_thick3", "y", (74.0, -34.5, 142.0), 23.0, 3.0, LIGHT_GRAY, 0.6), - _make_axis_cylinder("positive_y_elbow_raised_joint_cap_od46_thick3", "y", (74.0, 34.5, 142.0), 23.0, 3.0, LIGHT_GRAY, 0.6), - _make_forearm_housing(), - _make_forearm_panel("negative_y_dark_recessed_side_panel_on_tapered_forearm", -1.0), - _make_forearm_panel("positive_y_dark_recessed_side_panel_on_tapered_forearm", 1.0), - _make_axis_cylinder("dark_graphite_wrist_roll_housing_axis4_od30_length28", "x", WRIST_ROLL_CENTER, 15.0, 28.0, DARK_GRAPHITE, 1.1), - _make_wrist_yoke_lug("wrist_pitch_fork_negative_y_lug_axis5", -20.0), - _make_wrist_yoke_lug("wrist_pitch_fork_positive_y_lug_axis5", 20.0), - _make_axis_cylinder("dark_graphite_wrist_pitch_joint_axis5_od28_length46", "y", WRIST_PITCH_CENTER, 14.0, 46.0, DARK_GRAPHITE, 1.0), - _make_axis_cylinder("negative_y_wrist_pitch_cap_od31_thick2", "y", (174.0, -24.0, 128.0), 15.5, 2.0, LIGHT_GRAY, 0.4), - _make_axis_cylinder("positive_y_wrist_pitch_cap_od31_thick2", "y", (174.0, 24.0, 128.0), 15.5, 2.0, LIGHT_GRAY, 0.4), - _make_axis_cylinder("dark_graphite_final_wrist_tool_roll_cylinder_axis6_od24_length30", "x", TOOL_ROLL_CENTER, 12.0, 30.0, DARK_GRAPHITE, 0.9), - _make_axis_cylinder("short_steel_tool_flange_pilot_hub_axis6_od18_length5", "x", (211.0, 0.0, 128.0), 9.0, 5.0, BRUSHED_STEEL, 0.4), - _make_tool_flange(), - _make_cable_guide(), - ] - - _make_axis_label_ring_set(parts) - - _add_circular_screws(parts, "turntable_top_bolt_circle_58mm", "z", (0.0, 0.0, 0.0), (59.0,), 29.0, 8, 2.1, 1.7, start_angle=tau / 16.0) - _add_circular_screws(parts, "shoulder_axis2_joint_cap", "y", SHOULDER_CENTER, (-39.0, 39.0), 17.0, 6, 1.6, 1.1, start_angle=tau / 12.0) - _add_circular_screws(parts, "elbow_axis3_joint_cap", "y", ELBOW_CENTER, (-36.0, 36.0), 16.0, 6, 1.55, 1.1, start_angle=tau / 12.0) - _add_circular_screws(parts, "wrist_pitch_axis5_cap", "y", WRIST_PITCH_CENTER, (-25.2, 25.2), 10.5, 4, 1.25, 0.9, start_angle=tau / 8.0) - _add_circular_screws(parts, "wrist_roll_axis4_cap", "x", WRIST_ROLL_CENTER, (139.2, 168.8), 10.5, 4, 1.25, 0.9, start_angle=tau / 8.0) - _add_circular_screws(parts, "tool_flange_visible_fastener_circle", "x", TOOL_FLANGE_CENTER, (220.0,), 14.0, 6, 1.35, 0.8, start_angle=tau / 12.0) - _forearm_panel_screws(parts) - - upper_angle = -degrees(atan2(ELBOW_CENTER[2] - SHOULDER_CENTER[2], ELBOW_CENTER[0] - SHOULDER_CENTER[0])) - forearm_angle = -degrees(atan2(WRIST_ROLL_CENTER[2] - ELBOW_CENTER[2], WRIST_ROLL_CENTER[0] - ELBOW_CENTER[0])) - parts.extend( - [ - _make_cable_clamp("shoulder_cable_saddle_clamp_attached_to_rear_yoke", (-8.0, -29.5, 97.0), 0.0), - _make_cable_clamp("upper_arm_cable_saddle_clamp_following_raised_link", (39.0, -29.5, 121.0), upper_angle), - _make_cable_clamp("elbow_cable_saddle_clamp_at_cross_over", (78.0, -27.5, 148.0), 0.0), - _make_cable_clamp("forearm_cable_saddle_clamp_on_tapered_cover", (131.0, -27.5, 136.0), forearm_angle), - _make_cable_clamp("wrist_cable_saddle_clamp_near_axis4", (158.0, -21.5, 132.0), 0.0), - ] - ) - - assembly = Compound( - obj=parts, - children=parts, - label="six_axis_industrial_robot_arm_static_display_pose", - ) - return assembly diff --git a/models/fun/six_axis_industrial_robot_arm.step b/models/fun/six_axis_industrial_robot_arm.step deleted file mode 100644 index 0b8a95f6..00000000 --- a/models/fun/six_axis_industrial_robot_arm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:526430aabcf5d61ee8cfcbbf783dbcc0919800374a27f19a50b060f179dae31f -size 2704115 diff --git a/models/fun/six_blade_open_propeller.py b/models/fun/six_blade_open_propeller.py deleted file mode 100644 index f589c701..00000000 --- a/models/fun/six_blade_open_propeller.py +++ /dev/null @@ -1,375 +0,0 @@ -from __future__ import annotations - -from math import cos, hypot, sin, tau - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Color, - Compound, - Cylinder, - Face, - Location, - Locations, - Mode, - Plane, - Polygon, - Wire, - chamfer, - extrude, - fillet, - loft, -) - - -# Units: millimeters. -# Origin: central tube axis. XY is the blade layout plane. +Z is upward through -# the hollow hub. The image is interpreted as a stylized six-blade open propeller -# with separate visible blade/rim/hub solids rather than a single fused casting. - -MODEL_LABEL = "six_blade_open_propeller_from_reference" - -WARM_IVORY = Color(0.82, 0.78, 0.65, 1.0) -WARM_TAUPE = Color(0.72, 0.70, 0.61, 1.0) - -BLADE_COUNT = 6 -BLADE_ROOT_RADIUS = 10.8 -BLADE_TIP_RADIUS = 68.0 -BLADE_TIP_SWEEP = 14.5 -BLADE_CURVE_EXPONENT = 1.55 -BLADE_WIDTH = 31.5 -BLADE_THICKNESS = 2.8 -BLADE_SEGMENTS = 24 -BLADE_EDGE_BLEND_RADIUS = 0.55 -BLADE_EDGE_BLEND_MIN_SPAN = 0.2 -BLADE_Z_OFFSET = -0.35 -BLADE_TIP_TWIST_SPAN = 7.0 -BLADE_TIP_CENTER_Z_DROP = 1.4 - -HUB_OUTER_RADIUS = 29.0 -HUB_INNER_RADIUS = 23.5 -HUB_HEIGHT = 30.0 -HUB_WALL_FILLET = 0.65 -HUB_CUTOUT_WIDTH = 10.0 -HUB_CUTOUT_DEPTH = (HUB_OUTER_RADIUS - HUB_INNER_RADIUS) * 0.5 -HUB_CUTOUT_BOTTOM_Z = -0.5 -HUB_CUTOUT_HEIGHT = HUB_HEIGHT + 1.0 -HUB_CUTOUT_BORE_OVERLAP = 1.0 - - -def _label(part, label: str, color: Color): - part.label = label - part.color = color - return part - - -def _safe_chamfer(part, length: float): - try: - return chamfer(part.edges(), length=length) - except Exception: - return part - - -def _safe_fillet(part, radius: float): - try: - return fillet(part.edges(), radius=radius) - except Exception: - return part - - -def _safe_fillet_edges(part, edges, radius: float): - selected = list(edges) - if not selected: - return part - try: - return fillet(selected, radius=radius) - except Exception: - return part - - -def _safe_chamfer_edges(part, edges, length: float): - selected = list(edges) - if not selected: - return part - try: - return chamfer(selected, length=length) - except Exception: - return part - - -def _circle_radius(edge) -> float | None: - try: - return edge.radius - except Exception: - return None - - -def _edge_z(edge) -> float: - return edge.bounding_box().center().Z - - -def _edge_axis_spans(edge) -> tuple[float, float, float]: - bbox = edge.bounding_box() - return ( - bbox.max.X - bbox.min.X, - bbox.max.Y - bbox.min.Y, - bbox.max.Z - bbox.min.Z, - ) - - -def _non_planar_oriented_edges(part): - edges = [] - for edge in part.edges(): - if all(span > BLADE_EDGE_BLEND_MIN_SPAN for span in _edge_axis_spans(edge)): - edges.append(edge) - return edges - - -def _hub_rim_edges(part, outer_radius: float, inner_radius: float, height: float): - edges = [] - for edge in part.edges(): - radius = _circle_radius(edge) - if radius is None: - continue - if min(abs(radius - outer_radius), abs(radius - inner_radius)) > 0.08: - continue - if min(abs(_edge_z(edge)), abs(_edge_z(edge) - height)) > 0.08: - continue - edges.append(edge) - return edges - - -def _blade_center(t: float, *, root_radius: float, tip_radius: float) -> tuple[float, float]: - span = tip_radius - root_radius - x_pos = root_radius + span * t - y_pos = BLADE_TIP_SWEEP * (0.5 - t**BLADE_CURVE_EXPONENT) - return (x_pos, y_pos) - - -def _blade_tangent(t: float, *, root_radius: float, tip_radius: float) -> tuple[float, float]: - low = max(0.0, t - 0.01) - high = min(1.0, t + 0.01) - x0, y0 = _blade_center(low, root_radius=root_radius, tip_radius=tip_radius) - x1, y1 = _blade_center(high, root_radius=root_radius, tip_radius=tip_radius) - dx = x1 - x0 - dy = y1 - y0 - length = hypot(dx, dy) - return (dx / length, dy / length) - - -def _blade_width(t: float, width_offset: float) -> float: - return max(2.0, BLADE_WIDTH + width_offset) - - -def _blade_outline( - *, - root_offset: float = 0.0, - tip_offset: float = 0.0, - width_offset: float = 0.0, -) -> list[tuple[float, float]]: - root_radius = BLADE_ROOT_RADIUS + root_offset - tip_radius = BLADE_TIP_RADIUS + tip_offset - left_side: list[tuple[float, float]] = [] - right_side: list[tuple[float, float]] = [] - - for index in range(BLADE_SEGMENTS + 1): - t = index / BLADE_SEGMENTS - x_pos, y_pos = _blade_center(t, root_radius=root_radius, tip_radius=tip_radius) - tangent_x, tangent_y = _blade_tangent( - t, - root_radius=root_radius, - tip_radius=tip_radius, - ) - normal_x, normal_y = -tangent_y, tangent_x - half_width = _blade_width(t, width_offset) / 2.0 - left_side.append((x_pos + normal_x * half_width, y_pos + normal_y * half_width)) - right_side.append((x_pos - normal_x * half_width, y_pos - normal_y * half_width)) - - tip_tangent_x, tip_tangent_y = _blade_tangent( - 1.0, - root_radius=root_radius, - tip_radius=tip_radius, - ) - tip_normal_x, tip_normal_y = -tip_tangent_y, tip_tangent_x - tip_x, tip_y = _blade_center(1.0, root_radius=root_radius, tip_radius=tip_radius) - tip_half_width = _blade_width(1.0, width_offset) / 2.0 - cap_points = [] - for index in range(1, 11): - angle = index * tau / 20.0 - cap_points.append( - ( - tip_x - + tip_normal_x * cos(angle) * tip_half_width - + tip_tangent_x * sin(angle) * tip_half_width, - tip_y - + tip_normal_y * cos(angle) * tip_half_width - + tip_tangent_y * sin(angle) * tip_half_width, - ) - ) - - return left_side + cap_points + list(reversed(right_side)) - - -def _outline_solid(points: list[tuple[float, float]], height: float): - with BuildPart() as part: - with BuildSketch(Plane.XY): - Polygon(points, align=None) - extrude(amount=height) - return part.part - - -def _place_blade_part(part, index: int): - blade_angle = index * 360.0 / BLADE_COUNT - return part.moved(Location((0.0, 0.0, 0.0), (0.0, 0.0, blade_angle))) - - -def _blade_twist_span(t: float) -> float: - root_span = HUB_HEIGHT - BLADE_THICKNESS - blend = t**0.85 - return root_span * (1.0 - blend) + BLADE_TIP_TWIST_SPAN * blend - - -def _blade_center_z(t: float) -> float: - return HUB_HEIGHT / 2.0 + BLADE_Z_OFFSET - BLADE_TIP_CENTER_Z_DROP * t - - -def _make_blade_section_face( - *, - x_pos: float, - y_pos: float, - tangent_x: float, - tangent_y: float, - width: float, - twist_span: float, - center_z: float, -) -> Face: - normal_x, normal_y = -tangent_y, tangent_x - half_width = width / 2.0 - half_thickness = BLADE_THICKNESS / 2.0 - half_twist_span = twist_span / 2.0 - - high_edge = ( - x_pos + normal_x * half_width, - y_pos + normal_y * half_width, - center_z + half_twist_span, - ) - low_edge = ( - x_pos - normal_x * half_width, - y_pos - normal_y * half_width, - center_z - half_twist_span, - ) - points = [ - (high_edge[0], high_edge[1], high_edge[2] + half_thickness), - (low_edge[0], low_edge[1], low_edge[2] + half_thickness), - (low_edge[0], low_edge[1], low_edge[2] - half_thickness), - (high_edge[0], high_edge[1], high_edge[2] - half_thickness), - ] - return Face.make_surface(Wire.make_polygon(points, close=True)) - - -def _make_section_face(t: float) -> Face: - x_pos, y_pos = _blade_center(t, root_radius=BLADE_ROOT_RADIUS, tip_radius=BLADE_TIP_RADIUS) - tangent_x, tangent_y = _blade_tangent( - t, - root_radius=BLADE_ROOT_RADIUS, - tip_radius=BLADE_TIP_RADIUS, - ) - return _make_blade_section_face( - x_pos=x_pos, - y_pos=y_pos, - tangent_x=tangent_x, - tangent_y=tangent_y, - width=_blade_width(t, 0.0), - twist_span=_blade_twist_span(t), - center_z=_blade_center_z(t), - ) - - -def _make_blade_body_local(): - faces = [ - _make_section_face(index / BLADE_SEGMENTS) - for index in range(BLADE_SEGMENTS + 1) - ] - return loft(faces, ruled=False) - - -def _make_blade_body(index: int): - body = _make_blade_body_local() - hub_trim = Cylinder( - radius=HUB_OUTER_RADIUS, - height=120.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ).moved(Location((0.0, 0.0, -45.0))) - body = body - hub_trim - body = _safe_fillet_edges( - body, - _non_planar_oriented_edges(body), - BLADE_EDGE_BLEND_RADIUS, - ) - body = _place_blade_part(body, index) - return _label(body, f"blade_{index + 1:02d}_broad_swept_petal", WARM_IVORY) - - -def _annular_cylinder( - *, - label: str, - outer_radius: float, - inner_radius: float, - height: float, - z_bottom: float, - color: Color, -): - with BuildPart() as ring: - Cylinder( - radius=outer_radius, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - ) - Cylinder( - radius=inner_radius, - height=height + 2.0, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - # Internal keyway-style recess: open to the bore, blind before the - # outside wall, matching the rectangular cut visible inside the hub. - slot_depth = HUB_CUTOUT_DEPTH + HUB_CUTOUT_BORE_OVERLAP - slot_x = inner_radius - HUB_CUTOUT_BORE_OVERLAP + slot_depth / 2.0 - slot_z = HUB_CUTOUT_BOTTOM_Z + HUB_CUTOUT_HEIGHT / 2.0 - with Locations(Location((slot_x, 0.0, slot_z))): - Box( - slot_depth, - HUB_CUTOUT_WIDTH, - HUB_CUTOUT_HEIGHT, - mode=Mode.SUBTRACT, - ) - - part = ring.part.moved(Location((0.0, 0.0, z_bottom))) - part = _safe_chamfer_edges( - part, - _hub_rim_edges(part, outer_radius, inner_radius, height), - HUB_WALL_FILLET, - ) - return _label(part, label, color) - - -def _make_hub_shell(): - return _annular_cylinder( - label="hollow_cylindrical_center_tube_open_bore", - outer_radius=HUB_OUTER_RADIUS, - inner_radius=HUB_INNER_RADIUS, - height=HUB_HEIGHT, - z_bottom=0.0, - color=WARM_TAUPE, - ) - - -def gen_step(): - parts = [_make_hub_shell()] - - for index in range(BLADE_COUNT): - parts.append(_make_blade_body(index)) - - return Compound(obj=parts, children=parts, label=MODEL_LABEL) diff --git a/models/fun/six_blade_open_propeller.step b/models/fun/six_blade_open_propeller.step deleted file mode 100644 index bc8de101..00000000 --- a/models/fun/six_blade_open_propeller.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:334b4a18ea4107835eb14dde08ab136c1ce638c9a87ff21dbe4cc4e1a0649702 -size 3036466 diff --git a/models/gcode/README.md b/models/gcode/README.md deleted file mode 100644 index 8811cff9..00000000 --- a/models/gcode/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# G-code Fixtures - -Small FDM slicing fixtures used by the G-code skill and related validation -workflows. This folder keeps both slicer inputs and durable generated G-code -outputs for known simple parts. - -## Contents - -- `*_sample.stl`: mesh inputs used for slicing checks. -- `*_sample.gcode`: canonical generated G-code outputs for those inputs. - -These files are intentionally sample-sized. Printer credentials, slicer -profiles, logs, and transient dry-run outputs should not be committed here. diff --git a/models/gcode/circular_flange_plate_sample.gcode b/models/gcode/circular_flange_plate_sample.gcode deleted file mode 100644 index 199ef98d..00000000 --- a/models/gcode/circular_flange_plate_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ac3b9295593528ce8eda59f478e021c1601025d21ed10d7e624405fbcbfe9d6 -size 6281901 diff --git a/models/gcode/circular_flange_plate_sample.stl b/models/gcode/circular_flange_plate_sample.stl deleted file mode 100644 index e601e474..00000000 --- a/models/gcode/circular_flange_plate_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8a289509191b7811ee7b5a5a809f85d7ddcd313ca3f8e5d4f9c88ed962f7877 -size 304084 diff --git a/models/gcode/cylindrical_spacer_sleeve_sample.gcode b/models/gcode/cylindrical_spacer_sleeve_sample.gcode deleted file mode 100644 index c0632df3..00000000 --- a/models/gcode/cylindrical_spacer_sleeve_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:856a187639eda98d2a48cc623eea5ebf3f1756d99c2afe0a3a1a7b16aefc2ff0 -size 3895026 diff --git a/models/gcode/cylindrical_spacer_sleeve_sample.stl b/models/gcode/cylindrical_spacer_sleeve_sample.stl deleted file mode 100644 index 44f57c7d..00000000 --- a/models/gcode/cylindrical_spacer_sleeve_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f591a8f33b6326b06260c67df534c85870e532c9a69aed9ddbcbbced53ed06bc -size 1663284 diff --git a/models/gcode/flat_rectangular_plate_sample.gcode b/models/gcode/flat_rectangular_plate_sample.gcode deleted file mode 100644 index 3ceff700..00000000 --- a/models/gcode/flat_rectangular_plate_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73abfa60ed0809774166db13ad4e3d97ea01bcd6590a1bd090c3088541efa297 -size 2224488 diff --git a/models/gcode/flat_rectangular_plate_sample.stl b/models/gcode/flat_rectangular_plate_sample.stl deleted file mode 100644 index b0cb575d..00000000 --- a/models/gcode/flat_rectangular_plate_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0d3e138ec62effc4d02bcd8b8f2d034934afa6954c82ab121c17f6320723cc96 -size 103884 diff --git a/models/gcode/pulley_wheel_sample.gcode b/models/gcode/pulley_wheel_sample.gcode deleted file mode 100644 index 388ff04f..00000000 --- a/models/gcode/pulley_wheel_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cdffb0ba264fe3c76d9d4c3e4593623a42f4b8165be94dba86d51bac19f8ae71 -size 7004085 diff --git a/models/gcode/pulley_wheel_sample.stl b/models/gcode/pulley_wheel_sample.stl deleted file mode 100644 index 230fe903..00000000 --- a/models/gcode/pulley_wheel_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0ce7c23eef4e84efea0773ecba44922970f271c45804c1d211d80da4826dec9 -size 8444284 diff --git a/models/gcode/shaft_collar_sample.gcode b/models/gcode/shaft_collar_sample.gcode deleted file mode 100644 index 8bfbd585..00000000 --- a/models/gcode/shaft_collar_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:efbceee790ac14cedc1fa56056a7061ee8dc80cd211731bcab78dbc1dadf5403 -size 3051290 diff --git a/models/gcode/shaft_collar_sample.stl b/models/gcode/shaft_collar_sample.stl deleted file mode 100644 index f6b0ae10..00000000 --- a/models/gcode/shaft_collar_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:36f0681bb11e2a84ae231ebda518a6fcc3da70f8a51ceddacd97f897d2202eb9 -size 148684 diff --git a/models/gcode/square_mounting_block_sample.gcode b/models/gcode/square_mounting_block_sample.gcode deleted file mode 100644 index 4d4cbbfe..00000000 --- a/models/gcode/square_mounting_block_sample.gcode +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd440d9c860d5805afae49117e334f7ce6b7e843cdf74b24ccfaf4266eec370b -size 5219691 diff --git a/models/gcode/square_mounting_block_sample.stl b/models/gcode/square_mounting_block_sample.stl deleted file mode 100644 index 8bd74ceb..00000000 --- a/models/gcode/square_mounting_block_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aedb98dfe0764461401bf00426e8a9867536d65c42537d6dabdb931d0a89e6ed -size 78484 diff --git a/models/implicits/README.md b/models/implicits/README.md deleted file mode 100644 index 1b874ef5..00000000 --- a/models/implicits/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Implicit CAD Examples - -Browser-native implicit CAD examples for the implicit CAD runtime and CAD -Viewer raymarching/export workflows. Each file is self-contained JavaScript -describing one signed-distance-field model. - -## Contents - -- `*.implicit.js`: implicit CAD model definitions. - -There is no generated catalog sidecar in this committed tree. Viewers and tools -should discover examples from the files in this directory or generate any local -catalog as a transient artifact. diff --git a/models/implicits/boys-surface-sculpture.implicit.js b/models/implicits/boys-surface-sculpture.implicit.js deleted file mode 100644 index bc8da676..00000000 --- a/models/implicits/boys-surface-sculpture.implicit.js +++ /dev/null @@ -1,49 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float r = length(p); - float theta = atan(p.y, p.x); - float phi = atan(p.z, length(p.xy)); - float triLobe = sin(3.0 * theta) * cos(phi * 2.0) + 0.45 * cos(3.0 * phi); - float shellRadius = radius + lobeDepth * triLobe; - float shell = abs(r - shellRadius) - thickness; - float selfFold = implicit_capsule(p, vec3(-radius * 0.55, 0.0, -radius * 0.3), vec3(radius * 0.55, 0.0, radius * 0.3), thickness * (2.2 + pinch)); - float pinchCut = implicit_torus(vec3(p.x, p.z, p.y), radius * 0.46, thickness * (1.1 + pinch)); - return implicit_union_round(implicit_intersect_round(shell, -pinchCut, thickness), selfFold, thickness * 1.4); -} - -vec3 color(vec3 p, vec3 normal) { - float fold = smoothstep(0.25, 0.95, abs(normal.x * 1.2 + normal.y * 0.4)); - float tri = 0.5 + 0.5 * sin(atan(p.y, p.x) * 3.0 + p.z * 0.08); - return mix(mix(baseColor * 0.65, baseColor, tri), accentColor, fold * 0.52); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "boys surface sculpture", - description: "Prompt 15: compact non-orientable Boy's-surface-inspired implicit sculpture.", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 18, max: 42, step: 0.25, default: 30, unit: "mm" }, - lobeDepth: { type: "number", label: "Lobe depth", min: 1, max: 12, step: 0.25, default: 6.5, unit: "mm" }, - pinch: { type: "number", label: "Pinch", min: 0, max: 1, step: 0.02, default: 0.44 }, - thickness: { type: "number", label: "Shell thickness", min: 0.8, max: 6, step: 0.1, default: 2.4, unit: "mm" }, - baseColor: { type: "color", label: "Surface color", default: "#a78bfa" }, - accentColor: { type: "color", label: "Fold color", default: "#ffb86b" } - }, - animations: { - lobeTurn: { - label: "Lobe turn", - duration: 6.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("lobeDepth", 4 + (wave * 0.5 + 0.5) * 8); - set("pinch", 0.22 + (1 - Math.abs(wave)) * 0.58); - } - } - }, - bounds: [[-48, -48, -48], [48, 48, 48]], - render: { steps: 310, epsilon: 0.0045, normalEpsilon: 0.048 }, - glsl: GLSL -}; diff --git a/models/implicits/catenoid-ring-bridge.implicit.js b/models/implicits/catenoid-ring-bridge.implicit.js deleted file mode 100644 index a36fdcd9..00000000 --- a/models/implicits/catenoid-ring-bridge.implicit.js +++ /dev/null @@ -1,48 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float z = clamp(p.z, -height * 0.5, height * 0.5); - float profile = neckRadius * cosh(z / max(neckRadius, 0.1)); - float surface = abs(length(p.xy) - profile) - wallThickness; - float clip = abs(p.z) - height * 0.5; - float catenoid = max(surface, clip); - float topRing = implicit_torus(p - vec3(0.0, 0.0, height * 0.5), profile + ringRadius * 0.25, ringRadius); - float bottomRing = implicit_torus(p - vec3(0.0, 0.0, -height * 0.5), profile + ringRadius * 0.25, ringRadius); - return implicit_union_round(implicit_union_round(catenoid, topRing, 1.8), bottomRing, 1.8); -} - -vec3 color(vec3 p, vec3 normal) { - float ring = smoothstep(height * 0.36, height * 0.52, abs(p.z)); - float contour = 0.5 + 0.5 * sin(p.z * 0.48); - return mix(mix(baseColor * 0.65, baseColor, contour), accentColor, ring); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "catenoid ring bridge", - description: "Prompt 11: catenoid minimal surface between two rings with controllable neck.", - units: "mm", - params: { - neckRadius: { type: "number", label: "Neck radius", min: 7, max: 24, step: 0.25, default: 13, unit: "mm" }, - height: { type: "number", label: "Ring spacing", min: 22, max: 72, step: 1, default: 46, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.8, max: 5, step: 0.1, default: 2, unit: "mm" }, - ringRadius: { type: "number", label: "Ring radius", min: 2, max: 8, step: 0.25, default: 4.5, unit: "mm" }, - baseColor: { type: "color", label: "Neck color", default: "#ffb703" }, - accentColor: { type: "color", label: "Ring color", default: "#00d1ff" } - }, - animations: { - neckPulse: { - label: "Neck pulse", - duration: 5.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("neckRadius", 11 + (wave * 0.5 + 0.5) * 8); - set("height", 38 + wave * 12); - } - } - }, - bounds: [[-42, -42, -46], [42, 42, 46]], - render: { steps: 270, epsilon: 0.004, normalEpsilon: 0.045 }, - glsl: GLSL -}; diff --git a/models/implicits/chmutov-node-field.implicit.js b/models/implicits/chmutov-node-field.implicit.js deleted file mode 100644 index e86a5b1b..00000000 --- a/models/implicits/chmutov-node-field.implicit.js +++ /dev/null @@ -1,57 +0,0 @@ -const GLSL = ` -float t4(float x) { - float x2 = x * x; - return 8.0 * x2 * x2 - 8.0 * x2 + 1.0; -} - -float t5(float x) { - float x2 = x * x; - return 16.0 * x2 * x2 * x - 20.0 * x2 * x + 5.0 * x; -} - -float sdf(vec3 p) { - vec3 q = p / max(radius * 0.52, 0.1) * polynomialScale; - float f4 = t4(q.x) + t4(q.y) + t4(q.z); - float f5 = t5(q.x) + t5(q.y) + t5(q.z); - float field = mix(f4, f5, orderMix) + mix(-0.55, 0.12, orderMix); - float shell = abs(field) * radius * 0.055 - thickness; - float clip = implicit_sphere(p, vec3(0.0), radius); - return implicit_intersect_round(shell, clip, 1.0); -} - -vec3 color(vec3 p, vec3 normal) { - float nodes = 0.5 + 0.5 * sin((p.x * p.y + p.y * p.z + p.z * p.x) * 0.012); - float saddle = smoothstep(0.15, 0.95, abs(normal.x * normal.y * normal.z) * 6.0); - return mix(mix(baseColor * 0.62, baseColor, nodes), accentColor, saddle * 0.58); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "chmutov node field", - description: "Prompt 18: Chmutov-style high-order algebraic surface with many nodes.", - units: "mm", - params: { - radius: { type: "number", label: "Clip radius", min: 22, max: 52, step: 0.5, default: 38, unit: "mm" }, - polynomialScale: { type: "number", label: "Polynomial scale", min: 0.6, max: 1.6, step: 0.02, default: 1.05 }, - orderMix: { type: "number", label: "Order mix", min: 0, max: 1, step: 0.02, default: 0.35 }, - thickness: { type: "number", label: "Thickness", min: 0.2, max: 3, step: 0.05, default: 0.9, unit: "mm" }, - baseColor: { type: "color", label: "Node color", default: "#38bdf8" }, - accentColor: { type: "color", label: "Saddle color", default: "#ff8a3d" } - }, - animations: { - orderDrift: { - label: "Order drift", - duration: 7, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("orderMix", wave * 0.5 + 0.5); - set("polynomialScale", 0.92 + (1 - Math.abs(wave)) * 0.46); - } - } - }, - bounds: [[-56, -56, -56], [56, 56, 56]], - render: { steps: 356, epsilon: 0.005, normalEpsilon: 0.055 }, - glsl: GLSL -}; diff --git a/models/implicits/dragon-curve-ribbon.implicit.js b/models/implicits/dragon-curve-ribbon.implicit.js deleted file mode 100644 index 322e1912..00000000 --- a/models/implicits/dragon-curve-ribbon.implicit.js +++ /dev/null @@ -1,80 +0,0 @@ -const GLSL = ` -float sdSegment2(vec2 p, vec2 a, vec2 b) { - vec2 pa = p - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -vec2 dragonPoint(float i) { - float t = i / 72.0; - float a = t * 11.4 + sin(t * 31.0) * 0.28; - float r = size * (0.16 + 0.78 * sqrt(t)); - vec2 q = vec2(cos(a), sin(a)) * r; - q += vec2(cos(a * 2.0 + 1.1), sin(a * 2.0 - 0.7)) * size * 0.10; - if (mod(floor(i / 6.0), 2.0) > 0.5) { - q = vec2(q.x - q.y, q.x + q.y) * 0.70710678118; - } - return q; -} - -float dragonField(vec2 p) { - float d = 100000.0; - for (int i = 0; i < 72; i += 1) { - float fi = float(i); - d = min(d, sdSegment2(p, dragonPoint(fi), dragonPoint(fi + 1.0))); - } - return d; -} - -float sdf(vec3 p) { - float ribbon = dragonField(p.xy) - ribbonWidth; - float slab = abs(p.z) - thickness * 0.5; - return max(ribbon, slab); -} - -vec3 color(vec3 p, vec3 normal) { - float fold = 0.5 + 0.5 * sin(atan(p.y, p.x) * 5.0 + length(p.xy) * 0.16); - vec3 c = mix(baseColor, accentColor, fold); - return c * (0.58 + 0.42 * smoothstep(-0.2, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Dragon curve ribbon", - description: "A recursively folded dragon-curve-inspired ribbon, thickened into a smooth printable strip.", - units: "mm", - params: { - size: { type: "number", label: "Fold size", min: 12, max: 34, step: 0.5, default: 24, unit: "mm" }, - ribbonWidth: { type: "number", label: "Ribbon width", min: 0.45, max: 2.6, step: 0.05, default: 1.05, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.8, max: 4, step: 0.1, default: 1.55, unit: "mm" }, - baseColor: { type: "color", label: "Inner color", default: "#5b8cff" }, - accentColor: { type: "color", label: "Fold color", default: "#ff5c8a" } - }, - animations: { - foldPulse: { - label: "Fold pulse", - duration: 5.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("ribbonWidth", 1.05 + wave * 0.32); - set("thickness", 1.55 + (wave * 0.5 + 0.5) * 0.7); - } - } - }, - bounds: ({ params }) => { - const extent = params.size * 1.25 + params.ribbonWidth + 6; - const z = params.thickness + 3; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.64, - maxStep: Math.max(params.ribbonWidth * 3.2, 1.0), - epsilon: Math.max(params.ribbonWidth * 0.012, 0.004), - normalEpsilon: Math.max(params.ribbonWidth * 0.08, 0.032) - }), - glsl: GLSL -}; diff --git a/models/implicits/dragon-folding-path-3d.implicit.js b/models/implicits/dragon-folding-path-3d.implicit.js deleted file mode 100644 index 3001de51..00000000 --- a/models/implicits/dragon-folding-path-3d.implicit.js +++ /dev/null @@ -1,80 +0,0 @@ -const GLSL = ` -float sdSegment(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -vec3 dragon3Point(float i) { - float t = i / 54.0; - float angle = t * 13.2 + sin(t * 23.0) * 0.34; - float r = size * (0.2 + 0.72 * sqrt(t)); - vec3 q = vec3(cos(angle) * r, sin(angle) * r, sin(t * 18.8495559) * height); - if (mod(floor(i / 5.0), 2.0) > 0.5) { - q.xy = vec2(q.x - q.y, q.x + q.y) * 0.70710678118; - q.z = -q.z * 0.78; - } - return q; -} - -float sdf(vec3 p) { - float d = 100000.0; - for (int i = 0; i < 54; i += 1) { - float fi = float(i); - d = smoothMin(d, sdSegment(p, dragon3Point(fi), dragon3Point(fi + 1.0)), bendBlend); - } - return d - tubeRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float fold = 0.5 + 0.5 * sin(length(p.xy) * 0.18 + p.z * 0.32); - float light = pow(max(dot(normal, normalize(vec3(-0.52, 0.32, 0.79))), 0.0), 2.0); - return mix(baseColor, accentColor, fold) * (0.58 + light * 0.48); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "3D dragon folding path", - description: "A sculptural 3D recursive folding path inspired by dragon-curve turns.", - units: "mm", - params: { - size: { type: "number", label: "Planar spread", min: 12, max: 34, step: 0.5, default: 23, unit: "mm" }, - height: { type: "number", label: "Vertical fold", min: 2, max: 16, step: 0.25, default: 7.5, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 0.45, max: 2.5, step: 0.05, default: 1.05, unit: "mm" }, - bendBlend: { type: "number", label: "Bend blend", min: 0.1, max: 2.2, step: 0.05, default: 0.7, unit: "mm" }, - baseColor: { type: "color", label: "Base color", default: "#2df5a5" }, - accentColor: { type: "color", label: "Fold color", default: "#ffcc33" } - }, - animations: { - foldOrbit: { - label: "Fold orbit", - duration: 6.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("height", 7.5 + wave * 4.5); - set("bendBlend", 0.7 + (wave * 0.5 + 0.5) * 0.85); - } - } - }, - bounds: ({ params }) => { - const extent = params.size * 1.28 + params.tubeRadius + 6; - const z = params.height + params.tubeRadius + 6; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.64, - maxStep: Math.max(params.tubeRadius * 3.2, 1.0), - epsilon: Math.max(params.tubeRadius * 0.012, 0.0045), - normalEpsilon: Math.max(params.tubeRadius * 0.075, 0.034) - }), - glsl: GLSL -}; diff --git a/models/implicits/enneper-curvature-patch.implicit.js b/models/implicits/enneper-curvature-patch.implicit.js deleted file mode 100644 index 34a07d53..00000000 --- a/models/implicits/enneper-curvature-patch.implicit.js +++ /dev/null @@ -1,48 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float r = length(p.xy); - float x = p.x / max(saddleScale, 0.1); - float y = p.y / max(saddleScale, 0.1); - float enneperHeight = saddleScale * 0.42 * (x * x - y * y) * (1.0 - 0.18 * (x * x + y * y)); - enneperHeight += ripple * sin(atan(p.y, p.x) * 3.0) * smoothstep(radius, 0.0, r); - float sheet = abs(p.z - enneperHeight) - thickness; - float disk = r - radius; - return implicit_intersect_round(sheet, disk, thickness * 1.25); -} - -vec3 color(vec3 p, vec3 normal) { - float contour = smoothstep(0.42, 0.5, abs(fract((p.z + length(p.xy) * 0.12) / 4.0) - 0.5)); - float curve = smoothstep(0.0, 0.95, abs(normal.z)); - return mix(mix(baseColor * 0.62, baseColor, curve), accentColor, contour * 0.62); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "enneper curvature patch", - description: "Prompt 14: thickened Enneper-inspired minimal surface with contour coloring.", - units: "mm", - params: { - radius: { type: "number", label: "Patch radius", min: 16, max: 42, step: 0.25, default: 30, unit: "mm" }, - saddleScale: { type: "number", label: "Saddle scale", min: 12, max: 42, step: 0.5, default: 24, unit: "mm" }, - ripple: { type: "number", label: "Contour ripple", min: 0, max: 5, step: 0.1, default: 1.8, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.6, max: 5, step: 0.1, default: 1.8, unit: "mm" }, - baseColor: { type: "color", label: "Curvature color", default: "#ff8a3d" }, - accentColor: { type: "color", label: "Contour color", default: "#7c5cff" } - }, - animations: { - curvatureTide: { - label: "Curvature tide", - duration: 5.7, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("saddleScale", 21 + wave * 8); - set("ripple", (wave * 0.5 + 0.5) * 4.5); - } - } - }, - bounds: [[-46, -46, -34], [46, 46, 34]], - render: { steps: 288, epsilon: 0.004, normalEpsilon: 0.046 }, - glsl: GLSL -}; diff --git a/models/implicits/genus-2-double-torus.implicit.js b/models/implicits/genus-2-double-torus.implicit.js deleted file mode 100644 index aa0e2e47..00000000 --- a/models/implicits/genus-2-double-torus.implicit.js +++ /dev/null @@ -1,57 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float left = implicit_torus(p - vec3(-(separation * 0.5), 0.0, 0.0), majorRadius, tubeRadius); - float right = implicit_torus(p - vec3((separation * 0.5), 0.0, 0.0), majorRadius, tubeRadius); - float bridge = implicit_capsule(p, vec3(-max((separation * 0.5) - tubeRadius * 0.6, tubeRadius), 0.0, 0.0), vec3(max((separation * 0.5) - tubeRadius * 0.6, tubeRadius), 0.0, 0.0), bridgeRadius); - float handles = implicit_union_round(left, right, blend); - return implicit_union_round(handles, bridge, max(blend * 0.78, 0.4)); -} - - -vec3 color(vec3 p, vec3 normal) { - float side = smoothstep(-18.0, 18.0, p.x); - return mix(mix(leftColor, rightColor, side), highlightColor, smoothstep(0.65, 0.98, abs(normal.z)) * 0.28); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "genus-2 double torus", - description: "Two toroidal handles smoothly fused into one symmetric watertight genus-2 solid.", - units: "mm", - params: { - separation: { type: "number", label: "Handle spacing", min: 16, max: 34, step: 0.25, default: 26, unit: "mm" }, - majorRadius: { type: "number", label: "Major radius", min: 7, max: 15, step: 0.25, default: 10.5, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 2.2, max: 6.5, step: 0.1, default: 4.2, unit: "mm" }, - bridgeRadius: { type: "number", label: "Bridge radius", min: 2.6, max: 7, step: 0.1, default: 5.2, unit: "mm" }, - blend: { type: "number", label: "Blend", min: 0.5, max: 6, step: 0.1, default: 3.8, unit: "mm" }, - leftColor: { type: "color", label: "Left handle", default: "#ff6fb1" }, - rightColor: { type: "color", label: "Right handle", default: "#7c83ff" }, - highlightColor: { type: "color", label: "Bridge light", default: "#fff06a" } - }, - animations: { - handleBreath: { - label: "Handle breath", - duration: 5.0, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("separation", 26 + wave * 5); - set("tubeRadius", 4.2 + wave * 1.1); - set("blend", 3.8 + wave * 2.0); - } - } - }, - bounds: ({ params }) => { - const halfSpacing = params.separation / 2; - const x = halfSpacing + params.majorRadius + params.tubeRadius + params.blend + 8; - const yz = params.majorRadius + params.tubeRadius + params.blend + 8; - return [[-x, -yz, -yz], [x, yz, yz]]; - }, - render: ({ params }) => ({ - steps: 240, - epsilon: Math.max(0.004, params.tubeRadius * 0.0014), - normalEpsilon: Math.max(0.035, params.tubeRadius * 0.011) - }), - glsl: GLSL -}; diff --git a/models/implicits/gosper-curve-tube.implicit.js b/models/implicits/gosper-curve-tube.implicit.js deleted file mode 100644 index 9c8715ba..00000000 --- a/models/implicits/gosper-curve-tube.implicit.js +++ /dev/null @@ -1,79 +0,0 @@ -const GLSL = ` -float sdSegment(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -vec3 gosperPoint(float i) { - float t = i / 60.0; - float angle = t * 6.28318530718 * 3.0 + sin(t * 6.28318530718 * 7.0) * 0.26; - float hex = floor(mod(i, 6.0)); - float r = size * (0.25 + 0.55 * t + 0.12 * sin(t * 18.8495559)); - vec2 q = vec2(cos(angle), sin(angle)) * r; - q += vec2(cos(hex * 1.0471975512), sin(hex * 1.0471975512)) * cellJitter; - return vec3(q, sin(t * 12.566370614) * verticalLift); -} - -float sdf(vec3 p) { - float d = 100000.0; - for (int i = 0; i < 60; i += 1) { - float fi = float(i); - d = smoothMin(d, sdSegment(p, gosperPoint(fi), gosperPoint(fi + 1.0)), bendBlend); - } - return d - tubeRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float hexBand = 0.5 + 0.5 * sin(atan(p.y, p.x) * 6.0 + length(p.xy) * 0.16); - vec3 c = mix(baseColor, accentColor, hexBand); - return c * (0.56 + 0.44 * smoothstep(-0.25, 0.95, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Gosper curve tube", - description: "A hexagonal Gosper-flow inspired tube with smooth recursive-looking bends.", - units: "mm", - params: { - size: { type: "number", label: "Flow size", min: 14, max: 38, step: 0.5, default: 27, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 0.45, max: 2.6, step: 0.05, default: 1.12, unit: "mm" }, - bendBlend: { type: "number", label: "Bend blend", min: 0.1, max: 2.4, step: 0.05, default: 0.82, unit: "mm" }, - cellJitter: { type: "number", label: "Hex offset", min: 0, max: 5, step: 0.1, default: 2.2, unit: "mm" }, - verticalLift: { type: "number", label: "Vertical lift", min: 0, max: 8, step: 0.1, default: 2.4, unit: "mm" }, - baseColor: { type: "color", label: "Base color", default: "#24e6c2" }, - accentColor: { type: "color", label: "Accent color", default: "#fff45a" } - }, - animations: { - hexFlow: { - label: "Hex flow", - duration: 6.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("cellJitter", 2.2 + wave * 1.4); - set("verticalLift", 2.4 + (wave * 0.5 + 0.5) * 3.6); - } - } - }, - bounds: ({ params }) => { - const extent = params.size + params.cellJitter + params.tubeRadius + 6; - const z = params.verticalLift + params.tubeRadius + 5; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.64, - maxStep: Math.max(params.tubeRadius * 3.2, 1.0), - epsilon: Math.max(params.tubeRadius * 0.012, 0.004), - normalEpsilon: Math.max(params.tubeRadius * 0.075, 0.034) - }), - glsl: GLSL -}; diff --git a/models/implicits/gyroid-unit-cell.implicit.js b/models/implicits/gyroid-unit-cell.implicit.js deleted file mode 100644 index a4d3faea..00000000 --- a/models/implicits/gyroid-unit-cell.implicit.js +++ /dev/null @@ -1,53 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float field = implicit_tpms_gyroid(p, vec3(cellSize), vec3(1.0)); - float shell = implicit_shell(field, wallThickness, 0.0); - float cell = implicit_box_centered(p, vec3(cellSize), vec3(0.0)); - return max(shell, cell) * fieldScale; -} - - -vec3 color(vec3 p, vec3 normal) { - vec3 period = 0.5 + 0.5 * sin((p / cellSize) * 6.283185307179586 + vec3(0.0, 2.1, 4.2)); - vec3 body = mix(baseColor, accentColor, dot(period, vec3(0.28, 0.36, 0.36))); - float rim = pow(max(dot(normal, normalize(vec3(0.24, -0.52, 0.82))), 0.0), 2.0); - return mix(body, highlightColor, rim * 0.24); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "gyroid unit cell", - description: "Finite-thickness gyroid minimal surface clipped to one cubic periodic unit cell.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 32, max: 72, step: 0.5, default: 48, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 1.2, max: 7, step: 0.1, default: 3.6, unit: "mm" }, - fieldScale: { type: "number", label: "Field scale", min: 0.35, max: 0.9, step: 0.01, default: 0.52 }, - baseColor: { type: "color", label: "Surface color", default: "#12f0c3" }, - accentColor: { type: "color", label: "Opening color", default: "#ffd166" }, - highlightColor: { type: "color", label: "Highlight color", default: "#b993ff" } - }, - animations: { - wallPulse: { - label: "Wall pulse", - duration: 5.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("wallThickness", 3.6 + wave * 2.0); - set("fieldScale", 0.52 + wave * 0.16); - } - } - }, - bounds: ({ params }) => { - const half = params.cellSize / 2 + Math.max(params.wallThickness, 3); - return [[-half, -half, -half], [half, half, half]]; - }, - render: ({ params }) => ({ - steps: 340, - epsilon: Math.max(0.005, params.cellSize * 0.00014), - normalEpsilon: Math.max(0.04, params.wallThickness * 0.014) - }), - glsl: GLSL -}; diff --git a/models/implicits/gyroid-window.implicit.js b/models/implicits/gyroid-window.implicit.js deleted file mode 100644 index 27ec1617..00000000 --- a/models/implicits/gyroid-window.implicit.js +++ /dev/null @@ -1,54 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = p / periodScale; - float gyroid = implicit_tpms_gyroid(q, vec3(1.0), vec3(1.0)); - float sheet = implicit_shell(gyroid, wallThickness, 0.0) * periodScale; - float roundedWindow = implicit_box_centered(p, vec3(windowSize), vec3(0.0)); - float roundover = implicit_sphere(p, vec3(0.0), outerRadius); - return implicit_intersect_round(max(sheet, roundedWindow), roundover, roundover); -} - - -vec3 color(vec3 p, vec3 normal) { - vec3 wave = 0.5 + 0.5 * sin((p * 0.095) + vec3(0.0, 2.1, 4.2)); - return mix(baseColor, accentColor, dot(wave, vec3(0.28, 0.36, 0.36))); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "gyroid window", - units: "mm", - params: { - periodScale: { type: "number", label: "Period scale", min: 5, max: 13, step: 0.1, default: 8.5, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.14, max: 0.7, step: 0.01, default: 0.34 }, - windowSize: { type: "number", label: "Window size", min: 36, max: 66, step: 0.5, default: 52, unit: "mm" }, - outerRadius: { type: "number", label: "Outer radius", min: 24, max: 42, step: 0.5, default: 33.5, unit: "mm" }, - roundover: { type: "number", label: "Roundover", min: 0.5, max: 5, step: 0.1, default: 2.5, unit: "mm" }, - baseColor: { type: "color", label: "Window color", default: "#43e8ff" }, - accentColor: { type: "color", label: "Gold color", default: "#ffd166" } - }, - animations: { - windowPulse: { - label: "Window pulse", - duration: 5.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("wallThickness", 0.34 + wave * 0.16); - set("roundover", 2.5 + wave * 1.5); - set("windowSize", 52 + wave * 8); - } - } - }, - bounds: ({ params }) => { - const extent = Math.max(params.windowSize * 0.5, params.outerRadius) + params.roundover + 4; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 280, - epsilon: Math.max(0.004, params.periodScale * 0.0007), - normalEpsilon: Math.max(0.04, params.periodScale * 0.006) - }), - glsl: GLSL -}; diff --git a/models/implicits/h-tree-solid.implicit.js b/models/implicits/h-tree-solid.implicit.js deleted file mode 100644 index 9d7c40db..00000000 --- a/models/implicits/h-tree-solid.implicit.js +++ /dev/null @@ -1,86 +0,0 @@ -const GLSL = ` -float sdSegment2(vec2 p, vec2 a, vec2 b) { - vec2 pa = p - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float hLevel(vec2 p, float s, vec2 o) { - float d = sdSegment2(p, o + vec2(-s, 0.0), o + vec2(s, 0.0)); - d = min(d, sdSegment2(p, o + vec2(-s, -s * 0.5), o + vec2(-s, s * 0.5))); - d = min(d, sdSegment2(p, o + vec2(s, -s * 0.5), o + vec2(s, s * 0.5))); - return d; -} - -float hTree(vec2 p) { - float d = hLevel(p, size, vec2(0.0)); - if (levels > 1.5) { - d = min(d, hLevel(p, size * 0.5, vec2(-size, size * 0.5))); - d = min(d, hLevel(p, size * 0.5, vec2(size, size * 0.5))); - d = min(d, hLevel(p, size * 0.5, vec2(-size, -size * 0.5))); - d = min(d, hLevel(p, size * 0.5, vec2(size, -size * 0.5))); - } - if (levels > 2.5) { - float s = size * 0.25; - for (int ix = 0; ix < 4; ix += 1) { - float fx = float(ix); - vec2 o = vec2((mod(fx, 2.0) * 2.0 - 1.0) * size * 1.5, (floor(fx / 2.0) * 2.0 - 1.0) * size * 0.75); - d = min(d, hLevel(p, s, o)); - } - } - return d; -} - -float sdf(vec3 p) { - float beam = hTree(p.xy) - beamRadius; - float slab = abs(p.z) - thickness * 0.5; - return max(beam, slab); -} - -vec3 color(vec3 p, vec3 normal) { - float branch = 0.5 + 0.5 * sin((abs(p.x) + abs(p.y)) * 0.13); - vec3 c = mix(baseColor, accentColor, branch); - return c * (0.58 + 0.42 * smoothstep(-0.2, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "H-tree solid", - description: "A recursive H-tree beam structure for branching structural CAD demos.", - units: "mm", - params: { - size: { type: "number", label: "Branch size", min: 8, max: 24, step: 0.5, default: 15, unit: "mm" }, - levels: { type: "number", label: "Levels", min: 1, max: 3, step: 1, default: 3 }, - beamRadius: { type: "number", label: "Beam radius", min: 0.35, max: 2.2, step: 0.05, default: 0.95, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 1, max: 5, step: 0.1, default: 2.4, unit: "mm" }, - baseColor: { type: "color", label: "Core color", default: "#60d5ff" }, - accentColor: { type: "color", label: "Branch color", default: "#b8ff7a" } - }, - animations: { - branchPulse: { - label: "Branch pulse", - duration: 4.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("beamRadius", 0.95 + wave * 0.28); - set("thickness", 2.4 + (wave * 0.5 + 0.5) * 1.0); - } - } - }, - bounds: ({ params }) => { - const extent = params.size * 1.95 + params.beamRadius + 4; - const z = params.thickness + 3; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 170, - stepScale: 0.72, - maxStep: Math.max(params.beamRadius * 3.4, 0.9), - epsilon: Math.max(params.beamRadius * 0.012, 0.004), - normalEpsilon: Math.max(params.beamRadius * 0.08, 0.032) - }), - glsl: GLSL -}; diff --git a/models/implicits/helicoid-printable-ribbon.implicit.js b/models/implicits/helicoid-printable-ribbon.implicit.js deleted file mode 100644 index 36b2bd03..00000000 --- a/models/implicits/helicoid-printable-ribbon.implicit.js +++ /dev/null @@ -1,53 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float theta = atan(p.y, p.x); - float r = length(p.xy); - float periodZ = max(pitch, 0.1); - float phase = p.z - theta / 6.2831853 * periodZ; - phase = mod(phase + periodZ * 0.5, periodZ) - periodZ * 0.5; - float sheet = abs(phase) - thickness; - float radialBand = abs(r - radius) - ribbonWidth * 0.5; - float ribbon = max(sheet, radialBand); - float zClip = abs(p.z) - height * 0.5; - float hub = implicit_cylinder_capped(p, vec3(0.0, 0.0, -height * 0.52), vec3(0.0, 0.0, height * 0.52), thickness * 1.15); - return implicit_union_round(max(ribbon, zClip), hub, thickness * 0.8); -} - -vec3 color(vec3 p, vec3 normal) { - float helix = fract((atan(p.y, p.x) / 6.2831853) + p.z / max(pitch, 0.1)); - float edge = smoothstep(ribbonWidth * 0.36, ribbonWidth * 0.55, abs(length(p.xy) - radius)); - vec3 body = mix(baseColor * 0.65, baseColor, helix); - return mix(body, accentColor, edge * 0.75); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "helicoid printable ribbon", - description: "Prompt 10: adjustable helicoid thickened into a printable screw-like ribbon.", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 38, step: 0.25, default: 26, unit: "mm" }, - pitch: { type: "number", label: "Pitch", min: 14, max: 48, step: 0.5, default: 28, unit: "mm" }, - height: { type: "number", label: "Height", min: 24, max: 82, step: 1, default: 58, unit: "mm" }, - ribbonWidth: { type: "number", label: "Ribbon width", min: 5, max: 24, step: 0.25, default: 13, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.8, max: 6, step: 0.1, default: 2.3, unit: "mm" }, - baseColor: { type: "color", label: "Ribbon color", default: "#c77dff" }, - accentColor: { type: "color", label: "Edge color", default: "#35f0ff" } - }, - animations: { - pitchSweep: { - label: "Pitch sweep", - duration: 6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("pitch", 24 + wave * 12); - set("ribbonWidth", 10 + (1 - Math.abs(wave)) * 10); - } - } - }, - bounds: [[-44, -44, -48], [44, 44, 48]], - render: { steps: 280, epsilon: 0.004, normalEpsilon: 0.045 }, - glsl: GLSL -}; diff --git a/models/implicits/hilbert-curve-3d.implicit.js b/models/implicits/hilbert-curve-3d.implicit.js deleted file mode 100644 index 415c505e..00000000 --- a/models/implicits/hilbert-curve-3d.implicit.js +++ /dev/null @@ -1,129 +0,0 @@ -const GLSL = ` -float sdHilbertGridSegment(vec3 g, vec3 a, vec3 b) { - vec3 mn = min(a, b); - vec3 mx = max(a, b); - vec3 q = max(max(mn - g, g - mx), vec3(0.0)); - return length(q) * cellSize; -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -float sdf(vec3 p) { - vec3 g = p / max(cellSize, 0.0001) + vec3(1.5); - float d = 100000.0; - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 0.0, 1.0), vec3(1.0, 1.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 1.0, 1.0), vec3(0.0, 1.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 1.0, 1.0), vec3(0.0, 0.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 0.0, 2.0), vec3(0.0, 0.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 0.0, 3.0), vec3(1.0, 0.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 0.0, 3.0), vec3(1.0, 0.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 0.0, 2.0), vec3(1.0, 1.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 1.0, 2.0), vec3(1.0, 1.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 1.0, 3.0), vec3(0.0, 1.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 1.0, 3.0), vec3(0.0, 1.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 1.0, 2.0), vec3(0.0, 2.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 2.0, 2.0), vec3(0.0, 2.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 2.0, 3.0), vec3(0.0, 3.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 3.0, 3.0), vec3(0.0, 3.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 3.0, 2.0), vec3(1.0, 3.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 3.0, 2.0), vec3(1.0, 3.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 3.0, 3.0), vec3(1.0, 2.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 2.0, 3.0), vec3(1.0, 2.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 2.0, 2.0), vec3(1.0, 2.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 2.0, 1.0), vec3(0.0, 2.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 2.0, 1.0), vec3(0.0, 3.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 3.0, 1.0), vec3(1.0, 3.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 3.0, 1.0), vec3(1.0, 3.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 3.0, 0.0), vec3(0.0, 3.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 3.0, 0.0), vec3(0.0, 2.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(0.0, 2.0, 0.0), vec3(1.0, 2.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(1.0, 2.0, 0.0), vec3(2.0, 2.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 2.0, 0.0), vec3(3.0, 2.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 2.0, 0.0), vec3(3.0, 3.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 3.0, 0.0), vec3(2.0, 3.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 3.0, 0.0), vec3(2.0, 3.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 3.0, 1.0), vec3(3.0, 3.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 3.0, 1.0), vec3(3.0, 2.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 2.0, 1.0), vec3(2.0, 2.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 2.0, 1.0), vec3(2.0, 2.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 2.0, 2.0), vec3(2.0, 2.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 2.0, 3.0), vec3(2.0, 3.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 3.0, 3.0), vec3(2.0, 3.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 3.0, 2.0), vec3(3.0, 3.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 3.0, 2.0), vec3(3.0, 3.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 3.0, 3.0), vec3(3.0, 2.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 2.0, 3.0), vec3(3.0, 2.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 2.0, 2.0), vec3(3.0, 1.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 1.0, 2.0), vec3(3.0, 1.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 1.0, 3.0), vec3(2.0, 1.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 1.0, 3.0), vec3(2.0, 1.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 1.0, 2.0), vec3(2.0, 0.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 0.0, 2.0), vec3(2.0, 0.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 0.0, 3.0), vec3(3.0, 0.0, 3.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 0.0, 3.0), vec3(3.0, 0.0, 2.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 0.0, 2.0), vec3(3.0, 0.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 0.0, 1.0), vec3(3.0, 1.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 1.0, 1.0), vec3(2.0, 1.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 1.0, 1.0), vec3(2.0, 0.0, 1.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 0.0, 1.0), vec3(2.0, 0.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 0.0, 0.0), vec3(2.0, 1.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(2.0, 1.0, 0.0), vec3(3.0, 1.0, 0.0)), cornerBlend); - d = smoothMin(d, sdHilbertGridSegment(g, vec3(3.0, 1.0, 0.0), vec3(3.0, 0.0, 0.0)), cornerBlend); - return d - tubeRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float sweep = 0.5 + 0.5 * sin((p.x + p.y * 0.7 + p.z * 1.3) * 0.12); - float glow = pow(max(dot(normal, normalize(vec3(-0.35, 0.42, 0.84))), 0.0), 2.0); - vec3 c = mix(baseColor, midColor, smoothstep(0.0, 0.58, sweep)); - c = mix(c, accentColor, smoothstep(0.48, 1.0, sweep)); - return c * (0.62 + glow * 0.48) + vec3(0.04, 0.05, 0.08); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "3D Hilbert curve", - description: "A true order-2 3D Hilbert traversal through a 4x4x4 lattice, thickened into a rounded tube.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 5, max: 11, step: 0.25, default: 7.2, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 0.45, max: 2.6, step: 0.05, default: 1.2, unit: "mm" }, - cornerBlend: { type: "number", label: "Corner blend", min: 0.1, max: 2.6, step: 0.05, default: 0.85, unit: "mm" }, - baseColor: { type: "color", label: "Start color", default: "#35d0ff" }, - midColor: { type: "color", label: "Middle color", default: "#baff39" }, - accentColor: { type: "color", label: "End color", default: "#ff65b8" } - }, - animations: { - pathPulse: { - label: "Path pulse", - duration: 5.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("tubeRadius", 1.2 + wave * 0.38); - set("cornerBlend", 0.85 + (wave * 0.5 + 0.5) * 0.9); - } - } - }, - bounds: ({ params }) => { - const extent = params.cellSize * 2.05 + params.tubeRadius + params.cornerBlend + 6; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.62, - maxStep: Math.max(params.tubeRadius * 3.2, 1.1), - epsilon: Math.max(params.tubeRadius * 0.012, 0.005), - normalEpsilon: Math.max(params.tubeRadius * 0.07, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/hilbert-curve-block.implicit.js b/models/implicits/hilbert-curve-block.implicit.js deleted file mode 100644 index 01fcac6e..00000000 --- a/models/implicits/hilbert-curve-block.implicit.js +++ /dev/null @@ -1,150 +0,0 @@ -const GLSL = ` -float sdBox(vec3 p, vec3 b) { - vec3 q = abs(p) - b; - return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); -} - -float sdSegment2(vec2 p, vec2 a, vec2 b) { - vec2 pa = p - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -vec2 hilbertGridPoint2(float i) { - float index = clamp(floor(i + 0.5), 0.0, 63.0); - if (index < 0.5) { return vec2(0.0, 0.0); } - if (index < 1.5) { return vec2(0.0, 1.0); } - if (index < 2.5) { return vec2(1.0, 1.0); } - if (index < 3.5) { return vec2(1.0, 0.0); } - if (index < 4.5) { return vec2(2.0, 0.0); } - if (index < 5.5) { return vec2(3.0, 0.0); } - if (index < 6.5) { return vec2(3.0, 1.0); } - if (index < 7.5) { return vec2(2.0, 1.0); } - if (index < 8.5) { return vec2(2.0, 2.0); } - if (index < 9.5) { return vec2(3.0, 2.0); } - if (index < 10.5) { return vec2(3.0, 3.0); } - if (index < 11.5) { return vec2(2.0, 3.0); } - if (index < 12.5) { return vec2(1.0, 3.0); } - if (index < 13.5) { return vec2(1.0, 2.0); } - if (index < 14.5) { return vec2(0.0, 2.0); } - if (index < 15.5) { return vec2(0.0, 3.0); } - if (index < 16.5) { return vec2(0.0, 4.0); } - if (index < 17.5) { return vec2(1.0, 4.0); } - if (index < 18.5) { return vec2(1.0, 5.0); } - if (index < 19.5) { return vec2(0.0, 5.0); } - if (index < 20.5) { return vec2(0.0, 6.0); } - if (index < 21.5) { return vec2(0.0, 7.0); } - if (index < 22.5) { return vec2(1.0, 7.0); } - if (index < 23.5) { return vec2(1.0, 6.0); } - if (index < 24.5) { return vec2(2.0, 6.0); } - if (index < 25.5) { return vec2(2.0, 7.0); } - if (index < 26.5) { return vec2(3.0, 7.0); } - if (index < 27.5) { return vec2(3.0, 6.0); } - if (index < 28.5) { return vec2(3.0, 5.0); } - if (index < 29.5) { return vec2(2.0, 5.0); } - if (index < 30.5) { return vec2(2.0, 4.0); } - if (index < 31.5) { return vec2(3.0, 4.0); } - if (index < 32.5) { return vec2(4.0, 4.0); } - if (index < 33.5) { return vec2(5.0, 4.0); } - if (index < 34.5) { return vec2(5.0, 5.0); } - if (index < 35.5) { return vec2(4.0, 5.0); } - if (index < 36.5) { return vec2(4.0, 6.0); } - if (index < 37.5) { return vec2(4.0, 7.0); } - if (index < 38.5) { return vec2(5.0, 7.0); } - if (index < 39.5) { return vec2(5.0, 6.0); } - if (index < 40.5) { return vec2(6.0, 6.0); } - if (index < 41.5) { return vec2(6.0, 7.0); } - if (index < 42.5) { return vec2(7.0, 7.0); } - if (index < 43.5) { return vec2(7.0, 6.0); } - if (index < 44.5) { return vec2(7.0, 5.0); } - if (index < 45.5) { return vec2(6.0, 5.0); } - if (index < 46.5) { return vec2(6.0, 4.0); } - if (index < 47.5) { return vec2(7.0, 4.0); } - if (index < 48.5) { return vec2(7.0, 3.0); } - if (index < 49.5) { return vec2(7.0, 2.0); } - if (index < 50.5) { return vec2(6.0, 2.0); } - if (index < 51.5) { return vec2(6.0, 3.0); } - if (index < 52.5) { return vec2(5.0, 3.0); } - if (index < 53.5) { return vec2(4.0, 3.0); } - if (index < 54.5) { return vec2(4.0, 2.0); } - if (index < 55.5) { return vec2(5.0, 2.0); } - if (index < 56.5) { return vec2(5.0, 1.0); } - if (index < 57.5) { return vec2(4.0, 1.0); } - if (index < 58.5) { return vec2(4.0, 0.0); } - if (index < 59.5) { return vec2(5.0, 0.0); } - if (index < 60.5) { return vec2(6.0, 0.0); } - if (index < 61.5) { return vec2(6.0, 1.0); } - if (index < 62.5) { return vec2(7.0, 1.0); } - return vec2(7.0, 0.0); -} - -vec2 hilbertBlockPoint(float i) { - return (hilbertGridPoint2(i) - vec2(3.5)) * cellSize; -} - -float channelField(vec2 p) { - float d = 100000.0; - for (int i = 0; i < 63; i += 1) { - float fi = float(i); - d = min(d, sdSegment2(p, hilbertBlockPoint(fi), hilbertBlockPoint(fi + 1.0))); - } - return d; -} - -float sdf(vec3 p) { - float extent = cellSize * 4.1; - float plate = sdBox(p + vec3(0.0, 0.0, plateThickness * 0.18), vec3(extent, extent, plateThickness * 0.42)); - float channel = max(channelField(p.xy) - channelWidth, abs(p.z - plateThickness * 0.18) - plateThickness * 0.34); - float raisedRim = max(channelField(p.xy) - channelWidth * 1.35, abs(p.z - plateThickness * 0.42) - liftHeight * 0.36); - return min(max(plate, -channel), raisedRim); -} - -vec3 color(vec3 p, vec3 normal) { - float path = smoothstep(channelWidth * 1.8, channelWidth * 0.35, channelField(p.xy)); - float grid = 0.5 + 0.5 * sin((p.x + p.y) * 0.22); - vec3 plateColor = mix(baseColor * 0.52, baseColor, 0.45 + 0.55 * normal.z); - vec3 pathColor = mix(accentColor * 0.72, accentColor, grid); - return mix(plateColor, pathColor, path); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "2D Hilbert curve block", - description: "A true order-3 planar Hilbert curve cut into a solid plate with a raised colored trace.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 3.5, max: 8, step: 0.1, default: 5.3, unit: "mm" }, - channelWidth: { type: "number", label: "Channel width", min: 0.45, max: 2.4, step: 0.05, default: 1.15, unit: "mm" }, - plateThickness: { type: "number", label: "Plate thickness", min: 1.6, max: 6, step: 0.1, default: 3.2, unit: "mm" }, - liftHeight: { type: "number", label: "Trace lift", min: 0.3, max: 2.4, step: 0.05, default: 1.0, unit: "mm" }, - baseColor: { type: "color", label: "Plate color", default: "#a5b4fc" }, - accentColor: { type: "color", label: "Trace color", default: "#ccff33" } - }, - animations: { - channelBreath: { - label: "Channel breath", - duration: 4.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("channelWidth", 1.15 + wave * 0.42); - set("liftHeight", 1.0 + (wave * 0.5 + 0.5) * 0.75); - } - } - }, - bounds: ({ params }) => { - const extent = params.cellSize * 4.45 + params.channelWidth + 4; - const z = params.plateThickness + params.liftHeight + 3; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 190, - stepScale: 0.7, - maxStep: Math.max(params.channelWidth * 3.5, 1.1), - epsilon: Math.max(params.channelWidth * 0.012, 0.004), - normalEpsilon: Math.max(params.channelWidth * 0.08, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/jerusalem-cube.implicit.js b/models/implicits/jerusalem-cube.implicit.js deleted file mode 100644 index 544bb786..00000000 --- a/models/implicits/jerusalem-cube.implicit.js +++ /dev/null @@ -1,73 +0,0 @@ -const GLSL = ` -float sdBox(vec3 p, vec3 b) { - vec3 q = abs(p) - b; - return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); -} - -float crossVoid(vec3 p, float s) { - float w = voidWidth * s; - float tunnelX = max(abs(p.y) - w, abs(p.z) - w); - float tunnelY = max(abs(p.x) - w, abs(p.z) - w); - float tunnelZ = max(abs(p.x) - w, abs(p.y) - w); - return min(tunnelX, min(tunnelY, tunnelZ)); -} - -float sdf(vec3 p) { - float d = sdBox(p, vec3(size)); - float v = crossVoid(p, 1.0); - if (levels > 1.5) { - vec3 q = mod(p + size * 0.5, size) - size * 0.5; - v = min(v, crossVoid(q, 0.42)); - } - if (levels > 2.5) { - vec3 r = mod(p + size * 0.22, size * 0.44) - size * 0.22; - v = min(v, crossVoid(r, 0.18)); - } - return max(d - roundness, -(v + roundness)); -} - -vec3 color(vec3 p, vec3 normal) { - float nested = 0.5 + 0.5 * sin((abs(p.x) + abs(p.y) + abs(p.z)) * 0.22); - vec3 c = mix(baseColor, accentColor, nested); - return c * (0.56 + 0.44 * pow(max(dot(normal, normalize(vec3(-0.38, 0.42, 0.82))), 0.0), 1.8)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Jerusalem cube", - description: "A Jerusalem-cube-style fractal with nested orthogonal cutouts and rounded internal corners.", - units: "mm", - params: { - size: { type: "number", label: "Half size", min: 12, max: 34, step: 0.5, default: 24, unit: "mm" }, - levels: { type: "number", label: "Levels", min: 1, max: 3, step: 1, default: 3 }, - voidWidth: { type: "number", label: "Void width", min: 1.2, max: 6.5, step: 0.1, default: 3.3, unit: "mm" }, - roundness: { type: "number", label: "Corner round", min: 0.05, max: 1.6, step: 0.05, default: 0.42, unit: "mm" }, - baseColor: { type: "color", label: "Cube color", default: "#ffd166" }, - accentColor: { type: "color", label: "Interior color", default: "#ff5c8a" } - }, - animations: { - nestedCuts: { - label: "Nested cuts", - duration: 5.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("voidWidth", 3.3 + wave * 1.2); - set("roundness", 0.42 + (wave * 0.5 + 0.5) * 0.55); - } - } - }, - bounds: ({ params }) => { - const extent = params.size + params.roundness + 5; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 240, - stepScale: 0.64, - maxStep: Math.max(params.roundness * 5, 1.0), - epsilon: Math.max(params.roundness * 0.014, 0.004), - normalEpsilon: Math.max(params.roundness * 0.12, 0.036) - }), - glsl: GLSL -}; diff --git a/models/implicits/klein-bottle-immersion.implicit.js b/models/implicits/klein-bottle-immersion.implicit.js deleted file mode 100644 index f6085d0e..00000000 --- a/models/implicits/klein-bottle-immersion.implicit.js +++ /dev/null @@ -1,149 +0,0 @@ -const GLSL = ` -const float KLEIN_TAU = 6.283185307179586; - -float sdEllipsoid(vec3 p, vec3 r) { - float k0 = length(p / r); - float k1 = length(p / (r * r)); - return k0 * (k0 - 1.0) / max(k1, 0.0001); -} - -float sdCapsuleLine(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float sdTorus(vec3 p, vec2 t) { - vec2 q = vec2(length(p.xy) - t.x, p.z); - return length(q) - t.y; -} - -vec3 cubicBezier(vec3 a, vec3 b, vec3 c, vec3 d, float t) { - float s = 1.0 - t; - return s * s * s * a + 3.0 * s * s * t * b + 3.0 * s * t * t * c + t * t * t * d; -} - -float neckCenterDistance(vec3 q) { - vec3 p0 = vec3(0.0, -0.03, bulbHeight * 0.82); - vec3 p1 = vec3(0.12, -bulbDepth * 0.18, bulbHeight + neckRise); - vec3 p2 = vec3(-handleReach, -bulbDepth * 0.95, bulbHeight + neckRise * 0.72); - vec3 p3 = vec3(-entryOffset, -bulbDepth * 1.02, 0.12); - vec3 p4 = vec3(0.08, -bulbDepth * 0.22, -bulbHeight * 0.56); - - vec3 previous = p0; - float d = 10000.0; - for (int i = 1; i <= 28; i += 1) { - float t = float(i) / 28.0; - vec3 current = cubicBezier(p0, p1, p2, p3, t); - d = min(d, sdCapsuleLine(q, previous, current)); - previous = current; - } - - previous = p3; - for (int i = 1; i <= 20; i += 1) { - float t = float(i) / 20.0; - vec3 current = cubicBezier( - p3, - vec3(-entryOffset * 0.7, -bulbDepth * 0.72, -0.34), - vec3(0.16, -bulbDepth * 0.28, -bulbHeight * 0.86), - p4, - t - ); - d = min(d, sdCapsuleLine(q, previous, current)); - previous = current; - } - return d; -} - -float smoothUnion(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -float sdf(vec3 p) { - vec3 q = p / scale; - q.z -= verticalOffset; - - vec3 bulbCenter = vec3(0.08, 0.0, -0.26); - float shell = abs(sdEllipsoid(q - bulbCenter, vec3(bulbWidth, bulbDepth, bulbHeight))) - wallThickness / scale; - - vec3 entryPoint = vec3(-entryOffset, -bulbDepth * 1.02, 0.12); - float entryHole = sdEllipsoid(q - entryPoint, vec3(neckRadius * 1.85, neckRadius * 1.34, neckRadius * 2.05)); - shell = max(shell, -entryHole); - - float neckCore = neckCenterDistance(q) - neckRadius; - float neck = abs(neckCore) - wallThickness / scale; - - vec3 topPoint = q - vec3(0.0, -0.03, bulbHeight * 0.82); - float topLip = abs(sdTorus(vec3(topPoint.x, topPoint.y, topPoint.z * 0.76), vec2(neckRadius, wallThickness / scale * 1.15))) - wallThickness / scale * 0.5; - - float body = smoothUnion(shell, neck, blendRadius); - return min(body, topLip) * scale; -} - -vec3 color(vec3 p, vec3 normal) { - vec3 q = p / max(scale, 0.0001); - q.z -= verticalOffset; - float neckBand = 1.0 - smoothstep(neckRadius + 0.02, neckRadius + 0.54, neckCenterDistance(q)); - float entryGlow = 1.0 - smoothstep(0.0, neckRadius * 2.1, length(q - vec3(-entryOffset, -bulbDepth * 1.02, 0.12))); - float heightBand = smoothstep(-bulbHeight * 0.9, bulbHeight * 1.18, q.z); - float swirl = 0.5 + 0.5 * sin(KLEIN_TAU * (atan(q.y, q.x) / KLEIN_TAU + q.z * 0.22)); - float rim = pow(max(dot(normal, normalize(vec3(-0.35, 0.56, 0.75))), 0.0), 2.0); - vec3 body = mix(bulbColor, insideColor, heightBand * 0.24 + swirl * 0.12); - body = mix(body, neckColor, neckBand * 0.82); - body = mix(body, insideColor, entryGlow * 0.52); - return mix(body, rimColor, rim * 0.35); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Klein bottle immersion", - description: "A fast bounded classic Klein bottle model with a visible self-passing neck and front entry.", - units: "mm", - params: { - scale: { type: "number", label: "Scale", min: 10, max: 30, step: 0.25, default: 17, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.25, max: 2.2, step: 0.05, default: 0.62, unit: "mm" }, - bulbWidth: { type: "number", label: "Bulb width", min: 1.0, max: 2.0, step: 0.02, default: 1.25 }, - bulbDepth: { type: "number", label: "Bulb depth", min: 0.75, max: 1.55, step: 0.02, default: 1.05 }, - bulbHeight: { type: "number", label: "Bulb height", min: 1.35, max: 2.55, step: 0.02, default: 1.88 }, - neckRadius: { type: "number", label: "Neck radius", min: 0.16, max: 0.58, step: 0.01, default: 0.29 }, - handleReach: { type: "number", label: "Handle reach", min: 1.1, max: 2.5, step: 0.02, default: 1.58 }, - neckRise: { type: "number", label: "Neck rise", min: 0.45, max: 1.45, step: 0.02, default: 1.08 }, - entryOffset: { type: "number", label: "Entry offset", min: -0.45, max: 0.85, step: 0.02, default: 0.14 }, - blendRadius: { type: "number", label: "Blend", min: 0.02, max: 0.28, step: 0.01, default: 0.08 }, - verticalOffset: { type: "number", label: "Vertical offset", min: -0.8, max: 0.8, step: 0.02, default: -0.04 }, - bulbColor: { type: "color", label: "Bulb color", default: "#b36bff" }, - neckColor: { type: "color", label: "Neck color", default: "#35e7ff" }, - insideColor: { type: "color", label: "Inside color", default: "#ff86ca" }, - rimColor: { type: "color", label: "Rim color", default: "#ffe066" } - }, - animations: { - bottleBreath: { - label: "Bottle breath", - duration: 5.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("handleReach", 1.58 + wave * 0.14); - set("neckRise", 1.08 + wave * 0.08); - set("wallThickness", 0.62 + wave * 0.08); - } - } - }, - bounds: ({ params }) => { - const x = params.scale * Math.max(params.bulbWidth + 0.45, params.handleReach + params.neckRadius + 0.35) + params.wallThickness + 5; - const y = params.scale * (params.bulbDepth + params.neckRadius + 0.55) + params.wallThickness + 5; - const z = params.scale * (params.bulbHeight + params.neckRise + params.neckRadius + Math.abs(params.verticalOffset) + 0.45) + params.wallThickness + 5; - return [[-x, -y, -z], [x, y, z]]; - }, - render: ({ params }) => ({ - steps: 280, - stepScale: 0.62, - maxStep: Math.max(params.wallThickness * 2.5, params.scale * 0.05), - epsilon: Math.max(0.004, params.wallThickness * 0.006), - normalEpsilon: Math.max(0.035, params.wallThickness * 0.05) - }), - glsl: GLSL -}; diff --git a/models/implicits/koch-snowflake-extrusion.implicit.js b/models/implicits/koch-snowflake-extrusion.implicit.js deleted file mode 100644 index 9d08960c..00000000 --- a/models/implicits/koch-snowflake-extrusion.implicit.js +++ /dev/null @@ -1,75 +0,0 @@ -const GLSL = ` -float sdBox(vec3 p, vec3 b) { - vec3 q = abs(p) - b; - return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); -} - -float snowflakeRadius(float a) { - float r = radius * (0.72 + 0.12 * cos(3.0 * a)); - if (depth > 1.5) { - r += radius * 0.105 * cos(12.0 * a); - } - if (depth > 2.5) { - r += radius * 0.055 * cos(48.0 * a); - } - if (depth > 3.5) { - r += radius * 0.026 * cos(96.0 * a); - } - return r; -} - -float sdf(vec3 p) { - float a = atan(p.y, p.x); - float radial = length(p.xy) - snowflakeRadius(a); - float sideBevel = radial - edgeRound; - float slab = abs(p.z) - thickness * 0.5 + edgeRound; - return min(max(sideBevel, slab), 0.0) + length(max(vec2(sideBevel, slab), 0.0)) - edgeRound; -} - -vec3 color(vec3 p, vec3 normal) { - float a = atan(p.y, p.x); - float ice = 0.5 + 0.5 * cos(a * 12.0 + length(p.xy) * 0.11); - vec3 c = mix(baseColor, accentColor, ice); - return c * (0.62 + 0.38 * smoothstep(-0.3, 0.95, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Koch snowflake extrusion", - description: "A rounded extruded Koch-snowflake approximation with controllable recursive edge detail.", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 42, step: 0.5, default: 28, unit: "mm" }, - depth: { type: "number", label: "Recursion depth", min: 1, max: 4, step: 1, default: 4 }, - thickness: { type: "number", label: "Thickness", min: 1.5, max: 9, step: 0.1, default: 4.2, unit: "mm" }, - edgeRound: { type: "number", label: "Edge round", min: 0.05, max: 1.6, step: 0.05, default: 0.55, unit: "mm" }, - baseColor: { type: "color", label: "Ice color", default: "#e0fbff" }, - accentColor: { type: "color", label: "Edge color", default: "#00a6ff" } - }, - animations: { - frostBloom: { - label: "Frost bloom", - duration: 5.0, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("edgeRound", 0.55 + (wave * 0.5 + 0.5) * 0.75); - set("thickness", 4.2 + wave * 1.2); - } - } - }, - bounds: ({ params }) => { - const extent = params.radius + params.edgeRound + 7; - const z = params.thickness + params.edgeRound + 4; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 180, - stepScale: 0.72, - maxStep: Math.max(params.edgeRound * 4.5, 1.0), - epsilon: Math.max(params.edgeRound * 0.012, 0.004), - normalEpsilon: Math.max(params.edgeRound * 0.09, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/mandelbulb-distance-estimate.implicit.js b/models/implicits/mandelbulb-distance-estimate.implicit.js deleted file mode 100644 index 3e869e99..00000000 --- a/models/implicits/mandelbulb-distance-estimate.implicit.js +++ /dev/null @@ -1,68 +0,0 @@ -const GLSL = ` -const float IMPLICIT_MOBIUS_TAU = 6.283185307179586; - -float mandelbulbDE(vec3 p) { - vec3 z = p / max(radius * 0.72, 0.1) * detailScale; - float dr = 1.0; - float r = 0.0; - for (int i = 0; i < 8; i++) { - r = length(z); - if (r > 2.0 || float(i) >= iterations) { - break; - } - float theta = acos(clamp(z.z / max(r, 0.0001), -1.0, 1.0)); - float phi = atan(z.y, z.x); - dr = pow(r, power - 1.0) * power * dr + 1.0; - float zr = pow(r, power); - theta *= power; - phi *= power; - z = zr * vec3(sin(theta) * cos(phi), sin(phi) * sin(theta), cos(theta)) + p / max(radius * 0.72, 0.1) * detailScale; - } - return 0.5 * log(max(r, 0.0001)) * r / max(dr, 0.0001) * radius * 0.72; -} - -float sdf(vec3 p) { - float bulb = mandelbulbDE(p); - float clip = implicit_sphere(p, vec3(0.0), radius); - return max(bulb, clip); -} - -vec3 implicit_palette(float t) { - return 0.58 + 0.42 * cos(IMPLICIT_MOBIUS_TAU * (vec3(0.02, 0.38, 0.72) + t)); -} - -vec3 color(vec3 p, vec3 normal) { - float orbit = atan(p.y, p.x) / IMPLICIT_MOBIUS_TAU + length(p) * 0.018; - float ridge = pow(max(dot(normal, normalize(vec3(0.4, -0.7, 0.6))), 0.0), 2.0); - vec3 body = implicit_palette(orbit + p.z * 0.018); - return mix(body, vec3(1.0, 0.84, 0.48), ridge * 0.28); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "mandelbulb distance estimate", - description: "Prompt 19: Mandelbulb-style implicit fractal using distance estimation.", - units: "mm", - params: { - radius: { type: "number", label: "Fractal radius", min: 16, max: 42, step: 0.5, default: 30, unit: "mm" }, - power: { type: "number", label: "Power", min: 4, max: 10, step: 0.1, default: 8 }, - iterations: { type: "number", label: "Iterations", min: 3, max: 8, step: 1, default: 7 }, - detailScale: { type: "number", label: "Detail scale", min: 0.65, max: 1.45, step: 0.02, default: 1 } - }, - animations: { - powerOrbit: { - label: "Power orbit", - duration: 7.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("power", 6.2 + (wave * 0.5 + 0.5) * 3.2); - set("detailScale", 0.82 + (1 - Math.abs(wave)) * 0.48); - } - } - }, - bounds: [[-44, -44, -44], [44, 44, 44]], - render: { steps: 420, epsilon: 0.0035, normalEpsilon: 0.045, stepScale: 0.34 }, - glsl: GLSL -}; diff --git a/models/implicits/menger-sponge.implicit.js b/models/implicits/menger-sponge.implicit.js deleted file mode 100644 index 1cb58145..00000000 --- a/models/implicits/menger-sponge.implicit.js +++ /dev/null @@ -1,70 +0,0 @@ -const GLSL = ` -float sdBox(vec3 p, vec3 b) { - vec3 q = abs(p) - b; - return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); -} - -float menger(vec3 p) { - vec3 q = p / size; - float d = sdBox(q, vec3(1.0)); - float scale = 1.0; - for (int i = 0; i < 4; i += 1) { - if (float(i) < levels) { - vec3 a = mod(q * scale + 1.0, 2.0) - 1.0; - scale *= 3.0; - vec3 r = abs(1.0 - 3.0 * abs(a)); - float cross = min(max(r.x, r.y), min(max(r.y, r.z), max(r.z, r.x))) / scale; - d = max(d, cross); - } - } - return d * size - bevel; -} - -float sdf(vec3 p) { - return menger(p); -} - -vec3 color(vec3 p, vec3 normal) { - float cavities = 0.5 + 0.5 * sin((abs(p.x) + abs(p.y) + abs(p.z)) * 0.34); - vec3 c = mix(baseColor, accentColor, cavities); - return c * (0.55 + 0.45 * pow(max(dot(normal, normalize(vec3(0.45, 0.36, 0.82))), 0.0), 1.8)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Menger sponge", - description: "A recursive cubic sponge with orthogonal voids and softened printable interior edges.", - units: "mm", - params: { - size: { type: "number", label: "Half size", min: 12, max: 34, step: 0.5, default: 24, unit: "mm" }, - levels: { type: "number", label: "Levels", min: 1, max: 4, step: 1, default: 3 }, - bevel: { type: "number", label: "Edge soften", min: 0.05, max: 1.4, step: 0.05, default: 0.34, unit: "mm" }, - baseColor: { type: "color", label: "Outer color", default: "#b8c0ff" }, - accentColor: { type: "color", label: "Void color", default: "#2df5ff" } - }, - animations: { - voidBreath: { - label: "Void breath", - duration: 5.5, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("bevel", 0.34 + (wave * 0.5 + 0.5) * 0.55); - set("size", 24 + wave * 3.0); - } - } - }, - bounds: ({ params }) => { - const extent = params.size + params.bevel + 5; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 260, - stepScale: 0.62, - maxStep: Math.max(params.bevel * 5, 1.0), - epsilon: Math.max(params.bevel * 0.015, 0.004), - normalEpsilon: Math.max(params.bevel * 0.12, 0.036) - }), - glsl: GLSL -}; diff --git a/models/implicits/metaball-molecule-cluster.implicit.js b/models/implicits/metaball-molecule-cluster.implicit.js deleted file mode 100644 index 000d419b..00000000 --- a/models/implicits/metaball-molecule-cluster.implicit.js +++ /dev/null @@ -1,57 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float d = implicit_sphere(p, vec3(0.0), coreRadius); - d = implicit_union_round(d, implicit_sphere(p, vec3(spacing, 0.0, 0.0), satelliteRadius), blend); - d = implicit_union_round(d, implicit_sphere(p, vec3(-spacing, 0.0, 0.0), satelliteRadius), blend); - d = implicit_union_round(d, implicit_sphere(p, vec3(0.0, spacing, 0.0), satelliteRadius), blend); - d = implicit_union_round(d, implicit_sphere(p, vec3(0.0, -spacing, 0.0), satelliteRadius), blend); - d = implicit_union_round(d, implicit_sphere(p, vec3(0.0, 0.0, spacing), satelliteRadius), blend); - d = implicit_union_round(d, implicit_sphere(p, vec3(0.0, 0.0, -spacing), satelliteRadius), blend); - return d; -} - - -vec3 color(vec3 p, vec3 normal) { - vec3 directionTint = normalize(abs(p) + vec3(0.001)); - return xColor * directionTint.x + yColor * directionTint.y + zColor * directionTint.z; -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "metaball molecule cluster", - description: "Seven overlapping spheres blended into one smooth organic implicit solid.", - units: "mm", - params: { - coreRadius: { type: "number", label: "Core radius", min: 5, max: 14, step: 0.1, default: 9.5, unit: "mm" }, - satelliteRadius: { type: "number", label: "Satellite radius", min: 3.5, max: 10, step: 0.1, default: 7.2, unit: "mm" }, - spacing: { type: "number", label: "Spacing", min: 7, max: 20, step: 0.25, default: 13, unit: "mm" }, - blend: { type: "number", label: "Blend", min: 0.5, max: 8, step: 0.1, default: 5, unit: "mm" }, - xColor: { type: "color", label: "X lobes", default: "#4f8cff" }, - yColor: { type: "color", label: "Y lobes", default: "#5cff89" }, - zColor: { type: "color", label: "Z lobes", default: "#ff66d8" } - }, - animations: { - clusterPulse: { - label: "Cluster pulse", - duration: 4.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("spacing", 13 + wave * 4); - set("blend", 5 + wave * 2.2); - set("satelliteRadius", 7.2 + wave * 1.6); - } - } - }, - bounds: ({ params }) => { - const extent = params.spacing + Math.max(params.coreRadius, params.satelliteRadius) + params.blend + 5; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 240, - epsilon: Math.max(0.004, Math.min(params.coreRadius, params.satelliteRadius) * 0.0007), - normalEpsilon: Math.max(0.035, params.blend * 0.009) - }), - glsl: GLSL -}; diff --git a/models/implicits/mobius-strip.implicit.js b/models/implicits/mobius-strip.implicit.js deleted file mode 100644 index 8ef3b678..00000000 --- a/models/implicits/mobius-strip.implicit.js +++ /dev/null @@ -1,61 +0,0 @@ -const GLSL = ` -const float IMPLICIT_MOBIUS_TAU = 6.283185307179586; - -float sdf(vec3 p) { - float u = atan(p.y, p.x); - vec2 section = vec2(length(p.xy) - radius, p.z); - vec2 twistAxis = vec2(cos(0.5 * u), sin(0.5 * u)); - vec2 closest = twistAxis * clamp(dot(section, twistAxis), -(width * 0.5), (width * 0.5)); - return length(section - closest) - thickness; -} - - -vec3 implicit_palette(float t) { - return 0.58 + 0.42 * cos(6.283185307179586 * (vec3(0.02, 0.38, 0.72) + t)); -} - -vec3 color(vec3 p, vec3 normal) { - vec2 ring = vec2(p.x, p.y); - float angle = atan(ring.y, ring.x) / 6.283185307179586; - vec3 bandColor = implicit_palette(angle + p.z * 0.035); - return mix(bandColor, vec3(1.0, 0.84, 0.48), smoothstep(0.25, 0.95, abs(normal.z)) * 0.28); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "mobius strip", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 34, step: 0.25, default: 24, unit: "mm" }, - width: { type: "number", label: "Band width", min: 4, max: 22, step: 0.25, default: 13.6, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.25, max: 2.5, step: 0.05, default: 0.82, unit: "mm" } - }, - animations: { - bandBreath: { - label: "Band breath", - duration: 4.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("width", 13.6 + wave * 5.5); - set("thickness", 0.82 + wave * 0.35); - set("radius", 24 + wave * 4); - } - } - }, - bounds: ({ params }) => { - const halfWidth = params.width * 0.5; - const radialExtent = params.radius + halfWidth + params.thickness + 6; - const heightExtent = halfWidth + params.thickness + 6; - return [[-radialExtent, -radialExtent, -heightExtent], [radialExtent, radialExtent, heightExtent]]; - }, - render: ({ params }) => ({ - steps: 192, - stepScale: 0.72, - maxStep: Math.max(params.thickness * 3.8, 1.4), - epsilon: Math.max(params.thickness * 0.012, 0.006), - normalEpsilon: Math.max(params.thickness * 0.075, 0.04) - }), - glsl: GLSL -}; diff --git a/models/implicits/moore-curve-loop.implicit.js b/models/implicits/moore-curve-loop.implicit.js deleted file mode 100644 index a5f381bd..00000000 --- a/models/implicits/moore-curve-loop.implicit.js +++ /dev/null @@ -1,81 +0,0 @@ -const GLSL = ` -float sdSegment(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -vec3 moorePoint(float i) { - float row = floor(i / 6.0); - float col = mod(i, 6.0); - if (mod(row, 2.0) > 0.5) { - col = 5.0 - col; - } - vec2 planar = (vec2(col, row) - vec2(2.5)) * spacing; - float z = sin(i * 1.57079632679) * verticalWave; - return vec3(planar, z); -} - -float sdf(vec3 p) { - float d = 100000.0; - for (int i = 0; i < 35; i += 1) { - float fi = float(i); - d = smoothMin(d, sdSegment(p, moorePoint(fi), moorePoint(fi + 1.0)), bendBlend); - } - d = smoothMin(d, sdSegment(p, moorePoint(35.0), moorePoint(0.0)), bendBlend); - return d - tubeRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float ring = atan(p.y, p.x) * 0.15915494309 + length(p.xy) * 0.018; - float band = 0.5 + 0.5 * sin(ring * 6.28318530718 + p.z * 0.25); - vec3 c = mix(baseColor, accentColor, band); - return c * (0.58 + 0.42 * smoothstep(-0.4, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Moore curve loop", - description: "A closed Moore space-filling loop rendered as a continuous rounded tube.", - units: "mm", - params: { - spacing: { type: "number", label: "Cell spacing", min: 4, max: 9, step: 0.1, default: 6.2, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 0.4, max: 2.5, step: 0.05, default: 1.05, unit: "mm" }, - bendBlend: { type: "number", label: "Bend blend", min: 0.1, max: 2.4, step: 0.05, default: 0.72, unit: "mm" }, - verticalWave: { type: "number", label: "Vertical wave", min: 0, max: 5, step: 0.1, default: 1.4, unit: "mm" }, - baseColor: { type: "color", label: "Cool color", default: "#38d9ff" }, - accentColor: { type: "color", label: "Hot color", default: "#ff7ab6" } - }, - animations: { - loopWave: { - label: "Loop wave", - duration: 5.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("verticalWave", 1.4 + (wave * 0.5 + 0.5) * 3.2); - set("tubeRadius", 1.05 + wave * 0.26); - } - } - }, - bounds: ({ params }) => { - const extent = params.spacing * 3.2 + params.tubeRadius + 5; - const z = params.verticalWave + params.tubeRadius + 5; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 210, - stepScale: 0.66, - maxStep: Math.max(params.tubeRadius * 3.4, 1.2), - epsilon: Math.max(params.tubeRadius * 0.013, 0.005), - normalEpsilon: Math.max(params.tubeRadius * 0.075, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/morton-z-order-curve.implicit.js b/models/implicits/morton-z-order-curve.implicit.js deleted file mode 100644 index 45bcfaa7..00000000 --- a/models/implicits/morton-z-order-curve.implicit.js +++ /dev/null @@ -1,77 +0,0 @@ -const GLSL = ` -float sdSegment(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -float bitAt(float v, float place) { - return mod(floor(v / place), 2.0); -} - -vec3 mortonPoint(float i) { - float x = bitAt(i, 1.0) + bitAt(i, 8.0) * 2.0; - float y = bitAt(i, 2.0) + bitAt(i, 16.0) * 2.0; - float z = bitAt(i, 4.0) + bitAt(i, 32.0) * 2.0; - return (vec3(x, y, z) - vec3(1.5)) * spacing; -} - -float sdf(vec3 p) { - float d = 100000.0; - for (int i = 0; i < 63; i += 1) { - float fi = float(i); - d = smoothMin(d, sdSegment(p, mortonPoint(fi), mortonPoint(fi + 1.0)), cornerBlend); - } - return d - beamRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float codeBand = 0.5 + 0.5 * sin((p.x * 0.7 + p.y * 1.1 + p.z * 1.6) * 0.14); - vec3 c = mix(baseColor, accentColor, codeBand); - return c * (0.58 + 0.42 * pow(max(dot(normal, normalize(vec3(0.45, -0.36, 0.82))), 0.0), 1.8)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Morton Z-order curve", - description: "A stepped 3D Z-order traversal through a 4x4x4 grid, thickened into a printable beam network.", - units: "mm", - params: { - spacing: { type: "number", label: "Grid spacing", min: 4.5, max: 10.5, step: 0.1, default: 7.0, unit: "mm" }, - beamRadius: { type: "number", label: "Beam radius", min: 0.45, max: 2.4, step: 0.05, default: 1.05, unit: "mm" }, - cornerBlend: { type: "number", label: "Corner blend", min: 0.1, max: 2.2, step: 0.05, default: 0.62, unit: "mm" }, - baseColor: { type: "color", label: "Low bits", default: "#35e2ff" }, - accentColor: { type: "color", label: "High bits", default: "#c084fc" } - }, - animations: { - bitPulse: { - label: "Bit pulse", - duration: 5.5, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("spacing", 7.0 + wave * 1.25); - set("beamRadius", 1.05 + (wave * 0.5 + 0.5) * 0.42); - } - } - }, - bounds: ({ params }) => { - const extent = params.spacing * 2.05 + params.beamRadius + params.cornerBlend + 5; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.64, - maxStep: Math.max(params.beamRadius * 3.2, 1.1), - epsilon: Math.max(params.beamRadius * 0.012, 0.005), - normalEpsilon: Math.max(params.beamRadius * 0.075, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/neovius-porosity-cell.implicit.js b/models/implicits/neovius-porosity-cell.implicit.js deleted file mode 100644 index 41b19fa3..00000000 --- a/models/implicits/neovius-porosity-cell.implicit.js +++ /dev/null @@ -1,45 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float field = implicit_tpms_neovius(p, vec3(period), vec3(1.0), schwarzBlend); - float shell = implicit_shell(field * period * 0.13, wallThickness, 0.0); - float cube = implicit_box_centered(p, vec3(cellSize), vec3(0.0)); - return implicit_intersect_round(shell, cube, 1.5); -} - -vec3 color(vec3 p, vec3 normal) { - vec3 folded = abs(implicit_repeat_centered(p, vec3(period))); - float node = smoothstep(period * 0.18, period * 0.48, length(folded)); - float glow = pow(max(dot(normal, normalize(vec3(0.35, -0.4, 0.84))), 0.0), 2.0); - return mix(baseColor * (0.68 + 0.32 * glow), accentColor, node * 0.72); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "neovius porosity cell", - description: "Prompt 8: Neovius periodic implicit surface with complex internal channels.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 30, max: 80, step: 1, default: 56, unit: "mm" }, - period: { type: "number", label: "Period", min: 14, max: 36, step: 0.5, default: 23, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.8, max: 6, step: 0.1, default: 2.3, unit: "mm" }, - schwarzBlend: { type: "number", label: "Schwarz blend", min: 0, max: 1, step: 0.02, default: 0.22 }, - baseColor: { type: "color", label: "Channel color", default: "#20e6b8" }, - accentColor: { type: "color", label: "Node color", default: "#ffe45c" } - }, - animations: { - channelBloom: { - label: "Channel bloom", - duration: 6.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("wallThickness", 1.4 + (wave * 0.5 + 0.5) * 4.2); - set("schwarzBlend", 0.08 + (1 - Math.abs(wave)) * 0.74); - } - } - }, - bounds: [[-46, -46, -46], [46, 46, 46]], - render: { steps: 316, epsilon: 0.005, normalEpsilon: 0.055 }, - glsl: GLSL -}; diff --git a/models/implicits/parametric-pulse.implicit.js b/models/implicits/parametric-pulse.implicit.js deleted file mode 100644 index 8470a3b2..00000000 --- a/models/implicits/parametric-pulse.implicit.js +++ /dev/null @@ -1,47 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = implicit_rotate_axis(p, vec3(0.0), vec3(0.0, 0.0, 1.0), p.z * (twist * 0.018)); - float orb = implicit_sphere(q, vec3(0.0), radius); - float ring = implicit_torus(q.xzy, radius * 0.72, shell); - float groove = implicit_torus(q.yxz, radius * 0.54, shell * 0.62); - float carved = implicit_intersect_round(orb, -groove, max(shell * 0.35, 0.4)); - return implicit_union_round(carved, ring, max(shell * 0.55, 0.5)); -} - -vec3 color(vec3 p, vec3 normal) { - float bands = 0.5 + 0.5 * sin(0.07 * p.z + 2.5 * twist + normal.z * 1.4); - return mix(baseColor, accentColor, bands); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "parametric pulse", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 12, max: 34, step: 0.25, default: 22, unit: "mm" }, - twist: { type: "number", label: "Twist", min: -2.5, max: 2.5, step: 0.05, default: 0 }, - shell: { type: "number", label: "Shell", min: 1, max: 8, step: 0.1, default: 3.5, unit: "mm" }, - baseColor: { type: "color", label: "Cool color", default: "#20d9ff" }, - accentColor: { type: "color", label: "Pulse color", default: "#ff6b3d" } - }, - animations: { - breathe: { - label: "Breathe", - duration: 3.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("radius", 22 + wave * 7); - set("twist", wave * 1.6); - set("shell", 3.5 + wave * 1.8); - } - } - }, - bounds: ({ params }) => { - const extent = Math.max(params.radius + params.shell + 10, 42); - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - glsl: GLSL, - render: { steps: 224 } -}; diff --git a/models/implicits/peano-curve-solid.implicit.js b/models/implicits/peano-curve-solid.implicit.js deleted file mode 100644 index e195c67e..00000000 --- a/models/implicits/peano-curve-solid.implicit.js +++ /dev/null @@ -1,79 +0,0 @@ -const GLSL = ` -float sdSegment(vec3 p, vec3 a, vec3 b) { - vec3 pa = p - a; - vec3 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float smoothMin(float a, float b, float k) { - float h = clamp(0.5 + 0.5 * (b - a) / max(k, 0.0001), 0.0, 1.0); - return mix(b, a, h) - k * h * (1.0 - h); -} - -vec3 peanoPoint(float i) { - float row = floor(i / 9.0); - float col = mod(i, 9.0); - if (mod(row, 2.0) > 0.5) { - col = 8.0 - col; - } - vec2 planar = (vec2(col, row) - vec2(4.0)) * spacing; - float lift = sin((col * 2.0 + row) * 1.0471975512) * ripple; - return vec3(planar, lift); -} - -float sdf(vec3 p) { - float d = 100000.0; - for (int i = 0; i < 80; i += 1) { - float fi = float(i); - d = smoothMin(d, sdSegment(p, peanoPoint(fi), peanoPoint(fi + 1.0)), blendRadius); - } - return d - beamRadius; -} - -vec3 color(vec3 p, vec3 normal) { - float stripe = 0.5 + 0.5 * sin((p.x - p.y) * 0.26 + p.z * 0.45); - vec3 c = mix(baseColor, accentColor, stripe); - return c * (0.55 + 0.45 * smoothstep(-0.25, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Peano curve solid", - description: "A dense Peano-inspired recursive subdivision path thickened into a rounded solid beam.", - units: "mm", - params: { - spacing: { type: "number", label: "Subdivision spacing", min: 2.6, max: 6.5, step: 0.1, default: 4.2, unit: "mm" }, - beamRadius: { type: "number", label: "Beam radius", min: 0.35, max: 1.9, step: 0.05, default: 0.82, unit: "mm" }, - blendRadius: { type: "number", label: "Turn fillet", min: 0.1, max: 1.7, step: 0.05, default: 0.52, unit: "mm" }, - ripple: { type: "number", label: "Surface ripple", min: 0, max: 3, step: 0.05, default: 0.75, unit: "mm" }, - baseColor: { type: "color", label: "Base color", default: "#9b5cff" }, - accentColor: { type: "color", label: "Accent color", default: "#fff36b" } - }, - animations: { - subdivisionPulse: { - label: "Subdivision pulse", - duration: 4.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("beamRadius", 0.82 + wave * 0.2); - set("ripple", 0.75 + (wave * 0.5 + 0.5) * 1.55); - } - } - }, - bounds: ({ params }) => { - const extent = params.spacing * 4.45 + params.beamRadius + 4; - const z = params.ripple + params.beamRadius + 4; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 220, - stepScale: 0.62, - maxStep: Math.max(params.beamRadius * 3.0, 0.9), - epsilon: Math.max(params.beamRadius * 0.012, 0.004), - normalEpsilon: Math.max(params.beamRadius * 0.075, 0.032) - }), - glsl: GLSL -}; diff --git a/models/implicits/pelican-bicycle.implicit.js b/models/implicits/pelican-bicycle.implicit.js deleted file mode 100644 index aa7bd1fe..00000000 --- a/models/implicits/pelican-bicycle.implicit.js +++ /dev/null @@ -1,327 +0,0 @@ -export default { - schema: "implicit.js/0.1.0", - name: "Pelican on a Bicycle", - description: - "A cartoon pelican riding a classic diamond-frame bicycle. The crankAngle " + - "parameter drives the cranks, level pedals, IK-bent legs, chain-geared " + - "spoked wheels, and a gentle body bob; the Pedal animation loops one full " + - "crank revolution seamlessly.", - units: "mm", - bounds: { - min: [-134, -40, -3], - max: [140, 40, 234], - }, - params: { - crankAngle: { - type: "number", - label: "Crank angle", - min: 0, - max: 360, - default: 35, - step: 1, - unit: "deg", - }, - }, - animations: { - pedal: { - label: "Pedal", - duration: 2.4, - update({ progress, set }) { - set("crankAngle", progress * 360); - }, - }, - }, - render: { steps: 256, epsilon: 0.003 }, - glsl: ` -// ---------- shared helpers ---------- -vec2 pb_rot2(vec2 v, float a) { - float c = cos(a); - float s = sin(a); - return vec2(c * v.x - s * v.y, s * v.x + c * v.y); -} - -float pb_ellipsoid(vec3 p, vec3 c, vec3 r) { - vec3 q = (p - c) / r; - float k0 = length(q); - float k1 = length(q / r); - return k0 * (k0 - 1.0) / max(k1, 0.000001); -} - -float pb_crank() { - // Negative = forward pedalling for a bike facing +X. - return -crankAngle * 0.0174532925199433; -} - -float pb_bob() { - return sin(2.0 * pb_crank()) * 1.4; -} - -// Pedal-spindle center (on the bike midplane) for side s (+1 right, -1 left). -vec3 pb_pedal(float s) { - float a = pb_crank() + (s > 0.0 ? 0.0 : 3.14159265358979); - return vec3(3.0 + 20.0 * cos(a), 0.0, 45.0 + 20.0 * sin(a)); -} - -vec3 pb_hip(float s) { - return vec3(-6.0, s * 13.0, 136.0 + pb_bob()); -} - -vec3 pb_ankle(float s) { - vec3 pa = pb_pedal(s); - return vec3(pa.x, s * 18.0, pa.z + 5.2); -} - -// Two-bone IK knee (equal bone lengths), knee pushed forward (+X). -vec3 pb_knee(vec3 hip, vec3 ankle) { - float L = 54.0; - vec3 d = ankle - hip; - float dl = max(length(d), 0.0001); - float dcl = min(dl, 2.0 * L - 2.0); - vec3 n = d / dl; - vec3 e = vec3(-n.z, 0.0, n.x); - e = normalize(e) * (e.x < 0.0 ? -1.0 : 1.0); - float h = sqrt(max(L * L - 0.25 * dcl * dcl, 0.0)); - return hip + n * (0.5 * dcl) + e * h; -} - -// ---------- bicycle ---------- -float pb_dTires(vec3 p) { - vec3 qr = p - vec3(-72.0, 0.0, 57.0); - vec3 qf = p - vec3(78.0, 0.0, 57.0); - float rear = implicit_torus(qr.xzy, 50.0, 7.0); - float front = implicit_torus(qf.xzy, 50.0, 7.0); - return min(rear, front); -} - -float pb_wheelSilver(vec3 q, float spin) { - float rim = implicit_torus(q.xzy, 44.0, 2.6); - float hub = implicit_cylinder_capped(q, vec3(0.0, -9.0, 0.0), vec3(0.0, 9.0, 0.0), 4.5); - vec2 w = pb_rot2(vec2(q.x, q.z), spin); - float sect = 0.78539816339745; // 8 spokes - float ang = atan(w.y, w.x); - float snapped = floor(ang / sect + 0.5) * sect; - vec2 r = pb_rot2(w, -snapped); - float spokes = length(vec3(r.x - clamp(r.x, 8.0, 44.0), q.y, r.y)) - 1.1; - return min(rim, min(hub, spokes)); -} - -float pb_dSilver(vec3 p) { - // 1.25 wheel revs per crank rev: seamless loop (450 deg = 10 x 45 deg spoke - // symmetry) and reads as forward motion even in low-fps GIF captures. - // Negated because rotating the sample space by +a shows geometry at -a. - float spin = -1.25 * pb_crank(); - float d = pb_wheelSilver(p - vec3(-72.0, 0.0, 57.0), spin); - d = min(d, pb_wheelSilver(p - vec3(78.0, 0.0, 57.0), spin)); - // seatpost - d = min(d, implicit_capsule(p, vec3(-24.0, 0.0, 108.0), vec3(-26.0, 0.0, 128.0), 3.4)); - // steerer, stem, handlebar - d = min(d, implicit_capsule(p, vec3(50.0, 0.0, 110.0), vec3(42.4, 0.0, 124.5), 3.4)); - d = min(d, implicit_capsule(p, vec3(42.4, 0.0, 124.5), vec3(50.0, 0.0, 127.0), 3.2)); - d = min(d, implicit_capsule(p, vec3(50.0, -30.0, 127.0), vec3(50.0, 30.0, 127.0), 2.6)); - // crank arms + pedal spindles - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - vec3 pa = pb_pedal(s); - d = min(d, implicit_cone_capsule(p, vec3(3.0, s * 11.0, 45.0), pa + vec3(0.0, s * 14.0, 0.0), 3.2, 2.7)); - d = min(d, implicit_capsule(p, pa + vec3(0.0, s * 13.0, 0.0), pa + vec3(0.0, s * 23.0, 0.0), 1.7)); - } - return d; -} - -float pb_dFrame(vec3 p) { - vec3 BB = vec3(3.0, 0.0, 45.0); - vec3 S = vec3(-24.0, 0.0, 108.0); - vec3 H1 = vec3(50.0, 0.0, 110.0); - vec3 H2 = vec3(64.0, 0.0, 83.5); - float d = implicit_capsule(p, S, H1, 4.0); // top tube - d = min(d, implicit_capsule(p, BB, H2, 4.6)); // down tube - d = min(d, implicit_capsule(p, BB, S, 4.0)); // seat tube - d = min(d, implicit_capsule(p, H1, H2, 5.0)); // head tube - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - vec3 axleR = vec3(-72.0, s * 7.0, 57.0); - d = min(d, implicit_capsule(p, vec3(3.0, s * 5.0, 45.0), axleR, 3.0)); // chainstay - d = min(d, implicit_capsule(p, vec3(-24.0, s * 3.5, 108.0), axleR, 2.8)); // seatstay - d = min(d, implicit_capsule(p, vec3(64.0, s * 3.0, 83.5), vec3(78.0, s * 7.0, 57.0), 3.2)); // fork - } - return d; -} - -float pb_dDark(vec3 p) { - // chainring, rear cog, chain runs, pedal platforms - float d = implicit_cylinder_capped(p, vec3(3.0, 9.5, 45.0), vec3(3.0, 13.0, 45.0), 15.0); - d = min(d, implicit_cylinder_capped(p, vec3(-72.0, 9.5, 57.0), vec3(-72.0, 12.5, 57.0), 7.0)); - d = min(d, implicit_capsule(p, vec3(1.0, 11.2, 60.5), vec3(-72.0, 11.2, 64.5), 1.7)); - d = min(d, implicit_capsule(p, vec3(1.0, 11.2, 29.5), vec3(-72.0, 11.2, 50.5), 1.7)); - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - vec3 pa = pb_pedal(s); - d = min(d, implicit_box_centered(p, vec3(15.0, 6.0, 3.0), pa + vec3(0.0, s * 18.0, 0.0)) - 1.0); - } - return d; -} - -float pb_dLeather(vec3 p) { - // saddle - float d = pb_ellipsoid(p, vec3(-31.0, 0.0, 130.5), vec3(8.5, 7.0, 3.0)); - d = implicit_union_round(d, pb_ellipsoid(p, vec3(-17.0, 0.0, 130.2), vec3(9.0, 3.4, 2.4)), 2.0); - // grips - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - d = min(d, implicit_capsule(p, vec3(50.0, s * 19.0, 127.0), vec3(50.0, s * 29.5, 127.0), 4.2)); - } - return d; -} - -// ---------- pelican ---------- -float pb_dPlumage(vec3 p) { - float bob = pb_bob(); - // body + breast - float d = pb_ellipsoid(p, vec3(-18.0, 0.0, 152.0 + bob), vec3(36.0, 24.0, 26.0)); - d = implicit_union_round(d, implicit_sphere(p, vec3(6.0, 0.0, 143.0 + bob), 17.0), 10.0); - // tail (flattened in y) - vec3 tq = vec3(p.x, p.y * 1.6, p.z); - float tail = implicit_cone_capsule(tq, vec3(-46.0, 0.0, 158.0 + bob), vec3(-74.0, 0.0, 174.0 + bob), 11.0, 3.0) * 0.62; - d = implicit_union_round(d, tail, 5.0); - // neck (S-curve) + head + crest - float neck = implicit_cone_capsule(p, vec3(6.0, 0.0, 166.0 + bob), vec3(20.0, 0.0, 186.0 + bob), 9.5, 8.0); - neck = implicit_union_round(neck, implicit_cone_capsule(p, vec3(20.0, 0.0, 186.0 + bob), vec3(24.0, 0.0, 203.0 + bob), 8.0, 7.2), 4.0); - neck = implicit_union_round(neck, implicit_cone_capsule(p, vec3(24.0, 0.0, 203.0 + bob), vec3(33.0, 0.0, 213.0 + bob), 7.2, 7.0), 4.0); - float head = implicit_sphere(p, vec3(37.0, 0.0, 215.0 + bob), 11.5); - head = implicit_union_round(head, implicit_sphere(p, vec3(29.5, 0.0, 221.0 + bob), 6.5), 4.0); - neck = implicit_union_round(neck, head, 4.0); - d = implicit_union_round(d, neck, 7.0); - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - // folded wing bulge on the flank - d = implicit_union_round(d, pb_ellipsoid(p, vec3(-24.0, s * 19.0, 158.0 + bob), vec3(26.0, 9.0, 17.0)), 6.0); - // wing-arm reaching to the handlebar grip - float arm = implicit_cone_capsule(p, vec3(2.0, s * 18.0, 160.0 + bob), vec3(28.0, s * 26.0, 138.0), 8.0, 5.5); - arm = implicit_union_round(arm, implicit_cone_capsule(p, vec3(28.0, s * 26.0, 138.0), vec3(48.0, s * 27.5, 128.5), 5.5, 4.4), 3.5); - arm = implicit_union_round(arm, implicit_sphere(p, vec3(52.0, s * 28.0, 127.5), 6.0), 3.5); - d = implicit_union_round(d, arm, 5.0); - // feathered thigh - vec3 hip = pb_hip(s); - vec3 ankle = pb_ankle(s); - vec3 knee = pb_knee(hip, ankle); - d = implicit_union_round(d, implicit_cone_capsule(p, hip, knee, 7.5, 4.6), 6.0); - } - return d; -} - -float pb_dBill(vec3 p) { - float bob = pb_bob(); - // upper mandible (flattened in z) + tip hook - vec3 bq = vec3(p.x, p.y, (p.z - 214.5 - bob) * 1.7 + 214.5 + bob); - float d = implicit_cone_capsule(bq, vec3(42.0, 0.0, 214.5 + bob), vec3(92.0, 0.0, 203.0 + bob), 4.0, 2.4) * 0.58; - d = implicit_union_round(d, implicit_sphere(p, vec3(92.5, 0.0, 200.5 + bob), 2.6), 1.5); - // throat pouch - float pouch = implicit_cone_capsule(p, vec3(45.0, 0.0, 209.0 + bob), vec3(89.0, 0.0, 200.5 + bob), 2.4, 1.6); - pouch = implicit_union_round(pouch, pb_ellipsoid(p, vec3(60.0, 0.0, 198.5 + bob), vec3(23.0, 6.5, 10.0)), 8.0); - pouch = implicit_union_round(pouch, implicit_cone_capsule(p, vec3(28.0, 0.0, 196.0 + bob), vec3(50.0, 0.0, 200.0 + bob), 6.0, 5.0), 7.0); - return implicit_union_round(d, pouch, 2.5); -} - -float pb_dLegs(vec3 p) { - float d = 1.0e6; - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - vec3 hip = pb_hip(s); - vec3 ankle = pb_ankle(s); - vec3 knee = pb_knee(hip, ankle); - // bare tarsus - d = min(d, implicit_cone_capsule(p, knee, ankle, 3.4, 2.4)); - // webbed foot (flattened in z) - vec3 fq = vec3(p.x, p.y, (p.z - ankle.z) * 2.2 + ankle.z); - d = min(d, implicit_cone_capsule(fq, ankle + vec3(-5.0, 0.0, -1.5), ankle + vec3(12.0, 0.0, -3.8), 2.5, 5.5) * 0.45); - } - return d; -} - -// ---------- scene ---------- -float sdf(vec3 p) { - float d = pb_dTires(p); - d = min(d, pb_dSilver(p)); - d = min(d, pb_dFrame(p)); - d = min(d, pb_dDark(p)); - d = min(d, pb_dLeather(p)); - d = min(d, pb_dPlumage(p)); - d = min(d, pb_dBill(p)); - d = min(d, pb_dLegs(p)); - return d; -} - -vec3 color(vec3 p, vec3 normal) { - float bob = pb_bob(); - float dT = pb_dTires(p); - float dS = pb_dSilver(p); - float dF = pb_dFrame(p); - float dD = pb_dDark(p); - float dL = pb_dLeather(p); - float dP = pb_dPlumage(p); - float dB = pb_dBill(p); - float dG = pb_dLegs(p); - float best = min(min(min(dT, dS), min(dF, dD)), min(min(dL, dP), min(dB, dG))); - - float up = max(normal.z, 0.0); - - if (best == dP) { - // warm white plumage - vec3 col = vec3(0.985, 0.973, 0.95); - col = mix(col, vec3(0.98, 0.95, 0.875), 0.4 * smoothstep(170.0, 140.0, p.z)); - // folded-wing feather streaks - float wmask = smoothstep(12.0, 16.0, abs(p.y)) - * (1.0 - smoothstep(176.0, 182.0, p.z)) - * smoothstep(138.0, 146.0, p.z) - * (1.0 - smoothstep(2.0, 10.0, p.x)) - * smoothstep(-54.0, -46.0, p.x); - col *= 1.0 - 0.07 * wmask * (0.5 + 0.5 * sin(p.x * 0.5 + p.z * 0.15)); - // grey wing tips at the grips - float tip = smoothstep(40.0, 50.0, p.x) * smoothstep(17.0, 23.0, abs(p.y)) * (1.0 - smoothstep(150.0, 175.0, p.z)); - col = mix(col, vec3(0.36, 0.37, 0.40), 0.85 * tip); - // grey tail tips - float ttip = (1.0 - smoothstep(-72.0, -56.0, p.x)) * smoothstep(146.0, 160.0, p.z); - col = mix(col, vec3(0.46, 0.47, 0.50), 0.7 * ttip); - // eyes - for (int i = 0; i < 2; i++) { - float s = 1.0 - 2.0 * float(i); - vec3 E = vec3(43.6, s * 8.6, 219.0 + bob); - float de = length(p - E); - col = mix(col, vec3(0.94, 0.87, 0.60), smoothstep(4.2, 3.4, de)); - col = mix(col, vec3(0.06, 0.05, 0.05), smoothstep(2.6, 2.1, de)); - float dc = length(p - (E + vec3(0.8, s * 0.6, 1.1))); - col = mix(col, vec3(1.0), smoothstep(1.0, 0.5, dc)); - } - return col * (0.9 + 0.1 * up); - } - if (best == dB) { - vec3 col = mix(vec3(0.95, 0.62, 0.16), vec3(0.99, 0.84, 0.33), smoothstep(40.0, 94.0, p.x)); - col = mix(col, vec3(0.97, 0.52, 0.30), 0.65 * smoothstep(206.0, 193.0, p.z)); - col = mix(col, vec3(0.75, 0.40, 0.12), 0.5 * smoothstep(88.0, 93.0, p.x)); - // painted mouth seam between upper bill and pouch - float lz = 208.5 - (p.x - 45.0) * 0.16 + bob; - float seam = smoothstep(1.1, 0.4, abs(p.z - lz)) * smoothstep(45.0, 52.0, p.x) * (1.0 - smoothstep(86.0, 91.0, p.x)); - col = mix(col, vec3(0.62, 0.33, 0.10), 0.6 * seam); - return col * (0.9 + 0.1 * up); - } - if (best == dG) { - return vec3(0.95, 0.55, 0.16) * (0.87 + 0.13 * up); - } - if (best == dF) { - return vec3(0.72, 0.10, 0.14) * (0.88 + 0.12 * up); - } - if (best == dT) { - float ax = (p.x > 3.0) ? 78.0 : -72.0; - float rr = length(vec2(p.x - ax, p.z - 57.0)); - return mix(vec3(0.62, 0.50, 0.34), vec3(0.13, 0.13, 0.14), smoothstep(51.5, 53.5, rr)); - } - if (best == dL) { - return vec3(0.46, 0.28, 0.14) * (0.88 + 0.12 * up); - } - if (best == dD) { - return vec3(0.15, 0.16, 0.18); - } - return vec3(0.78, 0.80, 0.84) * (0.82 + 0.18 * up); -} -`, -}; diff --git a/models/implicits/planetary-gear.implicit.js b/models/implicits/planetary-gear.implicit.js deleted file mode 100644 index 36c243de..00000000 --- a/models/implicits/planetary-gear.implicit.js +++ /dev/null @@ -1,326 +0,0 @@ -const GLSL = ` -const float PLANETARY_TAU = 6.283185307179586; - -float planetaryRoundedSunTeeth() { - return clamp(floor(sunTeeth + 0.5), 12.0, 30.0); -} - -float planetaryPlanetTeeth() { - return max(floor(planetTeeth + 0.5), 3.0); -} - -float planetarySunTeeth() { - float sun = planetaryRoundedSunTeeth(); - float planet = planetaryPlanetTeeth(); - float remainder = mod(sun + planet, 3.0); - float adjusted = sun; - if (remainder > 0.5 && remainder < 1.5) { - adjusted -= 1.0; - } else if (remainder >= 1.5) { - adjusted += 1.0; - } - if (adjusted < 12.0) { - adjusted += 3.0; - } - if (adjusted > 30.0) { - adjusted -= 3.0; - } - return max(adjusted, 3.0); -} - -float planetaryRingTeeth() { - return planetarySunTeeth() + planetaryPlanetTeeth() * 2.0; -} - -float planetarySunPitch() { - return gearModule * planetarySunTeeth() * 0.5; -} - -float planetaryPlanetPitch() { - return gearModule * planetaryPlanetTeeth() * 0.5; -} - -float planetaryMeshSpread() { - return max(meshOffset, 0.0); -} - -float planetaryRingPitch() { - return gearModule * planetaryRingTeeth() * 0.5 + planetaryMeshSpread(); -} - -float planetaryDriveRadians() { - return meshPhase * 0.017453292519943295; -} - -float planetaryCarrierRadians() { - return carrierAngle * 0.017453292519943295; -} - -float planetaryDrivenRingRadians() { - float sun = planetarySunTeeth(); - float ring = planetaryRingTeeth(); - float carrier = planetaryCarrierRadians(); - return carrier - (sun / max(ring, 1.0)) * (planetaryDriveRadians() - carrier); -} - -float planetaryRingRadians() { - float oddPlanetOffset = mod(planetaryPlanetTeeth(), 2.0) * PLANETARY_TAU * 0.5 / max(planetaryRingTeeth(), 1.0); - return ringPhase * 0.017453292519943295 + planetaryDrivenRingRadians() + oddPlanetOffset; -} - -float planetaryToothWave(float angle, float teeth, float phase) { - float wave = 0.5 + 0.5 * cos((angle - phase) * teeth); - return wave * wave * (3.0 - 2.0 * wave); -} - -float planetaryExtrude(float d2, float z, float halfHeight, float bevel) { - vec2 q = vec2(d2, abs(z) - halfHeight + bevel); - return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - bevel; -} - -float planetaryExternalGear2(vec2 p, float pitchRadius, float teeth, float phase) { - float theta = atan(p.y, p.x); - float tooth = planetaryToothWave(theta, teeth, phase); - float root = toothDepth * 0.72; - float addendum = toothDepth * 0.82; - float meshClearance = max(backlash, max(gearModule * 0.05, toothDepth * 0.18)); - float boundary = pitchRadius - root + tooth * (root + addendum) - meshClearance * 0.52; - return length(p) - boundary; -} - -float planetaryInternalRing2(vec2 p, float pitchRadius, float teeth, float phase) { - float theta = atan(p.y, p.x); - float tooth = planetaryToothWave(theta, teeth, phase); - float root = toothDepth * 0.78; - float addendum = toothDepth * 0.9; - float meshClearance = max(backlash, max(gearModule * 0.05, toothDepth * 0.18)); - float innerBoundary = pitchRadius + root - tooth * (root + addendum) + meshClearance * 0.86; - float outerBoundary = pitchRadius + toothDepth * 2.55 + gearModule * 3.0; - return max(innerBoundary - length(p), length(p) - outerBoundary); -} - -float planetaryExternalGear(vec3 p, vec2 center, float pitchRadius, float teeth, float phase) { - float gear = planetaryExternalGear2(p.xy - center, pitchRadius, teeth, phase); - return planetaryExtrude(gear, p.z, thickness * 0.5, min(gearModule * 0.16, 0.38)); -} - -float planetaryRingGear(vec3 p) { - float ring = planetaryInternalRing2(p.xy, planetaryRingPitch(), planetaryRingTeeth(), planetaryRingRadians()); - return planetaryExtrude(ring, p.z, thickness * 0.5, min(gearModule * 0.14, 0.34)); -} - -float planetarySunGear(vec3 p) { - float gear = planetaryExternalGear(p, vec2(0.0), planetarySunPitch(), planetarySunTeeth(), planetaryDriveRadians()); - float bore = implicit_cylinder_capped( - p, - vec3(0.0, 0.0, -thickness), - vec3(0.0, 0.0, thickness), - max(gearModule * 1.15, planetarySunPitch() * 0.19) - ); - float hub = planetaryExtrude(length(p.xy) - planetarySunPitch() * 0.42, p.z, thickness * 0.58, min(gearModule * 0.18, 0.42)); - return implicit_intersect_round(implicit_union_round(gear, hub, gearModule * 0.18), -bore, gearModule * 0.12); -} - -vec2 planetaryPlanetCenter(float index) { - float angle = planetaryCarrierRadians() + index * PLANETARY_TAU / 3.0; - float orbit = planetarySunPitch() + planetaryPlanetPitch() + planetaryMeshSpread(); - return orbit * vec2(cos(angle), sin(angle)); -} - -float planetaryPlanetPhase(float index) { - float planet = planetaryPlanetTeeth(); - float centerAngle = planetaryCarrierRadians() + index * PLANETARY_TAU / 3.0; - float sunRatio = planetarySunTeeth() / max(planet, 1.0); - float carrierRatio = (planetarySunTeeth() + planet) / max(planet, 1.0); - float toothOffset = PLANETARY_TAU * 0.5 - PLANETARY_TAU * 0.5 / max(planet, 1.0); - return centerAngle * carrierRatio - planetaryDriveRadians() * sunRatio + toothOffset; -} - -float planetaryPlanetGear(vec3 p, float index) { - vec2 center = planetaryPlanetCenter(index); - float gear = planetaryExternalGear(p, center, planetaryPlanetPitch(), planetaryPlanetTeeth(), planetaryPlanetPhase(index)); - float bore = implicit_cylinder_capped( - p, - vec3(center, -thickness), - vec3(center, thickness), - max(gearModule * 0.86, planetaryPlanetPitch() * 0.2) - ); - float pinBoss = planetaryExtrude(length(p.xy - center) - max(gearModule * 1.35, planetaryPlanetPitch() * 0.34), p.z, thickness * 0.54, min(gearModule * 0.16, 0.36)); - return implicit_intersect_round(implicit_union_round(gear, pinBoss, gearModule * 0.15), -bore, gearModule * 0.1); -} - -float planetaryCarrierArm(vec3 p, vec2 center) { - float zCenter = -thickness * 0.72; - float arm2 = implicit_line_segment2(p.xy, vec2(0.0), center) - gearModule * 0.95; - return planetaryExtrude(arm2, p.z - zCenter, thickness * 0.16, min(gearModule * 0.16, 0.35)); -} - -float planetaryCarrier(vec3 p) { - float hub = planetaryExtrude(length(p.xy) - max(gearModule * 2.1, planetarySunPitch() * 0.34), p.z + thickness * 0.72, thickness * 0.17, min(gearModule * 0.16, 0.35)); - float carrier = hub; - for (int i = 0; i < 3; i += 1) { - vec2 center = planetaryPlanetCenter(float(i)); - float arm = planetaryCarrierArm(p, center); - float pin = planetaryExtrude(length(p.xy - center) - max(gearModule * 1.05, planetaryPlanetPitch() * 0.26), p.z + thickness * 0.72, thickness * 0.33, min(gearModule * 0.16, 0.35)); - carrier = implicit_union_round(carrier, arm, gearModule * 0.32); - carrier = implicit_union_round(carrier, pin, gearModule * 0.2); - } - return carrier; -} - -float sdf(vec3 p) { - float result = planetaryRingGear(p); - result = min(result, planetarySunGear(p)); - for (int i = 0; i < 3; i += 1) { - result = min(result, planetaryPlanetGear(p, float(i))); - } - if (showCarrier) { - result = min(result, planetaryCarrier(p)); - } - return result; -} - - -vec3 planetaryPalette(float component, vec3 p, vec3 normal) { - vec3 color = ringColor; - if (component > 0.5) { - color = sunColor; - } - if (component >= 1.5) { - color = planetAColor; - } - if (component >= 2.33) { - color = planetBColor; - } - if (component >= 2.66) { - color = planetCColor; - } - if (component >= 3.5) { - color = carrierColor; - } - float toothLine = 0.5 + 0.5 * sin(atan(p.y, p.x) * planetaryRingTeeth()); - float faceLight = smoothstep(0.22, 1.0, abs(normal.z)); - color *= 0.92 + 0.15 * toothLine; - color = mix(color * 0.72, min(color * 1.18 + vec3(0.05), vec3(1.0)), faceLight); - return clamp(color, vec3(0.0), vec3(1.0)); -} - -vec3 color(vec3 p, vec3 normal) { - float best = planetaryRingGear(p); - float component = 0.0; - - float sun = planetarySunGear(p); - if (sun < best) { - best = sun; - component = 1.0; - } - - for (int i = 0; i < 3; i += 1) { - float planet = planetaryPlanetGear(p, float(i)); - if (planet < best) { - best = planet; - component = 2.0 + float(i) * 0.34; - } - } - - if (showCarrier) { - float carrier = planetaryCarrier(p); - if (carrier < best) { - component = 4.0; - } - } - - return planetaryPalette(component, p, normal); -} -`; - -const PLANETARY_MIN_SUN_TEETH = 12; -const PLANETARY_MAX_SUN_TEETH = 30; - -function roundedTeeth(value, fallback) { - const numeric = Number(value); - return Math.round(Number.isFinite(numeric) ? numeric : fallback); -} - -function compatibleSunTeeth(params) { - const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12)); - const rawSunTeeth = Math.min( - Math.max(roundedTeeth(params.sunTeeth, 18), PLANETARY_MIN_SUN_TEETH), - PLANETARY_MAX_SUN_TEETH - ); - const candidates = [rawSunTeeth - 1, rawSunTeeth, rawSunTeeth + 1, rawSunTeeth - 2, rawSunTeeth + 2] - .filter((candidate) => candidate >= PLANETARY_MIN_SUN_TEETH && candidate <= PLANETARY_MAX_SUN_TEETH) - .filter((candidate) => (candidate + planetTeeth) % 3 === 0); - return candidates.length ? candidates[0] : rawSunTeeth; -} - -export default { - schema: "implicit.js/0.1.0", - name: "planetary gear", - description: "A shader-parametric planetary gear set with a sun gear, three planet gears, internal ring teeth, and an under-carrier.", - units: "mm", - params: { - sunTeeth: { type: "number", label: "Sun teeth", min: 12, max: 30, step: 1, default: 18 }, - planetTeeth: { type: "number", label: "Planet teeth", min: 8, max: 18, step: 1, default: 12 }, - gearModule: { type: "number", label: "Gear module", min: 1.2, max: 3.2, step: 0.05, default: 2.0, unit: "mm" }, - toothDepth: { type: "number", label: "Tooth depth", min: 0.6, max: 3.6, step: 0.05, default: 1.55, unit: "mm" }, - thickness: { type: "number", label: "Gear thickness", min: 3, max: 12, step: 0.25, default: 6.5, unit: "mm" }, - backlash: { type: "number", label: "Backlash", min: 0, max: 1.4, step: 0.02, default: 0.22, unit: "mm" }, - meshOffset: { type: "number", label: "Mesh spread", min: 0, max: 5, step: 0.05, default: 0, unit: "mm" }, - meshPhase: { type: "number", label: "Drive phase", min: 0, max: 360, step: 1, default: 16, unit: "deg" }, - carrierAngle: { type: "number", label: "Carrier orbit", min: -180, max: 180, step: 1, default: 0, unit: "deg" }, - ringPhase: { type: "number", label: "Ring phase", min: -180, max: 180, step: 1, default: 0, unit: "deg" }, - showCarrier: { type: "boolean", label: "Show carrier", default: true }, - ringColor: { type: "color", label: "Ring color", default: "#00a7ff" }, - sunColor: { type: "color", label: "Sun color", default: "#ffe83d" }, - planetAColor: { type: "color", label: "Planet A", default: "#ff5b2e" }, - planetBColor: { type: "color", label: "Planet B", default: "#d94dff" }, - planetCColor: { type: "color", label: "Planet C", default: "#7cff4f" }, - carrierColor: { type: "color", label: "Carrier color", default: "#8b6cff" } - }, - animations: { - meshCycle: { - label: "Mesh cycle", - duration: 8, - loop: true, - update({ progress, params, set }) { - const drive = progress * 360; - const sunTeeth = compatibleSunTeeth(params); - const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12)); - const ringTeeth = sunTeeth + planetTeeth * 2; - const carrier = drive * sunTeeth / Math.max(sunTeeth + ringTeeth, 1); - set("meshPhase", drive); - set("carrierAngle", ((carrier + 180) % 360) - 180); - set("ringPhase", 0); - } - }, - explodeMesh: { - label: "Explode mesh", - duration: 5.5, - loop: true, - update({ progress, set }) { - const wave = 0.5 - 0.5 * Math.cos(progress * Math.PI * 2); - set("meshOffset", wave * 4.2); - set("carrierAngle", -28 + wave * 56); - } - } - }, - bounds: ({ params }) => { - const sunTeeth = compatibleSunTeeth(params); - const planetTeeth = Math.max(3, roundedTeeth(params.planetTeeth, 12)); - const ringTeeth = sunTeeth + planetTeeth * 2; - const spread = Math.max(params.meshOffset, 0); - const ringPitch = params.gearModule * ringTeeth * 0.5 + spread; - const ringOuter = ringPitch + params.toothDepth * 2.8 + params.gearModule * 3.4 + 2; - const z = params.thickness * 0.92 + 5; - return [[-ringOuter, -ringOuter, -z], [ringOuter, ringOuter, z]]; - }, - render: ({ params }) => ({ - steps: 320, - epsilon: Math.max(0.005, params.gearModule * 0.0025), - normalEpsilon: Math.max(0.038, params.gearModule * 0.018), - stepScale: 0.5, - maxStep: Math.max(params.gearModule * 0.62, 0.65) - }), - glsl: GLSL -}; diff --git a/models/implicits/pythagoras-tree-solid.implicit.js b/models/implicits/pythagoras-tree-solid.implicit.js deleted file mode 100644 index 8031079f..00000000 --- a/models/implicits/pythagoras-tree-solid.implicit.js +++ /dev/null @@ -1,77 +0,0 @@ -const GLSL = ` -float sdBox(vec3 p, vec3 b) { - vec3 q = abs(p) - b; - return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0); -} - -float branchBox(vec3 p, vec2 center, vec2 halfSize, float zCenter) { - return sdBox(p - vec3(center, zCenter), vec3(halfSize, thickness * 0.5)); -} - -float sdf(vec3 p) { - float d = branchBox(p, vec2(0.0, -size * 0.25), vec2(size * 0.55, size * 0.32), 0.0); - d = min(d, branchBox(p, vec2(-size * 0.36, size * 0.34), vec2(size * 0.36, size * 0.24), thickness * 0.04)); - d = min(d, branchBox(p, vec2(size * 0.36, size * 0.34), vec2(size * 0.36, size * 0.24), thickness * 0.04)); - if (levels > 1.5) { - d = min(d, branchBox(p, vec2(-size * 0.72, size * 0.86), vec2(size * 0.24, size * 0.18), thickness * 0.1)); - d = min(d, branchBox(p, vec2(-size * 0.16, size * 0.92), vec2(size * 0.22, size * 0.17), thickness * 0.1)); - d = min(d, branchBox(p, vec2(size * 0.16, size * 0.92), vec2(size * 0.22, size * 0.17), thickness * 0.1)); - d = min(d, branchBox(p, vec2(size * 0.72, size * 0.86), vec2(size * 0.24, size * 0.18), thickness * 0.1)); - } - if (levels > 2.5) { - d = min(d, branchBox(p, vec2(-size * 0.95, size * 1.22), vec2(size * 0.16, size * 0.12), thickness * 0.16)); - d = min(d, branchBox(p, vec2(-size * 0.52, size * 1.32), vec2(size * 0.14, size * 0.11), thickness * 0.16)); - d = min(d, branchBox(p, vec2(0.0, size * 1.38), vec2(size * 0.14, size * 0.11), thickness * 0.16)); - d = min(d, branchBox(p, vec2(size * 0.52, size * 1.32), vec2(size * 0.14, size * 0.11), thickness * 0.16)); - d = min(d, branchBox(p, vec2(size * 0.95, size * 1.22), vec2(size * 0.16, size * 0.12), thickness * 0.16)); - } - return d - cornerRound; -} - -vec3 color(vec3 p, vec3 normal) { - float leaf = smoothstep(size * 0.35, size * 1.35, p.y); - vec3 c = mix(baseColor, accentColor, leaf); - return c * (0.58 + 0.42 * smoothstep(-0.1, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Pythagoras tree solid", - description: "A stable 3D Pythagoras tree made from recursively arranged softened blocks.", - units: "mm", - params: { - size: { type: "number", label: "Base size", min: 8, max: 22, step: 0.5, default: 14, unit: "mm" }, - levels: { type: "number", label: "Levels", min: 1, max: 3, step: 1, default: 3 }, - thickness: { type: "number", label: "Thickness", min: 1.5, max: 7, step: 0.1, default: 3.0, unit: "mm" }, - cornerRound: { type: "number", label: "Corner round", min: 0.05, max: 1.2, step: 0.05, default: 0.35, unit: "mm" }, - baseColor: { type: "color", label: "Trunk color", default: "#ff9f1c" }, - accentColor: { type: "color", label: "Canopy color", default: "#65f58a" } - }, - animations: { - canopySway: { - label: "Canopy sway", - duration: 5.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("cornerRound", 0.35 + (wave * 0.5 + 0.5) * 0.55); - set("thickness", 3.0 + wave * 0.7); - } - } - }, - bounds: ({ params }) => { - const x = params.size * 1.25 + 6; - const y = params.size * 1.65 + 6; - const z = params.thickness + params.cornerRound + 4; - return [[-x, -params.size - 4, -z], [x, y, z]]; - }, - render: ({ params }) => ({ - steps: 180, - stepScale: 0.72, - maxStep: Math.max(params.cornerRound * 5, 1.0), - epsilon: Math.max(params.cornerRound * 0.012, 0.004), - normalEpsilon: Math.max(params.cornerRound * 0.12, 0.035) - }), - glsl: GLSL -}; diff --git a/models/implicits/roman-surface-section.implicit.js b/models/implicits/roman-surface-section.implicit.js deleted file mode 100644 index 00205b6a..00000000 --- a/models/implicits/roman-surface-section.implicit.js +++ /dev/null @@ -1,48 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = p / max(radius, 0.1); - float f = q.x * q.x * q.y * q.y + q.y * q.y * q.z * q.z + q.z * q.z * q.x * q.x - scale * q.x * q.y * q.z; - float surface = abs(f) * radius * 2.8 - thickness; - float ball = implicit_sphere(p, vec3(0.0), radius); - vec3 n = normalize(vec3(cos(sectionAngle), sin(sectionAngle), 0.28)); - float sectionPlane = dot(p, n) - radius * 0.78; - return implicit_intersect_round(implicit_intersect_round(surface, ball, 1.2), sectionPlane, 0.8); -} - -vec3 color(vec3 p, vec3 normal) { - vec3 n = normalize(vec3(cos(sectionAngle), sin(sectionAngle), 0.28)); - float cut = smoothstep(radius * 0.70, radius * 0.80, dot(p, n)); - float self = smoothstep(0.45, 0.98, abs(normal.x * normal.y * normal.z) * 6.0); - return mix(mix(baseColor * 0.7, baseColor, self), accentColor, cut * 0.78); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "roman surface section", - description: "Prompt 16: Steiner Roman surface with smooth self-intersections and section visualization.", - units: "mm", - params: { - radius: { type: "number", label: "Clip radius", min: 18, max: 44, step: 0.25, default: 31, unit: "mm" }, - scale: { type: "number", label: "Roman scale", min: 0.45, max: 1.4, step: 0.02, default: 0.92 }, - thickness: { type: "number", label: "Thickness", min: 0.2, max: 4, step: 0.05, default: 1.15, unit: "mm" }, - sectionAngle: { type: "number", label: "Section angle", min: -1.57, max: 1.57, step: 0.02, default: 0.45, unit: "rad" }, - baseColor: { type: "color", label: "Surface color", default: "#ff5c8a" }, - accentColor: { type: "color", label: "Section color", default: "#fff06a" } - }, - animations: { - sectionTurn: { - label: "Section turn", - duration: 6.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("sectionAngle", wave * 1.25); - set("scale", 0.78 + (1 - Math.abs(wave)) * 0.45); - } - } - }, - bounds: [[-44, -44, -44], [44, 44, 44]], - render: { steps: 320, epsilon: 0.0045, normalEpsilon: 0.05 }, - glsl: GLSL -}; diff --git a/models/implicits/rounded-orb.implicit.js b/models/implicits/rounded-orb.implicit.js deleted file mode 100644 index 6e2ed9b5..00000000 --- a/models/implicits/rounded-orb.implicit.js +++ /dev/null @@ -1,61 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float sphereBody = implicit_sphere(p, vec3(0.0, 0.0, 0.0), radius); - float cutBox = implicit_box_centered(p, vec3(radius * 1.58, radius * 1.58, baseHeight), vec3(0.0, 0.0, -radius * 0.75)); - float bore = implicit_cylinder_capped(p, vec3(-radius * 1.25, 0.0, radius * 0.2), vec3(radius * 1.25, 0.0, radius * 0.2), boreRadius); - return implicit_intersect_round( - implicit_union_round(sphereBody, cutBox, blend), - -bore, - max(blend * 0.5, 0.35) - ); -} - - -vec3 color(vec3 p, vec3 normal) { - float sphereBlend = smoothstep(-18.0, 18.0, p.z); - vec3 base = mix(orbLowColor, orbHighColor, sphereBlend); - float plinthMask = 1.0 - smoothstep(-17.0, -7.0, p.z); - base = mix(base, baseColor, plinthMask * 0.9); - float boreAccent = smoothstep(0.72, 0.98, abs(normal.x)) * smoothstep(-2.0, 12.0, p.z); - return mix(base, accentColor, boreAccent); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "rounded orb", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 32, step: 0.25, default: 24, unit: "mm" }, - baseHeight: { type: "number", label: "Base height", min: 8, max: 24, step: 0.25, default: 16, unit: "mm" }, - boreRadius: { type: "number", label: "Bore radius", min: 2, max: 11, step: 0.1, default: 7, unit: "mm" }, - blend: { type: "number", label: "Blend", min: 0.5, max: 6, step: 0.1, default: 3, unit: "mm" }, - orbLowColor: { type: "color", label: "Lower orb", default: "#22d3ee" }, - orbHighColor: { type: "color", label: "Upper orb", default: "#ff7ac8" }, - baseColor: { type: "color", label: "Base color", default: "#2df5a5" }, - accentColor: { type: "color", label: "Bore accent", default: "#ffd166" } - }, - animations: { - boreBreath: { - label: "Bore breath", - duration: 4.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("boreRadius", 7 + wave * 3); - set("blend", 3 + wave * 2); - set("baseHeight", 16 + wave * 5); - } - } - }, - bounds: ({ params }) => { - const extent = params.radius + params.blend + 9; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 224, - epsilon: Math.max(0.004, params.radius * 0.00025), - normalEpsilon: Math.max(0.035, params.radius * 0.0018) - }), - glsl: GLSL -}; diff --git a/models/implicits/schwarz-d-diamond-cell.implicit.js b/models/implicits/schwarz-d-diamond-cell.implicit.js deleted file mode 100644 index 1216584a..00000000 --- a/models/implicits/schwarz-d-diamond-cell.implicit.js +++ /dev/null @@ -1,51 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = implicit_rotate_axis(p, vec3(0.0), vec3(0.0, 0.0, 1.0), twistBlend * 0.45); - float field = implicit_tpms_diamond(q, vec3(period), vec3(1.0), twistBlend); - float shell = implicit_shell(field * period * 0.15, wallThickness, 0.0); - float cube = implicit_box_centered(p, vec3(cellSize), vec3(0.0)); - return implicit_intersect_round(shell, cube, 1.35); -} - -vec3 color(vec3 p, vec3 normal) { - float diagonal = 0.5 + 0.5 * sin((p.x + p.y + p.z) / max(period, 0.1) * 6.2831853); - float crossBand = 0.5 + 0.5 * sin((p.x - p.y + p.z * 0.65) / max(period, 0.1) * 6.2831853 + 1.7); - float channel = smoothstep(0.48, 0.96, abs(dot(normal, normalize(vec3(1.0, -1.0, 1.0))))); - vec3 gold = vec3(1.0, 0.88, 0.18); - vec3 mint = vec3(0.18, 1.0, 0.72); - vec3 body = mix(baseColor, accentColor, diagonal); - body = mix(body, mint, crossBand * 0.34); - body = mix(body, gold, channel * 0.52); - return mix(body, vec3(1.0), 0.10 + channel * 0.08); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "schwarz d diamond cell", - description: "Prompt 7: Schwarz diamond minimal surface with uniform wall thickness.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 30, max: 78, step: 1, default: 54, unit: "mm" }, - period: { type: "number", label: "Period", min: 12, max: 34, step: 0.5, default: 21, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.8, max: 6, step: 0.1, default: 2.1, unit: "mm" }, - twistBlend: { type: "number", label: "Twist blend", min: 0, max: 1, step: 0.02, default: 0.08 }, - baseColor: { type: "color", label: "Diamond color", default: "#00d9ff" }, - accentColor: { type: "color", label: "Channel color", default: "#ff4fd8" } - }, - animations: { - diamondPhase: { - label: "Diamond phase", - duration: 6.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("twistBlend", 0.05 + (wave * 0.5 + 0.5) * 0.65); - set("wallThickness", 1.6 + (1 - Math.abs(wave)) * 3.2); - } - } - }, - bounds: [[-44, -44, -44], [44, 44, 44]], - render: { steps: 300, epsilon: 0.005, normalEpsilon: 0.052 }, - glsl: GLSL -}; diff --git a/models/implicits/schwarz-p-unit-cell.implicit.js b/models/implicits/schwarz-p-unit-cell.implicit.js deleted file mode 100644 index f9142238..00000000 --- a/models/implicits/schwarz-p-unit-cell.implicit.js +++ /dev/null @@ -1,52 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float field = implicit_tpms_schwarz(p, vec3(cellSize), vec3(1.0), 0.0) + fieldOffset; - float shell = implicit_shell(field, wallThickness, 0.0); - float cell = implicit_box_centered(p, vec3(cellSize), vec3(0.0)); - return max(shell, cell) * fieldScale; -} - - -vec3 color(vec3 p, vec3 normal) { - float chamber = 0.5 + 0.5 * cos((p.x + p.y - p.z) * 0.085); - return mix(baseColor, accentColor, chamber); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Schwarz P unit cell", - description: "Finite-thickness Schwarz P triply periodic shell clipped to one cubic unit cell.", - units: "mm", - params: { - cellSize: { type: "number", label: "Cell size", min: 32, max: 72, step: 0.5, default: 48, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 1.2, max: 7, step: 0.1, default: 3.5, unit: "mm" }, - fieldOffset: { type: "number", label: "Field offset", min: -0.8, max: 0.8, step: 0.02, default: 0 }, - fieldScale: { type: "number", label: "Field scale", min: 0.35, max: 0.9, step: 0.01, default: 0.52 }, - baseColor: { type: "color", label: "Chamber color", default: "#58a6ff" }, - accentColor: { type: "color", label: "Mint color", default: "#a7ffdf" } - }, - animations: { - chamberPulse: { - label: "Chamber pulse", - duration: 5.4, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("wallThickness", 3.5 + wave * 2.0); - set("fieldOffset", wave * 0.45); - set("fieldScale", 0.52 + wave * 0.16); - } - } - }, - bounds: ({ params }) => { - const half = params.cellSize / 2 + Math.max(params.wallThickness, 3); - return [[-half, -half, -half], [half, half, half]]; - }, - render: ({ params }) => ({ - steps: 340, - epsilon: Math.max(0.005, params.cellSize * 0.00014), - normalEpsilon: Math.max(0.04, params.wallThickness * 0.014) - }), - glsl: GLSL -}; diff --git a/models/implicits/sierpinski-arrowhead-curve.implicit.js b/models/implicits/sierpinski-arrowhead-curve.implicit.js deleted file mode 100644 index 6a770d3b..00000000 --- a/models/implicits/sierpinski-arrowhead-curve.implicit.js +++ /dev/null @@ -1,86 +0,0 @@ -const GLSL = ` -float sdSegment2(vec2 p, vec2 a, vec2 b) { - vec2 pa = p - a; - vec2 ba = b - a; - float h = clamp(dot(pa, ba) / max(dot(ba, ba), 0.0001), 0.0, 1.0); - return length(pa - ba * h); -} - -float arrowMotif(vec2 p) { - vec2 a = vec2(-0.86, -0.50); - vec2 b = vec2(0.0, 0.98); - vec2 c = vec2(0.86, -0.50); - float d = sdSegment2(p, a, b); - d = min(d, sdSegment2(p, b, c)); - d = min(d, sdSegment2(p, a, c) * 1.18); - return d; -} - -float recursiveArrow(vec2 p) { - vec2 q = p / size; - float d = arrowMotif(q) * size; - float scale = 1.0; - for (int i = 0; i < 5; i += 1) { - if (float(i) < levels) { - scale *= 2.0; - vec2 r = q * scale; - r = fract(r + vec2(0.5, 0.0)) - vec2(0.5); - r.y += 0.16 * sin(float(i) * 2.1); - d = min(d, arrowMotif(r * 2.0) * size / scale); - } - } - return d; -} - -float sdf(vec3 p) { - float curve = recursiveArrow(p.xy) - pathRadius; - float slab = abs(p.z) - thickness * 0.5; - return max(curve, slab); -} - -vec3 color(vec3 p, vec3 normal) { - float recursiveBand = 0.5 + 0.5 * sin((abs(p.x) + abs(p.y) * 1.7) * 0.26); - vec3 c = mix(baseColor, accentColor, recursiveBand); - return c * (0.56 + 0.44 * smoothstep(-0.2, 0.9, normal.z)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Sierpinski arrowhead curve", - description: "A raised recursive triangular arrowhead curve with sharp self-similar structure.", - units: "mm", - params: { - size: { type: "number", label: "Triangle size", min: 16, max: 42, step: 0.5, default: 28, unit: "mm" }, - levels: { type: "number", label: "Recursion levels", min: 1, max: 5, step: 1, default: 4 }, - pathRadius: { type: "number", label: "Path radius", min: 0.25, max: 1.8, step: 0.05, default: 0.75, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.8, max: 4, step: 0.1, default: 1.8, unit: "mm" }, - baseColor: { type: "color", label: "Base color", default: "#24b7ff" }, - accentColor: { type: "color", label: "Recursive color", default: "#ff9f43" } - }, - animations: { - recursionPulse: { - label: "Recursion pulse", - duration: 5.0, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("pathRadius", 0.75 + wave * 0.24); - set("thickness", 1.8 + (wave * 0.5 + 0.5) * 0.9); - } - } - }, - bounds: ({ params }) => { - const extent = params.size + params.pathRadius + 5; - const z = params.thickness + 3; - return [[-extent, -extent, -z], [extent, extent, z]]; - }, - render: ({ params }) => ({ - steps: 180, - stepScale: 0.72, - maxStep: Math.max(params.pathRadius * 3.5, 0.9), - epsilon: Math.max(params.pathRadius * 0.014, 0.004), - normalEpsilon: Math.max(params.pathRadius * 0.08, 0.03) - }), - glsl: GLSL -}; diff --git a/models/implicits/sierpinski-tetrahedron.implicit.js b/models/implicits/sierpinski-tetrahedron.implicit.js deleted file mode 100644 index 3ccdf275..00000000 --- a/models/implicits/sierpinski-tetrahedron.implicit.js +++ /dev/null @@ -1,75 +0,0 @@ -const GLSL = ` -float implicit_tetrahedron(vec3 p) { - vec4 planes = vec4( - p.x + p.y + p.z, - p.x - p.y - p.z, - -p.x + p.y - p.z, - -p.x - p.y + p.z - ); - return (max(max(planes.x, planes.y), max(planes.z, planes.w)) - 1.0) * 0.5773502691896258; -} - -float sdf(vec3 p) { - vec3 q = p / size; - float scale = 1.0; - - for (int level = 0; level < 5; level += 1) { - if (float(level) < levels) { - if (q.x + q.y < 0.0) { - q.xy = -q.yx; - } - if (q.x + q.z < 0.0) { - q.xz = -q.zx; - } - if (q.y + q.z < 0.0) { - q.zy = -q.yz; - } - q = q * 2.0 - vec3(1.0); - scale *= 2.0; - } - } - - return implicit_tetrahedron(q) * size / scale - edgeThickness; -} - -vec3 color(vec3 p, vec3 normal) { - float levelBands = 0.5 + 0.5 * sin((abs(p.x) + abs(p.y) + abs(p.z)) * 0.38); - return mix(baseColor, accentColor, levelBands); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Sierpinski tetrahedron approximation", - description: "Four-level folded implicit approximation of a Sierpinski tetrahedron with readable triangular voids.", - units: "mm", - params: { - size: { type: "number", label: "Size", min: 16, max: 36, step: 0.5, default: 25, unit: "mm" }, - levels: { type: "number", label: "Recursion levels", min: 1, max: 5, step: 1, default: 4 }, - edgeThickness: { type: "number", label: "Edge thickness", min: 0.1, max: 1.4, step: 0.05, default: 0.34, unit: "mm" }, - baseColor: { type: "color", label: "Void color", default: "#ff5c5c" }, - accentColor: { type: "color", label: "Edge color", default: "#ffd166" } - }, - animations: { - edgePulse: { - label: "Edge pulse", - duration: 5.0, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("size", 25 + wave * 6); - set("edgeThickness", 0.34 + wave * 0.22); - } - } - }, - bounds: ({ params }) => { - const extent = params.size + 7; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 360, - epsilon: Math.max(0.005, params.edgeThickness * 0.016), - normalEpsilon: Math.max(0.04, params.edgeThickness * 0.12) - }), - glsl: GLSL -}; diff --git a/models/implicits/sphere-boolean-cuts.implicit.js b/models/implicits/sphere-boolean-cuts.implicit.js deleted file mode 100644 index 69cf3cba..00000000 --- a/models/implicits/sphere-boolean-cuts.implicit.js +++ /dev/null @@ -1,62 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float body = implicit_sphere(p, vec3(0.0), radius); - float d = body; - if (sliceEnabled) { - float belowSlice = p.z - sliceHeight; - d = implicit_intersect_round(d, belowSlice, bevel); - } - if (boreEnabled && boreRadius > 0.01) { - float bore = implicit_cylinder_capped( - p, - vec3(-radius * 1.15, 0.0, 0.0), - vec3(radius * 1.15, 0.0, 0.0), - boreRadius - ); - d = implicit_intersect_round(d, -bore, max(bevel * 0.65, 0.25)); - } - return d; -} - -vec3 color(vec3 p, vec3 normal) { - float cap = sliceEnabled ? smoothstep(sliceHeight - 3.5, sliceHeight + 0.75, p.z) : 0.0; - float bore = boreEnabled ? smoothstep(0.78, 0.98, abs(normal.x)) * (1.0 - smoothstep(boreRadius + 2.0, boreRadius + 9.0, length(p.yz))) : 0.0; - float latitude = 0.5 + 0.5 * normal.z; - vec3 body = mix(baseColor * 0.55, baseColor, latitude); - body += vec3(0.08, 0.10, 0.12) * pow(max(dot(normal, normalize(vec3(-0.45, 0.35, 0.82))), 0.0), 3.0); - return mix(body, accentColor, clamp(max(cap, bore) * 0.9, 0.0, 1.0)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "sphere boolean cuts", - description: "Prompt 1: clean implicit sphere with smooth normals and optional boolean cuts.", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 44, step: 0.25, default: 28, unit: "mm" }, - sliceHeight: { type: "number", label: "Slice height", min: -10, max: 22, step: 0.25, default: 11, unit: "mm" }, - boreRadius: { type: "number", label: "Bore radius", min: 0, max: 14, step: 0.25, default: 7, unit: "mm" }, - bevel: { type: "number", label: "Cut bevel", min: 0.2, max: 5, step: 0.1, default: 2.4, unit: "mm" }, - sliceEnabled: { type: "boolean", label: "Slice cut", default: true }, - boreEnabled: { type: "boolean", label: "Bore cut", default: true }, - baseColor: { type: "color", label: "Base color", default: "#34d5ff" }, - accentColor: { type: "color", label: "Cut accent", default: "#ffd166" } - }, - animations: { - revealCuts: { - label: "Reveal cuts", - duration: 4.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("sliceHeight", 5 + wave * 13); - set("boreRadius", 4 + (wave * 0.5 + 0.5) * 8); - set("bevel", 1.2 + (wave * 0.5 + 0.5) * 2.8); - } - } - }, - bounds: [[-42, -42, -42], [42, 42, 42]], - render: { steps: 220, epsilon: 0.0045, normalEpsilon: 0.045 }, - glsl: GLSL -}; diff --git a/models/implicits/superquadric-rounded-cube.implicit.js b/models/implicits/superquadric-rounded-cube.implicit.js deleted file mode 100644 index 640e0005..00000000 --- a/models/implicits/superquadric-rounded-cube.implicit.js +++ /dev/null @@ -1,50 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = abs(p) / vec3(size, size, size * heightScale); - float superquadric = pow(pow(q.x, exponent) + pow(q.y, exponent) + pow(q.z, exponent), 1.0 / exponent) - 1.0; - return superquadric * size; -} - - -vec3 color(vec3 p, vec3 normal) { - float face = pow(max(abs(normal.x), max(abs(normal.y), abs(normal.z))), 3.0); - return mix(edgeColor, faceColor, face); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "superquadric rounded cube", - description: "A high-even-exponent superquadric with flat faces and smoothly rounded edges and corners.", - units: "mm", - params: { - size: { type: "number", label: "Size", min: 12, max: 34, step: 0.25, default: 21, unit: "mm" }, - exponent: { type: "number", label: "Squareness", min: 2, max: 16, step: 0.25, default: 8 }, - heightScale: { type: "number", label: "Height scale", min: 0.55, max: 1.45, step: 0.02, default: 1 }, - faceColor: { type: "color", label: "Face color", default: "#ffe45c" }, - edgeColor: { type: "color", label: "Edge color", default: "#ff7a66" } - }, - animations: { - cubeMorph: { - label: "Cube morph", - duration: 4.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("exponent", 8 + wave * 5.2); - set("heightScale", 1 + wave * 0.28); - } - } - }, - bounds: ({ params }) => { - const xy = params.size + 9; - const z = params.size * params.heightScale + 9; - return [[-xy, -xy, -z], [xy, xy, z]]; - }, - render: ({ params }) => ({ - steps: 220, - epsilon: Math.max(0.004, params.size * 0.00025), - normalEpsilon: Math.max(0.035, params.size * 0.002) - }), - glsl: GLSL -}; diff --git a/models/implicits/tanglecube-lobed-tunnels.implicit.js b/models/implicits/tanglecube-lobed-tunnels.implicit.js deleted file mode 100644 index 9411394e..00000000 --- a/models/implicits/tanglecube-lobed-tunnels.implicit.js +++ /dev/null @@ -1,45 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = p / max(scale, 0.1); - float f = q.x*q.x*q.x*q.x - 5.0*q.x*q.x + q.y*q.y*q.y*q.y - 5.0*q.y*q.y + q.z*q.z*q.z*q.z - 5.0*q.z*q.z + constant; - float shell = abs(f) * scale * 0.42 - thickness; - float clip = implicit_sphere(p, vec3(0.0), scale * 2.72); - return implicit_intersect_round(shell, clip, max(tunnelRound, 0.25)); -} - -vec3 color(vec3 p, vec3 normal) { - float lobe = smoothstep(0.15, 0.94, max(max(abs(normal.x), abs(normal.y)), abs(normal.z))); - float tunnel = 0.5 + 0.5 * sin((p.x + p.y + p.z) / max(scale, 0.1) * 2.6); - return mix(baseColor * (0.7 + 0.3 * tunnel), accentColor, lobe * 0.46); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "tanglecube lobed tunnels", - description: "Prompt 17: tanglecube algebraic surface with cubic symmetry and smooth tunnels.", - units: "mm", - params: { - scale: { type: "number", label: "Scale", min: 10, max: 24, step: 0.25, default: 16, unit: "mm" }, - constant: { type: "number", label: "Constant", min: 8, max: 15, step: 0.05, default: 11.8 }, - thickness: { type: "number", label: "Shell thickness", min: 0.4, max: 4, step: 0.05, default: 1.1, unit: "mm" }, - tunnelRound: { type: "number", label: "Tunnel round", min: 0, max: 6, step: 0.1, default: 2.4, unit: "mm" }, - baseColor: { type: "color", label: "Lobe color", default: "#4ade80" }, - accentColor: { type: "color", label: "Tunnel color", default: "#a78bfa" } - }, - animations: { - nodeBloom: { - label: "Node bloom", - duration: 6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("constant", 10.5 + (wave * 0.5 + 0.5) * 2.6); - set("tunnelRound", 1.2 + (1 - Math.abs(wave)) * 4.2); - } - } - }, - bounds: [[-48, -48, -48], [48, 48, 48]], - render: { steps: 330, epsilon: 0.0045, normalEpsilon: 0.05 }, - glsl: GLSL -}; diff --git a/models/implicits/thickened-mobius-strip.implicit.js b/models/implicits/thickened-mobius-strip.implicit.js deleted file mode 100644 index b71b0b82..00000000 --- a/models/implicits/thickened-mobius-strip.implicit.js +++ /dev/null @@ -1,68 +0,0 @@ -const GLSL = ` -const float IMPLICIT_MOBIUS_TAU = 6.283185307179586; - -float sdf(vec3 p) { - float u = atan(p.y, p.x); - vec2 section = vec2(length(p.xy) - radius, p.z); - vec2 twistAxis = vec2(cos(0.5 * u), sin(0.5 * u)); - vec2 closest = twistAxis * clamp(dot(section, twistAxis), -(width * 0.5), (width * 0.5)); - return length(section - closest) - thickness; -} - - -vec3 implicit_palette(float t) { - float waveA = 0.5 + 0.5 * cos(6.283185307179586 * (t + 0.1)); - float waveB = 0.5 + 0.5 * cos(6.283185307179586 * (t + 0.56)); - return mix(mix(baseColor, midColor, waveA), accentColor, waveB * 0.74); -} - -vec3 color(vec3 p, vec3 normal) { - vec2 ring = vec2(p.x, p.y); - float angle = atan(ring.y, ring.x) / 6.283185307179586; - vec3 bandColor = implicit_palette(angle + p.z * 0.035); - return mix(bandColor, edgeColor, smoothstep(0.25, 0.95, abs(normal.z)) * 0.28); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "thickened Mobius strip", - description: "A manufacturable one-half-twist Mobius strip with rounded boundary and finite shell thickness.", - units: "mm", - params: { - radius: { type: "number", label: "Radius", min: 14, max: 34, step: 0.25, default: 24, unit: "mm" }, - width: { type: "number", label: "Band width", min: 4, max: 22, step: 0.25, default: 13.6, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.25, max: 2.5, step: 0.05, default: 0.82, unit: "mm" }, - baseColor: { type: "color", label: "Rose color", default: "#ff4fa3" }, - midColor: { type: "color", label: "Lagoon color", default: "#7c5cff" }, - accentColor: { type: "color", label: "Iris color", default: "#35e6c9" }, - edgeColor: { type: "color", label: "Edge color", default: "#ffd166" } - }, - animations: { - bandBreath: { - label: "Band breath", - duration: 4.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("width", 13.6 + wave * 5.5); - set("thickness", 0.82 + wave * 0.35); - set("radius", 24 + wave * 4); - } - } - }, - bounds: ({ params }) => { - const halfWidth = params.width * 0.5; - const radialExtent = params.radius + halfWidth + params.thickness + 6; - const heightExtent = halfWidth + params.thickness + 6; - return [[-radialExtent, -radialExtent, -heightExtent], [radialExtent, radialExtent, heightExtent]]; - }, - render: ({ params }) => ({ - steps: 192, - stepScale: 0.72, - maxStep: Math.max(params.thickness * 3.8, 1.4), - epsilon: Math.max(params.thickness * 0.012, 0.006), - normalEpsilon: Math.max(params.thickness * 0.075, 0.04) - }), - glsl: GLSL -}; diff --git a/models/implicits/torus-boolean-lab.implicit.js b/models/implicits/torus-boolean-lab.implicit.js deleted file mode 100644 index e46bb0bf..00000000 --- a/models/implicits/torus-boolean-lab.implicit.js +++ /dev/null @@ -1,54 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - float ring = implicit_torus(p, majorRadius, minorRadius); - float beadA = implicit_sphere(p, vec3(majorRadius, 0.0, 0.0), beadRadius); - float beadB = implicit_sphere(p, vec3(-majorRadius, 0.0, 0.0), beadRadius * 0.82); - float joined = implicit_union_round(implicit_union_round(ring, beadA, blend), beadB, blend); - float drill = implicit_cylinder_capped(p, vec3(0.0, -majorRadius * 1.55, 0.0), vec3(0.0, majorRadius * 1.55, 0.0), cutRadius); - float window = implicit_box_centered(p, vec3(cutRadius * 2.4, majorRadius * 2.6, cutRadius * 1.2), vec3(0.0)); - return implicit_intersect_round(implicit_intersect_round(joined, -drill, blend), -window, blend * 0.6); -} - -vec3 color(vec3 p, vec3 normal) { - float theta = atan(p.y, p.x); - float stripe = 0.5 + 0.5 * sin(theta * 5.0 + p.z * 0.2); - float cutLight = smoothstep(0.65, 0.96, abs(normal.y)) * smoothstep(-6.0, 6.0, p.z); - vec3 body = mix(baseColor * 0.62, baseColor, stripe); - body = mix(body, highlightColor, smoothstep(0.78, 1.0, stripe) * 0.35); - vec3 rim = mix(accentColor * 0.55, accentColor, 0.5 + 0.5 * normal.z); - return mix(body, rim, clamp(cutLight, 0.0, 1.0)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "torus boolean lab", - description: "Prompt 2: adjustable implicit torus with smooth union beads and subtractive cuts.", - units: "mm", - params: { - majorRadius: { type: "number", label: "Major radius", min: 16, max: 42, step: 0.25, default: 28, unit: "mm" }, - minorRadius: { type: "number", label: "Minor radius", min: 3, max: 14, step: 0.25, default: 8, unit: "mm" }, - beadRadius: { type: "number", label: "Union bead", min: 0, max: 9, step: 0.25, default: 5.5, unit: "mm" }, - cutRadius: { type: "number", label: "Subtraction radius", min: 1, max: 12, step: 0.25, default: 6, unit: "mm" }, - blend: { type: "number", label: "Blend", min: 0.2, max: 5, step: 0.1, default: 2.2, unit: "mm" }, - baseColor: { type: "color", label: "Torus color", default: "#ff6b9a" }, - highlightColor: { type: "color", label: "Stripe highlight", default: "#fff06a" }, - accentColor: { type: "color", label: "Boolean accent", default: "#38f2b8" } - }, - animations: { - booleanSweep: { - label: "Boolean sweep", - duration: 5.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("minorRadius", 7 + wave * 3.5); - set("cutRadius", 4 + (wave * 0.5 + 0.5) * 6); - set("beadRadius", 3 + (1.0 - Math.abs(wave)) * 6); - } - } - }, - bounds: [[-54, -54, -22], [54, 54, 22]], - render: { steps: 240, epsilon: 0.004, normalEpsilon: 0.04 }, - glsl: GLSL -}; diff --git a/models/implicits/trefoil-torus-knot-tube.implicit.js b/models/implicits/trefoil-torus-knot-tube.implicit.js deleted file mode 100644 index d88d0652..00000000 --- a/models/implicits/trefoil-torus-knot-tube.implicit.js +++ /dev/null @@ -1,63 +0,0 @@ -const GLSL = ` -const float IMPLICIT_TREFOIL_TAU = 6.283185307179586; - -float sdf(vec3 p) { - p.z /= verticalScale; - float phi = atan(p.y, p.x); - vec2 torusSection = vec2(length(p.xy) - majorRadius, p.z); - float best = 1.0e6; - for (int branchIndex = 0; branchIndex < 3; branchIndex += 1) { - float t = (phi + IMPLICIT_TREFOIL_TAU * float(branchIndex)) / 3.0; - vec2 knotCenter = knotRadius * vec2(cos(2.0 * t), sin(2.0 * t)); - best = min(best, length(torusSection - knotCenter)); - } - return best - tubeRadius; -} - - -vec3 color(vec3 p, vec3 normal) { - float sweep = atan(p.y, p.x) / IMPLICIT_TREFOIL_TAU + p.z * 0.018; - return mix(baseColor, accentColor, 0.5 + 0.5 * sin(6.283185307179586 * sweep)); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "trefoil torus-knot tube", - description: "A constant-radius solid tube following a closed trefoil torus knot path.", - units: "mm", - params: { - majorRadius: { type: "number", label: "Major radius", min: 10, max: 24, step: 0.25, default: 16.5, unit: "mm" }, - knotRadius: { type: "number", label: "Lobe radius", min: 3.5, max: 10, step: 0.1, default: 6.9, unit: "mm" }, - tubeRadius: { type: "number", label: "Tube radius", min: 0.8, max: 4.5, step: 0.05, default: 2.05, unit: "mm" }, - verticalScale: { type: "number", label: "Vertical scale", min: 0.65, max: 1.6, step: 0.05, default: 1 }, - baseColor: { type: "color", label: "Copper color", default: "#ff7a33" }, - accentColor: { type: "color", label: "Cyan color", default: "#2ee8ff" } - }, - animations: { - knotBreath: { - label: "Knot breath", - duration: 5.2, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("tubeRadius", 2.05 + wave * 0.95); - set("knotRadius", 6.9 + wave * 1.7); - set("verticalScale", 1 + wave * 0.3); - } - } - }, - bounds: ({ params }) => { - const xy = params.majorRadius + params.knotRadius + params.tubeRadius + 5; - const z = params.knotRadius * params.verticalScale + params.tubeRadius + 5; - return [[-xy, -xy, -z], [xy, xy, z]]; - }, - render: ({ params }) => ({ - steps: 220, - epsilon: Math.max(0.006, params.tubeRadius * 0.004), - normalEpsilon: Math.max(0.04, params.tubeRadius * 0.028), - stepScale: 0.72, - maxStep: Math.max(params.tubeRadius * 0.68, 0.7) - }), - glsl: GLSL -}; diff --git a/models/implicits/voronoi-foam-lattice.implicit.js b/models/implicits/voronoi-foam-lattice.implicit.js deleted file mode 100644 index 060fa1c1..00000000 --- a/models/implicits/voronoi-foam-lattice.implicit.js +++ /dev/null @@ -1,76 +0,0 @@ -const GLSL = ` -vec3 hash33(vec3 p) { - p = vec3(dot(p, vec3(127.1, 311.7, 74.7)), dot(p, vec3(269.5, 183.3, 246.1)), dot(p, vec3(113.5, 271.9, 124.6))); - return fract(sin(p) * 43758.5453123); -} - -float voronoiWall(vec3 p) { - vec3 g = floor(p); - vec3 f = fract(p); - float f1 = 10.0; - float f2 = 10.0; - for (int z = -1; z <= 1; z++) { - for (int y = -1; y <= 1; y++) { - for (int x = -1; x <= 1; x++) { - vec3 o = vec3(float(x), float(y), float(z)); - vec3 r = o + hash33(g + o) - f; - float d = dot(r, r); - if (d < f1) { - f2 = f1; - f1 = d; - } else if (d < f2) { - f2 = d; - } - } - } - } - return abs(sqrt(f2) - sqrt(f1)); -} - -float sdf(vec3 p) { - float wall = voronoiWall(p / max(cellSize, 0.1)) * cellSize - wallThickness; - float roundedWall = wall - roundness * 0.22; - float box = implicit_box_centered(p, vec3(boundsSize), vec3(0.0)); - return implicit_intersect_round(roundedWall, box, roundness); -} - -vec3 color(vec3 p, vec3 normal) { - float cell = voronoiWall(p / max(cellSize, 0.1)); - float veins = smoothstep(0.0, 0.32, cell); - float depth = smoothstep(-boundsSize * 0.5, boundsSize * 0.5, p.z); - float rim = pow(max(dot(normal, normalize(vec3(-0.45, 0.35, 0.82))), 0.0), 2.4); - vec3 wall = mix(baseColor * (0.65 + 0.35 * depth), accentColor, veins * 0.62); - return mix(wall, rimColor, rim * 0.32); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "voronoi foam lattice", - description: "Prompt 20: bounded implicit Voronoi foam with rounded cell-wall transitions.", - units: "mm", - params: { - boundsSize: { type: "number", label: "Bounds size", min: 34, max: 84, step: 1, default: 58, unit: "mm" }, - cellSize: { type: "number", label: "Cell size", min: 10, max: 28, step: 0.5, default: 18, unit: "mm" }, - wallThickness: { type: "number", label: "Wall thickness", min: 0.4, max: 4, step: 0.1, default: 1.4, unit: "mm" }, - roundness: { type: "number", label: "Roundness", min: 0.2, max: 4, step: 0.1, default: 1.7, unit: "mm" }, - baseColor: { type: "color", label: "Wall color", default: "#24e6c2" }, - accentColor: { type: "color", label: "Cell color", default: "#ff9f43" }, - rimColor: { type: "color", label: "Rim color", default: "#c4a7ff" } - }, - animations: { - foamBreath: { - label: "Foam breath", - duration: 6.6, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("wallThickness", 0.9 + (wave * 0.5 + 0.5) * 2.4); - set("roundness", 0.8 + (1 - Math.abs(wave)) * 2.8); - } - } - }, - bounds: [[-48, -48, -48], [48, 48, 48]], - render: { steps: 340, epsilon: 0.005, normalEpsilon: 0.055 }, - glsl: GLSL -}; diff --git a/models/implicits/whitney-umbrella.implicit.js b/models/implicits/whitney-umbrella.implicit.js deleted file mode 100644 index 225db5a8..00000000 --- a/models/implicits/whitney-umbrella.implicit.js +++ /dev/null @@ -1,55 +0,0 @@ -const GLSL = ` -float sdf(vec3 p) { - vec3 q = p / scale; - float field = q.x * q.x - q.y * q.y * q.z; - float gradientScale = pinchSharpness + length(vec3(2.0 * q.x, -2.0 * q.y * q.z, -q.y * q.y)); - float sheet = abs(field) / gradientScale * scale - thickness; - float clip = implicit_sphere(p, vec3(0.0), clipRadius); - return max(sheet, clip) * 0.28; -} - - -vec3 color(vec3 p, vec3 normal) { - float pinch = 1.0 - smoothstep(0.0, 12.0, length(p.xy)); - return mix(mix(sheetLowColor, sheetHighColor, smoothstep(-14.0, 16.0, p.z)), pinchColor, pinch * 0.65); -} -`; - -export default { - schema: "implicit.js/0.1.0", - name: "Whitney umbrella bounded thickness", - description: "A sphere-clipped thickened Whitney umbrella retaining the pinch and self-intersecting sheet structure.", - units: "mm", - params: { - scale: { type: "number", label: "Field scale", min: 8, max: 20, step: 0.25, default: 13.5, unit: "mm" }, - thickness: { type: "number", label: "Thickness", min: 0.25, max: 2.2, step: 0.05, default: 0.95, unit: "mm" }, - clipRadius: { type: "number", label: "Clip radius", min: 16, max: 36, step: 0.5, default: 27, unit: "mm" }, - pinchSharpness: { type: "number", label: "Pinch sharpness", min: 0.25, max: 0.9, step: 0.01, default: 0.45 }, - sheetLowColor: { type: "color", label: "Lower sheet", default: "#8b5cf6" }, - sheetHighColor: { type: "color", label: "Upper sheet", default: "#38dff8" }, - pinchColor: { type: "color", label: "Pinch color", default: "#ff6fb1" } - }, - animations: { - pinchPulse: { - label: "Pinch pulse", - duration: 4.8, - loop: true, - update({ progress, set }) { - const wave = Math.sin(progress * Math.PI * 2); - set("thickness", 0.95 + wave * 0.55); - set("pinchSharpness", 0.45 + wave * 0.18); - set("clipRadius", 27 + wave * 6); - } - } - }, - bounds: ({ params }) => { - const extent = params.clipRadius + Math.max(params.thickness, 1) + 3; - return [[-extent, -extent, -extent], [extent, extent, extent]]; - }, - render: ({ params }) => ({ - steps: 520, - epsilon: Math.max(0.005, params.thickness * 0.009), - normalEpsilon: Math.max(0.045, params.thickness * 0.055) - }), - glsl: GLSL -}; diff --git a/models/mechanisms/.180_degree_flip_mechanism.step.glb b/models/mechanisms/.180_degree_flip_mechanism.step.glb deleted file mode 100644 index 5c66e57d..00000000 --- a/models/mechanisms/.180_degree_flip_mechanism.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6802ab7ac654eb1649d4ddb9a1463698b69a169ab81d0a9fd6dfb5a2e4e50f9 -size 2845048 diff --git a/models/mechanisms/.180_degree_flip_mechanism.step.js b/models/mechanisms/.180_degree_flip_mechanism.step.js deleted file mode 100644 index fbee0bcd..00000000 --- a/models/mechanisms/.180_degree_flip_mechanism.step.js +++ /dev/null @@ -1,281 +0,0 @@ -const Z_AXIS = [0, 0, 1]; -const FLAT_ON_GROUND_TRANSFORM = [ - 0, 0, -1, 0, - 0, 1, 0, 0, - 1, 0, 0, -322.461928, - 0, 0, 0, 1 -]; - -// CAD coordinates measured from the STEP occurrence bounds. The side-view -// mechanism is a toggle four-bar: the lower pink rocker drives the yellow/red -// panel coupler out to an over-center tangent pose, then the coupler returns on -// the opposite branch with the panel flipped. -const PIVOTS = { - upperFrame: [-15.002, 229.609, 0], - lowerFrame: [104.981, 29.599, 0], - lowerBoard: [45.03, 229.61, 0], - upperBoard: [44.984, 420.401, 0] -}; - -const PANEL_COUPLER_PARTS = ["panel", "redLink", "driveRod", "hingeRod", "panelHardware"]; -const LOWER_ROCKER_PARTS = ["rocker2", "greenRod", "handleHardware"]; -const FRAME_DETAIL_PARTS = ["stopper", "bearings"]; -const FRAME_PARTS = ["frame", ...FRAME_DETAIL_PARTS, "frameHardware"]; -const HARDWARE = ["frameHardware", "handleHardware", "panelHardware"]; -const CROSS_RODS = ["hingeRod", "driveRod"]; -const MOVING_PARTS = ["rockerBlue", ...PANEL_COUPLER_PARTS, ...LOWER_ROCKER_PARTS]; - -const INITIAL_PANEL_ANGLE_DEG = angleDeg2(PIVOTS.lowerBoard, PIVOTS.upperBoard); -const INITIAL_BLUE_ANGLE_DEG = angleDeg2(PIVOTS.upperFrame, PIVOTS.upperBoard); -const INITIAL_LOWER_ROCKER_ANGLE_DEG = angleDeg2(PIVOTS.lowerFrame, PIVOTS.lowerBoard); -const LOWER_LINK_LENGTH = distance2(PIVOTS.lowerFrame, PIVOTS.lowerBoard); -const UPPER_LINK_LENGTH = distance2(PIVOTS.upperFrame, PIVOTS.upperBoard); -const COUPLER_LINK_LENGTH = distance2(PIVOTS.lowerBoard, PIVOTS.upperBoard); -const OVER_CENTER_CRANK_ANGLE_DEG = -3.22068; - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -function clamp01(value) { - return clamp(value, 0, 1); -} - -function smooth01(value) { - const t = clamp01(value); - return t * t * (3 - (2 * t)); -} - -function lerp(start, end, t) { - return start + ((end - start) * t); -} - -function lerpPoint(start, end, t) { - return [ - lerp(start[0], end[0], t), - lerp(start[1], end[1], t), - lerp(start[2] || 0, end[2] || 0, t) - ]; -} - -function distance2(start, end) { - return Math.hypot(end[0] - start[0], end[1] - start[1]); -} - -function angleDeg2(start, end) { - return Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI; -} - -function pointFromAngle(origin, length, angleDeg) { - const angleRad = angleDeg * Math.PI / 180; - return [ - origin[0] + (Math.cos(angleRad) * length), - origin[1] + (Math.sin(angleRad) * length), - origin[2] || 0 - ]; -} - -function circleIntersections(centerA, radiusA, centerB, radiusB) { - const dx = centerB[0] - centerA[0]; - const dy = centerB[1] - centerA[1]; - const distance = Math.hypot(dx, dy); - if (distance <= 1e-6) { - return [centerA, centerA]; - } - const maxDistance = radiusA + radiusB; - const minDistance = Math.abs(radiusA - radiusB); - const solvedDistance = clamp(distance, minDistance + 1e-6, maxDistance - 1e-6); - const along = ((radiusA * radiusA) - (radiusB * radiusB) + (solvedDistance * solvedDistance)) / (2 * solvedDistance); - const height = Math.sqrt(Math.max(0, (radiusA * radiusA) - (along * along))); - const unitX = dx / distance; - const unitY = dy / distance; - const midX = centerA[0] + (along * unitX); - const midY = centerA[1] + (along * unitY); - const offsetX = -unitY * height; - const offsetY = unitX * height; - return [ - [midX + offsetX, midY + offsetY, centerA[2] || 0], - [midX - offsetX, midY - offsetY, centerA[2] || 0] - ]; -} - -function boardBranches(lower) { - const branches = circleIntersections(PIVOTS.upperFrame, UPPER_LINK_LENGTH, lower, COUPLER_LINK_LENGTH); - return branches.sort((a, b) => b[1] - a[1]); -} - -function translateVector(start, end) { - return [ - end[0] - start[0], - end[1] - start[1], - (end[2] || 0) - (start[2] || 0) - ]; -} - -function cycleFlip(cycle) { - const phase = ((finite(cycle, 0) % 1) + 1) % 1; - return 0.5 - (0.5 * Math.cos(phase * Math.PI * 2)); -} - -function sampleMotion(rawFlip, { extension = 1, turnover = 1 } = {}) { - const flip = clamp01(rawFlip); - const travel = clamp(extension, 0.2, 1); - const turnoverAmount = clamp(turnover, 0, 1); - const extendedAngle = lerp(INITIAL_LOWER_ROCKER_ANGLE_DEG, OVER_CENTER_CRANK_ANGLE_DEG, travel); - const firstHalf = flip <= 0.5; - const legT = firstHalf ? smooth01(flip / 0.5) : smooth01((flip - 0.5) / 0.5); - const lowerAngleDeg = firstHalf - ? lerp(INITIAL_LOWER_ROCKER_ANGLE_DEG, extendedAngle, legT) - : lerp(extendedAngle, INITIAL_LOWER_ROCKER_ANGLE_DEG, legT); - const lower = pointFromAngle(PIVOTS.lowerFrame, LOWER_LINK_LENGTH, lowerAngleDeg); - const [closedBranch, openBranch] = boardBranches(lower); - const upper = !firstHalf && turnoverAmount > 0 ? openBranch : closedBranch; - const angleDeg = angleDeg2(lower, upper); - return { lower, upper, angleDeg, lowerAngleDeg }; -} - -function applyVisibility(effects, params) { - effects.visible("*", true); - effects.visible("frame", params.showFrame !== false); - effects.visible(FRAME_DETAIL_PARTS, params.showFrame !== false && params.showDetails !== false); - effects.visible("frameHardware", params.showFrame !== false && params.showHardware === true); - effects.visible(["handleHardware", "panelHardware"], params.showHardware === true); - effects.visible(CROSS_RODS, params.showCrossRods !== false); -} - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/thang010146/180_degree_flip_mechanism.step" - }, - label: "180 degree flip mechanism", - description: "Planar four-bar viewer rig reconstructed from the reference video: blue upper link, pink lower rocker, red/board coupler, and an over-center turnover.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - frame: { ref: "#o1.1.1", label: "Frame", description: "Fixed frame body." }, - stopper: { ref: "#o1.1.2", label: "Stopper" }, - bearings: { ref: "#o1.1.3,o1.1.4", label: "Bearing blocks" }, - frameHardware: { ref: "#o1.1.5,o1.1.6,o1.1.7,o1.1.8,o1.1.9,o1.1.10,o1.1.11,o1.1.12,o1.1.13,o1.1.14,o1.1.15,o1.1.16,o1.1.17,o1.1.18,o1.1.19,o1.1.20,o1.1.21,o1.1.22,o1.1.23,o1.1.24,o1.1.25,o1.1.26,o1.1.27,o1.1.28", label: "Frame hardware" }, - - rockerBlue: { ref: "#o1.2", label: "Blue upper link", description: "Upper 200 mm link rotating about the upper fixed pivot." }, - rocker2: { ref: "#o1.3.1", label: "Pink lower rocker", description: "Lower rocker arm rotating about the lower fixed pivot." }, - greenRod: { ref: "#o1.3.2", label: "Handle rod" }, - handleHardware: { ref: "#o1.3.3,o1.3.4", label: "Handle hardware" }, - - redLink: { ref: "#o1.4.1", label: "Red coupler", description: "Rigid coupler between the upper and lower board pivots." }, - panel: { ref: "#o1.4.2", label: "Flip panel", description: "Panel fixed to the red coupler." }, - driveRod: { ref: "#o1.4.3", label: "Upper board rod" }, - hingeRod: { ref: "#o1.4.7", label: "Lower board rod" }, - panelHardware: { ref: "#o1.4.4,o1.4.5,o1.4.6,o1.4.8", label: "Panel nuts" } - }, - parameters: { - flip: { type: "number", label: "Flip", description: "Closed to open travel. The animation loops this 0 -> 1 -> 0.", default: 0, min: 0, max: 1, step: 0.01 }, - extension: { type: "number", label: "Extension", description: "How close the lower rocker drives to the over-center tangent pose.", default: 1, min: 0.2, max: 1, step: 0.01 }, - turnover: { type: "number", label: "Turnover", description: "Enables the returning coupler to use the open branch after the over-center pose.", default: 1, min: 0, max: 1, step: 1 }, - explode: { type: "number", label: "Explode", description: "Separates moving groups for inspection after the linkage motion is applied.", default: 0, min: 0, max: 1, step: 0.01 }, - showFrame: { type: "boolean", label: "Frame", default: true }, - showDetails: { type: "boolean", label: "Frame details", default: true }, - showCrossRods: { type: "boolean", label: "Cross rods", default: true }, - showHardware: { type: "boolean", label: "Hardware", default: true }, - highlightLinks: { type: "boolean", label: "Highlight moving parts", default: false } - }, - animations: { - flipLoop: { - label: "Reference flip loop", - description: "Closed -> over-center -> open -> over-center -> closed.", - duration: 5, - loop: true, - update({ cycle, set }) { - set("flip", cycleFlip(cycle)); - } - }, - inspectExplode: { - label: "Inspect explode", - duration: 5, - loop: true, - update({ cycle, set }) { - set("flip", cycleFlip(cycle)); - set("explode", Math.sin((((finite(cycle, 0) % 1) + 1) % 1) * Math.PI)); - } - } - } - }, - - update({ params, effects }) { - const extension = clamp(params.extension, 0.2, 1); - const turnover = clamp(params.turnover, 0, 1); - const explode = clamp01(params.explode); - const pose = sampleMotion(params.flip, { extension, turnover }); - - const panelAngleDelta = pose.angleDeg - INITIAL_PANEL_ANGLE_DEG; - const panelTranslate = translateVector(PIVOTS.lowerBoard, pose.lower); - const blueAngleDelta = angleDeg2(PIVOTS.upperFrame, pose.upper) - INITIAL_BLUE_ANGLE_DEG; - const lowerRockerAngleDelta = pose.lowerAngleDeg - INITIAL_LOWER_ROCKER_ANGLE_DEG; - - applyVisibility(effects, params); - - effects.style(FRAME_PARTS, { opacity: 1, edgeOpacity: 0.9 }); - effects.style(FRAME_DETAIL_PARTS, { opacity: 1, edgeOpacity: 0.8 }); - effects.style(HARDWARE, { opacity: 1, edgeOpacity: 0.7 }); - effects.style("panel", { opacity: 1, edgeOpacity: 1 }); - effects.style(["rocker2", "greenRod"], { opacity: 1, edgeOpacity: 1 }); - effects.style("rockerBlue", { opacity: 1, edgeOpacity: 1 }); - effects.style("redLink", { opacity: 1, edgeOpacity: 1 }); - effects.style(CROSS_RODS, { opacity: 1, edgeOpacity: 1 }); - - if (params.highlightLinks !== false) { - effects.highlight(MOVING_PARTS, true); - } - - effects.transform(PANEL_COUPLER_PARTS, { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: PIVOTS.lowerBoard, - angleDeg: panelAngleDelta - } - }, - { translate: [panelTranslate[0] + (explode * 16), panelTranslate[1] - (explode * 8), panelTranslate[2] + (explode * 24)] } - ] - }); - - effects.transform("rockerBlue", { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: PIVOTS.upperFrame, - angleDeg: blueAngleDelta - } - }, - { translate: [-explode * 10, -explode * 8, explode * 18] } - ] - }); - - effects.transform(LOWER_ROCKER_PARTS, { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: PIVOTS.lowerFrame, - angleDeg: lowerRockerAngleDelta - } - }, - { translate: [0, explode * 10, explode * 14] } - ] - }); - - effects.transform("*", { matrix: FLAT_ON_GROUND_TRANSFORM }); - } -}; diff --git a/models/mechanisms/.adjustable_height_table_2.step.glb b/models/mechanisms/.adjustable_height_table_2.step.glb deleted file mode 100644 index c0992633..00000000 --- a/models/mechanisms/.adjustable_height_table_2.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:08b02a64c14ace53c62918a0cd13d0a5cced1778bbae8c8791b51f359ec0e603 -size 3391852 diff --git a/models/mechanisms/.adjustable_height_table_2.step.js b/models/mechanisms/.adjustable_height_table_2.step.js deleted file mode 100644 index a81ed41d..00000000 --- a/models/mechanisms/.adjustable_height_table_2.step.js +++ /dev/null @@ -1,287 +0,0 @@ -const X_AXIS = [1, 0, 0]; - -const BOTTOM_FIXED_PIVOT = [14.610456, -171.775187, 23.0]; -const TOP_FIXED_PIVOT_INITIAL = [14.610456, -171.775187, 69.00033]; -const ROLLER_PIVOT_INITIAL = [14.610456, 124.677119, 69.00033]; -const LOWER_ROLLER_CENTER_INITIAL = [14.610456, 124.677119, 23.0]; -const UPPER_ROLLER_CENTER_INITIAL = [14.610456, 124.677119, 69.00033]; -const ACTUATOR_PIVOT_INITIAL = [14.610456, -142.775187, 27.499913]; - -const INITIAL_HEIGHT = TOP_FIXED_PIVOT_INITIAL[2] - BOTTOM_FIXED_PIVOT[2]; -const INITIAL_RUN = ROLLER_PIVOT_INITIAL[1] - BOTTOM_FIXED_PIVOT[1]; -const LINK_LENGTH = Math.hypot(INITIAL_RUN, INITIAL_HEIGHT); -const INITIAL_LINK_ANGLE_DEG = angleDeg(INITIAL_HEIGHT, INITIAL_RUN); -const ACTUATOR_Y_OFFSET = ACTUATOR_PIVOT_INITIAL[1] - BOTTOM_FIXED_PIVOT[1]; -const ROLLER_RADIUS = 20.0; -const RUNWAY_MIN_Y = -2.775187; -const RUNWAY_END_CLEARANCE = 12.0; -const MAX_RAISED_HEIGHT = maxSafeRaisedHeight(); -const DEFAULT_RAISED_HEIGHT = 215; - -const TOP_ASSEMBLY = ["topAssembly"]; -const RISING_LINKAGE = ["risingLinkage"]; -const DESCENDING_LINKAGE = ["descendingLinkage"]; -const LOWER_ROLLERS = ["lowerRollers"]; -const UPPER_ROLLERS = ["upperRollers"]; -const ACTUATOR_ROD = ["piston", "actuatorCrossShaft"]; -const ACTUATOR_SLIDER = ["actuatorSlider"]; -const MOVING_PARTS = [ - ...TOP_ASSEMBLY, - ...RISING_LINKAGE, - ...DESCENDING_LINKAGE, - ...LOWER_ROLLERS, - ...UPPER_ROLLERS, - ...ACTUATOR_ROD, - ...ACTUATOR_SLIDER -]; - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -function clamp01(value) { - return clamp(value, 0, 1); -} - -function smooth01(value) { - const t = clamp01(value); - return t * t * (3 - (2 * t)); -} - -function lerp(start, end, t) { - return start + ((end - start) * t); -} - -function angleDeg(height, run) { - return Math.atan2(height, run) * 180 / Math.PI; -} - -function maxSafeRaisedHeight() { - const minRollerCenterY = RUNWAY_MIN_Y + ROLLER_RADIUS + RUNWAY_END_CLEARANCE; - const minRun = minRollerCenterY - BOTTOM_FIXED_PIVOT[1]; - return Math.sqrt(Math.max(0, (LINK_LENGTH * LINK_LENGTH) - (minRun * minRun))); -} - -function cycleLift(cycle) { - const phase = ((finite(cycle, 0) % 1) + 1) % 1; - if (phase < 0.42) { - return smooth01(phase / 0.42); - } - if (phase < 0.52) { - return 1; - } - if (phase < 0.94) { - return 1 - smooth01((phase - 0.52) / 0.42); - } - return 0; -} - -function sampleLift(rawLift, rawRaisedHeight) { - const lift = smooth01(rawLift); - const raisedHeight = clamp(rawRaisedHeight, INITIAL_HEIGHT + 5, MAX_RAISED_HEIGHT); - const height = lerp(INITIAL_HEIGHT, raisedHeight, lift); - const run = Math.sqrt(Math.max(1e-6, (LINK_LENGTH * LINK_LENGTH) - (height * height))); - const angle = angleDeg(height, run); - const rollerY = BOTTOM_FIXED_PIVOT[1] + run; - const heightDelta = height - INITIAL_HEIGHT; - const rollerYDelta = rollerY - ROLLER_PIVOT_INITIAL[1]; - - // The reference video shows the actuator rising vertically while a green - // slider follows the lower blue link. Solve that slider by intersecting the - // lower link with the fixed actuator centerline. - const actuatorRatio = clamp(ACTUATOR_Y_OFFSET / run, 0, 1); - const actuatorZ = BOTTOM_FIXED_PIVOT[2] + (height * actuatorRatio); - const actuatorZDelta = actuatorZ - ACTUATOR_PIVOT_INITIAL[2]; - return { - angle, - heightDelta, - rollerYDelta, - actuatorZDelta, - risingAngleDelta: angle - INITIAL_LINK_ANGLE_DEG, - descendingAngleDelta: -(angle - INITIAL_LINK_ANGLE_DEG), - wheelSpinDeg: -(rollerYDelta / (2 * Math.PI * ROLLER_RADIUS)) * 360 - }; -} - -function applyVisibility(effects, params) { - effects.visible("*", true); - effects.visible("actuator", params.showActuator !== false); - effects.visible("hardware", params.showHardware !== false); -} - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/thang010146/adjustable_height_table_2.step" - }, - label: "Adjustable height table 2", - description: "Viewer-time scissor-lift animation matched to the reference video: the top stays level, right pivots roll in the runways, and the vertical actuator drives the lower-link slider.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - baseAssembly: { ref: "#o1.1", label: "Base assembly" }, - basePlate: { ref: "#o1.1.1", label: "Base plate" }, - baseBearings: { ref: "#o1.1.2,o1.1.3", label: "Base fixed bearings" }, - baseRunways: { ref: "#o1.1.9,o1.1.10", label: "Base roller runways" }, - cylinder: { ref: "#o1.1.8", label: "Fixed cylinder" }, - - topAssembly: { ref: "#o1.2", label: "Moving table top" }, - topPlate: { ref: "#o1.2.1", label: "Table top" }, - topBearings: { ref: "#o1.2.2,o1.2.3", label: "Top fixed bearings" }, - topRunways: { ref: "#o1.2.8,o1.2.9", label: "Top roller runways" }, - - risingLinkage: { ref: "#o1.3", label: "Rising scissor links", description: "Links from the lower fixed pivots to the upper rolling pivots." }, - descendingLinkage: { ref: "#o1.4", label: "Descending scissor links", description: "Links from the upper fixed pivots to the lower rolling pivots." }, - centralPivot: { ref: "#o1.3.3,o1.3.4,o1.3.5", label: "Central scissor pivot" }, - upperRollerShafts: { ref: "#o1.3.6,o1.3.7,o1.3.8,o1.3.9,o1.3.10,o1.3.11", label: "Upper roller shafts" }, - lowerRollerShafts: { ref: "#o1.4.3,o1.4.4,o1.4.5,o1.4.6,o1.4.7,o1.4.8", label: "Lower roller shafts" }, - upperRollers: { ref: "#o1.8,o1.9", label: "Upper pink rollers" }, - lowerRollers: { ref: "#o1.6,o1.7", label: "Lower pink rollers" }, - - piston: { ref: "#o1.5", label: "Piston rod" }, - actuatorSlider: { ref: "#o1.10", label: "Green actuator slider" }, - actuatorCrossShaft: { ref: "#o1.11", label: "Actuator cross shaft" }, - actuator: { ref: "#o1.1.8,o1.5,o1.10,o1.11", label: "Hydraulic actuator" }, - hardware: { - ref: "#o1.1.4,o1.1.5,o1.1.6,o1.1.7,o1.2.4,o1.2.5,o1.2.6,o1.2.7,o1.3.4,o1.3.5,o1.3.7,o1.3.9,o1.3.10,o1.3.11,o1.4.4,o1.4.5,o1.4.7,o1.4.8", - label: "Nuts and bolts" - } - }, - parameters: { - lift: { type: "number", label: "Lift", description: "Collapsed to raised scissor-lift travel.", default: 0, min: 0, max: 1, step: 0.01 }, - raisedHeightMm: { type: "number", label: "Raised height", description: "Upper fixed-pivot height above the base fixed pivots at full lift; clamped so rollers stay inside the runways.", default: DEFAULT_RAISED_HEIGHT, min: 80, max: MAX_RAISED_HEIGHT, step: 1, unit: "mm" }, - explode: { type: "number", label: "Explode", description: "Separates the motion groups for inspection after the linkage pose is applied.", default: 0, min: 0, max: 1, step: 0.01 }, - showActuator: { type: "boolean", label: "Actuator", default: true }, - showHardware: { type: "boolean", label: "Hardware", default: true }, - highlightMoving: { type: "boolean", label: "Highlight moving parts", default: false } - }, - animations: { - liftLoop: { - label: "Reference lift loop", - description: "Low table, hydraulic lift to raised position, brief dwell, and return.", - duration: 8, - loop: true, - update({ cycle, set }) { - set("lift", cycleLift(cycle)); - } - }, - inspectExplode: { - label: "Lift with exploded groups", - duration: 8, - loop: true, - update({ cycle, set }) { - const lift = cycleLift(cycle); - set("lift", lift); - set("explode", Math.sin(lift * Math.PI)); - } - } - } - }, - - update({ params, effects }) { - const pose = sampleLift(params.lift, params.raisedHeightMm); - const explode = clamp01(params.explode); - - applyVisibility(effects, params); - - effects.style("basePlate", { opacity: 1, edgeOpacity: 0.8 }); - effects.style(["baseBearings", "topBearings"], { opacity: 1, edgeOpacity: 0.8 }); - effects.style(["baseRunways", "topRunways"], { opacity: 1, edgeOpacity: 0.9 }); - effects.style("topPlate", { opacity: 1, edgeOpacity: 0.85 }); - effects.style("risingLinkage", { opacity: 1, edgeOpacity: 1 }); - effects.style("descendingLinkage", { opacity: 1, edgeOpacity: 1 }); - effects.style("centralPivot", { opacity: 1, edgeOpacity: 1 }); - effects.style(["lowerRollerShafts", "upperRollerShafts", "actuatorCrossShaft"], { opacity: 1, edgeOpacity: 0.9 }); - effects.style(["lowerRollers", "upperRollers"], { opacity: 1, edgeOpacity: 1 }); - effects.style("cylinder", { opacity: 1, edgeOpacity: 1 }); - effects.style("piston", { opacity: 1, edgeOpacity: 1 }); - effects.style("actuatorSlider", { opacity: 1, edgeOpacity: 1 }); - effects.style("hardware", { opacity: 1, edgeOpacity: 0.7 }); - - if (params.highlightMoving !== false) { - effects.highlight(MOVING_PARTS, true); - } - - effects.transform("topAssembly", { - translate: [0, explode * -10, pose.heightDelta + (explode * 22)] - }); - - effects.transform("risingLinkage", { - transforms: [ - { - rotate: { - axis: X_AXIS, - origin: BOTTOM_FIXED_PIVOT, - angleDeg: pose.risingAngleDelta - } - }, - { translate: [explode * 12, explode * -8, explode * 12] } - ] - }); - - effects.transform("descendingLinkage", { - transforms: [ - { - rotate: { - axis: X_AXIS, - origin: TOP_FIXED_PIVOT_INITIAL, - angleDeg: pose.descendingAngleDelta - } - }, - { translate: [explode * -12, explode * 8, pose.heightDelta + (explode * 12)] } - ] - }); - - effects.transform("lowerRollers", { - transforms: [ - { - rotate: { - axis: X_AXIS, - origin: LOWER_ROLLER_CENTER_INITIAL, - angleDeg: pose.wheelSpinDeg - } - }, - { translate: [0, pose.rollerYDelta + (explode * 10), explode * 8] } - ] - }); - - effects.transform("upperRollers", { - transforms: [ - { - rotate: { - axis: X_AXIS, - origin: UPPER_ROLLER_CENTER_INITIAL, - angleDeg: pose.wheelSpinDeg - } - }, - { translate: [0, pose.rollerYDelta + (explode * -10), pose.heightDelta + (explode * 18)] } - ] - }); - - effects.transform(ACTUATOR_ROD, { - translate: [0, 0, pose.actuatorZDelta + (explode * 10)] - }); - - effects.transform("actuatorSlider", { - transforms: [ - { - rotate: { - axis: X_AXIS, - origin: ACTUATOR_PIVOT_INITIAL, - angleDeg: pose.risingAngleDelta - } - }, - { translate: [0, 0, pose.actuatorZDelta + (explode * 14)] } - ] - }); - } -}; diff --git a/models/mechanisms/.circular_iris_shutter_2.step.glb b/models/mechanisms/.circular_iris_shutter_2.step.glb deleted file mode 100644 index eb5c2062..00000000 --- a/models/mechanisms/.circular_iris_shutter_2.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:637efc96a27503262091b8a7c53b5d94ce6aae312d2da401f9e67278decba25c -size 2539184 diff --git a/models/mechanisms/.deployable_drone.step.glb b/models/mechanisms/.deployable_drone.step.glb deleted file mode 100644 index 0d13678c..00000000 --- a/models/mechanisms/.deployable_drone.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae05209174842a654125481d3a1d3c6d69a55f082f59edba999fc733fdb1fab4 -size 4135708 diff --git a/models/mechanisms/.gear_rack_gripper.step.glb b/models/mechanisms/.gear_rack_gripper.step.glb deleted file mode 100644 index 7cb18002..00000000 --- a/models/mechanisms/.gear_rack_gripper.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:106bbd8427742fa8498df2ec99c82e3a57ae5beab1c3dd5c8fbefa470c9299bd -size 3102568 diff --git a/models/mechanisms/.gear_rack_gripper.step.js b/models/mechanisms/.gear_rack_gripper.step.js deleted file mode 100644 index ccc638ad..00000000 --- a/models/mechanisms/.gear_rack_gripper.step.js +++ /dev/null @@ -1,288 +0,0 @@ -const Z_AXIS = [0, 0, 1]; - -const GEAR_PITCH_RADIUS_MM = 20; -const MAX_GEAR_ANGLE_DEG = 82; - -const LEFT_GEAR_CENTER = [-40, 0, 14.000004]; -const RIGHT_GEAR_CENTER = [40, 0, 14.000004]; -const LEFT_CRANK_PIN_INITIAL = [-40, -15, 14.000004]; -const RIGHT_CRANK_PIN_INITIAL = [40, -15, 14.000004]; -const LEFT_PISTON_PIN_INITIAL = [-20, -10, 14.000004]; -const RIGHT_PISTON_PIN_INITIAL = [20, -10, 14.000004]; - -const LINK_LENGTH_MM = distance2(LEFT_PISTON_PIN_INITIAL, LEFT_CRANK_PIN_INITIAL); -const INITIAL_LEFT_LINK_ANGLE_DEG = angleDeg2(LEFT_PISTON_PIN_INITIAL, LEFT_CRANK_PIN_INITIAL); -const INITIAL_RIGHT_LINK_ANGLE_DEG = angleDeg2(RIGHT_PISTON_PIN_INITIAL, RIGHT_CRANK_PIN_INITIAL); - -const MOVING_FEATURES = [ - "piston", - "leftConrod", - "rightConrod", - "leftPinion", - "rightPinion", - "leftRack", - "rightRack" -]; - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -function clamp01(value) { - return clamp(value, 0, 1); -} - -function smooth01(value) { - const t = clamp01(value); - return t * t * (3 - (2 * t)); -} - -function distance2(start, end) { - return Math.hypot(end[0] - start[0], end[1] - start[1]); -} - -function angleDeg2(start, end) { - return Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI; -} - -function rotatePointZ(point, origin, angleDeg) { - const angleRad = angleDeg * Math.PI / 180; - const cosA = Math.cos(angleRad); - const sinA = Math.sin(angleRad); - const dx = point[0] - origin[0]; - const dy = point[1] - origin[1]; - return [ - origin[0] + (dx * cosA) - (dy * sinA), - origin[1] + (dx * sinA) + (dy * cosA), - point[2] - ]; -} - -function normalizeAngleDeg(value) { - let angle = finite(value, 0); - while (angle > 180) angle -= 360; - while (angle < -180) angle += 360; - return angle; -} - -function solvePistonY(leftCrankPin) { - const dx = LEFT_PISTON_PIN_INITIAL[0] - leftCrankPin[0]; - const allowedDx = clamp(dx, -LINK_LENGTH_MM + 1e-6, LINK_LENGTH_MM - 1e-6); - const dy = Math.sqrt(Math.max(1e-6, (LINK_LENGTH_MM * LINK_LENGTH_MM) - (allowedDx * allowedDx))); - - // The uploaded STEP starts with the piston pin slightly ahead of the crank - // pin. Keep that same branch so the conrod never flips through the gear. - return leftCrankPin[1] + dy; -} - -function samplePose(rawStroke) { - const stroke = smooth01(rawStroke); - const leftGearAngleDeg = stroke * MAX_GEAR_ANGLE_DEG; - const rightGearAngleDeg = -leftGearAngleDeg; - const rackTravelMm = (leftGearAngleDeg * Math.PI / 180) * GEAR_PITCH_RADIUS_MM; - - const leftCrankPin = rotatePointZ(LEFT_CRANK_PIN_INITIAL, LEFT_GEAR_CENTER, leftGearAngleDeg); - const rightCrankPin = rotatePointZ(RIGHT_CRANK_PIN_INITIAL, RIGHT_GEAR_CENTER, rightGearAngleDeg); - const pistonY = solvePistonY(leftCrankPin); - const pistonDeltaY = pistonY - LEFT_PISTON_PIN_INITIAL[1]; - - const leftPistonPin = [LEFT_PISTON_PIN_INITIAL[0], pistonY, LEFT_PISTON_PIN_INITIAL[2]]; - const rightPistonPin = [RIGHT_PISTON_PIN_INITIAL[0], pistonY, RIGHT_PISTON_PIN_INITIAL[2]]; - - return { - stroke, - pistonDeltaY, - rackTravelMm, - leftGearAngleDeg, - rightGearAngleDeg, - leftLinkAngleDeltaDeg: normalizeAngleDeg(angleDeg2(leftPistonPin, leftCrankPin) - INITIAL_LEFT_LINK_ANGLE_DEG), - rightLinkAngleDeltaDeg: normalizeAngleDeg(angleDeg2(rightPistonPin, rightCrankPin) - INITIAL_RIGHT_LINK_ANGLE_DEG) - }; -} - -function cycleStroke(cycle) { - const phase = ((finite(cycle, 0) % 1) + 1) % 1; - if (phase < 0.38) { - return smooth01(phase / 0.38); - } - if (phase < 0.50) { - return 1; - } - if (phase < 0.88) { - return 1 - smooth01((phase - 0.50) / 0.38); - } - return 0; -} - -function applyVisibility(effects, params) { - effects.visible("*", true); - effects.visible("hardware", params.showHardware !== false); -} - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/thang010146/gear_rack_gripper.step" - }, - label: "Robot gripper gear-rack assembly", - description: "Uploaded STEP assembly with viewer-time animation based on the reference video: a sliding piston drives two blue conrods, counter-rotating pinions, and opposing rack jaws.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - base: { ref: "#o1.1.10", label: "Fixed base plate" }, - cylinder: { ref: "#o1.1.9", label: "Fixed green cylinder" }, - piston: { ref: "#o1.1.17", label: "Sliding piston rod" }, - leftRack: { ref: "#o1.1.11", label: "Left moving rack jaw" }, - rightRack: { ref: "#o1.1.12", label: "Right moving rack jaw" }, - leftPinion: { - ref: "#o1.1.13", - label: "Left yellow pinion and crank", - origin: LEFT_GEAR_CENTER, - axis: Z_AXIS - }, - rightPinion: { - ref: "#o1.1.14", - label: "Right yellow pinion and crank", - origin: RIGHT_GEAR_CENTER, - axis: Z_AXIS - }, - leftConrod: { ref: "#o1.1.15", label: "Left blue connecting rod" }, - rightConrod: { ref: "#o1.1.16", label: "Right blue connecting rod" }, - hardware: { - ref: "#o1.1.1,#o1.1.2,#o1.1.3,#o1.1.4,#o1.1.5,#o1.1.6,#o1.1.7,#o1.1.8", - label: "Fixed bolts and pins" - }, - movingAssembly: { - ref: "#o1.1.11,#o1.1.12,#o1.1.13,#o1.1.14,#o1.1.15,#o1.1.16,#o1.1.17", - label: "Moving drive train" - } - }, - parameters: { - stroke: { - type: "number", - label: "Stroke", - description: "0 is the imported closed pose; 1 pushes the piston forward and opens the two rack jaws with the pinion pitch relationship preserved.", - default: 0, - min: 0, - max: 1, - step: 0.01 - }, - explode: { - type: "number", - label: "Explode", - description: "Separates the moving groups after the solved pose for mechanism inspection.", - default: 0, - min: 0, - max: 1, - step: 0.01 - }, - showHardware: { type: "boolean", label: "Hardware", default: true }, - highlightMoving: { type: "boolean", label: "Highlight moving", default: false } - }, - animations: { - driveLoop: { - label: "Piston rack drive", - description: "Closed pose, piston-driven rack opening, short dwell, then return.", - duration: 6, - loop: true, - update({ cycle, set }) { - set("stroke", cycleStroke(cycle)); - } - }, - inspectDrive: { - label: "Exploded drive inspection", - duration: 6, - loop: true, - update({ cycle, set }) { - const stroke = cycleStroke(cycle); - set("stroke", stroke); - set("explode", Math.sin(stroke * Math.PI)); - } - } - } - }, - - update({ params, effects }) { - const pose = samplePose(params.stroke); - const explode = clamp01(params.explode); - - applyVisibility(effects, params); - - if (params.highlightMoving === true) { - effects.highlight(MOVING_FEATURES, true); - } - - effects.transform("piston", { - translate: [0, pose.pistonDeltaY, explode * 12] - }); - - effects.transform("leftConrod", { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: LEFT_PISTON_PIN_INITIAL, - angleDeg: pose.leftLinkAngleDeltaDeg - } - }, - { translate: [0, pose.pistonDeltaY, explode * 16] } - ] - }); - - effects.transform("rightConrod", { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: RIGHT_PISTON_PIN_INITIAL, - angleDeg: pose.rightLinkAngleDeltaDeg - } - }, - { translate: [0, pose.pistonDeltaY, explode * 16] } - ] - }); - - effects.transform("leftPinion", { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: LEFT_GEAR_CENTER, - angleDeg: pose.leftGearAngleDeg - } - }, - { translate: [-explode * 8, -explode * 5, explode * 18] } - ] - }); - - effects.transform("rightPinion", { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: RIGHT_GEAR_CENTER, - angleDeg: pose.rightGearAngleDeg - } - }, - { translate: [explode * 8, -explode * 5, explode * 18] } - ] - }); - - effects.transform("leftRack", { - translate: [-pose.rackTravelMm - (explode * 14), 0, explode * 8] - }); - - effects.transform("rightRack", { - translate: [pose.rackTravelMm + (explode * 14), 0, explode * 8] - }); - } -}; diff --git a/models/mechanisms/.hinge_2_dof.step.glb b/models/mechanisms/.hinge_2_dof.step.glb deleted file mode 100644 index 2ced36e0..00000000 --- a/models/mechanisms/.hinge_2_dof.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a857a4f20fa6df7d799319c1e4d1e110ee53ba2dc5662ec0e34e962d7a6b252e -size 2400448 diff --git a/models/mechanisms/.kinetic_pyramid_facade.step.glb b/models/mechanisms/.kinetic_pyramid_facade.step.glb deleted file mode 100644 index bfc3239b..00000000 --- a/models/mechanisms/.kinetic_pyramid_facade.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c00b768ee9d0b89b91e6f7876a4fb80fcd01c37c399d9f08b0e148ea8e1cb80 -size 5742712 diff --git a/models/mechanisms/.knuckle_joint_press.step.glb b/models/mechanisms/.knuckle_joint_press.step.glb deleted file mode 100644 index 3232c561..00000000 --- a/models/mechanisms/.knuckle_joint_press.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0b2de0351ebf5279ab72de65c2a629a6b5eec387a590c366c15ca64072b2bd0 -size 3836680 diff --git a/models/mechanisms/.meat_hash_machine.step.glb b/models/mechanisms/.meat_hash_machine.step.glb deleted file mode 100644 index 09455af2..00000000 --- a/models/mechanisms/.meat_hash_machine.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad250199ef08c6f98a409243b44af9835f84140c08cd90463f0b0ff741e5cc7d -size 6118016 diff --git a/models/mechanisms/.object_8_shaped_runway.step.glb b/models/mechanisms/.object_8_shaped_runway.step.glb deleted file mode 100644 index b5841277..00000000 --- a/models/mechanisms/.object_8_shaped_runway.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d71e3bcd03f6937d2c51dd3d9fbf44a237521a7fbfca98c8dae6ccbdd062fb36 -size 5799992 diff --git a/models/mechanisms/.planetary_gearless_mechanism_2.step.glb b/models/mechanisms/.planetary_gearless_mechanism_2.step.glb deleted file mode 100644 index c54acd41..00000000 --- a/models/mechanisms/.planetary_gearless_mechanism_2.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e6c25b6b51080cdabb4a1ae81675b5286a7a2de338b2fdfadf2e077755a75cd2 -size 1904468 diff --git a/models/mechanisms/.rc_plane_retractable_landing_gear.step.glb b/models/mechanisms/.rc_plane_retractable_landing_gear.step.glb deleted file mode 100644 index 7852cb88..00000000 --- a/models/mechanisms/.rc_plane_retractable_landing_gear.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e8df921721232afb5c5be6429d9dbed127fd35e7640c92f2aa8a47ac468398dc -size 5796116 diff --git a/models/mechanisms/.screw_3_stage_telescopic_actuator_2b.step.glb b/models/mechanisms/.screw_3_stage_telescopic_actuator_2b.step.glb deleted file mode 100644 index f17d0772..00000000 --- a/models/mechanisms/.screw_3_stage_telescopic_actuator_2b.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f9f078b1d6cc6d0241905a2b71b07cda34b537aa16adf58ea1f7c44d964a5239 -size 5592804 diff --git a/models/mechanisms/.spatial_planetary_belt_drive.step.glb b/models/mechanisms/.spatial_planetary_belt_drive.step.glb deleted file mode 100644 index 5e4f1b17..00000000 --- a/models/mechanisms/.spatial_planetary_belt_drive.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e48535943ec1965849e14a01eebd46785f866fd98a8019e1165897a32001691 -size 3421276 diff --git a/models/mechanisms/.static_balancing_gears_spring.step.glb b/models/mechanisms/.static_balancing_gears_spring.step.glb deleted file mode 100644 index 493f8a22..00000000 --- a/models/mechanisms/.static_balancing_gears_spring.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed1150b347a930c9b60c9041b133edf03ba52b987387f4aeabdf9f106e0b8a09 -size 5519684 diff --git a/models/mechanisms/.two_cam_draw_rectangles.step.glb b/models/mechanisms/.two_cam_draw_rectangles.step.glb deleted file mode 100644 index 58e9781c..00000000 --- a/models/mechanisms/.two_cam_draw_rectangles.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2fac40cb869411c99d1fd72a0c9ac03de73caf8070ae24dbfb7b57c6d9b94ec6 -size 2666548 diff --git a/models/mechanisms/.two_input_shaft_differential_bevel_gearbox.step.glb b/models/mechanisms/.two_input_shaft_differential_bevel_gearbox.step.glb deleted file mode 100644 index fbc498ca..00000000 --- a/models/mechanisms/.two_input_shaft_differential_bevel_gearbox.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e8fd03107e922ef70d3c874db85466094dfabb6d19fcb6d9619ffde30044e67e -size 3753152 diff --git a/models/mechanisms/.two_stage_gear_multiplier.step.glb b/models/mechanisms/.two_stage_gear_multiplier.step.glb deleted file mode 100644 index bf273ddb..00000000 --- a/models/mechanisms/.two_stage_gear_multiplier.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bc285f127af282c459b1ab1fdeb18030033a5a951dfe427e0023040d64fcfbb -size 6666512 diff --git a/models/mechanisms/.two_way_linear_to_one_way_rotation_9b.step.glb b/models/mechanisms/.two_way_linear_to_one_way_rotation_9b.step.glb deleted file mode 100644 index dbbc8cca..00000000 --- a/models/mechanisms/.two_way_linear_to_one_way_rotation_9b.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d5db5e6bdeaaf55ece404ad6515aa306c53e6ab1b1d1513b33896789c5b4b28 -size 3582108 diff --git a/models/mechanisms/.typewriter_desk.step.glb b/models/mechanisms/.typewriter_desk.step.glb deleted file mode 100644 index d7df92eb..00000000 --- a/models/mechanisms/.typewriter_desk.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1af71b56e2d5d8c4ec0e441e4df26853f3f398909f2d3a75c456d978b6591d85 -size 2052772 diff --git a/models/mechanisms/.wall_mounted_car_shed.step.glb b/models/mechanisms/.wall_mounted_car_shed.step.glb deleted file mode 100644 index 5012ccbe..00000000 --- a/models/mechanisms/.wall_mounted_car_shed.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1856feb10da4db31c8d6dabe3ad30302ce7fefa14fb21eb968f9af2b56920e9a -size 4149212 diff --git a/models/mechanisms/180_degree_flip_mechanism.step b/models/mechanisms/180_degree_flip_mechanism.step deleted file mode 100644 index e212a688..00000000 --- a/models/mechanisms/180_degree_flip_mechanism.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f93c32a11a320ce5f10f4ed8c8250866a04e0ec2f2683349803672c9dc924d53 -size 414957 diff --git a/models/mechanisms/README.md b/models/mechanisms/README.md deleted file mode 100644 index 51ca4816..00000000 --- a/models/mechanisms/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Mechanism STEP Demos - -Flattened STEP mechanism assemblies inspired by the YouTube channel -[thang010146](https://www.youtube.com/@thang010146/videos). Original -mechanism design, animation, and downloadable source files are credited to -`thang010146`. - -This folder is intentionally flat so CAD Viewer can catalog the top-level STEP -assemblies and their hidden sidecars directly. - -## Contents - -- `*.step`: canonical mechanism assemblies. -- `.*.step.glb`: generated render/selector sidecars paired with STEP files. -- `.*.step.js`: optional interaction sidecars paired with STEP files. - -Extracted source archives, Inventor files, SDF working files, videos, and -intermediate generation scripts are intentionally omitted from this fixture -bundle. - -## Manifest - -| Mechanism | STEP | Required sidecar | Optional sidecar | Source | -|---|---|---|---|---| -| 180 deg. flip mechanism | [180_degree_flip_mechanism.step](180_degree_flip_mechanism.step) | `.180_degree_flip_mechanism.step.glb` | `.180_degree_flip_mechanism.step.js` | [Video](https://www.youtube.com/watch?v=IGexfslM_5Y), [STEP archive](https://www.mediafire.com/file/pcjk004x96r6ibu/180FlipMechanismSTEP.zip/file) | -| Table of adjustable height 2 | [adjustable_height_table_2.step](adjustable_height_table_2.step) | `.adjustable_height_table_2.step.glb` | `.adjustable_height_table_2.step.js` | [Video](https://www.youtube.com/watch?v=c30g2UszMws), [STEP archive](https://www.mediafire.com/file/ulf0n6zbbp1veo4/TableAdjustHeight2STEP.zip/file) | -| Circular window iris shutter 2 | [circular_iris_shutter_2.step](circular_iris_shutter_2.step) | `.circular_iris_shutter_2.step.glb` | | [Video](https://www.youtube.com/watch?v=4ckPpHpe4TM), [STEP archive](https://www.mediafire.com/file/sz86hz0w9aknnen/CircularIrisShutter2STEP.zip/file) | -| Deployable drone | [deployable_drone.step](deployable_drone.step) | `.deployable_drone.step.glb` | | [Video](https://www.youtube.com/watch?v=QYt_wUP52T0), [STEP archive](https://www.mediafire.com/file/7533syysab1zhrg/DeployableDroneSTEP.zip/file) | -| Robot gripper of gear-rack drives | [gear_rack_gripper.step](gear_rack_gripper.step) | `.gear_rack_gripper.step.glb` | `.gear_rack_gripper.step.js` | [Video](https://www.youtube.com/watch?v=CP5q6YxyeQ8), user upload `RobotGripperGearRackSTEP.zip` | -| Hinge of 2 DoF | [hinge_2_dof.step](hinge_2_dof.step) | `.hinge_2_dof.step.glb` | | [Video](https://www.youtube.com/watch?v=Mvmi2AyNqx0), [STEP archive](https://www.mediafire.com/file/nl2m3asaa31dvnf/Hinge2DoFSTEP.zip/file) | -| Kinetic pyramid facade | [kinetic_pyramid_facade.step](kinetic_pyramid_facade.step) | `.kinetic_pyramid_facade.step.glb` | | [Video](https://www.youtube.com/watch?v=wA9d9uJIBGw), [STEP archive](https://www.mediafire.com/file/vri2hopfzj0k731/KineticPyramidFacadeSTEP.zip/file) | -| Knuckle joint press | [knuckle_joint_press.step](knuckle_joint_press.step) | `.knuckle_joint_press.step.glb` | | [Video](https://www.youtube.com/watch?v=Vd-vErlQPPk), [STEP archive](https://www.mediafire.com/file/8cy26hg2dlovi1b/KnuckleJointPressSTEP.zip/file) | -| Meat hash machine | [meat_hash_machine.step](meat_hash_machine.step) | `.meat_hash_machine.step.glb` | | [Video](https://www.youtube.com/watch?v=QVlepLAtYJk), [STEP archive](https://www.mediafire.com/file/q1hksit7cbdm8c6/MeatHashMachineSTEP.zip/file) | -| Object moving along 8-shaped runway | [object_8_shaped_runway.step](object_8_shaped_runway.step) | `.object_8_shaped_runway.step.glb` | | [Video](https://www.youtube.com/watch?v=KUD4qqCNmZk), [STEP archive](https://www.mediafire.com/file/lrpeog3wavxumek/Object8runwaySTEP.zip/file) | -| Planetary gearless mechanism 2 | [planetary_gearless_mechanism_2.step](planetary_gearless_mechanism_2.step) | `.planetary_gearless_mechanism_2.step.glb` | | [Video](https://www.youtube.com/watch?v=Vf1mpbv8DO4), [STEP archive](https://www.mediafire.com/file/jpwetalgq5ctqvr/PlanetaryGearlessMechanism2STEP.zip/file) | -| RC plane retractable landing gear | [rc_plane_retractable_landing_gear.step](rc_plane_retractable_landing_gear.step) | `.rc_plane_retractable_landing_gear.step.glb` | | [Video](https://www.youtube.com/watch?v=J8K3uAWMSRo), [STEP archive](https://www.mediafire.com/file/ma5cw3isx7oncfw/RCPlaneRetractableLandingGearSTEP.zip/file) | -| Screw 3-stage telescopic actuator 2b | [screw_3_stage_telescopic_actuator_2b.step](screw_3_stage_telescopic_actuator_2b.step) | `.screw_3_stage_telescopic_actuator_2b.step.glb` | | [Video](https://www.youtube.com/watch?v=LvDbXBtNrlQ), [STEP archive](https://www.mediafire.com/file/ohra998t0bjy8qj/Screw3stageTelescopicActuator2bSTEP.zip/file) | -| Spatial planetary belt drive | [spatial_planetary_belt_drive.step](spatial_planetary_belt_drive.step) | `.spatial_planetary_belt_drive.step.glb` | | [Video](https://www.youtube.com/watch?v=wnk6a-lBYCg), [STEP archive](https://www.mediafire.com/file/gbmj0t4ad11ely9/SpatialPlanetaryBeltSTEP.zip/file) | -| Static balancing mechanism using gears and spring | [static_balancing_gears_spring.step](static_balancing_gears_spring.step) | `.static_balancing_gears_spring.step.glb` | | [Video](https://www.youtube.com/watch?v=WnDc7tv79HM), [STEP archive](https://www.mediafire.com/file/1y4nqt4jer40uyc/StaticBalancingMechanismGearSpringSTEP.zip/file) | -| Two cam mechanism for drawing rectangles | [two_cam_draw_rectangles.step](two_cam_draw_rectangles.step) | `.two_cam_draw_rectangles.step.glb` | | [Video](https://www.youtube.com/watch?v=rEmLvLY65E8), [STEP archive](https://www.mediafire.com/file/09cbcf5of21szpn/TwoCamDrawRectangleSTEP.zip/file) | -| Two input shaft differential bevel gearbox | [two_input_shaft_differential_bevel_gearbox.step](two_input_shaft_differential_bevel_gearbox.step) | `.two_input_shaft_differential_bevel_gearbox.step.glb` | | [Video](https://www.youtube.com/watch?v=MLw1eTFPnDE), [STEP archive](https://www.mediafire.com/file/e09s8h9yoxe5luz/TwoInputShaftGearboxSTEP.zip/file) | -| Two stage gear multiplier | [two_stage_gear_multiplier.step](two_stage_gear_multiplier.step) | `.two_stage_gear_multiplier.step.glb` | | [Video](https://www.youtube.com/watch?v=y9Z5eSPG2TU), [STEP archive](https://www.mediafire.com/file/yt8nu74susrgcv3/TwoStageGearMultiplierSTEP.zip/file) | -| Converting two way linear motion into one way rotation 9b | [two_way_linear_to_one_way_rotation_9b.step](two_way_linear_to_one_way_rotation_9b.step) | `.two_way_linear_to_one_way_rotation_9b.step.glb` | | [Video](https://www.youtube.com/watch?v=PE_Wfw5BZXk), [STEP archive](https://www.mediafire.com/file/gue5rvk6b4dgt1s/Convert2WLto1WR9bSTEP.zip/file) | -| Typewriter desk | [typewriter_desk.step](typewriter_desk.step) | `.typewriter_desk.step.glb` | | [Video](https://www.youtube.com/watch?v=OTLIxTTfnWk), [STEP archive](https://www.mediafire.com/file/xk4fsefxcdl3mtr/TypewriterDeskSTEP.zip/file) | -| Wall mounted car shed | [wall_mounted_car_shed.step](wall_mounted_car_shed.step) | `.wall_mounted_car_shed.step.glb` | | [Video](https://www.youtube.com/watch?v=SnVo3Zg83Pk), [STEP archive](https://www.mediafire.com/file/gepccrx2g9h3d3b/WallMountedCarShedSTEP.zip/file) | diff --git a/models/mechanisms/adjustable_height_table_2.step b/models/mechanisms/adjustable_height_table_2.step deleted file mode 100644 index 19917b99..00000000 --- a/models/mechanisms/adjustable_height_table_2.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b0a24e12c5a284d862872d17d3cff01134e6c69384a7cad0ce9fc2661c0e8d9 -size 544010 diff --git a/models/mechanisms/circular_iris_shutter_2.step b/models/mechanisms/circular_iris_shutter_2.step deleted file mode 100644 index c96d3993..00000000 --- a/models/mechanisms/circular_iris_shutter_2.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4522596f96812e274ef2525f0fbad42ae2bdd1871cf134d90e35b23890d3491 -size 515213 diff --git a/models/mechanisms/deployable_drone.step b/models/mechanisms/deployable_drone.step deleted file mode 100644 index a149a5ad..00000000 --- a/models/mechanisms/deployable_drone.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c08c2b4694e37d2db4b586b92003cdc292f9a3d02a7846621167934657ed292c -size 470354 diff --git a/models/mechanisms/gear_rack_gripper.step b/models/mechanisms/gear_rack_gripper.step deleted file mode 100644 index 3c1c467b..00000000 --- a/models/mechanisms/gear_rack_gripper.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:050ca8578071acf833d5f119244865677a4b198b4c7d2dd5ad0267467074fe13 -size 2447733 diff --git a/models/mechanisms/hinge_2_dof.step b/models/mechanisms/hinge_2_dof.step deleted file mode 100644 index 17dba6e4..00000000 --- a/models/mechanisms/hinge_2_dof.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3547f532b0f3f7477134cdf07d65f3615f7c1ff2619366819e8fed3978b93207 -size 292710 diff --git a/models/mechanisms/kinetic_pyramid_facade.step b/models/mechanisms/kinetic_pyramid_facade.step deleted file mode 100644 index 799e79ab..00000000 --- a/models/mechanisms/kinetic_pyramid_facade.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a0eb6abc01a5b9600ba286f562044fadab0cbf7db46a02b0cfd09a48d6e0088 -size 704100 diff --git a/models/mechanisms/knuckle_joint_press.step b/models/mechanisms/knuckle_joint_press.step deleted file mode 100644 index 768c0ab6..00000000 --- a/models/mechanisms/knuckle_joint_press.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d78b1a0ef141175033c8b8129345fa87cc874a49f2c09e95b9cd45aa486442b6 -size 1162772 diff --git a/models/mechanisms/meat_hash_machine.step b/models/mechanisms/meat_hash_machine.step deleted file mode 100644 index 8120cb95..00000000 --- a/models/mechanisms/meat_hash_machine.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ec010f4977e623f946031da3e47dc32ce17a1445b73232a289a183ce26add63 -size 1883751 diff --git a/models/mechanisms/object_8_shaped_runway.step b/models/mechanisms/object_8_shaped_runway.step deleted file mode 100644 index 5c49b78b..00000000 --- a/models/mechanisms/object_8_shaped_runway.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7af7fecdf4a0a5ee46427b2f9f68f08ab0ffb12f47deee25d1e8fc549948e5a -size 1567551 diff --git a/models/mechanisms/planetary_gearless_mechanism_2.step b/models/mechanisms/planetary_gearless_mechanism_2.step deleted file mode 100644 index a29a6c0e..00000000 --- a/models/mechanisms/planetary_gearless_mechanism_2.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21a6714380c3d2fbe3ebb610cf32777d02481e98df79e5c83caf8e801e83e916 -size 392548 diff --git a/models/mechanisms/rc_plane_retractable_landing_gear.step b/models/mechanisms/rc_plane_retractable_landing_gear.step deleted file mode 100644 index 89a0ba6b..00000000 --- a/models/mechanisms/rc_plane_retractable_landing_gear.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2fb3445311ca61cf2bf7d66d74e2f42854e19ce97113299e5148d4d0a01623f -size 1120895 diff --git a/models/mechanisms/screw_3_stage_telescopic_actuator_2b.step b/models/mechanisms/screw_3_stage_telescopic_actuator_2b.step deleted file mode 100644 index 01314d3a..00000000 --- a/models/mechanisms/screw_3_stage_telescopic_actuator_2b.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7385ecc71537d57c4d2df5f5c5e6dbe35d4cdf21f513ec2f4539cb6128db3a51 -size 3783478 diff --git a/models/mechanisms/spatial_planetary_belt_drive.step b/models/mechanisms/spatial_planetary_belt_drive.step deleted file mode 100644 index fd3c6d13..00000000 --- a/models/mechanisms/spatial_planetary_belt_drive.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9921e8a8f88c5ef6da687decd8cdc38cf62aa956a381c44ee20697e5668ab29c -size 1320034 diff --git a/models/mechanisms/static_balancing_gears_spring.step b/models/mechanisms/static_balancing_gears_spring.step deleted file mode 100644 index d647c810..00000000 --- a/models/mechanisms/static_balancing_gears_spring.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a3c3e7f0bc30761e78037c9799c4c38ccaadb6a6c003fc8f3ddda4ec4f2f00b5 -size 1523818 diff --git a/models/mechanisms/two_cam_draw_rectangles.step b/models/mechanisms/two_cam_draw_rectangles.step deleted file mode 100644 index 9a5da1f3..00000000 --- a/models/mechanisms/two_cam_draw_rectangles.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1aaadb14c915d7fee983c77478cae9c355c4c8ca0b73226c753d8fd4ccbd716 -size 543632 diff --git a/models/mechanisms/two_input_shaft_differential_bevel_gearbox.step b/models/mechanisms/two_input_shaft_differential_bevel_gearbox.step deleted file mode 100644 index 71ac4d15..00000000 --- a/models/mechanisms/two_input_shaft_differential_bevel_gearbox.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e1b8ea12d0047710d82dfac59e73f54288bdad89511c42499cc83da5d435467 -size 1172666 diff --git a/models/mechanisms/two_stage_gear_multiplier.step b/models/mechanisms/two_stage_gear_multiplier.step deleted file mode 100644 index 9d6818f0..00000000 --- a/models/mechanisms/two_stage_gear_multiplier.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2656020db1feb0739db7f0bbd5d673c7a87df4c95572a8969a3c76e0d244a4b8 -size 1860752 diff --git a/models/mechanisms/two_way_linear_to_one_way_rotation_9b.step b/models/mechanisms/two_way_linear_to_one_way_rotation_9b.step deleted file mode 100644 index a2c171d6..00000000 --- a/models/mechanisms/two_way_linear_to_one_way_rotation_9b.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:17c3681ea377ff03c14769bada14e01b4f3f41a4c24a6dc956c00559e7c63074 -size 629536 diff --git a/models/mechanisms/typewriter_desk.step b/models/mechanisms/typewriter_desk.step deleted file mode 100644 index 9f9b228f..00000000 --- a/models/mechanisms/typewriter_desk.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:094763564baa5d233397bf19f53480cf8d4e13ac8b6c91761f5cf978ab95fe3a -size 282942 diff --git a/models/mechanisms/wall_mounted_car_shed.step b/models/mechanisms/wall_mounted_car_shed.step deleted file mode 100644 index 251252c2..00000000 --- a/models/mechanisms/wall_mounted_car_shed.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4832144ab1cd1dc8dcfef225a74d4f1bf5105cf945fa83c29383b82b3e78e3fb -size 584159 diff --git a/models/robots/README.md b/models/robots/README.md deleted file mode 100644 index 5b91333f..00000000 --- a/models/robots/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Robot Fixtures - -Robot description fixtures and supporting meshes for URDF, SRDF, STEP, STL, -and CAD Viewer workflows. - -## Directory Map - -- `elrobot/`: ELRobot follower URDF/SRDF plus STL meshes. -- `lekiwi/`: LeKiwi URDF/SRDF plus STL meshes for mobile-base and arm parts. -- `lyra/`: authored dexterous humanoid-hand source (16 DOF), generated - STEP/URDF/SRDF outputs, per-link 3MF meshes, and a CAD Viewer pose - animation sidecar. -- `openarm/`: OpenArm bimanual URDF/SRDF fixture. -- `so101/`: SO-101 URDF/SRDF plus STL meshes. -- `tom/`: authored text-to-cad robot arm source, generated STEP/URDF outputs, - STL exports, gripper parts, and sheet-metal DXF fixtures. - -## Conventions - -- Keep URDF/SRDF text files readable in normal Git. -- Keep mesh, STEP, STL, DXF, and render sidecar artifacts in Git LFS. -- Do not commit simulator cache directories, generated preview media, local - package-resolution experiments, or printer credentials. - -For TOM-specific generated CAD sources, keep Python generators near their -corresponding STEP output so the fixture remains inspectable without a separate -source tree. diff --git a/models/robots/elrobot/elrobot-follower.srdf b/models/robots/elrobot/elrobot-follower.srdf deleted file mode 100644 index b2999717..00000000 --- a/models/robots/elrobot/elrobot-follower.srdf +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/elrobot/elrobot-follower.urdf b/models/robots/elrobot/elrobot-follower.urdf deleted file mode 100644 index 190939bd..00000000 --- a/models/robots/elrobot/elrobot-follower.urdf +++ /dev/null @@ -1,522 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/elrobot/meshes/Gripper_Base_v1_1.stl b/models/robots/elrobot/meshes/Gripper_Base_v1_1.stl deleted file mode 100644 index feeebc99..00000000 --- a/models/robots/elrobot/meshes/Gripper_Base_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff0c113090056f6211368eb0a20138caef245c7e490793821e85c92c53301900 -size 433484 diff --git a/models/robots/elrobot/meshes/Gripper_Gear_v1_1.stl b/models/robots/elrobot/meshes/Gripper_Gear_v1_1.stl deleted file mode 100644 index 46aa5481..00000000 --- a/models/robots/elrobot/meshes/Gripper_Gear_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0c6e45bbd7115b1e6a346af11e5ee331d2eaebb2780b51f8c519893e14bff3a5 -size 162884 diff --git a/models/robots/elrobot/meshes/Gripper_Jaw_01_v1_1.stl b/models/robots/elrobot/meshes/Gripper_Jaw_01_v1_1.stl deleted file mode 100644 index 69b62c2c..00000000 --- a/models/robots/elrobot/meshes/Gripper_Jaw_01_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3efe42f96b3cbe4b9c5db59fb3c3f395647b098f1a93de98889672dc07ecaaa1 -size 26184 diff --git a/models/robots/elrobot/meshes/Gripper_Jaw_02_v1_1.stl b/models/robots/elrobot/meshes/Gripper_Jaw_02_v1_1.stl deleted file mode 100644 index 8bb0c9c9..00000000 --- a/models/robots/elrobot/meshes/Gripper_Jaw_02_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d615ada9f3bee9d44b75cefbbbe1fb8d3687664a57871891f1a6047a59df13b7 -size 26184 diff --git a/models/robots/elrobot/meshes/Joint_01_1.stl b/models/robots/elrobot/meshes/Joint_01_1.stl deleted file mode 100644 index d4c7d175..00000000 --- a/models/robots/elrobot/meshes/Joint_01_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01e4f97fae289bb95ca7cd7f3960cfde856631304f69d9baf32c286027d741a1 -size 196384 diff --git a/models/robots/elrobot/meshes/Joint_02_1.stl b/models/robots/elrobot/meshes/Joint_02_1.stl deleted file mode 100644 index 2ddf28d9..00000000 --- a/models/robots/elrobot/meshes/Joint_02_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5027924d6376f834466850b35b44b9580bbb7ece3be7234fd5d34de5ff855ad0 -size 483484 diff --git a/models/robots/elrobot/meshes/Joint_03_v1_1.stl b/models/robots/elrobot/meshes/Joint_03_v1_1.stl deleted file mode 100644 index e748a4d3..00000000 --- a/models/robots/elrobot/meshes/Joint_03_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01d26b864af1206871ee60f8152634f5ab2b69176ff49297afdace782df0219b -size 499184 diff --git a/models/robots/elrobot/meshes/Joint_04_v1_1.stl b/models/robots/elrobot/meshes/Joint_04_v1_1.stl deleted file mode 100644 index c0fd9739..00000000 --- a/models/robots/elrobot/meshes/Joint_04_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e87a54fd8408b53112ccff324f9bfead774cedf026cda75cf5af1a91848217e4 -size 464484 diff --git a/models/robots/elrobot/meshes/Joint_05_v1_1.stl b/models/robots/elrobot/meshes/Joint_05_v1_1.stl deleted file mode 100644 index f97feb54..00000000 --- a/models/robots/elrobot/meshes/Joint_05_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e2b22739cc431d2c18b1b22619f67dd61225365c22956a229ed88bbb711fa25 -size 184084 diff --git a/models/robots/elrobot/meshes/Joint_06_v1_1.stl b/models/robots/elrobot/meshes/Joint_06_v1_1.stl deleted file mode 100644 index cd2a3ec0..00000000 --- a/models/robots/elrobot/meshes/Joint_06_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7660c75f8cb0ad0c3bc5efd885c2957e942d4678173035b4bff037ddb8380f68 -size 478284 diff --git a/models/robots/elrobot/meshes/ST3215_1_v1_1.stl b/models/robots/elrobot/meshes/ST3215_1_v1_1.stl deleted file mode 100644 index 0fe05f70..00000000 --- a/models/robots/elrobot/meshes/ST3215_1_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9b2cbd3c5d7d178706ef11c3983c7e01190b0f80008bb7ff52381d6660ebe55 -size 1116584 diff --git a/models/robots/elrobot/meshes/ST3215_2_v1_1.stl b/models/robots/elrobot/meshes/ST3215_2_v1_1.stl deleted file mode 100644 index 0a54185b..00000000 --- a/models/robots/elrobot/meshes/ST3215_2_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9f865ebcc8faec47dfcbceb998ebc168e288a741407c49be4c565a6c6ce70616 -size 1116284 diff --git a/models/robots/elrobot/meshes/ST3215_3_v1_1.stl b/models/robots/elrobot/meshes/ST3215_3_v1_1.stl deleted file mode 100644 index e8d63703..00000000 --- a/models/robots/elrobot/meshes/ST3215_3_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:920cced28d36977266402505951091cc25ee01f9f0f6a27408410083bc634511 -size 1115184 diff --git a/models/robots/elrobot/meshes/ST3215_4_v1_1.stl b/models/robots/elrobot/meshes/ST3215_4_v1_1.stl deleted file mode 100644 index 477d97c2..00000000 --- a/models/robots/elrobot/meshes/ST3215_4_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c437e44f0161831e0138cb8a3d6886d6003a7af80454a1d53a3f6cfbb49b72bc -size 1115084 diff --git a/models/robots/elrobot/meshes/ST3215_5_v1_1.stl b/models/robots/elrobot/meshes/ST3215_5_v1_1.stl deleted file mode 100644 index a97e5e8b..00000000 --- a/models/robots/elrobot/meshes/ST3215_5_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a40d2890c001dc024c2cfefc1f76b05560f3bfebadba42efffd4d870accd931 -size 1114984 diff --git a/models/robots/elrobot/meshes/ST3215_6_v1_1.stl b/models/robots/elrobot/meshes/ST3215_6_v1_1.stl deleted file mode 100644 index 80db9437..00000000 --- a/models/robots/elrobot/meshes/ST3215_6_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b30b1012b497cb9c2169a21e297ef6fb1d2f58f509d94d4ec3c41ac4a760e1d0 -size 1116284 diff --git a/models/robots/elrobot/meshes/ST3215_7_v1_1.stl b/models/robots/elrobot/meshes/ST3215_7_v1_1.stl deleted file mode 100644 index 89046319..00000000 --- a/models/robots/elrobot/meshes/ST3215_7_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:67dcba6fb9cae33942aed852146787060953b4f0f1de091d9fd2d45e52fbac4d -size 1115084 diff --git a/models/robots/elrobot/meshes/ST3215_8_v1_1.stl b/models/robots/elrobot/meshes/ST3215_8_v1_1.stl deleted file mode 100644 index 94c4da76..00000000 --- a/models/robots/elrobot/meshes/ST3215_8_v1_1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4aebce2fef7b4a946c332c94075e369c3c2667237c0a89c07ff6dddcf5dd3507 -size 1114984 diff --git a/models/robots/elrobot/meshes/base_link.stl b/models/robots/elrobot/meshes/base_link.stl deleted file mode 100644 index 684e3b5f..00000000 --- a/models/robots/elrobot/meshes/base_link.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8551974af5e59e1aedfd863ca1c8e401a0b52dd96449d3ca83406c7fa05c5012 -size 122484 diff --git a/models/robots/juno/.juno.step.glb b/models/robots/juno/.juno.step.glb deleted file mode 100644 index 2fdf8d9b..00000000 --- a/models/robots/juno/.juno.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a50c7096601e12979242713dddba566764a404867eebfd9ccfb1e161a62b4482 -size 20853764 diff --git a/models/robots/juno/.juno.step.js b/models/robots/juno/.juno.step.js deleted file mode 100644 index 38c7a68b..00000000 --- a/models/robots/juno/.juno.step.js +++ /dev/null @@ -1,1342 +0,0 @@ -// Walking-in-place animation for the juno humanoid STEP assembly. -// -// The STEP geometry is baked in the athletic ready stance (see juno.py / -// juno_parts/chain.py). This module recomputes full-chain FK each frame at -// (athletic pose + gait deltas) and applies, per link subtree, the rigid -// delta matrix T_target * inverse(T_athletic) in the fixed model frame -// (mm, +X forward, +Y robot-left, +Z up, pelvis waist-yaw center at the -// origin). -// -// Gait design: an in-place march. Each leg swings inside its own window of -// the cycle with a raised-cosine envelope that starts and ends at zero, and -// is EXACTLY at the athletic pose for the rest of the cycle — so the stance -// foot stays planted on the ground with no IK. The pelvis root is -// intentionally static for the same reason. Arms swing continuously, -// antiphase to their own-side leg; the torso counter-rotates about the -// waist with a hint of roll, and the neck compensates so the head keeps -// facing forward. - -const X = [1, 0, 0]; -const Y = [0, 1, 0]; -const Z = [0, 0, 1]; - -const HIP_Y = 90; -const SHOULDER_Y = 148; - -// Athletic ready stance angles (deg), mirrored from juno_parts/chain.py. -const ATHLETIC = { - HIP_PITCH: -16, - KNEE: 32, - ANKLE_PITCH: -16, - HIP_ROLL_ABDUCT: 3, - HIP_YAW: 0, - WAIST_YAW: 0, - SHOULDER_PITCH: -8, - SHOULDER_ROLL_ABDUCT: 8, - SHOULDER_YAW_INTERNAL: 8, - ELBOW: -20, - WRIST_ROLL: 0, - WRIST_PITCH: -2, - NECK_YAW: 0, - NECK_PITCH: -2 -}; - -// Gait amplitudes (deg) at parameter scale 1. Sign conventions follow the -// chain ledger: pitch about +Y (negative = forward), roll about +X -// (positive = toward robot-left), yaw about +Z (positive = turn left). -const HIP_LIFT_DEG = 24; // swing-leg thigh raise (forward = negative pitch) -const KNEE_FOLD_DEG = 44; // extra swing-leg knee flexion -const TOE_POINT_DEG = 8; // slight toes-down of the airborne foot -const ARM_SWING_DEG = 12; // shoulder pitch swing, antiphase to own-side leg -const ELBOW_EXTRA_DEG = 10; // extra elbow flexion on the forward arm swing -const WAIST_SWAY_DEG = 4; // torso counter-rotation about the waist yaw axis -const TORSO_ROLL_DEG = 1.2; // weight-shift hint, roll about +X at the waist -const NECK_COMPENSATION = 0.8; // head stabilization fraction against waist sway - -// Swing windows of the normalized cycle (left lifts first), with -// double-support dwell between them. -const SWING = { left: [0.05, 0.45], right: [0.55, 0.95] }; - -// Stride gait (treadmill-style walk in place): the stance foot slides -// backward flat on the ground while the swing leg passes through the air, -// both solved with planar two-link leg IK in each leg's (slightly rolled) -// sagittal plane. The athletic stance is exactly the IK solution at stride -// center, so the gait converges to plain standing as strideLength -> 0. -const THIGH_LEN = 290; // hip-pitch to knee (mm) -const SHIN_LEN = 290; // knee to ankle-pitch (mm) -const STANCE_FRACTION = 0.58; // ground-contact share of each leg cycle -const STRIDE_LIFT_MM = 60; // swing-foot apex clearance at legLift 1 -const STRIDE_TOE_OFF_DEG = 14; // toes-down just after toe-off -const STRIDE_HEEL_DEG = 10; // toes-up heel-first approach to contact -const STRIDE_ARM_FACTOR = 1.5; // arms swing wider with long strides -const STRIDE_SWAY_FACTOR = 1.25; -const MAX_STRIDE_MM = 300; // keeps two-link IK inside reach at the extremes -const DEFAULT_STRIDE_MM = 230; // lift/pitch accents are normalized to this -const STRIDE_PHASE = { left: 0, right: 0.5 }; -// Mid-swing cycle positions (offset + stance + half the swing window). -const STRIDE_MID_SWING = { - left: (STRIDE_PHASE.left + STANCE_FRACTION + ((1 - STANCE_FRACTION) / 2)) % 1, - right: (STRIDE_PHASE.right + STANCE_FRACTION + ((1 - STANCE_FRACTION) / 2)) % 1 -}; - -// Run gait: short ground contacts with a FLIGHT phase between them (both -// feet airborne), a vertical body bounce absorbed by the stance-leg IK -// (lowest at mid-stance, highest at mid-flight), toe-pivot heel-off into -// push-off, high heel-recovery swing, forward torso lean, and pumping bent -// arms. Foot contact lands slightly ahead of the hip and pushes off well -// behind it (asymmetric split of the stride travel). -const RUN_STANCE_FRACTION = 0.35; -const RUN_PHASE = { left: 0, right: 0.5 }; -const RUN_MID_STANCE = { - left: RUN_STANCE_FRACTION / 2, - right: (RUN_PHASE.right + (RUN_STANCE_FRACTION / 2)) % 1 -}; -const RUN_MID_SWING = { - left: (RUN_STANCE_FRACTION + ((1 - RUN_STANCE_FRACTION) / 2)) % 1, - right: (RUN_PHASE.right + RUN_STANCE_FRACTION + ((1 - RUN_STANCE_FRACTION) / 2)) % 1 -}; -const RUN_CONTACT_SHARE = 0.3; // stride share landing ahead of the hip -const RUN_BOUNCE_MM = 22; // body bounce amplitude at legLift 1 -const RUN_APEX_MM = 140; // swing-ankle apex over flat stance (heel tuck) -const RUN_HEEL_OFF_DEG = 26; // toe-pivot heel rise into push-off -const RUN_HEEL_OFF_START = 0.62; // stance progress where the heel starts rising -const RUN_HEEL_IN_DEG = 8; // toes-up into the next contact -const RUN_TOE_PIVOT_X_MM = 112; // sole front edge (foot bbox +x) — pivoting -const RUN_ANKLE_HEIGHT_MM = 56; // there keeps the toe from digging in -const RUN_LEAN_DEG = 7; // forward torso lean (waist pitch extra) -const RUN_ARM_SWING_DEG = 26; // shoulder pump amplitude -const RUN_ELBOW_BASE_DEG = -60; // extra constant elbow flexion (runner's arms) -const RUN_ELBOW_PUMP_DEG = 15; // extra flexion on the forward pump -const RUN_SWAY_DEG = 5; // waist yaw counter-rotation -const RUN_ROLL_DEG = 1.6; // lean toward the stance side - -// Jump gait: countermovement crouch -> push -> ballistic flight with toe -// point and leg tuck -> springy landing (underdamped body-height response -// absorbed by the leg IK) -> recover. Arms swing back in the crouch, sweep -// overhead for the flight ("hands in the air"), dip with the landing -// spring, and settle back. Both legs work symmetrically; takeoff/landing -// fall out of the IK reach clamp (targets go out of reach -> legs straight -// -> feet leave/meet the ground smoothly). -const JUMP_TIMING = { crouch: 0.18, push: 0.3, land: 0.62, absorb: 0.74 }; -const JUMP_CROUCH_MM = 120; // countermovement depth at legLift 1 -const JUMP_TAKEOFF_MM = 20; // body rise at full leg extension (reach limit) -const JUMP_APEX_MM = 160; // ballistic apex above standing at legLift 1 -const JUMP_TUCK_MM = 250; // mid-flight ankle tuck toward the body -const JUMP_TOE_POINT_DEG = 25; // toes point down in the air -const JUMP_LAND_DIP_MM = -80; // first landing compression -const JUMP_SPRING_DECAY = 3.5; // landing spring: exp decay rate ... -const JUMP_SPRING_CYCLES = 1.25; // ... and bounce count (ends at zero crossing) -const JUMP_ARM_BACK_DEG = 30; // crouch arm backswing (shoulder pitch) -const JUMP_ARM_UP_DEG = -155; // overhead shoulder pitch in flight -const JUMP_ARM_V_DEG = 20; // shoulder roll abduction for an overhead V -const JUMP_ARM_SPRING_DEG_PER_MM = 0.3; // landing arm dip coupled to body spring -const JUMP_LEAN_DEG = 8; // crouch/landing forward lean -const JUMP_HEAD_UP_DEG = -8; // look up slightly while airborne - -// Showpiece animations: karate kick, handstand, and the Elvis dance each -// run as their own loop; every one starts and ends in the athletic stance. -// Lateral pendulum for weight shifts: the hip-roll joint sits at z=-184 in -// the pelvis frame; rolling there (with the ankle roll compensating) moves -// the planted sole sideways by ~(soleZ - HIP_ROLL_Z) * sin(roll). -const HIP_ROLL_Z_MM = -184; -// Kick (segment-local): weight shifts over the left leg, the right leg -// chambers and snaps a front kick at hip height with a guard up. -const KICK_WEIGHT_SHIFT_MM = 42; -const KICK_DIP_MM = 18; -const KICK_CHAMBER_HIP_DEG = -70; -const KICK_CHAMBER_KNEE_DEG = 115; -const KICK_CHAMBER_ANKLE_DEG = 10; -const KICK_EXTEND_HIP_DEG = -20; // added on top of the chamber -const KICK_EXTEND_KNEE_DEG = -105; // snaps the shin out nearly straight -const KICK_EXTEND_ANKLE_DEG = -25; // ball-of-foot strike (toes pulled back) -const KICK_GUARD_SHOULDER_DEG = -45; -const KICK_GUARD_ELBOW_DEG = -105; -const KICK_GUARD_ROLL_OUT_DEG = 18; -const KICK_HIP_TURN_DEG = -14; // hips rotate into the kick -const KICK_LEAN_BACK_DEG = -7; // counterbalance during the snap -// Handstand: keyframed root rotation about +Y with contact anchoring — -// toe-pinned while folding, palm-pinned once the hands plant. -const HS_PLANT_X_MM = 430; // hand plant line in front of the feet -// Palm contact point in the hand frame: the hands plant palm-down with the -// fingers pointing forward (total hand pitch -90 at the hold), so contact -// is on the local -x palm face — the one extent that is symmetric between -// the two hands (the posed finger curl differs in local z). -const HS_PALM_LOCAL_MM = [-35, 0, -70]; -const HS_TOE_LOCAL_MM = [112, 0, -26]; // sole FRONT EDGE (pivoting there keeps the whole sole above ground) -const HS_WOBBLE_DEG = 1.6; // leg wobble while holding the stand -// Elvis dance (4 beats): weight over the right leg, left leg kicked out -// to the left planted on a pointed toe (closed-form lateral+sagittal leg -// IK), rubber-leg shake, right arm pointing up to the right with a beat -// pulse, hip swivel, head turned toward the pointing hand. -const ELVIS_BEATS = 4; -const ELVIS_WEIGHT_SHIFT_MM = -55; // pelvis shifts over the right leg -const ELVIS_DIP_MM = 20; // settled crouch on the stance leg -const ELVIS_BOUNCE_MM = 8; // beat bounce on top of the dip -const ELVIS_TOE_X_MM = 80; // kicked-out toe plant, forward of the hips -const ELVIS_TOE_Y_MM = 330; // ... and out to the robot-left -const ELVIS_TOE_SHAKE_MM = 28; // rubber-leg lateral toe shake (2x beat) -const ELVIS_FOOT_POINT_DEG = 55; // pointed toe of the kicked-out foot -const ELVIS_FOOT_POINT_SHAKE_DEG = 7; -const ELVIS_TOE_TIP_LOCAL = { x: 112, drop: 56 }; // toe tip rel ankle-pitch (fwd, down) -const ELVIS_POINT_ROLL_DEG = -115; // right arm raised out-and-up to the right -const ELVIS_POINT_PITCH_DEG = -25; -const ELVIS_POINT_ELBOW_DEG = -12; // nearly straight: the finger points -const ELVIS_POINT_PULSE_DEG = 8; // beat pulse of the pointing arm -const ELVIS_OFF_ARM_PITCH_DEG = -30; // left arm low and bent across -const ELVIS_OFF_ARM_ELBOW_DEG = -70; -const ELVIS_TWIST_DEG = 10; // hip swivel via the waist -const ELVIS_LEAN_RIGHT_DEG = -3; // lean into the point -const ELVIS_HEAD_TURN_DEG = -20; // look toward the pointing hand -const ELVIS_CHIN_UP_DEG = -5; - -const SIDES = ["left", "right"]; - -function sideSign(side) { - return side === "left" ? 1 : -1; -} - -// Kinematic chain in root-to-leaf order, mirrored from juno_parts/chain.py. -// origin is the joint center in the parent link frame (mm); each child link -// frame sits at its joint center. -function buildJoints() { - const joints = [ - { name: "waist_yaw", parent: "pelvis", child: "torso", origin: [0, 0, 0], axis: Z }, - { name: "neck_yaw", parent: "torso", child: "neck_collar", origin: [0, 0, 324], axis: Z }, - { name: "neck_pitch", parent: "neck_collar", child: "head", origin: [0, 0, 46], axis: Y } - ]; - for (const side of SIDES) { - const s = sideSign(side); - joints.push( - { name: `hip_yaw_${side}`, parent: "pelvis", child: `hip_bracket_${side}`, origin: [0, s * HIP_Y, -120], axis: Z }, - { name: `hip_roll_${side}`, parent: `hip_bracket_${side}`, child: `hip_carrier_${side}`, origin: [0, 0, -64], axis: X }, - { name: `hip_pitch_${side}`, parent: `hip_carrier_${side}`, child: `thigh_${side}`, origin: [0, 0, -78], axis: Y }, - { name: `knee_${side}`, parent: `thigh_${side}`, child: `shin_${side}`, origin: [0, 0, -290], axis: Y }, - { name: `ankle_pitch_${side}`, parent: `shin_${side}`, child: `ankle_link_${side}`, origin: [0, 0, -290], axis: Y }, - { name: `ankle_roll_${side}`, parent: `ankle_link_${side}`, child: `foot_${side}`, origin: [0, 0, -30], axis: X } - ); - } - for (const side of SIDES) { - const s = sideSign(side); - joints.push( - { name: `shoulder_pitch_${side}`, parent: "torso", child: `shoulder_pod_${side}`, origin: [0, s * SHOULDER_Y, 290], axis: Y }, - { name: `shoulder_roll_${side}`, parent: `shoulder_pod_${side}`, child: `yaw_housing_${side}`, origin: [0, s * 34, -72], axis: X }, - { name: `shoulder_yaw_${side}`, parent: `yaw_housing_${side}`, child: `bicep_${side}`, origin: [0, 0, -24], axis: Z }, - { name: `elbow_${side}`, parent: `bicep_${side}`, child: `forearm_${side}`, origin: [0, 0, -156], axis: Y }, - { name: `wrist_roll_${side}`, parent: `forearm_${side}`, child: `wrist_carrier_${side}`, origin: [0, 0, -150], axis: Z }, - { name: `wrist_pitch_${side}`, parent: `wrist_carrier_${side}`, child: `hand_${side}`, origin: [0, 0, -28], axis: Y } - ); - } - return joints; -} - -const JOINTS = buildJoints(); - -// Optional artistic extras applied on top of joint FK, keyed by joint name: -// { rollDeg } adds a small rotation about +X at the same joint center. -function athleticAnglesDeg() { - const angles = { - waist_yaw: ATHLETIC.WAIST_YAW, - neck_yaw: ATHLETIC.NECK_YAW, - neck_pitch: ATHLETIC.NECK_PITCH - }; - for (const side of SIDES) { - const s = sideSign(side); - angles[`hip_yaw_${side}`] = ATHLETIC.HIP_YAW; - angles[`hip_roll_${side}`] = s * ATHLETIC.HIP_ROLL_ABDUCT; - angles[`hip_pitch_${side}`] = ATHLETIC.HIP_PITCH; - angles[`knee_${side}`] = ATHLETIC.KNEE; - angles[`ankle_pitch_${side}`] = ATHLETIC.ANKLE_PITCH; - angles[`ankle_roll_${side}`] = -s * ATHLETIC.HIP_ROLL_ABDUCT; - angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH; - angles[`shoulder_roll_${side}`] = s * ATHLETIC.SHOULDER_ROLL_ABDUCT; - angles[`shoulder_yaw_${side}`] = -s * ATHLETIC.SHOULDER_YAW_INTERNAL; - angles[`elbow_${side}`] = ATHLETIC.ELBOW; - angles[`wrist_roll_${side}`] = ATHLETIC.WRIST_ROLL; - angles[`wrist_pitch_${side}`] = ATHLETIC.WRIST_PITCH; - } - return angles; -} - -const ATHLETIC_ANGLES = athleticAnglesDeg(); - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -const IDENTITY3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; - -function rotAxisDeg(axis, deg) { - const rad = (deg * Math.PI) / 180; - const c = Math.cos(rad); - const s = Math.sin(rad); - if (axis === X) { - return [[1, 0, 0], [0, c, -s], [0, s, c]]; - } - if (axis === Y) { - return [[c, 0, s], [0, 1, 0], [-s, 0, c]]; - } - return [[c, -s, 0], [s, c, 0], [0, 0, 1]]; -} - -function matMul3(a, b) { - const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; - for (let i = 0; i < 3; i += 1) { - for (let j = 0; j < 3; j += 1) { - out[i][j] = (a[i][0] * b[0][j]) + (a[i][1] * b[1][j]) + (a[i][2] * b[2][j]); - } - } - return out; -} - -function matVec3(a, v) { - return [ - (a[0][0] * v[0]) + (a[0][1] * v[1]) + (a[0][2] * v[2]), - (a[1][0] * v[0]) + (a[1][1] * v[1]) + (a[1][2] * v[2]), - (a[2][0] * v[0]) + (a[2][1] * v[1]) + (a[2][2] * v[2]) - ]; -} - -function matTranspose3(a) { - return [ - [a[0][0], a[1][0], a[2][0]], - [a[0][1], a[1][1], a[2][1]], - [a[0][2], a[1][2], a[2][2]] - ]; -} - -// FK over the whole chain. extras[jointName] = { pitchDeg, rollDeg } folds -// small artistic +Y/+X rotations in at that joint center (torso lean and -// weight shift); rootOffset translates the pelvis (body bounce in the air -// gaits — the stance-leg IK absorbs it on the ground side); rootPitchDeg -// rotates the whole body about +Y at the pelvis (handstand inversion — -// callers solve the matching translation so contacts stay put). -function fkFrames(anglesDeg, extras = {}, rootOffset = [0, 0, 0], rootPitchDeg = 0) { - const rootR = rootPitchDeg ? rotAxisDeg(Y, rootPitchDeg) : IDENTITY3; - const frames = { pelvis: { R: rootR, p: [...rootOffset] } }; - for (const joint of JOINTS) { - const parent = frames[joint.parent]; - const offset = matVec3(parent.R, joint.origin); - const p = [parent.p[0] + offset[0], parent.p[1] + offset[1], parent.p[2] + offset[2]]; - let R = matMul3(parent.R, rotAxisDeg(joint.axis, finite(anglesDeg[joint.name], 0))); - const extra = extras[joint.name]; - if (extra && finite(extra.pitchDeg, 0) !== 0) { - R = matMul3(R, rotAxisDeg(Y, extra.pitchDeg)); - } - if (extra && finite(extra.rollDeg, 0) !== 0) { - R = matMul3(R, rotAxisDeg(X, extra.rollDeg)); - } - frames[joint.child] = { R, p }; - } - return frames; -} - -const ATHLETIC_FRAMES = fkFrames(ATHLETIC_ANGLES); - -// Per-side stride-IK constants derived from the athletic FK: the hip-pitch -// joint center stays fixed in the world (static pelvis, athletic hip -// yaw/roll), and the compensated ankle roll keeps the foot flat, so the -// ankle-pitch target maps exactly onto the rolled sagittal plane. -const STRIDE_RIG = (() => { - const rig = {}; - for (const side of SIDES) { - const hip = ATHLETIC_FRAMES[`thigh_${side}`].p; // hip-pitch joint center - const foot = ATHLETIC_FRAMES[`foot_${side}`]; - const rollRad = (sideSign(side) * ATHLETIC.HIP_ROLL_ABDUCT * Math.PI) / 180; - // Sole plane and the flat-foot vertical offset from the ankle-pitch - // joint: the 30 mm ankle-roll drop is still rolled by the hip roll, the - // 26 mm sole sits below the roll-compensated (world-aligned) foot frame. - const soleZ = foot.p[2] + matVec3(foot.R, [0, 0, -26])[2]; - const ankleToSole = (30 * Math.cos(rollRad)) + 26; - rig[side] = { hipZ: hip[2], cosRoll: Math.cos(rollRad), soleZ, ankleToSole }; - } - return rig; -})(); - -// Athletic left toe-tip world position (x, y): the Elvis kick-out leg IK -// starts and ends its toe-target path here so the dance loop rests -// exactly on the athletic stance. -const ELVIS_ATHLETIC_TOE = (() => { - const foot = ATHLETIC_FRAMES.foot_left; - const toe = matVec3(foot.R, [112, 0, -26]).map((v, k) => v + foot.p[k]); - return [toe[0], toe[1]]; -})(); - -// Row-major 4x4 mapping the baked athletic placement of a link onto its -// target placement: M = [R_t R_o^T | p_t - R_t R_o^T p_o]. -function deltaMatrixRowMajor(original, target) { - const Rd = matMul3(target.R, matTranspose3(original.R)); - const moved = matVec3(Rd, original.p); - const t = [target.p[0] - moved[0], target.p[1] - moved[1], target.p[2] - moved[2]]; - return [ - Rd[0][0], Rd[0][1], Rd[0][2], t[0], - Rd[1][0], Rd[1][1], Rd[1][2], t[1], - Rd[2][0], Rd[2][1], Rd[2][2], t[2], - 0, 0, 0, 1 - ]; -} - -// Raised-cosine swing envelope: 0 at lift-off and touch-down, 1 mid-swing, -// identically 0 outside the window so the stance foot never moves. -function swingEnvelope(phase, [start, end]) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - if (p <= start || p >= end) { - return 0; - } - const u = (p - start) / (end - start); - const lobe = Math.sin(Math.PI * u); - return lobe * lobe; -} - -// Cosine forwardness peaking at the given cycle position. -function forwardness(phase, peakAt) { - return Math.cos(2 * Math.PI * (finite(phase, 0) - peakAt)); -} - -function gaitAnglesDeg(phase, { legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { - const angles = { ...ATHLETIC_ANGLES }; - - for (const side of SIDES) { - const env = swingEnvelope(phase, SWING[side]) * legLift; - const hipDelta = -HIP_LIFT_DEG * env; - const kneeDelta = KNEE_FOLD_DEG * env; - // Cancel hip+knee so the airborne foot stays near level, then point - // the toes down a touch mid-swing. - const ankleDelta = -(hipDelta + kneeDelta) + (TOE_POINT_DEG * env); - angles[`hip_pitch_${side}`] += hipDelta; - angles[`knee_${side}`] += kneeDelta; - angles[`ankle_pitch_${side}`] += ankleDelta; - - // Arms swing antiphase to the own-side leg: the left arm leads when the - // right leg lifts (left swing centered at 0.25, right at 0.75). - const ownLegPeak = side === "left" ? 0.25 : 0.75; - const armForward = forwardness(phase, ownLegPeak + 0.5) * armSwing; - angles[`shoulder_pitch_${side}`] += -ARM_SWING_DEG * armForward; - angles[`elbow_${side}`] += -ELBOW_EXTRA_DEG * Math.max(0, armForward); - } - - // Torso counter-rotation: turning left brings the right shoulder forward - // exactly while the left leg swings; the neck compensates most of it so - // the head keeps facing forward. - const sway = WAIST_SWAY_DEG * torsoSway * Math.sin(2 * Math.PI * finite(phase, 0)); - angles.waist_yaw += sway; - angles.neck_yaw += -sway * NECK_COMPENSATION; - - return angles; -} - -function torsoRollExtras(phase, torsoSway) { - // Lean toward the stance side: negative roll (lean robot-right) while the - // left leg is lifted at phase 0.25. - const rollDeg = -TORSO_ROLL_DEG * torsoSway * Math.sin(2 * Math.PI * finite(phase, 0)); - return { waist_yaw: { rollDeg } }; -} - -function smooth01(value) { - const t = clamp(value, 0, 1); - return t * t * (3 - (2 * t)); -} - -// Two-link planar leg IK in the rolled sagittal plane. Targets the -// ankle-pitch joint at forward offset dx (mm, from the hip-pitch joint) and -// plane depth d (mm); footPitchDeg is the desired world pitch of the foot. -// Returns hip-pitch / knee / ankle-pitch angles in the chain's conventions -// (hip negative = forward, knee positive = flexion). -function legPitchIk(dx, d, footPitchDeg) { - const dist = clamp(Math.hypot(dx, d), Math.abs(THIGH_LEN - SHIN_LEN) + 1, THIGH_LEN + SHIN_LEN - 1); - const phi = Math.atan2(dx, d); // forward angle of the hip->ankle line from straight down - const cosInterior = clamp( - ((THIGH_LEN * THIGH_LEN) + (SHIN_LEN * SHIN_LEN) - (dist * dist)) / (2 * THIGH_LEN * SHIN_LEN), - -1, - 1 - ); - const kneeRad = Math.PI - Math.acos(cosInterior); // knee flexion, >= 0 (knee bends forward) - const cosBeta = clamp( - ((THIGH_LEN * THIGH_LEN) + (dist * dist) - (SHIN_LEN * SHIN_LEN)) / (2 * THIGH_LEN * dist), - -1, - 1 - ); - const thighRad = phi + Math.acos(cosBeta); // thigh angle from straight down, + forward - const hipDeg = (-thighRad * 180) / Math.PI; - const kneeDeg = (kneeRad * 180) / Math.PI; - const ankleDeg = footPitchDeg - (hipDeg + kneeDeg); - return { hipDeg, kneeDeg, ankleDeg }; -} - -// Treadmill stride: stance foot slides backward flat on the ground from -// +stride/2 to -stride/2; the swing leg returns through the air with toe-off -// and heel-strike accents. Both halves meet at zero velocity mismatch in -// position, so the loop is seamless. -function strideAnglesDeg(phase, { strideLength = 230, legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { - const angles = { ...ATHLETIC_ANGLES }; - const stride = clamp(strideLength, 0, MAX_STRIDE_MM); - - for (const side of SIDES) { - const rig = STRIDE_RIG[side]; - const q = ((finite(phase, 0) + STRIDE_PHASE[side]) % 1 + 1) % 1; - let ankleX; - let soleTargetZ; - let footPitchDeg; - if (q < STANCE_FRACTION) { - const s = q / STANCE_FRACTION; - ankleX = (stride / 2) - (stride * s); // front contact -> push-off behind - soleTargetZ = rig.soleZ; - footPitchDeg = 0; - } else { - const u = (q - STANCE_FRACTION) / (1 - STANCE_FRACTION); - ankleX = (-stride / 2) + (stride * smooth01(u)); - const lobe = Math.sin(Math.PI * u); - // Clearance and toe-off/heel accents scale with the stride so a zero - // stride degenerates to plain athletic standing. - const reach = stride / DEFAULT_STRIDE_MM; - soleTargetZ = rig.soleZ + (STRIDE_LIFT_MM * clamp(legLift, 0, 1.4) * lobe * reach); - // Toes down right after toe-off, toes up (heel first) into contact. - footPitchDeg = ((STRIDE_TOE_OFF_DEG * lobe * (1 - u)) - (STRIDE_HEEL_DEG * lobe * u)) * reach; - } - const ankleTargetZ = soleTargetZ + rig.ankleToSole; - const depth = (rig.hipZ - ankleTargetZ) / rig.cosRoll; - // ankleX is authored relative to the hip-pitch joint; the athletic ankle - // sits exactly under the hip, so stride 0 reproduces the athletic legs. - const ik = legPitchIk(ankleX, depth, footPitchDeg); - angles[`hip_pitch_${side}`] = ik.hipDeg; - angles[`knee_${side}`] = ik.kneeDeg; - angles[`ankle_pitch_${side}`] = ik.ankleDeg; - - // Arms counter the opposite leg: forward peak at the other side's - // mid-swing, wider than the march to match the longer stride. - const otherSide = side === "left" ? "right" : "left"; - const armForward = forwardness(phase, STRIDE_MID_SWING[otherSide]) * armSwing * STRIDE_ARM_FACTOR; - angles[`shoulder_pitch_${side}`] += -ARM_SWING_DEG * armForward; - angles[`elbow_${side}`] += -ELBOW_EXTRA_DEG * Math.max(0, armForward); - } - - // Shoulder girdle counters the hips: the right shoulder leads while the - // right arm swings forward (right-arm peak == left leg mid-swing). - const sway = WAIST_SWAY_DEG * STRIDE_SWAY_FACTOR * torsoSway - * forwardness(phase, STRIDE_MID_SWING.left); - angles.waist_yaw += sway; - angles.neck_yaw += -sway * NECK_COMPENSATION; - - return angles; -} - -function strideTorsoRollExtras(phase, torsoSway) { - // Lean toward the stance side: robot-right lean while the left leg swings. - const rollDeg = -TORSO_ROLL_DEG * STRIDE_SWAY_FACTOR * torsoSway - * forwardness(phase, STRIDE_MID_SWING.left); - return { waist_yaw: { rollDeg } }; -} - -// ------------------------------------------------------------------- run -function runBounceMm(phase, legLift) { - // Twice per cycle: lowest at each mid-stance, highest at mid-flight. - return -RUN_BOUNCE_MM * clamp(legLift, 0, 1.4) - * Math.cos(4 * Math.PI * (finite(phase, 0) - RUN_MID_STANCE.left)); -} - -// Stance-foot targets with a toe-pivot heel-off: the foot pivots about the -// sole's front edge so the heel rises into push-off without the toe digging -// below the ground. -function runStanceTarget(s, stride, reach, rig) { - const flatAnkleX = (stride * RUN_CONTACT_SHARE) - (stride * s); - let pitchDeg = 0; - if (s > RUN_HEEL_OFF_START) { - pitchDeg = RUN_HEEL_OFF_DEG * reach * smooth01((s - RUN_HEEL_OFF_START) / (1 - RUN_HEEL_OFF_START)); - } - const pitchRad = (pitchDeg * Math.PI) / 180; - const c = Math.cos(pitchRad); - const sn = Math.sin(pitchRad); - // Ankle relative to the toe pivot when flat: (-RUN_TOE_PIVOT_X_MM, +ankle height). - const ankleX = flatAnkleX + (RUN_TOE_PIVOT_X_MM * (1 - c)) + (RUN_ANKLE_HEIGHT_MM * sn); - const ankleZ = rig.soleZ + (RUN_TOE_PIVOT_X_MM * sn) + (rig.ankleToSole * c); - return { ankleX, ankleZ, pitchDeg }; -} - -function runAnglesDeg(phase, { strideLength = 230, legLift = 1, armSwing = 1, torsoSway = 1 } = {}) { - const angles = { ...ATHLETIC_ANGLES }; - const stride = clamp(strideLength, 0, MAX_STRIDE_MM); - const reach = stride / DEFAULT_STRIDE_MM; - const lift = clamp(legLift, 0, 1.4); - const bounce = runBounceMm(phase, legLift); - - for (const side of SIDES) { - const rig = STRIDE_RIG[side]; - const q = ((finite(phase, 0) + RUN_PHASE[side]) % 1 + 1) % 1; - let target; - if (q < RUN_STANCE_FRACTION) { - target = runStanceTarget(q / RUN_STANCE_FRACTION, stride, reach, rig); - } else { - const u = (q - RUN_STANCE_FRACTION) / (1 - RUN_STANCE_FRACTION); - const t = smooth01(u); - const end = runStanceTarget(1, stride, reach, rig); - const contactX = stride * RUN_CONTACT_SHARE; - const contactZ = rig.soleZ + rig.ankleToSole; - target = { - ankleX: end.ankleX + ((contactX - end.ankleX) * t), - ankleZ: end.ankleZ + ((contactZ - end.ankleZ) * t) - + (RUN_APEX_MM * lift * reach * Math.sin(Math.PI * u)), - pitchDeg: (end.pitchDeg * (1 - t) * (1 - t)) - - (RUN_HEEL_IN_DEG * reach * Math.sin(Math.PI * u) * u) - }; - } - const depth = ((rig.hipZ + bounce) - target.ankleZ) / rig.cosRoll; - const ik = legPitchIk(target.ankleX, depth, target.pitchDeg); - angles[`hip_pitch_${side}`] = ik.hipDeg; - angles[`knee_${side}`] = ik.kneeDeg; - angles[`ankle_pitch_${side}`] = ik.ankleDeg; - - // Runner's arm pump: bent elbows, antiphase to the own-side leg. - const otherSide = side === "left" ? "right" : "left"; - const armForward = forwardness(phase, RUN_MID_SWING[otherSide]) * armSwing; - angles[`shoulder_pitch_${side}`] += -RUN_ARM_SWING_DEG * armForward; - angles[`elbow_${side}`] += (RUN_ELBOW_BASE_DEG * clamp(armSwing, 0, 1.5)) - - (RUN_ELBOW_PUMP_DEG * Math.max(0, armForward)); - } - - // Shoulder girdle counters the hips; the head keeps level against both - // the sway and the forward lean. - const sway = RUN_SWAY_DEG * torsoSway * forwardness(phase, RUN_MID_SWING.left); - angles.waist_yaw += sway; - angles.neck_yaw += -sway * NECK_COMPENSATION; - angles.neck_pitch += -RUN_LEAN_DEG * NECK_COMPENSATION; - - return angles; -} - -function runTorsoExtras(phase, torsoSway) { - // Constant forward lean plus a lean toward the stance side. - const rollDeg = RUN_ROLL_DEG * torsoSway * forwardness(phase, RUN_MID_STANCE.left); - return { waist_yaw: { pitchDeg: RUN_LEAN_DEG, rollDeg } }; -} - -// ------------------------------------------------------------------ jump -function jumpBodyZMm(phase, lift) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - const { crouch, push, land, absorb } = JUMP_TIMING; - if (p < crouch) { - return -JUMP_CROUCH_MM * lift * smooth01(p / crouch); - } - if (p < push) { - const t = smooth01((p - crouch) / (push - crouch)); - return (-JUMP_CROUCH_MM * lift) + (((JUMP_TAKEOFF_MM) + (JUMP_CROUCH_MM * lift)) * t); - } - if (p < land) { - // Ballistic parabola between takeoff and landing at the same height. - const t = (p - push) / (land - push); - const apex = JUMP_APEX_MM * lift; - return JUMP_TAKEOFF_MM + ((apex - JUMP_TAKEOFF_MM) * (1 - ((2 * t) - 1) * ((2 * t) - 1))); - } - if (p < absorb) { - // Impact: ride down into the first compression. - const t = smooth01((p - land) / (absorb - land)); - return JUMP_TAKEOFF_MM + (((JUMP_LAND_DIP_MM * lift) - JUMP_TAKEOFF_MM) * t); - } - // Springy recovery: underdamped wobble that ends exactly at zero. - const t = (p - absorb) / (1 - absorb); - return JUMP_LAND_DIP_MM * lift * Math.exp(-JUMP_SPRING_DECAY * t) - * Math.cos(2 * Math.PI * JUMP_SPRING_CYCLES * t); -} - -function jumpFlightProgress(phase) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - const { push, land } = JUMP_TIMING; - if (p <= push || p >= land) { - return 0; - } - return (p - push) / (land - push); -} - -// Arms: backswing in the crouch, sweep overhead for the flight, dip with -// the landing spring, settle back to athletic by the end of the cycle. -function jumpShoulderPitchDeg(phase, bodyZ) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - const { crouch, push, land, absorb } = JUMP_TIMING; - const athletic = ATHLETIC.SHOULDER_PITCH; - if (p < crouch) { - return athletic + ((JUMP_ARM_BACK_DEG - athletic) * smooth01(p / crouch)); - } - if (p < push) { - return JUMP_ARM_BACK_DEG + ((JUMP_ARM_UP_DEG - JUMP_ARM_BACK_DEG) * smooth01((p - crouch) / (push - crouch))); - } - if (p < land) { - return JUMP_ARM_UP_DEG; - } - if (p < absorb) { - // Dip with the impact: arms lower in proportion to the body drop. - return JUMP_ARM_UP_DEG + (Math.abs(Math.min(0, bodyZ)) * JUMP_ARM_SPRING_DEG_PER_MM); - } - const t = smooth01((p - absorb) / (1 - absorb)); - const dipped = JUMP_ARM_UP_DEG + (Math.abs(Math.min(0, bodyZ)) * JUMP_ARM_SPRING_DEG_PER_MM); - return dipped + ((athletic - dipped) * t); -} - -function jumpAnglesDeg(phase, { legLift = 1, armSwing = 1 } = {}) { - const angles = { ...ATHLETIC_ANGLES }; - const lift = clamp(legLift, 0, 1.4); - const bodyZ = jumpBodyZMm(phase, lift); - const flight = jumpFlightProgress(phase); - const tuckLobe = Math.sin(Math.PI * flight); - - for (const side of SIDES) { - const rig = STRIDE_RIG[side]; - // Feet stay planted under the hips; in flight the ankle target rises - // (tuck) and the IK reach clamp straightens the legs at takeoff/landing. - const ankleZ = rig.soleZ + rig.ankleToSole + (flight > 0 ? JUMP_TUCK_MM * lift * tuckLobe : 0); - const pitchDeg = flight > 0 ? JUMP_TOE_POINT_DEG * tuckLobe : 0; - const depth = ((rig.hipZ + bodyZ) - ankleZ) / rig.cosRoll; - const ik = legPitchIk(0, depth, pitchDeg); - angles[`hip_pitch_${side}`] = ik.hipDeg; - angles[`knee_${side}`] = ik.kneeDeg; - angles[`ankle_pitch_${side}`] = ik.ankleDeg; - - // Overhead V: pitch sweeps the arms up; roll spreads them outward in - // proportion to how raised they are. - const s = sideSign(side); - const shoulderPitch = jumpShoulderPitchDeg(phase, bodyZ); - const raised = clamp( - (shoulderPitch - ATHLETIC.SHOULDER_PITCH) / (JUMP_ARM_UP_DEG - ATHLETIC.SHOULDER_PITCH), - 0, - 1 - ); - const armScale = clamp(armSwing, 0, 1.5); - angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH - + ((shoulderPitch - ATHLETIC.SHOULDER_PITCH) * armScale); - angles[`shoulder_roll_${side}`] += s * JUMP_ARM_V_DEG * raised * armScale; - angles[`elbow_${side}`] = ATHLETIC.ELBOW + (5 * raised * armScale); - } - - // Look up a touch while airborne. - angles.neck_pitch += JUMP_HEAD_UP_DEG * tuckLobe; - return { angles, bodyZ, flight }; -} - -function jumpTorsoExtras(phase, bodyZ, flight, torsoSway) { - // Lean into the crouch and the landing compression, upright in the air. - const compression = clamp(-Math.min(0, bodyZ) / JUMP_CROUCH_MM, 0, 1.2); - const pitchDeg = JUMP_LEAN_DEG * compression * (1 - flight) * clamp(torsoSway, 0, 1.5); - return { waist_yaw: { pitchDeg } }; -} - -// --------------------------------------------------------------- routine -function wrap01(value) { - return ((finite(value, 0) % 1) + 1) % 1; -} - -// Smooth on/off window: ramps up over [inStart, inEnd], back down over -// [outStart, outEnd], zero outside. -function pulse(t, inStart, inEnd, outStart, outEnd) { - return smooth01((t - inStart) / Math.max(1e-9, inEnd - inStart)) - * (1 - smooth01((t - outStart) / Math.max(1e-9, outEnd - outStart))); -} - -// Planted-leg solve shared by the kick support leg and the dance: the -// pitch IK keeps the sole at ground height under a bobbing pelvis, and a -// hip/ankle roll pair absorbs a lateral pelvis shift dy (the second-order -// foot lift from the roll arc is under 2 mm at these amplitudes). -function plantedLegAngles(side, bodyZMm, pelvisDyMm) { - const rig = STRIDE_RIG[side]; - const s = sideSign(side); - const depth = ((rig.hipZ + bodyZMm) - (rig.soleZ + rig.ankleToSole)) / rig.cosRoll; - const ik = legPitchIk(0, depth, 0); - const lateralDrop = HIP_ROLL_Z_MM - rig.soleZ; - const rollDeltaDeg = (Math.asin(clamp(pelvisDyMm / lateralDrop, -0.5, 0.5)) * 180) / Math.PI; - return { - [`hip_pitch_${side}`]: ik.hipDeg, - [`knee_${side}`]: ik.kneeDeg, - [`ankle_pitch_${side}`]: ik.ankleDeg, - [`hip_roll_${side}`]: (s * ATHLETIC.HIP_ROLL_ABDUCT) - rollDeltaDeg, - [`ankle_roll_${side}`]: -((s * ATHLETIC.HIP_ROLL_ABDUCT) - rollDeltaDeg) - }; -} - -// ---- karate front kick (segment-local t): weight onto the left leg, -// chamber, snap, re-chamber, plant. -function kickPose(t) { - const angles = { ...ATHLETIC_ANGLES }; - const weight = pulse(t, 0.0, 0.12, 0.86, 0.98); - const chamber = pulse(t, 0.14, 0.3, 0.72, 0.88); - const extend = pulse(t, 0.36, 0.48, 0.58, 0.7); - const guard = pulse(t, 0.04, 0.16, 0.8, 0.94); - - const dy = KICK_WEIGHT_SHIFT_MM * weight; - const bodyZ = -KICK_DIP_MM * chamber; - Object.assign(angles, plantedLegAngles("left", bodyZ, dy)); - - // Kicking leg, authored in joint space (airborne). - angles.hip_pitch_right = ATHLETIC.HIP_PITCH - + ((KICK_CHAMBER_HIP_DEG - ATHLETIC.HIP_PITCH) * chamber) - + (KICK_EXTEND_HIP_DEG * extend); - angles.knee_right = ATHLETIC.KNEE - + ((KICK_CHAMBER_KNEE_DEG - ATHLETIC.KNEE) * chamber) - + (KICK_EXTEND_KNEE_DEG * extend); - angles.ankle_pitch_right = ATHLETIC.ANKLE_PITCH - + ((KICK_CHAMBER_ANKLE_DEG - ATHLETIC.ANKLE_PITCH) * chamber) - + (KICK_EXTEND_ANKLE_DEG * extend); - angles.ankle_roll_right = -sideSign("right") * ATHLETIC.HIP_ROLL_ABDUCT; - - // Guard: both fists up, elbows tight. - for (const side of SIDES) { - const s = sideSign(side); - angles[`shoulder_pitch_${side}`] = ATHLETIC.SHOULDER_PITCH - + ((KICK_GUARD_SHOULDER_DEG - ATHLETIC.SHOULDER_PITCH) * guard); - angles[`shoulder_roll_${side}`] = (s * ATHLETIC.SHOULDER_ROLL_ABDUCT) - + (s * KICK_GUARD_ROLL_OUT_DEG * guard); - angles[`elbow_${side}`] = ATHLETIC.ELBOW - + ((KICK_GUARD_ELBOW_DEG - ATHLETIC.ELBOW) * guard); - } - - const turn = KICK_HIP_TURN_DEG * ((0.5 * chamber) + (0.5 * extend)); - angles.waist_yaw += turn; - angles.neck_yaw += -turn * NECK_COMPENSATION; - - return { - angles, - extras: { waist_yaw: { pitchDeg: KICK_LEAN_BACK_DEG * extend } }, - rootOffset: [0, dy, bodyZ], - rootPitchDeg: 0 - }; -} - -// ---- handstand: keyframed whole-body rotation about +Y. While folding, -// the root is anchored so the sole front edge stays at its athletic spot -// (heels rise, toe pivot); once the hands plant, the root is anchored so -// the palm contact point stays on the ground at the plant line. Anchors -// are solved once at init by evaluating the FK and shifting the root. -function anchoredRootOffset(angles, rootPitchDeg, link, localMm, targetMm) { - const frames = fkFrames(angles, {}, [0, 0, 0], rootPitchDeg); - const f = frames[link]; - const world = matVec3(f.R, localMm).map((v, k) => v + f.p[k]); - // null target components leave that axis where the pose naturally put it. - return targetMm.map((t, k) => (t === null ? 0 : t - world[k])); -} - -function handstandKeyAngles(overrides) { - const angles = { ...ATHLETIC_ANGLES }; - for (const side of SIDES) { - // Square the limbs for the gymnastic line: no abduction or twist. - angles[`hip_roll_${side}`] = 0; - angles[`ankle_roll_${side}`] = 0; - angles[`shoulder_yaw_${side}`] = 0; - angles[`wrist_pitch_${side}`] = 0; - for (const [name, value] of Object.entries(overrides.legs || {})) { - angles[`${name}_${side}`] = value; - } - for (const [name, value] of Object.entries(overrides.arms || {})) { - angles[`${name}_${side}`] = value; - } - } - if (overrides.neckPitch !== undefined) { - angles.neck_pitch = overrides.neckPitch; - } - return angles; -} - -const HANDSTAND_KEYS = (() => { - const toeFrame = ATHLETIC_FRAMES.foot_left; - const toeWorld = matVec3(toeFrame.R, HS_TOE_LOCAL_MM).map((v, k) => v + toeFrame.p[k]); - // Anchor x/z only; the lateral position stays where the pose puts it. - const toeTarget = [toeWorld[0], null, toeWorld[2]]; - const ground = STRIDE_RIG.left.soleZ; - const palmTarget = [HS_PLANT_X_MM, null, ground]; - - // The arms stay roughly vertical under the rotating body — the world - // arm direction is rootPitch + shoulderPitch, so the shoulder tracks - // -rootPitch as the body goes over, reaching shoulder -180 / elbow -18 / - // wrist -70 at the hold (total hand pitch -90: palms flat, fingers - // forward), all inside the URDF joint limits. - const HOLD_ARMS = { shoulder_pitch: -180, shoulder_roll: 0, elbow: -18, wrist_roll: 0, wrist_pitch: -70 }; - const spec = [ - { t: 0.0, pitch: 0, anchor: null, overrides: null }, - { - t: 0.09, pitch: 12, anchor: "toe", - overrides: { - legs: { hip_pitch: -30, knee: 20, ankle_pitch: 3 }, // foot pitch +5: heel just off the ground - arms: { shoulder_pitch: -52, shoulder_roll: 0, elbow: -10, wrist_roll: 0, wrist_pitch: -20 } - } - }, - { - t: 0.2, pitch: 55, anchor: "toe", - overrides: { - legs: { hip_pitch: -100, knee: 30, ankle_pitch: 27 }, // foot pitch +12: heels rise in the fold - arms: { shoulder_pitch: -70, shoulder_roll: 0, elbow: -8, wrist_roll: 0, wrist_pitch: -45 } - } - }, - { - // Press position: palms planted, arms vertical, pike fold with the - // toes still resting on the ground (hip solved at init below). - t: 0.3, pitch: 95, anchor: "palm", solvePressHip: true, - overrides: { - legs: { hip_pitch: -80, knee: 8, ankle_pitch: 30 }, - arms: { shoulder_pitch: -95, shoulder_roll: 0, elbow: -20, wrist_roll: 0, wrist_pitch: -70 } - } - }, - { - t: 0.4, pitch: 140, anchor: "palm", - overrides: { - legs: { hip_pitch: -45, knee: 25, ankle_pitch: 10 }, - arms: { shoulder_pitch: -145, shoulder_roll: 0, elbow: -18, wrist_roll: 0, wrist_pitch: -67 } - } - }, - { - t: 0.48, pitch: 178, anchor: "palm", - overrides: { - legs: { hip_pitch: -6, knee: 4, ankle_pitch: 25 }, - arms: HOLD_ARMS - } - }, - { - t: 0.58, pitch: 178, anchor: "palm", - overrides: { - legs: { hip_pitch: -6, knee: 4, ankle_pitch: 25 }, - arms: HOLD_ARMS - } - } - ]; - // Mirror the way back down through the same shapes. - const back = [ - { ...spec[4], t: 0.66 }, - { ...spec[3], t: 0.76 }, - { ...spec[2], t: 0.86 }, - { ...spec[1], t: 0.94 }, - { ...spec[0], t: 1.0 } - ]; - - // Resolve every key with its anchor TYPE plus the toe-line x position - // it implies; the root translation itself is solved per frame at - // runtime, so contacts stay exact through every interpolated pose. - // Palm-typed keys still record their natural toe x so toe<->palm - // boundary intervals can slide the toe target continuously. - const resolved = [...spec, ...back].map((key) => { - const angles = key.overrides ? handstandKeyAngles(key.overrides) : { ...ATHLETIC_ANGLES }; - const type = key.anchor === "palm" ? "palm" : "toe"; - if (key.solvePressHip) { - // Bisect the pike fold so the toes rest exactly on the ground - // behind the planted hands (toe height grows monotonically with - // hip angle on the toes-behind branch). The palm anchor depends - // only on the arm chain, so the hip search does not disturb it. - const rootOffset = anchoredRootOffset(angles, key.pitch, "hand_left", HS_PALM_LOCAL_MM, palmTarget); - let lo = -95; - let hi = -40; - for (let i = 0; i < 48; i += 1) { - const mid = (lo + hi) / 2; - for (const side of SIDES) { - angles[`hip_pitch_${side}`] = mid; - } - const f = fkFrames(angles, {}, rootOffset, key.pitch).foot_left; - const toeZ = f.p[2] + matVec3(f.R, HS_TOE_LOCAL_MM)[2]; - if (toeZ < ground) { - lo = mid; - } else { - hi = mid; - } - } - } - const rootOffset = type === "palm" - ? anchoredRootOffset(angles, key.pitch, "hand_left", HS_PALM_LOCAL_MM, palmTarget) - : anchoredRootOffset(angles, key.pitch, "foot_left", HS_TOE_LOCAL_MM, toeTarget); - const foot = fkFrames(angles, {}, rootOffset, key.pitch).foot_left; - const toeX = foot.p[0] + matVec3(foot.R, HS_TOE_LOCAL_MM)[0]; - return { t: key.t, pitch: key.pitch, type, toeX, angles }; - }); - return { keys: resolved.sort((a, b) => a.t - b.t), toeZ: toeWorld[2], palmTarget }; -})(); - -function handstandPose(t) { - const { keys, toeZ, palmTarget } = HANDSTAND_KEYS; - let a = keys[0]; - let b = keys[keys.length - 1]; - for (let i = 0; i < keys.length - 1; i += 1) { - if (t >= keys[i].t && t <= keys[i + 1].t) { - a = keys[i]; - b = keys[i + 1]; - break; - } - } - const s = smooth01((t - a.t) / Math.max(1e-9, b.t - a.t)); - const angles = {}; - for (const name of Object.keys(ATHLETIC_ANGLES)) { - angles[name] = a.angles[name] + ((b.angles[name] - a.angles[name]) * s); - } - // A breath of leg wobble during the hold (hips only, so the planted - // hands stay put). - const hold = pulse(t, 0.46, 0.5, 0.56, 0.6); - const wobble = HS_WOBBLE_DEG * hold * Math.sin(2 * Math.PI * ((t - 0.46) / 0.14) * 2); - for (const side of SIDES) { - angles[`hip_pitch_${side}`] += wobble; - angles[`knee_${side}`] += -wobble * 0.6; - } - const rootPitchDeg = a.pitch + ((b.pitch - a.pitch) * s); - // Solve the contact anchor for THIS frame: palm-anchored only while both - // bracketing keys are palm keys (the press key satisfies both contacts, - // so the anchor handoff there is seamless); otherwise toe-anchored with - // the toe target sliding along the ground line between the keys. - let rootOffset; - if (a.type === "palm" && b.type === "palm") { - rootOffset = anchoredRootOffset(angles, rootPitchDeg, "hand_left", HS_PALM_LOCAL_MM, palmTarget); - } else { - const toeX = a.toeX + ((b.toeX - a.toeX) * s); - rootOffset = anchoredRootOffset(angles, rootPitchDeg, "foot_left", HS_TOE_LOCAL_MM, [toeX, null, toeZ]); - } - return { angles, extras: {}, rootOffset, rootPitchDeg }; -} - -// ---- Elvis dance. The left leg kicks out to the robot-left and plants -// on a pointed toe, solved with closed-form leg IK: the hip roll angle -// follows directly from requiring the toe target to lie in the leg's -// (rolled) pitch plane, then the in-plane two-link IK places the -// ankle-pitch joint so the toe tip lands exactly on the target. The -// ankle roll stays 0, so the pointed foot rolls onto its outer toe edge -// with the leg — the classic look. -function elvisKickOutLeg(bodyZMm, pelvisDyMm, toeXMm, toeYMm, footPointDeg, liftMm, edgeScale) { - const rig = STRIDE_RIG.left; - // Left hip-roll joint center in the world, with the shifted pelvis. - const hipRoll = [0, HIP_Y + pelvisDyMm, HIP_ROLL_Z_MM + bodyZMm]; - // The foot rolls with the leg (ankle roll 0), so at full kick-out the - // contact is the OUTER toe corner: lift the centerline target by the - // rolled half foot width (scaled in with the pose; roll barely changes - // with the lift, so one extra pass converges). - let target = [toeXMm, toeYMm, rig.soleZ + liftMm]; - let v = [target[0] - hipRoll[0], target[1] - hipRoll[1], target[2] - hipRoll[2]]; - const edgeLift = ((35 * Math.abs(Math.sin(Math.atan2(v[1], -v[2])))) + 1) * edgeScale; - target = [toeXMm, toeYMm, rig.soleZ + liftMm + edgeLift]; - v = [target[0] - hipRoll[0], target[1] - hipRoll[1], target[2] - hipRoll[2]]; - // Roll that brings the target into the leg's pitch plane. - const rollRad = Math.atan2(v[1], -v[2]); - const planeDepth = Math.hypot(v[1], v[2]); // hip-roll -> target, in-plane - // Toe tip relative to the ankle-pitch joint at the pointed foot pitch. - const pointRad = (footPointDeg * Math.PI) / 180; - const toeForward = (ELVIS_TOE_TIP_LOCAL.x * Math.cos(pointRad)) - - (ELVIS_TOE_TIP_LOCAL.drop * Math.sin(pointRad)); - const toeDrop = (ELVIS_TOE_TIP_LOCAL.x * Math.sin(pointRad)) - + (ELVIS_TOE_TIP_LOCAL.drop * Math.cos(pointRad)); - // Ankle-pitch target measured from the hip-PITCH joint (78 mm further - // down the plane from the hip-roll joint). - const ik = legPitchIk(v[0] - toeForward, planeDepth - 78 - toeDrop, footPointDeg); - const rollDeg = (rollRad * 180) / Math.PI; - return { - hip_pitch_left: ik.hipDeg, - knee_left: ik.kneeDeg, - ankle_pitch_left: ik.ankleDeg, - hip_roll_left: rollDeg, - // Flat (roll-compensated) foot at rest, rolling onto the outer toe - // edge with the leg as the kick-out engages. - ankle_roll_left: -rollDeg * (1 - edgeScale) - }; -} - -function dancePose(t) { - const angles = { ...ATHLETIC_ANGLES }; - const env = pulse(t, 0.0, 0.12, 0.88, 1.0); - const beat = t * ELVIS_BEATS; - - const dy = ELVIS_WEIGHT_SHIFT_MM * env; - const bodyZ = (-ELVIS_DIP_MM * env) - - (ELVIS_BOUNCE_MM * env * Math.abs(Math.sin(Math.PI * beat))); - // Stance leg: planted under the shifted, bobbing pelvis. - Object.assign(angles, plantedLegAngles("right", bodyZ, dy)); - - // Kicked-out leg: toe planted out to the left, shaking with the beat. - // The whole transition is IK-driven — the toe target slides from the - // athletic toe spot to the kick-out point with a lift bump mid-blend, - // so the foot never sweeps through the floor (at env=0 the IK lands - // exactly back on the athletic leg). - const shake = env * Math.sin(2 * Math.PI * 2 * beat); - const toeX = ELVIS_ATHLETIC_TOE[0] + ((ELVIS_TOE_X_MM - ELVIS_ATHLETIC_TOE[0]) * env); - const toeY = ELVIS_ATHLETIC_TOE[1] - + (((ELVIS_TOE_Y_MM + (ELVIS_TOE_SHAKE_MM * shake)) - ELVIS_ATHLETIC_TOE[1]) * env); - const point = (ELVIS_FOOT_POINT_DEG * env) + (ELVIS_FOOT_POINT_SHAKE_DEG * shake); - const lift = 45 * Math.sin(Math.PI * env); - Object.assign(angles, elvisKickOutLeg(bodyZ, dy, toeX, toeY, point, lift, env)); - - // The point: right arm up and out to the right, pulsing on the beat. - const pulseDeg = ELVIS_POINT_PULSE_DEG * Math.sin(2 * Math.PI * beat); - angles.shoulder_roll_right = (sideSign("right") * ATHLETIC.SHOULDER_ROLL_ABDUCT) - + (((ELVIS_POINT_ROLL_DEG + pulseDeg) - (sideSign("right") * ATHLETIC.SHOULDER_ROLL_ABDUCT)) * env); - angles.shoulder_pitch_right = ATHLETIC.SHOULDER_PITCH - + ((ELVIS_POINT_PITCH_DEG - ATHLETIC.SHOULDER_PITCH) * env); - angles.elbow_right = ATHLETIC.ELBOW + ((ELVIS_POINT_ELBOW_DEG - ATHLETIC.ELBOW) * env); - // Off arm low and bent across the body. - angles.shoulder_pitch_left = ATHLETIC.SHOULDER_PITCH - + ((ELVIS_OFF_ARM_PITCH_DEG - ATHLETIC.SHOULDER_PITCH) * env); - angles.elbow_left = ATHLETIC.ELBOW + ((ELVIS_OFF_ARM_ELBOW_DEG - ATHLETIC.ELBOW) * env); - - // Hip swivel, lean into the point, head to the pointing hand, chin up. - const twist = ELVIS_TWIST_DEG * env * Math.sin(Math.PI * beat); - angles.waist_yaw += twist; - angles.neck_yaw += (ELVIS_HEAD_TURN_DEG * env) - (twist * 0.6); - angles.neck_pitch += ELVIS_CHIN_UP_DEG * env; - - return { - angles, - extras: { waist_yaw: { rollDeg: ELVIS_LEAN_RIGHT_DEG * env } }, - rootOffset: [0, dy, bodyZ], - rootPitchDeg: 0 - }; -} - -const FEATURE_BY_LINK = { - pelvis: "pelvis", - torso: "torso", - neck_collar: "neckCollar", - head: "head", - hip_bracket_left: "hipBracketLeft", - hip_carrier_left: "hipCarrierLeft", - thigh_left: "thighLeft", - shin_left: "shinLeft", - ankle_link_left: "ankleLinkLeft", - foot_left: "footLeft", - shoulder_pod_left: "shoulderPodLeft", - yaw_housing_left: "yawHousingLeft", - bicep_left: "bicepLeft", - forearm_left: "forearmLeft", - wrist_carrier_left: "wristCarrierLeft", - hand_left: "handLeft", - hip_bracket_right: "hipBracketRight", - hip_carrier_right: "hipCarrierRight", - thigh_right: "thighRight", - shin_right: "shinRight", - ankle_link_right: "ankleLinkRight", - foot_right: "footRight", - shoulder_pod_right: "shoulderPodRight", - yaw_housing_right: "yawHousingRight", - bicep_right: "bicepRight", - forearm_right: "forearmRight", - wrist_carrier_right: "wristCarrierRight", - hand_right: "handRight" -}; - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/robots/juno/juno.step" - }, - label: "juno humanoid", - description: "Walking-in-place march for the juno humanoid: alternating leg lift with planted stance feet, antiphase arm swing, torso counter-sway, and a head that keeps facing forward.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - pelvis: { ref: "#o1.1", label: "Pelvis", description: "Root link; static during the in-place march." }, - torso: { ref: "#o1.2", label: "Torso" }, - neckCollar: { ref: "#o1.3", label: "Neck collar" }, - head: { ref: "#o1.4", label: "Head" }, - hipBracketLeft: { ref: "#o1.5", label: "Left hip bracket" }, - hipCarrierLeft: { ref: "#o1.6", label: "Left hip carrier" }, - thighLeft: { ref: "#o1.7", label: "Left thigh" }, - shinLeft: { ref: "#o1.8", label: "Left shin" }, - ankleLinkLeft: { ref: "#o1.9", label: "Left ankle link" }, - footLeft: { ref: "#o1.10", label: "Left foot" }, - shoulderPodLeft: { ref: "#o1.11", label: "Left shoulder pod" }, - yawHousingLeft: { ref: "#o1.12", label: "Left yaw housing" }, - bicepLeft: { ref: "#o1.13", label: "Left bicep" }, - forearmLeft: { ref: "#o1.14", label: "Left forearm" }, - wristCarrierLeft: { ref: "#o1.15", label: "Left wrist carrier" }, - handLeft: { ref: "#o1.16", label: "Left hand" }, - hipBracketRight: { ref: "#o1.17", label: "Right hip bracket" }, - hipCarrierRight: { ref: "#o1.18", label: "Right hip carrier" }, - thighRight: { ref: "#o1.19", label: "Right thigh" }, - shinRight: { ref: "#o1.20", label: "Right shin" }, - ankleLinkRight: { ref: "#o1.21", label: "Right ankle link" }, - footRight: { ref: "#o1.22", label: "Right foot" }, - shoulderPodRight: { ref: "#o1.23", label: "Right shoulder pod" }, - yawHousingRight: { ref: "#o1.24", label: "Right yaw housing" }, - bicepRight: { ref: "#o1.25", label: "Right bicep" }, - forearmRight: { ref: "#o1.26", label: "Right forearm" }, - wristCarrierRight: { ref: "#o1.27", label: "Right wrist carrier" }, - handRight: { ref: "#o1.28", label: "Right hand" } - }, - parameters: { - phase: { - type: "number", - label: "Gait phase", - description: "One full gait cycle; the walk animations drive this 0 -> 1.", - default: 0, - min: 0, - max: 1, - step: 0.001 - }, - gait: { - type: "select", - label: "Gait", - description: "Showpieces (Elvis dance, handstand, karate kick) and in-place gaits (jump, run, stride, march). Each walk animation drives the matching mode.", - default: "march", - options: [ - { value: "dance", label: "Elvis dance" }, - { value: "handstand", label: "Handstand" }, - { value: "kick", label: "Karate kick" }, - { value: "jump", label: "Jump in place" }, - { value: "run", label: "Run in place" }, - { value: "stride", label: "Stride in place" }, - { value: "march", label: "March in place" } - ] - }, - strideLength: { - type: "number", - label: "Stride length", - description: "Front-to-back foot travel (mm) of the stride gait; capped so the leg IK stays inside reach. Ignored by the march.", - default: 230, - min: 0, - max: 300, - step: 5 - }, - legLift: { - type: "number", - label: "Leg lift", - description: "Scales hip raise, knee fold, and toe point of the swing leg. Stance feet stay planted at any value.", - default: 1, - min: 0, - max: 1.4, - step: 0.01 - }, - armSwing: { - type: "number", - label: "Arm swing", - description: "Scales the antiphase shoulder swing and forward-arm elbow flexion.", - default: 1, - min: 0, - max: 1.5, - step: 0.01 - }, - torsoSway: { - type: "number", - label: "Torso sway", - description: "Scales the waist counter-rotation and weight-shift roll; the head compensates to keep facing forward.", - default: 1, - min: 0, - max: 1.5, - step: 0.01 - } - }, - // Ordered most exciting first — this is the order the viewer lists them. - animations: { - danceLoop: { - label: "Elvis dance", - description: "The King: weight on the right leg, left leg kicked out to the left on a shaking pointed toe, right finger pointing up to the right with a beat pulse, hip swivel and chin up.", - duration: 3.2, - loop: true, - update({ cycle, set }) { - set("gait", "dance"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - handstandLoop: { - label: "Handstand", - description: "Toe-pivot fold, press to a palms-flat inverted hold with a breathing wobble, and back up to the ready stance.", - duration: 4.6, - loop: true, - update({ cycle, set }) { - set("gait", "handstand"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - kickLoop: { - label: "Karate kick", - description: "Weight shifts over the left leg, the right leg chambers and snaps a ball-of-foot front kick at hip height behind a fists-up guard.", - duration: 2.8, - loop: true, - update({ cycle, set }) { - set("gait", "kick"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - jumpLoop: { - label: "Jump in place", - description: "Crouch, leap with hands overhead, and land with a springy knee-absorbed wobble before settling back to the ready stance.", - duration: 1.8, - loop: true, - update({ cycle, set }) { - set("gait", "jump"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - runLoop: { - label: "Run in place", - description: "Running cadence with flight phases: body bounce, toe-pivot push-off, high heel recovery, forward lean, and pumping bent arms.", - duration: 0.8, - loop: true, - update({ cycle, set }) { - set("gait", "run"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - strideLoop: { - label: "Stride in place", - description: "Bigger strides: legs sweep back and forward with the stance foot sliding flat on the ground, wider arm swing, stronger torso counter-sway.", - duration: 1.9, - loop: true, - update({ cycle, set }) { - set("gait", "stride"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - walkLoop: { - label: "Walk in place", - description: "Continuous march loop; scrub the gait phase or scale lift/swing/sway live.", - duration: 1.5, - loop: true, - update({ cycle, set }) { - set("gait", "march"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - } - } - }, - - update({ params, effects }) { - const phase = ((finite(params.phase, 0) % 1) + 1) % 1; - const scales = { - legLift: clamp(params.legLift, 0, 1.4), - armSwing: clamp(params.armSwing, 0, 1.5), - torsoSway: clamp(params.torsoSway, 0, 1.5), - strideLength: clamp(params.strideLength, 0, MAX_STRIDE_MM) - }; - const gait = String(params.gait || "march"); - let targetFrames; - if (gait === "stride") { - targetFrames = fkFrames( - strideAnglesDeg(phase, scales), - strideTorsoRollExtras(phase, scales.torsoSway) - ); - } else if (gait === "run") { - targetFrames = fkFrames( - runAnglesDeg(phase, scales), - runTorsoExtras(phase, scales.torsoSway), - [0, 0, runBounceMm(phase, scales.legLift)] - ); - } else if (gait === "jump") { - const jump = jumpAnglesDeg(phase, scales); - targetFrames = fkFrames( - jump.angles, - jumpTorsoExtras(phase, jump.bodyZ, jump.flight, scales.torsoSway), - [0, 0, jump.bodyZ] - ); - } else if (gait === "kick" || gait === "handstand" || gait === "dance") { - const pose = gait === "kick" - ? kickPose(phase) - : gait === "handstand" - ? handstandPose(phase) - : dancePose(phase); - targetFrames = fkFrames(pose.angles, pose.extras, pose.rootOffset, pose.rootPitchDeg); - } else { - targetFrames = fkFrames( - gaitAnglesDeg(phase, scales), - torsoRollExtras(phase, scales.torsoSway) - ); - } - - // The pelvis root is included: it bobs in the run and jump gaits and - // resolves to an identity delta in the march and stride. - for (const [link, feature] of Object.entries(FEATURE_BY_LINK)) { - effects.transform(feature, { - matrix: deltaMatrixRowMajor(ATHLETIC_FRAMES[link], targetFrames[link]) - }); - } - } -}; diff --git a/models/robots/juno/3MF/ankle_link_left.3mf b/models/robots/juno/3MF/ankle_link_left.3mf deleted file mode 100644 index 2c149f5d..00000000 --- a/models/robots/juno/3MF/ankle_link_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c60800c6b0f57def4bcb903617ebd80fd5bf0aa13a58973b08a9a7d803158d9c -size 11079 diff --git a/models/robots/juno/3MF/ankle_link_right.3mf b/models/robots/juno/3MF/ankle_link_right.3mf deleted file mode 100644 index 4d5cdecc..00000000 --- a/models/robots/juno/3MF/ankle_link_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b18dfcc9353eb3335678a5bb152ad4633102ca40207063ad528e3affde54277 -size 11081 diff --git a/models/robots/juno/3MF/bicep_left.3mf b/models/robots/juno/3MF/bicep_left.3mf deleted file mode 100644 index 2bb6a72d..00000000 --- a/models/robots/juno/3MF/bicep_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:98f75d262812b8bb73878c203e7bb6d46ae1786da8b79e900523c9cce81ee482 -size 245829 diff --git a/models/robots/juno/3MF/bicep_right.3mf b/models/robots/juno/3MF/bicep_right.3mf deleted file mode 100644 index fa969f01..00000000 --- a/models/robots/juno/3MF/bicep_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:693607329f1cc9d08f5f4d1db037933e12b683b54ff7ed4c1f5daa6953d1ba18 -size 245842 diff --git a/models/robots/juno/3MF/foot_left.3mf b/models/robots/juno/3MF/foot_left.3mf deleted file mode 100644 index c62fc43e..00000000 --- a/models/robots/juno/3MF/foot_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:461a399690d6d489b42150488c93310650abbefbc77c32554a0211db30c7cd5a -size 55125 diff --git a/models/robots/juno/3MF/foot_right.3mf b/models/robots/juno/3MF/foot_right.3mf deleted file mode 100644 index 3be0861d..00000000 --- a/models/robots/juno/3MF/foot_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:336011418148770b412eb86b5e90e39c8c3e9eaa157fba0eef9265146a6a2e60 -size 55011 diff --git a/models/robots/juno/3MF/forearm_left.3mf b/models/robots/juno/3MF/forearm_left.3mf deleted file mode 100644 index 2d7065a6..00000000 --- a/models/robots/juno/3MF/forearm_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be6567baf0572c2bfacfe2b4034ee6115418b5c4fdf4e5344ecbaf003772cd7a -size 268896 diff --git a/models/robots/juno/3MF/forearm_right.3mf b/models/robots/juno/3MF/forearm_right.3mf deleted file mode 100644 index 385c777f..00000000 --- a/models/robots/juno/3MF/forearm_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6af3d584b7a905236057f9527eb3cc8baa16970d3bd2c057df3939ac9b6481b0 -size 263351 diff --git a/models/robots/juno/3MF/hand_left.3mf b/models/robots/juno/3MF/hand_left.3mf deleted file mode 100644 index cb116a6b..00000000 --- a/models/robots/juno/3MF/hand_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8daa5e14b0644137ef1fc741427c8ec2a97dd87177de188a4b2ed77bab96f991 -size 211743 diff --git a/models/robots/juno/3MF/hand_right.3mf b/models/robots/juno/3MF/hand_right.3mf deleted file mode 100644 index dcac482e..00000000 --- a/models/robots/juno/3MF/hand_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd2bd8eb35d68a51dfdf2564dea1abecbad22d9f73767bc969761639fa5be8fe -size 181896 diff --git a/models/robots/juno/3MF/head.3mf b/models/robots/juno/3MF/head.3mf deleted file mode 100644 index fc1a52da..00000000 --- a/models/robots/juno/3MF/head.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92499b2aec1392f378ae7087cf00c064575850e2da5e2545c500d34c87b9f9a9 -size 211695 diff --git a/models/robots/juno/3MF/hip_bracket_left.3mf b/models/robots/juno/3MF/hip_bracket_left.3mf deleted file mode 100644 index cd9bb0f4..00000000 --- a/models/robots/juno/3MF/hip_bracket_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:303094cb0d42db2dbe9e64a70271425f407efb8bd736de98bb5098d22472f6a0 -size 67024 diff --git a/models/robots/juno/3MF/hip_bracket_right.3mf b/models/robots/juno/3MF/hip_bracket_right.3mf deleted file mode 100644 index 0b5a8e24..00000000 --- a/models/robots/juno/3MF/hip_bracket_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:48910af198cfe89aaa290ea9b935a5829c33bea2e67b998f535d693f755f2475 -size 67025 diff --git a/models/robots/juno/3MF/hip_carrier_left.3mf b/models/robots/juno/3MF/hip_carrier_left.3mf deleted file mode 100644 index 711ffbd6..00000000 --- a/models/robots/juno/3MF/hip_carrier_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b400dfe673ce2e6b5adbbe287810df5d6b1bbfca2ec28426798f3e04adc54f54 -size 8848 diff --git a/models/robots/juno/3MF/hip_carrier_right.3mf b/models/robots/juno/3MF/hip_carrier_right.3mf deleted file mode 100644 index 93eceb87..00000000 --- a/models/robots/juno/3MF/hip_carrier_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:559649e2aeec8c1d77f145c7c6e6b905cb9cfc5cf6098193aa8bf711502827fd -size 8877 diff --git a/models/robots/juno/3MF/neck_collar.3mf b/models/robots/juno/3MF/neck_collar.3mf deleted file mode 100644 index c6faaba0..00000000 --- a/models/robots/juno/3MF/neck_collar.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a2537b6bd29f0101298653760a19b66c6b4fc32d46df1ce63ade8b27f73a915e -size 135624 diff --git a/models/robots/juno/3MF/pelvis.3mf b/models/robots/juno/3MF/pelvis.3mf deleted file mode 100644 index 22c37b64..00000000 --- a/models/robots/juno/3MF/pelvis.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe4f1eaf601cfcb948945a4ac7455205bf1df1cfe7c1aa73bc57527a076da748 -size 56544 diff --git a/models/robots/juno/3MF/shin_left.3mf b/models/robots/juno/3MF/shin_left.3mf deleted file mode 100644 index e2ac79f6..00000000 --- a/models/robots/juno/3MF/shin_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:287e0188a6eacbe1433cc6c4c492a3fa643408894e0ae9970a891d47083246a2 -size 84215 diff --git a/models/robots/juno/3MF/shin_right.3mf b/models/robots/juno/3MF/shin_right.3mf deleted file mode 100644 index e9aecce3..00000000 --- a/models/robots/juno/3MF/shin_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e52cc32aa14901a88074b28a9ec206b31caa8a55bb95740debd975645b500152 -size 87862 diff --git a/models/robots/juno/3MF/shoulder_pod_left.3mf b/models/robots/juno/3MF/shoulder_pod_left.3mf deleted file mode 100644 index 4cb567c2..00000000 --- a/models/robots/juno/3MF/shoulder_pod_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:947563ca767f154191e45ae9f58dea02b6fe7ebf5681bf273be674dd3844c905 -size 34893 diff --git a/models/robots/juno/3MF/shoulder_pod_right.3mf b/models/robots/juno/3MF/shoulder_pod_right.3mf deleted file mode 100644 index 00ad780c..00000000 --- a/models/robots/juno/3MF/shoulder_pod_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4d99107c4e9eb2c3e4bde279410c5e5732c328cf782373f7172c3ef7e2bdcdec -size 34809 diff --git a/models/robots/juno/3MF/thigh_left.3mf b/models/robots/juno/3MF/thigh_left.3mf deleted file mode 100644 index 4a891483..00000000 --- a/models/robots/juno/3MF/thigh_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ebe9a93f8c9305ebf35d62fa797f895874b92d35c59154e6fc1bfb8f8b998337 -size 94918 diff --git a/models/robots/juno/3MF/thigh_right.3mf b/models/robots/juno/3MF/thigh_right.3mf deleted file mode 100644 index 772fcf4c..00000000 --- a/models/robots/juno/3MF/thigh_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79f9ce4f6031f6024b9c69f90af835f0286ff801a6469e85b3cd6594a8e24edf -size 99017 diff --git a/models/robots/juno/3MF/torso.3mf b/models/robots/juno/3MF/torso.3mf deleted file mode 100644 index 434999e8..00000000 --- a/models/robots/juno/3MF/torso.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cbf445511748b2932e26f787bbb392e62382e1c0118cefb3a48122a7d1a266dd -size 457880 diff --git a/models/robots/juno/3MF/wrist_carrier_left.3mf b/models/robots/juno/3MF/wrist_carrier_left.3mf deleted file mode 100644 index c0d01a38..00000000 --- a/models/robots/juno/3MF/wrist_carrier_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ed6a47632bc0e48d33ee548206b97918fb71cffef6abc0ddc35e4efe2e5472d -size 59000 diff --git a/models/robots/juno/3MF/wrist_carrier_right.3mf b/models/robots/juno/3MF/wrist_carrier_right.3mf deleted file mode 100644 index 8a56010d..00000000 --- a/models/robots/juno/3MF/wrist_carrier_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71312189e206d64957bceaaf2a757407917d70b6beff25cf47f9ca89f490e388 -size 59001 diff --git a/models/robots/juno/3MF/yaw_housing_left.3mf b/models/robots/juno/3MF/yaw_housing_left.3mf deleted file mode 100644 index bbc5ff05..00000000 --- a/models/robots/juno/3MF/yaw_housing_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c3e7fef019969e22cfdb84c2a1b70b292e35580a69317c3a9295d3972489653 -size 69535 diff --git a/models/robots/juno/3MF/yaw_housing_right.3mf b/models/robots/juno/3MF/yaw_housing_right.3mf deleted file mode 100644 index 489bd5b2..00000000 --- a/models/robots/juno/3MF/yaw_housing_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d14630363a41a75be6368b4bc025e7741379a910f4605caf5bf10969dcc77678 -size 69538 diff --git a/models/robots/juno/README.md b/models/robots/juno/README.md deleted file mode 100644 index 9d0a5859..00000000 --- a/models/robots/juno/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# juno — compact humanoid robotics platform concept - -A sleek research-humanoid CAD concept with Unitree-G1-like proportions: -~1.40 m tall, athletic ready stance, exposed cylindrical actuator modules at -every joint, warm-porcelain composite shells over graphite structure with -machined-aluminum joint rims, coral-orange accents on repeated functional -details (actuator hubs, toe/heel bumpers, head vents, fingertip pads), a -gloss midnight-blue sensor visor displaying cyan pixel-grid eyes (Anki -Cozmo style), and dexterous five-digit hands. Clean industrial design, -no logos. - -## Degrees of freedom (27 body DOF) - -| Group | Joints | DOF | -| --- | --- | --- | -| Each leg (x2) | hip yaw, hip roll, hip pitch, knee, ankle pitch, ankle roll | 12 | -| Each arm (x2) | shoulder pitch, shoulder roll, shoulder yaw, elbow, wrist roll, wrist pitch | 12 | -| Waist | yaw | 1 | -| Neck | yaw, pitch | 2 | - -Hands add posed (cosmetic) finger articulation on top of the 27 counted DOF. - -## Files - -- `juno.py` — build123d generator (`gen_step()`); authoritative source. - Pose angles are module-level parameters; joints are authored as - `cadpy.assembly.AssemblyHelper` revolute frames driven by those angles. - Also exposes `gen_urdf()` / `gen_srdf()` for the robot description. -- `juno_parts/` — part-builder package (sculpted segments, joint hardware, - shared style library). Each builder returns an identity-location labeled - compound in its part-local frame. `chain.py` is the shared kinematic - chain/pose/limit spec used by both the CAD assembly and the URDF/SRDF - generators; `description.py` emits the URDF/SRDF XML; `mass_props.py` - holds baked CAD volume/COM/inertia/bbox data. -- `juno.step` — generated STEP assembly (derived artifact). -- `.juno.step.js` — CAD Viewer animation sidecar with four in-place gaits - built on per-frame chain FK plus two-link leg IK around the baked - athletic stance: `walkLoop` (march, planted stance feet), `strideLoop` - (treadmill-style strides, stance foot slides flat on the ground), - `runLoop` (flight phases, body bounce, toe-pivot push-off, pumping bent - arms, forward lean), and `jumpLoop` (countermovement hop with hands - overhead and an underdamped springy landing). All gaits share antiphase - arm swing, torso counter-sway, and head stabilization. Three showpiece - loops: `danceLoop` (Elvis: right finger pointing up to the right, left - leg kicked out to the left on a shaking planted toe via closed-form - lateral+sagittal leg IK), `handstandLoop` (toe-pivot fold to a - palms-flat inverted hold, whole-body root rotation with per-frame - toe/palm contact anchoring), and `kickLoop` (chambered karate front - kick behind a fists-up guard). Controls: `phase`, `gait`, - `strideLength`, `legLift`, `armSwing`, `torsoSway`. Occurrence refs - `#o1.1..#o1.28` follow the `asm.add` order in `juno.py`. -- `juno.urdf` — generated URDF (derived artifact): a frame-only - `base_footprint` ground root plus 28 physical links and 27 revolute - joints (zero pose stands with soles on z = 0), per-link 3MF mesh - visuals, bbox collisions, CAD-derived inertials at an assumed 35 kg - total mass. -- `juno.srdf` — generated MoveIt2 SRDF (derived artifact): limb/torso/head - planning groups, hand end effectors, disabled collisions, and whole-body - group states (`zero`, `athletic_ready`, `t_pose`, `wave_right`, `squat`). -- `STEP/` — per-link wrapper sources (`gen_step()` per link) and their - generated STEP parts; `3MF/` — per-link mm mesh sidecars referenced by - the URDF. - -## Conventions - -Units mm. Pelvis waist-yaw joint center is the world origin; +X forward, -+Y robot-left, +Z up. Soles rest near z = -774 in the default stance. -Regenerate with the CAD skill: `python scripts/step models/robots/juno/juno.py`. -Regenerate link meshes per link with the CAD skill, e.g. -`python scripts/step models/robots/juno/STEP/.py --3mf ../3MF/.3mf`. -Regenerate the robot description with the URDF/SRDF skills: -`python scripts/urdf models/robots/juno/juno.py=models/robots/juno/juno.urdf` -and `python scripts/srdf models/robots/juno/juno.py=models/robots/juno/juno.srdf`. diff --git a/models/robots/juno/STEP/.ankle_link_left.step.glb b/models/robots/juno/STEP/.ankle_link_left.step.glb deleted file mode 100644 index d941442b..00000000 --- a/models/robots/juno/STEP/.ankle_link_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ab563bf521d1e41af62c7e52c163abf95de0b5e66ae38fe05e56e6c995f222c1 -size 284224 diff --git a/models/robots/juno/STEP/.ankle_link_right.step.glb b/models/robots/juno/STEP/.ankle_link_right.step.glb deleted file mode 100644 index dd5224ac..00000000 --- a/models/robots/juno/STEP/.ankle_link_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a30866ceb9b143faa7d75fc0cb166ea528dc0548104c0ae211c31aef9f6eaa4c -size 284232 diff --git a/models/robots/juno/STEP/.bicep_left.step.glb b/models/robots/juno/STEP/.bicep_left.step.glb deleted file mode 100644 index 4cdceeb1..00000000 --- a/models/robots/juno/STEP/.bicep_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9071fca2e3ec22b2770b22f00c88b464a5253556bb599ec552f0c3dd6d913e72 -size 2719792 diff --git a/models/robots/juno/STEP/.bicep_right.step.glb b/models/robots/juno/STEP/.bicep_right.step.glb deleted file mode 100644 index 4173daf8..00000000 --- a/models/robots/juno/STEP/.bicep_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:072336a210c247a25c2b16495671d52c5c12b5a3fc403bf86dc6e64f6a2295bb -size 2719856 diff --git a/models/robots/juno/STEP/.foot_left.step.glb b/models/robots/juno/STEP/.foot_left.step.glb deleted file mode 100644 index a6c67b2f..00000000 --- a/models/robots/juno/STEP/.foot_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a99760bc2975d25a381aa885e37e8606683d1548e5069a8b34a490088780026 -size 995156 diff --git a/models/robots/juno/STEP/.foot_right.step.glb b/models/robots/juno/STEP/.foot_right.step.glb deleted file mode 100644 index 31c0c019..00000000 --- a/models/robots/juno/STEP/.foot_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f23e4eba6377b1f7ff69aece806f29deefafb5246fd5455c68f46a007a86514 -size 995484 diff --git a/models/robots/juno/STEP/.forearm_left.step.glb b/models/robots/juno/STEP/.forearm_left.step.glb deleted file mode 100644 index 229ff0fd..00000000 --- a/models/robots/juno/STEP/.forearm_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5db78ee94cd59e336e7e915a049d5e79e0e6fd38a531ffbda9427c58f8dda3e5 -size 2807308 diff --git a/models/robots/juno/STEP/.forearm_right.step.glb b/models/robots/juno/STEP/.forearm_right.step.glb deleted file mode 100644 index 21886a46..00000000 --- a/models/robots/juno/STEP/.forearm_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a83bed8e6e0c77d2a1047b4708dba42daf7363982a28bd2e158742a68de00d5b -size 2807724 diff --git a/models/robots/juno/STEP/.hand_left.step.glb b/models/robots/juno/STEP/.hand_left.step.glb deleted file mode 100644 index e012e029..00000000 --- a/models/robots/juno/STEP/.hand_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:faea023f3e93c9612e18d762bd30c1116b3354699ad38fd417a984dbe3f2e529 -size 3225868 diff --git a/models/robots/juno/STEP/.hand_right.step.glb b/models/robots/juno/STEP/.hand_right.step.glb deleted file mode 100644 index 411f449a..00000000 --- a/models/robots/juno/STEP/.hand_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cfbabb86ab977ad44c61df5847568a73255316b0d20a20dd933edaec89accf39 -size 3237960 diff --git a/models/robots/juno/STEP/.head.step.glb b/models/robots/juno/STEP/.head.step.glb deleted file mode 100644 index 146af7fa..00000000 --- a/models/robots/juno/STEP/.head.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b637b314f717d4138c60bc9c6cdcd047a9b39f282f0a3fa523b143b26764027 -size 2855236 diff --git a/models/robots/juno/STEP/.hip_bracket_left.step.glb b/models/robots/juno/STEP/.hip_bracket_left.step.glb deleted file mode 100644 index 66fac6e2..00000000 --- a/models/robots/juno/STEP/.hip_bracket_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ab95c5208d024cf0351ea84cb8588c2f4f82d4439ce1de4ee3c2e66155948f4 -size 924936 diff --git a/models/robots/juno/STEP/.hip_bracket_right.step.glb b/models/robots/juno/STEP/.hip_bracket_right.step.glb deleted file mode 100644 index c18db32c..00000000 --- a/models/robots/juno/STEP/.hip_bracket_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:764c32817d581a22ae8425830e26c5fdfe98f7030977c858e65039c0139564b8 -size 924948 diff --git a/models/robots/juno/STEP/.hip_carrier_left.step.glb b/models/robots/juno/STEP/.hip_carrier_left.step.glb deleted file mode 100644 index 15a43483..00000000 --- a/models/robots/juno/STEP/.hip_carrier_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d34d9a28e71d8ad3429856d6b462fe030299f8d72aeaa9828558a3d2164566b -size 213620 diff --git a/models/robots/juno/STEP/.hip_carrier_right.step.glb b/models/robots/juno/STEP/.hip_carrier_right.step.glb deleted file mode 100644 index a3974c36..00000000 --- a/models/robots/juno/STEP/.hip_carrier_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0aaf659fca402ed6f39da34e79535c564337d058fc226dda958c9f1a213510f3 -size 213416 diff --git a/models/robots/juno/STEP/.neck_collar.step.glb b/models/robots/juno/STEP/.neck_collar.step.glb deleted file mode 100644 index d1fb49cc..00000000 --- a/models/robots/juno/STEP/.neck_collar.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8eefab1919d1cf14524a0429d975e5a4fda6d5ba8f0758ecc198e8ca07e91b40 -size 1733516 diff --git a/models/robots/juno/STEP/.pelvis.step.glb b/models/robots/juno/STEP/.pelvis.step.glb deleted file mode 100644 index fcd67da9..00000000 --- a/models/robots/juno/STEP/.pelvis.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1297db6ea6beb7eec93606f3c4bb2deea1967c487079781a48b5b9ccb8942a8a -size 1177936 diff --git a/models/robots/juno/STEP/.shin_left.step.glb b/models/robots/juno/STEP/.shin_left.step.glb deleted file mode 100644 index bdb48094..00000000 --- a/models/robots/juno/STEP/.shin_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f83bcb2e31f2528eb55b81bd1b2cfab3b3de4b5baa86799e30a4be66e4587da0 -size 1543196 diff --git a/models/robots/juno/STEP/.shin_right.step.glb b/models/robots/juno/STEP/.shin_right.step.glb deleted file mode 100644 index da0fa1b6..00000000 --- a/models/robots/juno/STEP/.shin_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f75728ab64cda33e9b3ec1b603a1a70ebcdc8a0d75da91ddb8d6234cf8366d98 -size 1542404 diff --git a/models/robots/juno/STEP/.shoulder_pod_left.step.glb b/models/robots/juno/STEP/.shoulder_pod_left.step.glb deleted file mode 100644 index beeb9aff..00000000 --- a/models/robots/juno/STEP/.shoulder_pod_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e72b92e89d0dab81a38f9131136dca710b5f49be4540bab029897b9324a3c91 -size 836400 diff --git a/models/robots/juno/STEP/.shoulder_pod_right.step.glb b/models/robots/juno/STEP/.shoulder_pod_right.step.glb deleted file mode 100644 index 71bd3554..00000000 --- a/models/robots/juno/STEP/.shoulder_pod_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dbc414bc1eb73bdaf467531f96acf0f4c63546253bf663d6adfb9737a7924c38 -size 836836 diff --git a/models/robots/juno/STEP/.thigh_left.step.glb b/models/robots/juno/STEP/.thigh_left.step.glb deleted file mode 100644 index 75eee29c..00000000 --- a/models/robots/juno/STEP/.thigh_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:26ee37f3217bce936999c63d3c6c41e6b1e4c4b91f1ece389da17040d069b6ac -size 1654180 diff --git a/models/robots/juno/STEP/.thigh_right.step.glb b/models/robots/juno/STEP/.thigh_right.step.glb deleted file mode 100644 index 4fff1a96..00000000 --- a/models/robots/juno/STEP/.thigh_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:97b916efcb7b4ab78600370220567a6a8642492cc09f82c6a6bf6459b32b6967 -size 1653476 diff --git a/models/robots/juno/STEP/.torso.step.glb b/models/robots/juno/STEP/.torso.step.glb deleted file mode 100644 index 170f00c2..00000000 --- a/models/robots/juno/STEP/.torso.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:041591a2eec4aa4a61b95bb664a3de50a08a376810364b768d209cdd9d21b242 -size 4758276 diff --git a/models/robots/juno/STEP/.wrist_carrier_left.step.glb b/models/robots/juno/STEP/.wrist_carrier_left.step.glb deleted file mode 100644 index 9551c8b4..00000000 --- a/models/robots/juno/STEP/.wrist_carrier_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b02a681346eeaa56a4a544dee3bedd753e4f39cb875657080df1c358fcc05c35 -size 731860 diff --git a/models/robots/juno/STEP/.wrist_carrier_right.step.glb b/models/robots/juno/STEP/.wrist_carrier_right.step.glb deleted file mode 100644 index 5bc44700..00000000 --- a/models/robots/juno/STEP/.wrist_carrier_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4cdf37eee76335cc5e0be5a96d69c4c949d66cf0ead6affe6a906cfc27caed47 -size 731872 diff --git a/models/robots/juno/STEP/.yaw_housing_left.step.glb b/models/robots/juno/STEP/.yaw_housing_left.step.glb deleted file mode 100644 index e7beb46e..00000000 --- a/models/robots/juno/STEP/.yaw_housing_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a9407c698ebe29a3b78b3f2786f6091837e792df62053e6be9e6d237b9f686e -size 1162776 diff --git a/models/robots/juno/STEP/.yaw_housing_right.step.glb b/models/robots/juno/STEP/.yaw_housing_right.step.glb deleted file mode 100644 index 6c8b7562..00000000 --- a/models/robots/juno/STEP/.yaw_housing_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ecc92d8b8895efa77bcd9b7ebd31eaed39bb07d30fca3cf8b69a6a6b578aca55 -size 1162792 diff --git a/models/robots/juno/STEP/__init__.py b/models/robots/juno/STEP/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/models/robots/juno/STEP/ankle_link_left.py b/models/robots/juno/STEP/ankle_link_left.py deleted file mode 100644 index 346df38c..00000000 --- a/models/robots/juno/STEP/ankle_link_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left ankle link mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_ankle_link("left")` exactly; the URDF -link frame coincides with this part-local frame (ankle link (ankle-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_ankle_link - - -def gen_step(): - return build_ankle_link("left") diff --git a/models/robots/juno/STEP/ankle_link_left.step b/models/robots/juno/STEP/ankle_link_left.step deleted file mode 100644 index ada72f99..00000000 --- a/models/robots/juno/STEP/ankle_link_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54092c21fd7e372e02218318cd10497a8da75434ee0135e7749643552935f9f7 -size 81889 diff --git a/models/robots/juno/STEP/ankle_link_right.py b/models/robots/juno/STEP/ankle_link_right.py deleted file mode 100644 index adc9aff8..00000000 --- a/models/robots/juno/STEP/ankle_link_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right ankle link mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_ankle_link("right")` exactly; the URDF -link frame coincides with this part-local frame (ankle link (ankle-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_ankle_link - - -def gen_step(): - return build_ankle_link("right") diff --git a/models/robots/juno/STEP/ankle_link_right.step b/models/robots/juno/STEP/ankle_link_right.step deleted file mode 100644 index eab53a44..00000000 --- a/models/robots/juno/STEP/ankle_link_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:554803702690830a77918a23b103a3006a28650d58e89272c6f3d680e40f68f9 -size 81893 diff --git a/models/robots/juno/STEP/bicep_left.py b/models/robots/juno/STEP/bicep_left.py deleted file mode 100644 index bc0d9734..00000000 --- a/models/robots/juno/STEP/bicep_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left bicep mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.arms.build_bicep("left")` exactly; the URDF -link frame coincides with this part-local frame (bicep link (shoulder-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.arms import build_bicep - - -def gen_step(): - return build_bicep("left") diff --git a/models/robots/juno/STEP/bicep_left.step b/models/robots/juno/STEP/bicep_left.step deleted file mode 100644 index b7052aad..00000000 --- a/models/robots/juno/STEP/bicep_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff3953d709daf71858058a853d21ea0c2396c818b0086013f0ddfd8ba9da07d5 -size 954982 diff --git a/models/robots/juno/STEP/bicep_right.py b/models/robots/juno/STEP/bicep_right.py deleted file mode 100644 index 124d55d1..00000000 --- a/models/robots/juno/STEP/bicep_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right bicep mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.arms.build_bicep("right")` exactly; the URDF -link frame coincides with this part-local frame (bicep link (shoulder-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.arms import build_bicep - - -def gen_step(): - return build_bicep("right") diff --git a/models/robots/juno/STEP/bicep_right.step b/models/robots/juno/STEP/bicep_right.step deleted file mode 100644 index f21fc38d..00000000 --- a/models/robots/juno/STEP/bicep_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1131f34e7536d6ed410963efb99c0be1d66c65b18602f0c047287b72dabe25f2 -size 955009 diff --git a/models/robots/juno/STEP/foot_left.py b/models/robots/juno/STEP/foot_left.py deleted file mode 100644 index 2f00db48..00000000 --- a/models/robots/juno/STEP/foot_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left foot mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_foot("left")` exactly; the URDF -link frame coincides with this part-local frame (foot link (ankle-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_foot - - -def gen_step(): - return build_foot("left") diff --git a/models/robots/juno/STEP/foot_left.step b/models/robots/juno/STEP/foot_left.step deleted file mode 100644 index b5fc6d3c..00000000 --- a/models/robots/juno/STEP/foot_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb1a27145edda97a67447ec8faeb9784ee896062ff978470bb896de99ef7a6e7 -size 1056745 diff --git a/models/robots/juno/STEP/foot_right.py b/models/robots/juno/STEP/foot_right.py deleted file mode 100644 index c5629156..00000000 --- a/models/robots/juno/STEP/foot_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right foot mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_foot("right")` exactly; the URDF -link frame coincides with this part-local frame (foot link (ankle-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_foot - - -def gen_step(): - return build_foot("right") diff --git a/models/robots/juno/STEP/foot_right.step b/models/robots/juno/STEP/foot_right.step deleted file mode 100644 index 6d87a5a9..00000000 --- a/models/robots/juno/STEP/foot_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:db8754c3976a94a3f8cf4a60a03b88901f7b347fe5cc4e2180960896db12da8e -size 1063487 diff --git a/models/robots/juno/STEP/forearm_left.py b/models/robots/juno/STEP/forearm_left.py deleted file mode 100644 index c17c116e..00000000 --- a/models/robots/juno/STEP/forearm_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left forearm mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.arms.build_forearm("left")` exactly; the URDF -link frame coincides with this part-local frame (forearm link (elbow joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.arms import build_forearm - - -def gen_step(): - return build_forearm("left") diff --git a/models/robots/juno/STEP/forearm_left.step b/models/robots/juno/STEP/forearm_left.step deleted file mode 100644 index 0ce49c19..00000000 --- a/models/robots/juno/STEP/forearm_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0041aa340d6fe0f508273baea55ba3140b4329b5c415dc44c47acab722d882fd -size 1254594 diff --git a/models/robots/juno/STEP/forearm_right.py b/models/robots/juno/STEP/forearm_right.py deleted file mode 100644 index aef79fbe..00000000 --- a/models/robots/juno/STEP/forearm_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right forearm mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.arms.build_forearm("right")` exactly; the URDF -link frame coincides with this part-local frame (forearm link (elbow joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.arms import build_forearm - - -def gen_step(): - return build_forearm("right") diff --git a/models/robots/juno/STEP/forearm_right.step b/models/robots/juno/STEP/forearm_right.step deleted file mode 100644 index 625af070..00000000 --- a/models/robots/juno/STEP/forearm_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2a5f65b07179cb42f574d979d0ae1510229e56586f9d2b06ee0abdc57e74e3bc -size 1257067 diff --git a/models/robots/juno/STEP/hand_left.py b/models/robots/juno/STEP/hand_left.py deleted file mode 100644 index 54cdafaa..00000000 --- a/models/robots/juno/STEP/hand_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left hand mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.hand.build_hand("left")` exactly; the URDF -link frame coincides with this part-local frame (hand link (wrist-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.hand import build_hand - - -def gen_step(): - return build_hand("left") diff --git a/models/robots/juno/STEP/hand_left.step b/models/robots/juno/STEP/hand_left.step deleted file mode 100644 index 362fb287..00000000 --- a/models/robots/juno/STEP/hand_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:347cea5d1273261f8806089ca520bd2e5dacd3be5eeb4c25c27d4c0f73c7aabe -size 2251992 diff --git a/models/robots/juno/STEP/hand_right.py b/models/robots/juno/STEP/hand_right.py deleted file mode 100644 index 8cb673b7..00000000 --- a/models/robots/juno/STEP/hand_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right hand mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.hand.build_hand("right")` exactly; the URDF -link frame coincides with this part-local frame (hand link (wrist-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.hand import build_hand - - -def gen_step(): - return build_hand("right") diff --git a/models/robots/juno/STEP/hand_right.step b/models/robots/juno/STEP/hand_right.step deleted file mode 100644 index d59e814c..00000000 --- a/models/robots/juno/STEP/hand_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b3d02f608e504519bd4e19437e7369a0f3bd654aebb6200879374d56c2f53156 -size 2101231 diff --git a/models/robots/juno/STEP/head.py b/models/robots/juno/STEP/head.py deleted file mode 100644 index 9c1ef8da..00000000 --- a/models/robots/juno/STEP/head.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Head mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.head.build_head()` exactly; the URDF -link frame coincides with this part-local frame (head link (neck-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.head import build_head - - -def gen_step(): - return build_head() diff --git a/models/robots/juno/STEP/head.step b/models/robots/juno/STEP/head.step deleted file mode 100644 index 01956d4b..00000000 --- a/models/robots/juno/STEP/head.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a50a7e8220b9c01bad82ebf7ac8b351cca8b73cecdc337c5e537ec9cdff2f314 -size 3592317 diff --git a/models/robots/juno/STEP/hip_bracket_left.py b/models/robots/juno/STEP/hip_bracket_left.py deleted file mode 100644 index fcff0225..00000000 --- a/models/robots/juno/STEP/hip_bracket_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left hip bracket mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_hip_bracket("left")` exactly; the URDF -link frame coincides with this part-local frame (hip bracket link (hip-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_hip_bracket - - -def gen_step(): - return build_hip_bracket("left") diff --git a/models/robots/juno/STEP/hip_bracket_left.step b/models/robots/juno/STEP/hip_bracket_left.step deleted file mode 100644 index 2c5b95ac..00000000 --- a/models/robots/juno/STEP/hip_bracket_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec79c608c9577394e119a0976e3b0e607d193cfddcce5ac56ae4ed474eb6de49 -size 88571 diff --git a/models/robots/juno/STEP/hip_bracket_right.py b/models/robots/juno/STEP/hip_bracket_right.py deleted file mode 100644 index 438e6baa..00000000 --- a/models/robots/juno/STEP/hip_bracket_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right hip bracket mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_hip_bracket("right")` exactly; the URDF -link frame coincides with this part-local frame (hip bracket link (hip-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_hip_bracket - - -def gen_step(): - return build_hip_bracket("right") diff --git a/models/robots/juno/STEP/hip_bracket_right.step b/models/robots/juno/STEP/hip_bracket_right.step deleted file mode 100644 index b3e18507..00000000 --- a/models/robots/juno/STEP/hip_bracket_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:978ed16bd8d7e807cfe6f5a637a4c8f18e0d9345a80ab6fa81b44e21ac81088a -size 88575 diff --git a/models/robots/juno/STEP/hip_carrier_left.py b/models/robots/juno/STEP/hip_carrier_left.py deleted file mode 100644 index 016ae0f6..00000000 --- a/models/robots/juno/STEP/hip_carrier_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left hip carrier mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_hip_carrier("left")` exactly; the URDF -link frame coincides with this part-local frame (hip carrier link (hip-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_hip_carrier - - -def gen_step(): - return build_hip_carrier("left") diff --git a/models/robots/juno/STEP/hip_carrier_left.step b/models/robots/juno/STEP/hip_carrier_left.step deleted file mode 100644 index 07dc1d57..00000000 --- a/models/robots/juno/STEP/hip_carrier_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:992f18498ad43fc60c72b48d33785591971b1fbf6167bed5414feafdf3c52867 -size 69326 diff --git a/models/robots/juno/STEP/hip_carrier_right.py b/models/robots/juno/STEP/hip_carrier_right.py deleted file mode 100644 index 1de2132e..00000000 --- a/models/robots/juno/STEP/hip_carrier_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right hip carrier mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_hip_carrier("right")` exactly; the URDF -link frame coincides with this part-local frame (hip carrier link (hip-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_hip_carrier - - -def gen_step(): - return build_hip_carrier("right") diff --git a/models/robots/juno/STEP/hip_carrier_right.step b/models/robots/juno/STEP/hip_carrier_right.step deleted file mode 100644 index f38cdf10..00000000 --- a/models/robots/juno/STEP/hip_carrier_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:453deef90f904bf11e98ac023801a6253070105c5d895b2ca9469bbdd7ba7b4c -size 69279 diff --git a/models/robots/juno/STEP/neck_collar.py b/models/robots/juno/STEP/neck_collar.py deleted file mode 100644 index 1f31503f..00000000 --- a/models/robots/juno/STEP/neck_collar.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Neck collar mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_neck_collar()` exactly; the URDF -link frame coincides with this part-local frame (neck collar link (neck-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_neck_collar - - -def gen_step(): - return build_neck_collar() diff --git a/models/robots/juno/STEP/neck_collar.step b/models/robots/juno/STEP/neck_collar.step deleted file mode 100644 index cbb8c6f1..00000000 --- a/models/robots/juno/STEP/neck_collar.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:609475438f3fec76e8feea723658739e70d665d6da9b6426a002352e80d97236 -size 134209 diff --git a/models/robots/juno/STEP/pelvis.py b/models/robots/juno/STEP/pelvis.py deleted file mode 100644 index 582a94c2..00000000 --- a/models/robots/juno/STEP/pelvis.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -"""Pelvis link mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.pelvis.build_pelvis()` exactly; the URDF -link frame coincides with this part-local frame (waist-yaw joint center at -the origin). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.pelvis import build_pelvis - - -def gen_step(): - return build_pelvis() diff --git a/models/robots/juno/STEP/pelvis.step b/models/robots/juno/STEP/pelvis.step deleted file mode 100644 index bc4f99e7..00000000 --- a/models/robots/juno/STEP/pelvis.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3454b540b531b8da1f3c08224cd0e434ba363eb25a6ea838c7d0bc985fa1dcf7 -size 810774 diff --git a/models/robots/juno/STEP/shin_left.py b/models/robots/juno/STEP/shin_left.py deleted file mode 100644 index 0b891477..00000000 --- a/models/robots/juno/STEP/shin_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left shin mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_shin("left")` exactly; the URDF -link frame coincides with this part-local frame (shin link (knee joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_shin - - -def gen_step(): - return build_shin("left") diff --git a/models/robots/juno/STEP/shin_left.step b/models/robots/juno/STEP/shin_left.step deleted file mode 100644 index a6448bf9..00000000 --- a/models/robots/juno/STEP/shin_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58dcdc673867d4dba775d7862afc8cbd358c89aeaef922fd208f58f268482cf7 -size 622355 diff --git a/models/robots/juno/STEP/shin_right.py b/models/robots/juno/STEP/shin_right.py deleted file mode 100644 index 541c6642..00000000 --- a/models/robots/juno/STEP/shin_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right shin mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_shin("right")` exactly; the URDF -link frame coincides with this part-local frame (shin link (knee joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_shin - - -def gen_step(): - return build_shin("right") diff --git a/models/robots/juno/STEP/shin_right.step b/models/robots/juno/STEP/shin_right.step deleted file mode 100644 index bf6633d4..00000000 --- a/models/robots/juno/STEP/shin_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:966b897621fa0d177763a10d0e528346488df667d74930a6ca63cfeaeea59787 -size 645615 diff --git a/models/robots/juno/STEP/shoulder_pod_left.py b/models/robots/juno/STEP/shoulder_pod_left.py deleted file mode 100644 index d6c87286..00000000 --- a/models/robots/juno/STEP/shoulder_pod_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left shoulder pod mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_shoulder_pod("left")` exactly; the URDF -link frame coincides with this part-local frame (shoulder pod link (shoulder-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_shoulder_pod - - -def gen_step(): - return build_shoulder_pod("left") diff --git a/models/robots/juno/STEP/shoulder_pod_left.step b/models/robots/juno/STEP/shoulder_pod_left.step deleted file mode 100644 index 9d91241c..00000000 --- a/models/robots/juno/STEP/shoulder_pod_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:db4a914cbd1f7257c4422aab270d442e5689a75640dfe39f07669c08a671eb39 -size 171644 diff --git a/models/robots/juno/STEP/shoulder_pod_right.py b/models/robots/juno/STEP/shoulder_pod_right.py deleted file mode 100644 index 37ff841b..00000000 --- a/models/robots/juno/STEP/shoulder_pod_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right shoulder pod mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_shoulder_pod("right")` exactly; the URDF -link frame coincides with this part-local frame (shoulder pod link (shoulder-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_shoulder_pod - - -def gen_step(): - return build_shoulder_pod("right") diff --git a/models/robots/juno/STEP/shoulder_pod_right.step b/models/robots/juno/STEP/shoulder_pod_right.step deleted file mode 100644 index 96f8f08f..00000000 --- a/models/robots/juno/STEP/shoulder_pod_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:58871bb2d1aa77b415fdb1d954513a887bf4d8cb2124d6a617f218e88040b8e6 -size 171694 diff --git a/models/robots/juno/STEP/thigh_left.py b/models/robots/juno/STEP/thigh_left.py deleted file mode 100644 index 6b904fc8..00000000 --- a/models/robots/juno/STEP/thigh_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left thigh mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_thigh("left")` exactly; the URDF -link frame coincides with this part-local frame (thigh link (hip-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_thigh - - -def gen_step(): - return build_thigh("left") diff --git a/models/robots/juno/STEP/thigh_left.step b/models/robots/juno/STEP/thigh_left.step deleted file mode 100644 index 4b2210e6..00000000 --- a/models/robots/juno/STEP/thigh_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc11e9c72fe370c5c854be27ba23fa30c8546067bb2f06647b16618e16ec7f59 -size 682272 diff --git a/models/robots/juno/STEP/thigh_right.py b/models/robots/juno/STEP/thigh_right.py deleted file mode 100644 index 61a26d0a..00000000 --- a/models/robots/juno/STEP/thigh_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right thigh mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.legs.build_thigh("right")` exactly; the URDF -link frame coincides with this part-local frame (thigh link (hip-pitch joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.legs import build_thigh - - -def gen_step(): - return build_thigh("right") diff --git a/models/robots/juno/STEP/thigh_right.step b/models/robots/juno/STEP/thigh_right.step deleted file mode 100644 index e8946238..00000000 --- a/models/robots/juno/STEP/thigh_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:02e2bb616e603ebc3c313df267808fa8ea2798f61c005e01610cd983713a1256 -size 701432 diff --git a/models/robots/juno/STEP/torso.py b/models/robots/juno/STEP/torso.py deleted file mode 100644 index b81ee6a1..00000000 --- a/models/robots/juno/STEP/torso.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Torso mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.torso.build_torso()` exactly; the URDF -link frame coincides with this part-local frame (torso link (waist-yaw joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.torso import build_torso - - -def gen_step(): - return build_torso() diff --git a/models/robots/juno/STEP/torso.step b/models/robots/juno/STEP/torso.step deleted file mode 100644 index d52581b8..00000000 --- a/models/robots/juno/STEP/torso.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1aed69fa1e28f952623dd13f7325e6dde0248594d8cd4c10b80a32b7912416f4 -size 4814277 diff --git a/models/robots/juno/STEP/wrist_carrier_left.py b/models/robots/juno/STEP/wrist_carrier_left.py deleted file mode 100644 index 4114ee2e..00000000 --- a/models/robots/juno/STEP/wrist_carrier_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left wrist carrier mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_wrist_carrier("left")` exactly; the URDF -link frame coincides with this part-local frame (wrist carrier link (wrist-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_wrist_carrier - - -def gen_step(): - return build_wrist_carrier("left") diff --git a/models/robots/juno/STEP/wrist_carrier_left.step b/models/robots/juno/STEP/wrist_carrier_left.step deleted file mode 100644 index 5bc19ae9..00000000 --- a/models/robots/juno/STEP/wrist_carrier_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b872d27689ed7c94d03bd9043609b3c142601e774b89f127924b955f1d8215b -size 101899 diff --git a/models/robots/juno/STEP/wrist_carrier_right.py b/models/robots/juno/STEP/wrist_carrier_right.py deleted file mode 100644 index 5b4e7389..00000000 --- a/models/robots/juno/STEP/wrist_carrier_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right wrist carrier mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_wrist_carrier("right")` exactly; the URDF -link frame coincides with this part-local frame (wrist carrier link (wrist-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_wrist_carrier - - -def gen_step(): - return build_wrist_carrier("right") diff --git a/models/robots/juno/STEP/wrist_carrier_right.step b/models/robots/juno/STEP/wrist_carrier_right.step deleted file mode 100644 index f6c91b71..00000000 --- a/models/robots/juno/STEP/wrist_carrier_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c238eb6c0034d54e7d5acaf48257a78d08c1ac17f9302979fc81d0a85b10779 -size 101903 diff --git a/models/robots/juno/STEP/yaw_housing_left.py b/models/robots/juno/STEP/yaw_housing_left.py deleted file mode 100644 index 1a6561c8..00000000 --- a/models/robots/juno/STEP/yaw_housing_left.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Left yaw housing mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_yaw_housing("left")` exactly; the URDF -link frame coincides with this part-local frame (yaw housing link (shoulder-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_yaw_housing - - -def gen_step(): - return build_yaw_housing("left") diff --git a/models/robots/juno/STEP/yaw_housing_left.step b/models/robots/juno/STEP/yaw_housing_left.step deleted file mode 100644 index 5dd49789..00000000 --- a/models/robots/juno/STEP/yaw_housing_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2cc1376b7d91713c36342918634c602ec7007d3001e3218f3c2522a991629551 -size 110104 diff --git a/models/robots/juno/STEP/yaw_housing_right.py b/models/robots/juno/STEP/yaw_housing_right.py deleted file mode 100644 index d01d801a..00000000 --- a/models/robots/juno/STEP/yaw_housing_right.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""Right yaw housing mesh source for the juno URDF (part-local frame, mm). - -The compound matches `juno_parts.joints.build_yaw_housing("right")` exactly; the URDF -link frame coincides with this part-local frame (yaw housing link (shoulder-roll joint center at the origin)). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -JUNO_ROOT = Path(__file__).resolve().parents[1] -if str(JUNO_ROOT) not in sys.path: - sys.path.insert(0, str(JUNO_ROOT)) - -from juno_parts.joints import build_yaw_housing - - -def gen_step(): - return build_yaw_housing("right") diff --git a/models/robots/juno/STEP/yaw_housing_right.step b/models/robots/juno/STEP/yaw_housing_right.step deleted file mode 100644 index 9afe4642..00000000 --- a/models/robots/juno/STEP/yaw_housing_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc99432be6c4848cfd4f3b4da813585e6230f5524171efa26cdd0ec15f5a4525 -size 110108 diff --git a/models/robots/juno/juno.py b/models/robots/juno/juno.py deleted file mode 100644 index 1c1702a8..00000000 --- a/models/robots/juno/juno.py +++ /dev/null @@ -1,201 +0,0 @@ -"""juno — compact humanoid robotics platform concept. - -A sleek research humanoid with Unitree-G1-like proportions: ~1.40 m tall, -athletic stance, exposed cylindrical actuator modules at every joint, -warm-porcelain composite shells over graphite structure with machined -aluminum joint rims, coral accents on repeated functional details, a gloss -midnight-blue sensor visor with cyan pixel-grid eyes, and dexterous -five-digit hands. No logos. - -Degrees of freedom (27 body DOF, statically posed): - - each leg (x2): hip yaw, hip roll, hip pitch, knee pitch, - ankle pitch, ankle roll -> 12 - - each arm (x2): shoulder pitch, shoulder roll, shoulder yaw, - elbow pitch, wrist roll, wrist pitch -> 12 - - waist yaw -> 1 - - neck yaw, neck pitch -> 2 - Hands add cosmetic posed finger articulation (not counted). - -Coordinates: pelvis waist-yaw joint center = world origin, +X forward, -+Y robot-left, +Z up. Soles rest near z = -876 in the athletic stance. - -Chain offsets (parent-local joint origins, mm): - pelvis: waist yaw (0,0,0); hip yaw (0,+-90,-120) - bracket: hip roll (0,0,-64) - carrier: hip pitch (0,0,-78) - thigh: knee (0,0,-290) - shin: ankle pitch (0,0,-290) - ankle: ankle roll (0,0,-30) - foot: sole 26 below origin - torso: shoulder pitch (0,+-148,290); neck yaw (0,0,324) - pod: shoulder roll (0, s*34, -72) - housing: shoulder yaw (0,0,-24) - bicep: elbow (0,0,-156) - forearm: wrist roll (0,0,-150) - wrist: wrist pitch (0,0,-28) - collar: neck pitch (0,0,46) - -Chain offsets and pose angles are shared with the URDF/SRDF generators via -juno_parts/chain.py; gen_urdf()/gen_srdf() emit juno.urdf/juno.srdf from -the same spec (see juno_parts/description.py). -""" - -from __future__ import annotations - -from build123d import Compound - -from cadpy.assembly import AssemblyHelper - -from juno_parts import chain -from juno_parts.arms import build_bicep, build_forearm -from juno_parts.hand import build_hand -from juno_parts.head import build_head -from juno_parts.joints import ( - build_ankle_link, - build_hip_bracket, - build_hip_carrier, - build_neck_collar, - build_shoulder_pod, - build_wrist_carrier, - build_yaw_housing, -) -from juno_parts.juno_lib import revolute_attach -from juno_parts.legs import build_foot, build_shin, build_thigh -from juno_parts.pelvis import build_pelvis -from juno_parts.torso import build_torso - -# ----------------------------------------------------------- pose (degrees) -# Athletic ready stance: knees bent, feet flat, arms relaxed forward. -# Pose angles and chain offsets are shared with the URDF/SRDF generators -# through juno_parts/chain.py; edit them there. -HIP_PITCH_DEG = chain.HIP_PITCH_DEG -KNEE_DEG = chain.KNEE_DEG -ANKLE_PITCH_DEG = chain.ANKLE_PITCH_DEG -HIP_ROLL_ABDUCT_DEG = chain.HIP_ROLL_ABDUCT_DEG -HIP_YAW_DEG = chain.HIP_YAW_DEG -WAIST_YAW_DEG = chain.WAIST_YAW_DEG -SHOULDER_PITCH_DEG = chain.SHOULDER_PITCH_DEG -SHOULDER_ROLL_ABDUCT_DEG = chain.SHOULDER_ROLL_ABDUCT_DEG -SHOULDER_YAW_INTERNAL_DEG = chain.SHOULDER_YAW_INTERNAL_DEG -ELBOW_DEG = chain.ELBOW_DEG -WRIST_ROLL_DEG = chain.WRIST_ROLL_DEG -WRIST_PITCH_DEG = chain.WRIST_PITCH_DEG -NECK_YAW_DEG = chain.NECK_YAW_DEG -NECK_PITCH_DEG = chain.NECK_PITCH_DEG - -HIP_Y = chain.HIP_Y_MM -SHOULDER_Y = chain.SHOULDER_Y_MM - -X = chain.X_AXIS -Y = chain.Y_AXIS -Z = chain.Z_AXIS - -_s = chain.side_sign - - -def assemble() -> Compound: - asm = AssemblyHelper("juno") - - pelvis = asm.add(build_pelvis(), "pelvis") - torso = asm.add(build_torso(), "torso") - revolute_attach( - asm, pelvis, torso, "waist_yaw", - chain.WAIST_YAW_ORIGIN_MM, Z, X, (0, 0, 0), Z, X, WAIST_YAW_DEG, - ) - - collar = asm.add(build_neck_collar(), "neck_collar") - revolute_attach( - asm, torso, collar, "neck_yaw", - chain.NECK_YAW_ORIGIN_MM, Z, X, (0, 0, 0), Z, X, NECK_YAW_DEG, - ) - head = asm.add(build_head(), "head") - revolute_attach( - asm, collar, head, "neck_pitch", - chain.NECK_PITCH_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, NECK_PITCH_DEG, - ) - - for side in ("left", "right"): - s = _s(side) - - # ---- leg chain (6 DOF) - bracket = asm.add(build_hip_bracket(side), f"hip_bracket_{side}") - revolute_attach( - asm, pelvis, bracket, f"hip_yaw_{side}", - (0, s * HIP_Y, chain.HIP_YAW_DROP_Z_MM), Z, X, (0, 0, 0), Z, X, HIP_YAW_DEG, - ) - carrier = asm.add(build_hip_carrier(side), f"hip_carrier_{side}") - revolute_attach( - asm, bracket, carrier, f"hip_roll_{side}", - chain.HIP_ROLL_ORIGIN_MM, X, Y, (0, 0, 0), X, Y, s * HIP_ROLL_ABDUCT_DEG, - ) - thigh = asm.add(build_thigh(side), f"thigh_{side}") - revolute_attach( - asm, carrier, thigh, f"hip_pitch_{side}", - chain.HIP_PITCH_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, HIP_PITCH_DEG, - ) - shin = asm.add(build_shin(side), f"shin_{side}") - revolute_attach( - asm, thigh, shin, f"knee_{side}", - chain.KNEE_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, KNEE_DEG, - ) - ankle = asm.add(build_ankle_link(side), f"ankle_link_{side}") - revolute_attach( - asm, shin, ankle, f"ankle_pitch_{side}", - chain.ANKLE_PITCH_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, ANKLE_PITCH_DEG, - ) - foot = asm.add(build_foot(side), f"foot_{side}") - revolute_attach( - asm, ankle, foot, f"ankle_roll_{side}", - chain.ANKLE_ROLL_ORIGIN_MM, X, Y, (0, 0, 0), X, Y, -s * HIP_ROLL_ABDUCT_DEG, - ) - - # ---- arm chain (6 DOF) - pod = asm.add(build_shoulder_pod(side), f"shoulder_pod_{side}") - revolute_attach( - asm, torso, pod, f"shoulder_pitch_{side}", - (0, s * SHOULDER_Y, chain.SHOULDER_PITCH_RAISE_Z_MM), Y, X, (0, 0, 0), Y, X, SHOULDER_PITCH_DEG, - ) - housing = asm.add(build_yaw_housing(side), f"yaw_housing_{side}") - revolute_attach( - asm, pod, housing, f"shoulder_roll_{side}", - (0, s * chain.SHOULDER_ROLL_Y_MM, chain.SHOULDER_ROLL_Z_MM), X, Y, - (0, 0, 0), X, Y, s * SHOULDER_ROLL_ABDUCT_DEG, - ) - bicep = asm.add(build_bicep(side), f"bicep_{side}") - revolute_attach( - asm, housing, bicep, f"shoulder_yaw_{side}", - chain.SHOULDER_YAW_ORIGIN_MM, Z, X, (0, 0, 0), Z, X, -s * SHOULDER_YAW_INTERNAL_DEG, - ) - forearm = asm.add(build_forearm(side), f"forearm_{side}") - revolute_attach( - asm, bicep, forearm, f"elbow_{side}", - chain.ELBOW_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, ELBOW_DEG, - ) - wrist = asm.add(build_wrist_carrier(side), f"wrist_carrier_{side}") - revolute_attach( - asm, forearm, wrist, f"wrist_roll_{side}", - chain.WRIST_ROLL_ORIGIN_MM, Z, X, (0, 0, 0), Z, X, WRIST_ROLL_DEG, - ) - hand = asm.add(build_hand(side), f"hand_{side}") - revolute_attach( - asm, wrist, hand, f"wrist_pitch_{side}", - chain.WRIST_PITCH_ORIGIN_MM, Y, X, (0, 0, 0), Y, X, WRIST_PITCH_DEG, - ) - - return asm.build() - - -def gen_step(): - return assemble() - - -def gen_urdf(): - from juno_parts.description import build_urdf - - return {"xml": build_urdf()} - - -def gen_srdf(): - from juno_parts.description import build_srdf - - return {"xml": build_srdf(), "urdf": "juno.urdf"} diff --git a/models/robots/juno/juno.srdf b/models/robots/juno/juno.srdf deleted file mode 100644 index a0f5a6d3..00000000 --- a/models/robots/juno/juno.srdf +++ /dev/null @@ -1,223 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/juno/juno.step b/models/robots/juno/juno.step deleted file mode 100644 index 9bdf7466..00000000 --- a/models/robots/juno/juno.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:580a8a4ade5ab1b8887cb88a73689de76b0cb7f56b989fda856a832fa00e7aa4 -size 25103342 diff --git a/models/robots/juno/juno.urdf b/models/robots/juno/juno.urdf deleted file mode 100644 index a3b1b313..00000000 --- a/models/robots/juno/juno.urdf +++ /dev/null @@ -1,734 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/juno/juno_parts/__init__.py b/models/robots/juno/juno_parts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/models/robots/juno/juno_parts/arms.py b/models/robots/juno/juno_parts/arms.py deleted file mode 100644 index d105ce22..00000000 --- a/models/robots/juno/juno_parts/arms.py +++ /dev/null @@ -1,228 +0,0 @@ -"""juno humanoid - arm segment builders (bicep, forearm). - -Local frames (units mm, robot faces +X, +Y robot-left, +Z up): -- bicep: origin = shoulder-yaw joint center at the TOP of the bicep, - segment extends down -Z; elbow axis = Y through (0, 0, -156). -- forearm: origin = elbow joint center; wrist-roll seam at z = -150. - -Styling: graphite structural core with ice-gray clamshell panels split -front/back (graphite reveal strips on the +-Y sides), machined-aluminum -joint rims, exposed cylindrical elbow actuator. -""" - -from __future__ import annotations - -import math # noqa: F401 (kept per module convention) -import sys -from pathlib import Path - - - -from build123d import ( - Axis, - Box, - Circle, - Cylinder, - Ellipse, - Plane, - Pos, - RectangleRounded, - Rot, - chamfer, - fillet, - loft, -) - -from .juno_lib import ( - ALU_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - actuator_solids, - part_compound, - styled, -) - - -# ---------------------------------------------------------------- helpers - - -def _loft(specs): - """Loft elliptical sections; specs = [(z, half_x, half_y), ...].""" - return loft([Plane.XY.offset(z) * Ellipse(rx, ry) for z, rx, ry in specs]) - - -def _try_fillet(solid, edges, radius): - try: - return fillet(edges, radius) - except Exception: - return solid - - -def _try_chamfer(solid, edges, length): - try: - return chamfer(edges, length) - except Exception: - return solid - - -def _ring_groove(solid, z, r_out, depth=0.9, width=1.5): - """Shallow machined groove around a Z-axis ring at height z.""" - try: - g = Pos(0, 0, z) * ( - Cylinder(radius=r_out + 1.0, height=width) - - Cylinder(radius=r_out - depth, height=width) - ) - return solid - g - except Exception: - return solid - - -def _y_cyl(y_center, z_center, radius, height): - """Cylinder whose axis is Y, centered at (0, y_center, z_center).""" - return Pos(0, y_center, z_center) * Rot(-90, 0, 0) * Cylinder( - radius=radius, height=height - ) - - -# ---------------------------------------------------------------- bicep - - -def build_bicep(side: str): - s = 1.0 if side == "left" else -1.0 - p = f"bicep_{side}" - solids = [] - - # -- shoulder-yaw rotation ring (LOCKED: dia 56, z 0..-10) -- - ring = Pos(0, 0, -5.0) * Cylinder(radius=28.0, height=10.0) - ring = _try_chamfer(ring, ring.edges(), 0.8) - solids.append(styled(ring, f"{p}_yaw_ring", ALU_COLOR)) - - # -- upper-arm capsule: graphite core + clamshell panels (dia ~54 -> 46) -- - outer_specs = [ - (-10.0, 26.5, 26.5), - (-44.0, 27.4, 27.4), - (-78.0, 25.2, 25.2), - (-106.0, 23.0, 23.0), - ] - core_specs = [(z, rx - 2.5, ry - 2.5) for z, rx, ry in outer_specs] - core = _loft(core_specs) - outer = _loft(outer_specs) - blank = outer - core - seam = 3.2 # graphite reveal half-width on each side - front = blank & (Pos(45.0 + seam, 0, -58.0) * Box(90, 90, 130)) - rear = blank & (Pos(-45.0 - seam, 0, -58.0) * Box(90, 90, 130)) - front = _try_fillet(front, front.edges(), 0.8) - rear = _try_fillet(rear, rear.edges(), 0.8) - solids.append(styled(core, f"{p}_core", STRUCT_COLOR)) - solids.append(styled(front, f"{p}_shell_front", SHELL_COLOR)) - solids.append(styled(rear, f"{p}_shell_rear", SHELL_COLOR)) - - # -- distal elbow fork (LOCKED: axis Y @ (0,0,-156), plates |y| 28..36, - # bosses dia 44 @ (0,+-32,-156), blends in by z -110) -- - trans = loft( - [ - Plane.XY.offset(-108.0) * Circle(22.9), - Plane.XY.offset(-120.0) * RectangleRounded(44.0, 54.0, 14.0), - Plane.XY.offset(-132.0) * RectangleRounded(50.0, 72.0, 10.0), - ] - ) - fork = trans - for ys in (1.0, -1.0): - slab = Pos(0, ys * 32.0, -144.0) * Box(50.0, 8.0, 24.0) - # soften plate edges on the primitives (post-boolean chamfers on this - # shape hang OCC, so detail is applied before fusing) - slab = _try_fillet(slab, slab.edges().filter_by(Axis.Y), 2.0) - tip = _y_cyl(ys * 32.0, -156.0, 25.0, 8.0) - tip = _try_chamfer(tip, tip.edges(), 1.0) - fork = fork + slab + tip - # clearance arch for the forearm elbow can (dia 62 + margin, |y|<=27.2) - fork = fork - _y_cyl(0.0, -156.0, 34.0, 54.4) - # through-bores for the aluminum joint bosses - for ys in (1.0, -1.0): - fork = fork - _y_cyl(ys * 32.0, -156.0, 22.05, 8.4) - solids.append(styled(fork, f"{p}_fork", STRUCT_COLOR)) - - # -- aluminum elbow bosses (LOCKED dia 44 at (0,+-32,-156)) -- - for ys in (1.0, -1.0): - tag = "outboard" if ys * s > 0 else "inboard" - body = _y_cyl(ys * 32.0, -156.0, 21.9, 8.0) - lip = _y_cyl(ys * 37.0, -156.0, 16.0, 2.0) - boss = body + lip - boss = _try_chamfer(boss, boss.edges(), 0.8) - solids.append(styled(boss, f"{p}_elbow_boss_{tag}", ALU_COLOR)) - - return part_compound(p, solids) - - -# ---------------------------------------------------------------- forearm - - -def build_forearm(side: str): - s = 1.0 if side == "left" else -1.0 - p = f"forearm_{side}" - solids = [] - - # -- exposed elbow actuator (LOCKED: dia 62, len 50, axis Y) -- - # output hub faces outboard (+y on left, -y on right) - act = actuator_solids(f"{p}_elbow", 62.0, 50.0, center=(0, 0, 0), axis=(0, s, 0)) - can, rest = act[0], act[1:] - solids.extend(rest) - - # -- graphite chassis: yoke wrapping the can, tapering to a wrist flare -- - core_specs = [ - (-4.0, 24.0, 16.0), - (-26.0, 26.0, 18.0), - (-72.0, 19.0, 14.5), - (-112.0, 15.5, 12.5), - (-126.0, 15.2, 12.4), - ] - flare = loft( - [ - Plane.XY.offset(-126.0) * Ellipse(15.2, 12.4), - Plane.XY.offset(-140.0) * Ellipse(19.0, 19.0), - ] - ) - chassis = can + _loft(core_specs) + flare - solids.append(styled(chassis, f"{p}_chassis", STRUCT_COLOR)) - - # -- ice-gray clamshell panels over the chassis -- - outer_specs = [ - (-26.0, 28.5, 20.5), - (-72.0, 21.5, 17.0), - (-112.0, 18.0, 15.0), - (-124.0, 17.6, 14.7), - ] - blank = _loft(outer_specs) - chassis - # wrap the panel tops around the actuator with a 2.5mm reveal - blank = blank - _y_cyl(0.0, 0.0, 33.5, 80.0) - seam = 3.0 - front = blank & (Pos(45.0 + seam, 0, -76.0) * Box(90, 80, 110)) - rear = blank & (Pos(-45.0 - seam, 0, -76.0) * Box(90, 80, 110)) - front = _try_fillet(front, front.edges(), 0.8) - rear = _try_fillet(rear, rear.edges(), 0.8) - solids.append(styled(front, f"{p}_shell_front", SHELL_COLOR)) - solids.append(styled(rear, f"{p}_shell_rear", SHELL_COLOR)) - - # -- wrist-roll collar (LOCKED: dia 44, z -140..-150) -- - collar = Pos(0, 0, -145.0) * Cylinder(radius=22.0, height=10.0) - collar = _try_chamfer(collar, collar.edges(), 0.8) - solids.append(styled(collar, f"{p}_wrist_collar", ALU_COLOR)) - - return part_compound(p, solids) - - -# ---------------------------------------------------------------- demo - - -def gen_step(): - layout = [ - (build_bicep("left"), 0.0), - (build_forearm("left"), 220.0), - (build_bicep("right"), 440.0), - (build_forearm("right"), 660.0), - ] - parts = [] - for comp, x in layout: - comp.location = Pos(x, 0, 0) - parts.append(comp) - return part_compound("juno_arms", parts) diff --git a/models/robots/juno/juno_parts/chain.py b/models/robots/juno/juno_parts/chain.py deleted file mode 100644 index 5effea5a..00000000 --- a/models/robots/juno/juno_parts/chain.py +++ /dev/null @@ -1,412 +0,0 @@ -"""juno kinematic chain spec shared by the CAD assembly and URDF/SRDF. - -Stdlib-only (no build123d) so robot-description generators can import it -cheaply. `juno.py` consumes the same offsets and CAD pose angles, keeping -the STEP assembly and the URDF tree in lockstep. - -Design ledger (URDF): -- Units: chain offsets are millimeters in each parent part's local frame; - URDF emission converts to meters/radians. Frames follow REP-103: +X - forward, +Y robot-left, +Z up. The pelvis waist-yaw joint center is the - robot origin; every child link frame sits at its joint center with axes - parallel to the parent at zero joint angle (pure translations). -- Source: offsets and axes mirror the `revolute_attach()` calls in - `juno.py` (the build123d assembly), which is the CAD source of truth. -- Positive-motion conventions (verified against the CAD athletic stance): - pitch joints rotate about +Y (positive swings the distal segment - backward: knee flexion positive, hip/shoulder flexion forward negative); - roll joints rotate about +X (positive moves the distal segment toward - +Y, robot-left); yaw joints rotate about +Z (positive turns toward - robot-left). Side-dependent signs (abduction, internal rotation) are - encoded per side in limits and poses. -- Joint limits, efforts, and velocities are ASSUMED plausible values for a - 1.4 m research-humanoid concept (G1-class actuators); the CAD does not - model hard stops. Documented here, not measured. -""" - -from __future__ import annotations - -# Lateral offsets (mm) from juno.py. -HIP_Y_MM = 90.0 -SHOULDER_Y_MM = 148.0 - -# Parent-local joint origin offsets (mm); see juno.py chain-offset docstring. -WAIST_YAW_ORIGIN_MM = (0.0, 0.0, 0.0) -NECK_YAW_ORIGIN_MM = (0.0, 0.0, 324.0) -NECK_PITCH_ORIGIN_MM = (0.0, 0.0, 46.0) -HIP_YAW_DROP_Z_MM = -120.0 -HIP_ROLL_ORIGIN_MM = (0.0, 0.0, -64.0) -HIP_PITCH_ORIGIN_MM = (0.0, 0.0, -78.0) -KNEE_ORIGIN_MM = (0.0, 0.0, -290.0) -ANKLE_PITCH_ORIGIN_MM = (0.0, 0.0, -290.0) -ANKLE_ROLL_ORIGIN_MM = (0.0, 0.0, -30.0) -SHOULDER_PITCH_RAISE_Z_MM = 290.0 -SHOULDER_ROLL_Y_MM = 34.0 -SHOULDER_ROLL_Z_MM = -72.0 -SHOULDER_YAW_ORIGIN_MM = (0.0, 0.0, -24.0) -ELBOW_ORIGIN_MM = (0.0, 0.0, -156.0) -WRIST_ROLL_ORIGIN_MM = (0.0, 0.0, -150.0) -WRIST_PITCH_ORIGIN_MM = (0.0, 0.0, -28.0) - -X_AXIS = (1.0, 0.0, 0.0) -Y_AXIS = (0.0, 1.0, 0.0) -Z_AXIS = (0.0, 0.0, 1.0) - -# Sole plane below the foot link origin (foot part bbox bottom; the rubber -# sole pads are modeled 26 mm under the ankle-roll center). -SOLE_BELOW_FOOT_MM = 26.0 - -# Standing pelvis height at the all-zero pose: the URDF roots a frame-only -# `base_footprint` link at ground level and lifts the pelvis by this fixed -# offset so the zero pose stands exactly on z = 0. Posed states with bent -# knees (athletic_ready, squat) keep the pelvis fixed and hover slightly. -BASE_FOOTPRINT_TO_PELVIS_Z_MM = -( - HIP_YAW_DROP_Z_MM - + HIP_ROLL_ORIGIN_MM[2] - + HIP_PITCH_ORIGIN_MM[2] - + KNEE_ORIGIN_MM[2] - + ANKLE_PITCH_ORIGIN_MM[2] - + ANKLE_ROLL_ORIGIN_MM[2] - - SOLE_BELOW_FOOT_MM -) - -SIDES = ("left", "right") - - -def side_sign(side: str) -> float: - """+1 for the robot-left side, -1 for the robot-right side.""" - return 1.0 if side == "left" else -1.0 - - -# CAD athletic ready stance (degrees), shared with juno.py and the SRDF -# `athletic_ready` group state. -HIP_PITCH_DEG = -16.0 -KNEE_DEG = 32.0 -ANKLE_PITCH_DEG = -16.0 -HIP_ROLL_ABDUCT_DEG = 3.0 # left +, right -; ankle roll compensates -HIP_YAW_DEG = 0.0 -WAIST_YAW_DEG = 0.0 -SHOULDER_PITCH_DEG = -8.0 # negative swings the arm forward -SHOULDER_ROLL_ABDUCT_DEG = 8.0 # elbows out -SHOULDER_YAW_INTERNAL_DEG = 8.0 -ELBOW_DEG = -20.0 # negative flexes the forearm forward -WRIST_ROLL_DEG = 0.0 -WRIST_PITCH_DEG = -2.0 -NECK_YAW_DEG = 0.0 -NECK_PITCH_DEG = -2.0 - - -# ---------------------------------------------------------------- limits -# ASSUMED actuator classes (Nm rated torque / rad/s no-load class speed) -# for a G1-class concept humanoid; the CAD does not model actuators' data. -LEG_PRIMARY_EFFORT_NM = 120.0 # hip yaw/roll/pitch, knee -LEG_PRIMARY_VELOCITY_RAD_S = 15.0 -ANKLE_EFFORT_NM = 50.0 -ANKLE_VELOCITY_RAD_S = 12.0 -WAIST_EFFORT_NM = 60.0 -WAIST_VELOCITY_RAD_S = 8.0 -SHOULDER_EFFORT_NM = 40.0 -SHOULDER_VELOCITY_RAD_S = 12.0 -ELBOW_EFFORT_NM = 35.0 -ELBOW_VELOCITY_RAD_S = 12.0 -WRIST_EFFORT_NM = 15.0 -WRIST_VELOCITY_RAD_S = 15.0 -NECK_EFFORT_NM = 8.0 -NECK_VELOCITY_RAD_S = 8.0 - -# ASSUMED joint travel (degrees), humanoid-plausible and containing the CAD -# athletic stance. Asymmetric ranges are authored for the LEFT side with -# the convention noted; the right side mirrors lower/upper. -WAIST_YAW_RANGE_DEG = (-150.0, 150.0) -NECK_YAW_RANGE_DEG = (-90.0, 90.0) -NECK_PITCH_RANGE_DEG = (-35.0, 45.0) # negative looks up, positive down -HIP_YAW_RANGE_DEG = (-60.0, 60.0) -HIP_ROLL_LEFT_RANGE_DEG = (-20.0, 45.0) # +abduction for the left leg -HIP_PITCH_RANGE_DEG = (-120.0, 30.0) # negative flexes forward -KNEE_RANGE_DEG = (-5.0, 140.0) # positive flexes backward -ANKLE_PITCH_RANGE_DEG = (-30.0, 50.0) # negative toes-up -ANKLE_ROLL_LEFT_RANGE_DEG = (-25.0, 25.0) -SHOULDER_PITCH_RANGE_DEG = (-180.0, 60.0) # negative swings forward -SHOULDER_ROLL_LEFT_RANGE_DEG = (-25.0, 160.0) # +abduction for the left arm -SHOULDER_YAW_RANGE_DEG = (-120.0, 120.0) -ELBOW_RANGE_DEG = (-145.0, 5.0) # negative flexes forward -WRIST_ROLL_RANGE_DEG = (-170.0, 170.0) -WRIST_PITCH_RANGE_DEG = (-70.0, 70.0) - - -def _mirror_range(range_deg: tuple[float, float], side: str) -> tuple[float, float]: - if side == "left": - return range_deg - lower, upper = range_deg - return (-upper, -lower) - - -def central_joints() -> list[dict]: - """The three non-sided joints in root-to-leaf order.""" - return [ - { - "name": "waist_yaw", - "parent": "pelvis", - "child": "torso", - "origin_mm": WAIST_YAW_ORIGIN_MM, - "axis": Z_AXIS, - "range_deg": WAIST_YAW_RANGE_DEG, - "effort_nm": WAIST_EFFORT_NM, - "velocity_rad_s": WAIST_VELOCITY_RAD_S, - }, - { - "name": "neck_yaw", - "parent": "torso", - "child": "neck_collar", - "origin_mm": NECK_YAW_ORIGIN_MM, - "axis": Z_AXIS, - "range_deg": NECK_YAW_RANGE_DEG, - "effort_nm": NECK_EFFORT_NM, - "velocity_rad_s": NECK_VELOCITY_RAD_S, - }, - { - "name": "neck_pitch", - "parent": "neck_collar", - "child": "head", - "origin_mm": NECK_PITCH_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": NECK_PITCH_RANGE_DEG, - "effort_nm": NECK_EFFORT_NM, - "velocity_rad_s": NECK_VELOCITY_RAD_S, - }, - ] - - -def leg_joints(side: str) -> list[dict]: - s = side_sign(side) - return [ - { - "name": f"hip_yaw_{side}", - "parent": "pelvis", - "child": f"hip_bracket_{side}", - "origin_mm": (0.0, s * HIP_Y_MM, HIP_YAW_DROP_Z_MM), - "axis": Z_AXIS, - "range_deg": HIP_YAW_RANGE_DEG, - "effort_nm": LEG_PRIMARY_EFFORT_NM, - "velocity_rad_s": LEG_PRIMARY_VELOCITY_RAD_S, - }, - { - "name": f"hip_roll_{side}", - "parent": f"hip_bracket_{side}", - "child": f"hip_carrier_{side}", - "origin_mm": HIP_ROLL_ORIGIN_MM, - "axis": X_AXIS, - "range_deg": _mirror_range(HIP_ROLL_LEFT_RANGE_DEG, side), - "effort_nm": LEG_PRIMARY_EFFORT_NM, - "velocity_rad_s": LEG_PRIMARY_VELOCITY_RAD_S, - }, - { - "name": f"hip_pitch_{side}", - "parent": f"hip_carrier_{side}", - "child": f"thigh_{side}", - "origin_mm": HIP_PITCH_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": HIP_PITCH_RANGE_DEG, - "effort_nm": LEG_PRIMARY_EFFORT_NM, - "velocity_rad_s": LEG_PRIMARY_VELOCITY_RAD_S, - }, - { - "name": f"knee_{side}", - "parent": f"thigh_{side}", - "child": f"shin_{side}", - "origin_mm": KNEE_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": KNEE_RANGE_DEG, - "effort_nm": LEG_PRIMARY_EFFORT_NM, - "velocity_rad_s": LEG_PRIMARY_VELOCITY_RAD_S, - }, - { - "name": f"ankle_pitch_{side}", - "parent": f"shin_{side}", - "child": f"ankle_link_{side}", - "origin_mm": ANKLE_PITCH_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": ANKLE_PITCH_RANGE_DEG, - "effort_nm": ANKLE_EFFORT_NM, - "velocity_rad_s": ANKLE_VELOCITY_RAD_S, - }, - { - "name": f"ankle_roll_{side}", - "parent": f"ankle_link_{side}", - "child": f"foot_{side}", - "origin_mm": ANKLE_ROLL_ORIGIN_MM, - "axis": X_AXIS, - "range_deg": _mirror_range(ANKLE_ROLL_LEFT_RANGE_DEG, side), - "effort_nm": ANKLE_EFFORT_NM, - "velocity_rad_s": ANKLE_VELOCITY_RAD_S, - }, - ] - - -def arm_joints(side: str) -> list[dict]: - s = side_sign(side) - return [ - { - "name": f"shoulder_pitch_{side}", - "parent": "torso", - "child": f"shoulder_pod_{side}", - "origin_mm": (0.0, s * SHOULDER_Y_MM, SHOULDER_PITCH_RAISE_Z_MM), - "axis": Y_AXIS, - "range_deg": SHOULDER_PITCH_RANGE_DEG, - "effort_nm": SHOULDER_EFFORT_NM, - "velocity_rad_s": SHOULDER_VELOCITY_RAD_S, - }, - { - "name": f"shoulder_roll_{side}", - "parent": f"shoulder_pod_{side}", - "child": f"yaw_housing_{side}", - "origin_mm": (0.0, s * SHOULDER_ROLL_Y_MM, SHOULDER_ROLL_Z_MM), - "axis": X_AXIS, - "range_deg": _mirror_range(SHOULDER_ROLL_LEFT_RANGE_DEG, side), - "effort_nm": SHOULDER_EFFORT_NM, - "velocity_rad_s": SHOULDER_VELOCITY_RAD_S, - }, - { - "name": f"shoulder_yaw_{side}", - "parent": f"yaw_housing_{side}", - "child": f"bicep_{side}", - "origin_mm": SHOULDER_YAW_ORIGIN_MM, - "axis": Z_AXIS, - "range_deg": SHOULDER_YAW_RANGE_DEG, - "effort_nm": SHOULDER_EFFORT_NM, - "velocity_rad_s": SHOULDER_VELOCITY_RAD_S, - }, - { - "name": f"elbow_{side}", - "parent": f"bicep_{side}", - "child": f"forearm_{side}", - "origin_mm": ELBOW_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": ELBOW_RANGE_DEG, - "effort_nm": ELBOW_EFFORT_NM, - "velocity_rad_s": ELBOW_VELOCITY_RAD_S, - }, - { - "name": f"wrist_roll_{side}", - "parent": f"forearm_{side}", - "child": f"wrist_carrier_{side}", - "origin_mm": WRIST_ROLL_ORIGIN_MM, - "axis": Z_AXIS, - "range_deg": WRIST_ROLL_RANGE_DEG, - "effort_nm": WRIST_EFFORT_NM, - "velocity_rad_s": WRIST_VELOCITY_RAD_S, - }, - { - "name": f"wrist_pitch_{side}", - "parent": f"wrist_carrier_{side}", - "child": f"hand_{side}", - "origin_mm": WRIST_PITCH_ORIGIN_MM, - "axis": Y_AXIS, - "range_deg": WRIST_PITCH_RANGE_DEG, - "effort_nm": WRIST_EFFORT_NM, - "velocity_rad_s": WRIST_VELOCITY_RAD_S, - }, - ] - - -def all_joints() -> list[dict]: - """All 27 movable joints in root-to-leaf order (legs then arms per side).""" - joints = central_joints() - for side in SIDES: - joints.extend(leg_joints(side)) - for side in SIDES: - joints.extend(arm_joints(side)) - return joints - - -def all_links() -> list[str]: - """All 28 link names, root first, in joint emission order.""" - links = ["pelvis"] - for joint in all_joints(): - links.append(joint["child"]) - return links - - -# ----------------------------------------------------------------- poses -def _athletic_ready_deg() -> dict[str, float]: - pose = { - "waist_yaw": WAIST_YAW_DEG, - "neck_yaw": NECK_YAW_DEG, - "neck_pitch": NECK_PITCH_DEG, - } - for side in SIDES: - s = side_sign(side) - pose.update( - { - f"hip_yaw_{side}": HIP_YAW_DEG, - f"hip_roll_{side}": s * HIP_ROLL_ABDUCT_DEG, - f"hip_pitch_{side}": HIP_PITCH_DEG, - f"knee_{side}": KNEE_DEG, - f"ankle_pitch_{side}": ANKLE_PITCH_DEG, - f"ankle_roll_{side}": -s * HIP_ROLL_ABDUCT_DEG, - f"shoulder_pitch_{side}": SHOULDER_PITCH_DEG, - f"shoulder_roll_{side}": s * SHOULDER_ROLL_ABDUCT_DEG, - f"shoulder_yaw_{side}": -s * SHOULDER_YAW_INTERNAL_DEG, - f"elbow_{side}": ELBOW_DEG, - f"wrist_roll_{side}": WRIST_ROLL_DEG, - f"wrist_pitch_{side}": WRIST_PITCH_DEG, - } - ) - return pose - - -def _wave_right_deg() -> dict[str, float]: - """Friendly right-hand wave on top of the athletic lower body. - - Right upper arm raised out to the side just above horizontal - (shoulder roll), full external shoulder yaw so the 90 deg elbow - carries the forearm vertically up, slight wrist-pitch flick, head - turned toward the waving hand. - """ - pose = _athletic_ready_deg() - pose.update( - { - "shoulder_pitch_right": 0.0, - "shoulder_roll_right": -115.0, - "shoulder_yaw_right": -90.0, - "elbow_right": -90.0, - "wrist_roll_right": 0.0, - "wrist_pitch_right": -15.0, - "neck_yaw": -20.0, - "neck_pitch": -5.0, - } - ) - return pose - - -def _squat_deg() -> dict[str, float]: - """Deep flat-footed squat: pitch sum stays zero so the soles stay level.""" - pose = _athletic_ready_deg() - for side in SIDES: - pose.update( - { - f"hip_pitch_{side}": -55.0, - f"knee_{side}": 85.0, - f"ankle_pitch_{side}": -30.0, - } - ) - return pose - - -def _t_pose_deg() -> dict[str, float]: - pose = {name: 0.0 for name in (joint["name"] for joint in all_joints())} - pose["shoulder_roll_left"] = 90.0 - pose["shoulder_roll_right"] = -90.0 - return pose - - -def named_poses_deg() -> dict[str, dict[str, float]]: - """SRDF group-state poses (degrees) for the whole_body group.""" - zero = {name: 0.0 for name in (joint["name"] for joint in all_joints())} - return { - "zero": zero, - "athletic_ready": _athletic_ready_deg(), - "t_pose": _t_pose_deg(), - "wave_right": _wave_right_deg(), - "squat": _squat_deg(), - } diff --git a/models/robots/juno/juno_parts/description.py b/models/robots/juno/juno_parts/description.py deleted file mode 100644 index cc51ca39..00000000 --- a/models/robots/juno/juno_parts/description.py +++ /dev/null @@ -1,284 +0,0 @@ -"""URDF and SRDF emission for the juno humanoid. - -Stdlib-only builders consumed by `juno.py` `gen_urdf()` / `gen_srdf()`. - -Design ledger (in addition to the chain ledger in `juno_parts/chain.py`): -- Units: URDF output is meters/kilograms/radians; chain offsets and baked - mass properties are millimeters (mesh sources are mm, hence the 0.001 - mesh scale). Frames: +X forward, +Y robot-left, +Z up; the pelvis link - is the root with the waist-yaw joint center at its origin. -- Link frames: every link frame coincides with its parent joint frame and - with the part-local modeling frame of the matching `juno_parts` builder, - so all visual mesh origins are identity. -- Visual geometry: per-link 3MF meshes under `3MF/` exported from the - `STEP/.py` wrapper sources (XCAF colors preserved). -- Collision geometry: axis-aligned bounding boxes of each part in its - link frame — intentionally coarse, for viewer/planning sanity rather - than contact-accurate simulation. -- Inertials: CAD volume properties (OCP BRepGProp) at an ASSUMED uniform - density chosen so the whole robot masses ASSUMED_TOTAL_MASS_KG. COM and - the full inertia tensor (about the COM, link frame) come from the CAD - solids; only the density is assumed. Confidence: estimated. -- SRDF: planning groups are the standard humanoid split (arms, legs, - torso, head, hand end effectors, whole_body union). Group states are - whole-body named poses from `chain.named_poses_deg()` converted to - radians. Disabled collisions: URDF-adjacent pairs (reason Adjacent) - plus pairs whose collision boxes already overlap in the zero pose - (reason Default), computed from the chain offsets at import time. -""" - -from __future__ import annotations - -import math -import xml.etree.ElementTree as ET - -from juno_parts import chain -from juno_parts.mass_props import LINK_MASS_PROPS, TOTAL_VOLUME_MM3 - -ROBOT_NAME = "juno" - -# ~1.40 m G1-class research humanoid; the CAD models styled shells, not -# component masses, so total mass is an assumption distributed by volume. -ASSUMED_TOTAL_MASS_KG = 35.0 -UNIFORM_DENSITY_KG_PER_MM3 = ASSUMED_TOTAL_MASS_KG / TOTAL_VOLUME_MM3 - -MM_TO_M = 0.001 -# Unit-density inertia integrates r^2 dV in mm^5; kg*m^2 = density * mm^5 * 1e-6. -INERTIA_MM5_TO_KG_M2 = UNIFORM_DENSITY_KG_PER_MM3 * 1e-6 -MESH_DIR = "3MF" -MESH_SCALE_FROM_MM = "0.001 0.001 0.001" - -HEADER_NOTE = ( - " Generated by juno.py (juno_parts/description.py) for CAD Viewer and " - "external URDF consumers. Link frames coincide with juno_parts part-local " - "frames; meshes are mm 3MF exports of the same builders. Inertials use CAD " - "volume properties at a uniform assumed density totalling 35 kg; collision " - "geometry is per-link CAD bounding boxes; joint limits/efforts are assumed " - "G1-class values documented in juno_parts/chain.py. " -) - - -def _fmt(value: float) -> str: - text = f"{value:.9f}".rstrip("0").rstrip(".") - if text in ("-0", ""): - return "0" - return text - - -def _vec(values, scale: float = 1.0) -> str: - return " ".join(_fmt(v * scale) for v in values) - - -def _link_element(robot: ET.Element, name: str) -> None: - volume_mm3, com_mm, inertia_mm5, bbox_min_mm, bbox_max_mm = LINK_MASS_PROPS[name] - link = ET.SubElement(robot, "link", {"name": name}) - - inertial = ET.SubElement(link, "inertial") - ET.SubElement(inertial, "origin", {"xyz": _vec(com_mm, MM_TO_M), "rpy": "0 0 0"}) - ET.SubElement(inertial, "mass", {"value": _fmt(volume_mm3 * UNIFORM_DENSITY_KG_PER_MM3)}) - ixx, iyy, izz, ixy, ixz, iyz = (v * INERTIA_MM5_TO_KG_M2 for v in inertia_mm5) - ET.SubElement( - inertial, - "inertia", - { - "ixx": _fmt(ixx), "ixy": _fmt(ixy), "ixz": _fmt(ixz), - "iyy": _fmt(iyy), "iyz": _fmt(iyz), "izz": _fmt(izz), - }, - ) - - visual = ET.SubElement(link, "visual") - ET.SubElement(visual, "origin", {"xyz": "0 0 0", "rpy": "0 0 0"}) - geometry = ET.SubElement(visual, "geometry") - ET.SubElement( - geometry, - "mesh", - {"filename": f"{MESH_DIR}/{name}.3mf", "scale": MESH_SCALE_FROM_MM}, - ) - - bbox_center_mm = [(lo + hi) / 2.0 for lo, hi in zip(bbox_min_mm, bbox_max_mm)] - bbox_size_mm = [hi - lo for lo, hi in zip(bbox_min_mm, bbox_max_mm)] - collision = ET.SubElement(link, "collision") - ET.SubElement(collision, "origin", {"xyz": _vec(bbox_center_mm, MM_TO_M), "rpy": "0 0 0"}) - geometry = ET.SubElement(collision, "geometry") - ET.SubElement(geometry, "box", {"size": _vec(bbox_size_mm, MM_TO_M)}) - - -def _joint_element(robot: ET.Element, joint: dict) -> None: - element = ET.SubElement(robot, "joint", {"name": joint["name"], "type": "revolute"}) - ET.SubElement(element, "parent", {"link": joint["parent"]}) - ET.SubElement(element, "child", {"link": joint["child"]}) - ET.SubElement(element, "origin", {"xyz": _vec(joint["origin_mm"], MM_TO_M), "rpy": "0 0 0"}) - ET.SubElement(element, "axis", {"xyz": _vec(joint["axis"])}) - lower_deg, upper_deg = joint["range_deg"] - ET.SubElement( - element, - "limit", - { - "lower": _fmt(math.radians(lower_deg)), - "upper": _fmt(math.radians(upper_deg)), - "effort": _fmt(joint["effort_nm"]), - "velocity": _fmt(joint["velocity_rad_s"]), - }, - ) - - -def build_urdf() -> ET.Element: - robot = ET.Element("robot", {"name": ROBOT_NAME}) - robot.append(ET.Comment(HEADER_NOTE)) - # Frame-only ground root (no inertial/visual/collision by intent): the - # pelvis is lifted so the all-zero pose stands with its soles on z = 0. - ET.SubElement(robot, "link", {"name": "base_footprint"}) - for name in chain.all_links(): - _link_element(robot, name) - footprint_joint = ET.SubElement( - robot, "joint", {"name": "base_footprint_to_pelvis", "type": "fixed"} - ) - ET.SubElement(footprint_joint, "parent", {"link": "base_footprint"}) - ET.SubElement(footprint_joint, "child", {"link": "pelvis"}) - ET.SubElement( - footprint_joint, - "origin", - {"xyz": _vec((0.0, 0.0, chain.BASE_FOOTPRINT_TO_PELVIS_Z_MM), MM_TO_M), "rpy": "0 0 0"}, - ) - for joint in chain.all_joints(): - _joint_element(robot, joint) - return robot - - -# ------------------------------------------------------------------ SRDF -def _link_origins_world_mm() -> dict[str, tuple[float, float, float]]: - """Link-frame origins in the pelvis frame at the all-zero pose. - - At zero joint angles every chain transform is a pure translation, so - origins are cumulative offset sums. - """ - origins = {"pelvis": (0.0, 0.0, 0.0)} - for joint in chain.all_joints(): - px, py, pz = origins[joint["parent"]] - ox, oy, oz = joint["origin_mm"] - origins[joint["child"]] = (px + ox, py + oy, pz + oz) - return origins - - -def _zero_pose_overlapping_pairs() -> list[tuple[str, str]]: - """Collision-box pairs that already overlap in the zero pose.""" - origins = _link_origins_world_mm() - boxes = {} - for name in chain.all_links(): - _, _, _, bbox_min_mm, bbox_max_mm = LINK_MASS_PROPS[name] - origin = origins[name] - boxes[name] = ( - [o + lo for o, lo in zip(origin, bbox_min_mm)], - [o + hi for o, hi in zip(origin, bbox_max_mm)], - ) - names = chain.all_links() - pairs = [] - for i, a in enumerate(names): - a_min, a_max = boxes[a] - for b in names[i + 1 :]: - b_min, b_max = boxes[b] - if all(a_min[k] < b_max[k] and a_max[k] > b_min[k] for k in range(3)): - pairs.append((a, b)) - return pairs - - -def _adjacent_pairs() -> list[tuple[str, str]]: - return [(joint["parent"], joint["child"]) for joint in chain.all_joints()] - - -def _arm_group(robot: ET.Element, side: str) -> None: - # The arm chain tips at the wrist carrier (the end-effector attach link) - # so the hand end-effector group shares no links with its parent group. - group = ET.SubElement(robot, "group", {"name": f"{side}_arm"}) - ET.SubElement(group, "chain", {"base_link": "torso", "tip_link": f"wrist_carrier_{side}"}) - - -def _leg_group(robot: ET.Element, side: str) -> None: - group = ET.SubElement(robot, "group", {"name": f"{side}_leg"}) - ET.SubElement(group, "chain", {"base_link": "pelvis", "tip_link": f"foot_{side}"}) - - -def build_srdf() -> ET.Element: - robot = ET.Element("robot", {"name": ROBOT_NAME}) - robot.append( - ET.Comment( - " MoveIt2 planning semantics for juno.urdf. Groups: per-limb chains, " - "torso/head joint groups, hand end-effector groups, and a whole_body " - "union. Group states are whole-body named poses (radians) from " - "juno_parts/chain.py. Disabled collisions: URDF-adjacent pairs plus " - "zero-pose bounding-box overlaps (reason Default). " - ) - ) - - ET.SubElement( - robot, - "virtual_joint", - { - "name": "world_to_base", - "type": "floating", - "parent_frame": "world", - "child_link": "base_footprint", - }, - ) - - torso_group = ET.SubElement(robot, "group", {"name": "torso"}) - ET.SubElement(torso_group, "joint", {"name": "waist_yaw"}) - head_group = ET.SubElement(robot, "group", {"name": "head"}) - ET.SubElement(head_group, "joint", {"name": "neck_yaw"}) - ET.SubElement(head_group, "joint", {"name": "neck_pitch"}) - for side in chain.SIDES: - _leg_group(robot, side) - for side in chain.SIDES: - _arm_group(robot, side) - for side in chain.SIDES: - # Owns wrist_pitch (and thereby the hand link) so the end-effector - # group is disjoint from the arm chain ending at wrist_carrier. - hand_group = ET.SubElement(robot, "group", {"name": f"{side}_hand"}) - ET.SubElement(hand_group, "joint", {"name": f"wrist_pitch_{side}"}) - - whole_body = ET.SubElement(robot, "group", {"name": "whole_body"}) - for name in ( - "torso", "head", "left_leg", "right_leg", - "left_arm", "right_arm", "left_hand", "right_hand", - ): - ET.SubElement(whole_body, "group", {"name": name}) - - joint_order = [joint["name"] for joint in chain.all_joints()] - for pose_name, pose_deg in chain.named_poses_deg().items(): - state = ET.SubElement(robot, "group_state", {"name": pose_name, "group": "whole_body"}) - for joint_name in joint_order: - ET.SubElement( - state, - "joint", - {"name": joint_name, "value": _fmt(math.radians(pose_deg[joint_name]))}, - ) - - for side in chain.SIDES: - ET.SubElement( - robot, - "end_effector", - { - "name": f"{side}_hand_eef", - "parent_link": f"wrist_carrier_{side}", - "group": f"{side}_hand", - "parent_group": f"{side}_arm", - }, - ) - - adjacent = _adjacent_pairs() - adjacent_set = {frozenset(pair) for pair in adjacent} - for link1, link2 in adjacent: - ET.SubElement( - robot, - "disable_collisions", - {"link1": link1, "link2": link2, "reason": "Adjacent"}, - ) - for link1, link2 in _zero_pose_overlapping_pairs(): - if frozenset((link1, link2)) in adjacent_set: - continue - ET.SubElement( - robot, - "disable_collisions", - {"link1": link1, "link2": link2, "reason": "Default"}, - ) - return robot diff --git a/models/robots/juno/juno_parts/hand.py b/models/robots/juno/juno_parts/hand.py deleted file mode 100644 index 8af2583c..00000000 --- a/models/robots/juno/juno_parts/hand.py +++ /dev/null @@ -1,380 +0,0 @@ -"""juno humanoid: dexterous 5-digit hand (4 fingers + opposed thumb). - -Local frame: origin = wrist-pitch joint center, fingers extend -Z, thumb on -the +X (forward) edge. RIGHT hand palm normal = +Y; LEFT is the XZ-mirror. - -Locked interfaces: -- Clevis cheek plates normal to Y at y in [22,30] / [-30,-22], z 0..-44, - x -20..+24, aluminum boss disc dia 34 coaxial with Y on each outer face. -- Keep |y| < 21 clear for r < 26 around the Y axis near the origin - (wrist-pitch actuator dia 48 x len 40 lives there; owned by the forearm). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - - - -import math # noqa: F401 (kept for tuning) - -from build123d import ( - Axis, - Box, - Cone, - Cylinder, - Plane, - Polyline, - Pos, - Rot, - Sphere, - chamfer, - extrude, - fillet, - make_face, - mirror, -) - -from .juno_lib import ( - ACCENT_COLOR, - ALU_COLOR, - RUBBER_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - VISOR_COLOR, - part_compound, - styled, -) - -# ---------------------------------------------------------------- helpers - - -def _safe_chamfer(part, edges, length, length2=None): - try: - edges = list(edges) - if not edges: - return part - if length2 is not None: - return chamfer(edges, length, length2) - return chamfer(edges, length) - except Exception: - return part - - -def _safe_fillet(part, edges, radius): - try: - edges = list(edges) - if not edges: - return part - return fillet(edges, radius) - except Exception: - return part - - -def _xcyl(r, h): - """Cylinder along +X centered at origin.""" - return Rot(0, 90, 0) * Cylinder(radius=r, height=h) - - -def _ycyl(r, h): - """Cylinder along +Y centered at origin.""" - return Rot(-90, 0, 0) * Cylinder(radius=r, height=h) - - -# ---------------------------------------------------------------- wrist clevis - -CHEEK_X0, CHEEK_X1 = -20.0, 24.0 -CHEEK_Z0, CHEEK_Z1 = 0.0, -44.0 -LOBE_R = 24.0 - - -def _wrist_clevis(): - """Twin cheek plates + pivot lobes + lower cross bridge (one solid).""" - cheeks = [] - for ys in (1.0, -1.0): - yc = ys * 26.0 - plate = Pos( - (CHEEK_X0 + CHEEK_X1) / 2.0, yc, (CHEEK_Z0 + CHEEK_Z1) / 2.0 - ) * Box(CHEEK_X1 - CHEEK_X0, 8.0, CHEEK_Z0 - CHEEK_Z1) - lobe = Pos(0, yc, 0) * _ycyl(LOBE_R, 8.0) - cheek = plate + lobe - # bevel the face outlines (machined plate look) - face_edges = [] - for f in cheek.faces().filter_by(Axis.Y): - face_edges.extend(f.edges()) - cheek = _safe_chamfer(cheek, face_edges, 1.2) - cheeks.append(cheek) - - bridge = Pos(2.0, 0.0, -37.0) * Box(44.0, 44.0, 14.0) - bridge = _safe_chamfer(bridge, bridge.edges().filter_by(Axis.X), 2.0) - bridge = _safe_chamfer(bridge, bridge.edges().filter_by(Axis.Y), 2.0) - - clevis = cheeks[0] + cheeks[1] + bridge - - # shallow turned ring groove on each outer face, around the boss disc - for ys in (1.0, -1.0): - try: - ring = (Pos(0, ys * 30.0, 0) * _ycyl(21.0, 1.6)) - ( - Pos(0, ys * 30.0, 0) * _ycyl(19.0, 3.0) - ) - clevis -= ring - except Exception: - pass - return styled(clevis, "wrist_clevis", STRUCT_COLOR) - - -def _cheek_bolts(): - out = [] - i = 0 - for ys in (1.0, -1.0): - for bx in (-13.0, 17.0): - bolt = Pos(bx, ys * 30.7, -37.0) * _ycyl(2.2, 1.4) - bolt = _safe_chamfer(bolt, bolt.edges(), 0.5) - out.append(styled(bolt, f"cheek_bolt_{i}", ALU_COLOR)) - i += 1 - return out - - -def _boss_discs(): - out = [] - for ys, tag in ((1.0, "l"), (-1.0, "r")): - boss = Pos(0, ys * 31.6, 0) * _ycyl(17.0, 3.2) - boss = _safe_chamfer(boss, boss.edges(), 0.8) - out.append(styled(boss, f"wrist_boss_{tag}", ALU_COLOR)) - return out - - -# ---------------------------------------------------------------- palm - -KNUCKLE_Z = -112.0 -KNUCKLE_Y = -2.0 -KNUCKLE_XS = (-27.0, -9.0, 9.0, 27.0) - -# thumb CMC frame (root on the +X palm edge) -THUMB_ROOT = (39.5, 2.0, -60.0) -T_CMC = Pos(*THUMB_ROOT) * Rot(0, -28, 0) * Rot(0, 0, 38) # hinge axis = local X -T_SEG1 = T_CMC * Rot(12, 0, 0) # CMC curl -T_SEG2 = T_SEG1 * Pos(0, 0, -32.0) * Rot(30, 0, 0) # IP curl - - -def _palm_outline_face(pts, y): - pts3 = [(x, y, z) for x, z in pts] - wire = Polyline(*pts3, close=True) - return make_face(wire) - - -def _palm_core(): - pts = [ - (-20.0, -44.0), - (24.0, -44.0), - (44.0, -58.0), - (44.0, -96.0), - (36.0, -108.0), - (-36.0, -108.0), - ] - core = extrude(_palm_outline_face(pts, -16.0), amount=34.0, dir=(0, 1, 0)) - core = _safe_fillet(core, core.edges().filter_by(Axis.Y), 4.0) - - # raked palm-side bottom edge over the knuckle line - rake = [ - e - for e in core.edges() - if e.center().Y > 16.5 and e.center().Z < -107.0 - ] - core = _safe_chamfer(core, rake, 5.0) - - # knuckle scallops (clearance arches over the proximal barrels) - for kx in KNUCKLE_XS: - core -= Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * _xcyl(8.0, 15.5) - - # thumb CMC pocket + diagonal thumb relief groove - core -= T_CMC * _xcyl(9.5, 30.0) - core -= T_SEG1 * Pos(0, 0, -14.0) * Rot(0, 90, 0) * Cylinder( - radius=11.2, height=60.0 - ) - - # palm sensor recess - core -= Pos(2.0, 17.4, -80.0) * _ycyl(8.3, 3.2) - - return styled(core, "palm_core", STRUCT_COLOR) - - -def _dorsal_cover(): - pts = [ - (-18.0, -46.0), - (22.0, -46.0), - (42.0, -59.0), - (42.0, -95.0), - (34.5, -106.0), - (-34.0, -106.0), - ] - cover = extrude(_palm_outline_face(pts, -20.0), amount=4.0, dir=(0, 1, 0)) - cover = _safe_fillet(cover, cover.edges().filter_by(Axis.Y), 4.0) - try: - outer = cover.faces().sort_by(Axis.Y)[0] - cover = _safe_chamfer(cover, outer.edges(), 1.2) - except Exception: - pass - # shallow panel-line grooves on the outer face - for gz in (-64.0, -69.0): - try: - cover -= Pos(2.0, -20.0, gz) * Box(52.0, 1.2, 1.5) - except Exception: - pass - return styled(cover, "dorsal_cover", SHELL_COLOR) - - -def _palm_sensor(): - ring_raw = Cylinder(radius=8.1, height=1.2) - Cylinder(radius=6.3, height=4.0) - ring = Pos(2.0, 16.5, -80.0) * Rot(-90, 0, 0) * ring_raw - lens = Pos(2.0, 16.7, -80.0) * _ycyl(6.1, 1.6) - return [ - styled(ring, "palm_sensor_ring", ALU_COLOR), - styled(lens, "palm_sensor_lens", VISOR_COLOR), - ] - - -# ---------------------------------------------------------------- digits - - -def _link(length, width, r_a, r_b, shaft_w, shaft_d, slot_w, bore_r): - """Phalanx link: barrel at z=0, shaft, forked barrel at z=-length.""" - barrel_a = _xcyl(r_a, width) - shaft_h = length - 7.0 - shaft = Pos(0, 0, -(4.0 + shaft_h / 2.0)) * Box(shaft_w, shaft_d, shaft_h) - barrel_b = Pos(0, 0, -length) * _xcyl(r_b, width) - link = barrel_a + shaft + barrel_b - link = _safe_chamfer(link, link.edges().filter_by(Axis.Z), 1.2) - # fork slot for the next segment - link -= Pos(0, 0, -(length + 0.5)) * Box(slot_w, 40.0, 15.0) - # pin bores - link -= _xcyl(bore_r, width + 8.0) - link -= Pos(0, 0, -length) * _xcyl(bore_r, width + 8.0) - return link - - -def _tip_segment(r_barrel, w_barrel, r_top, r_tip, tip_z, cone_top_z, pad_c, pad_r, bore_r): - """Distal segment: hinge barrel + tapered body + rounded tip. - - Returns (body, pad) where pad is a rubber fingertip cap split off the - body by a boolean intersection (face-to-face, no overlap), or None. - """ - barrel = _xcyl(r_barrel, w_barrel) - cone_h = cone_top_z - tip_z - cone = Pos(0, 0, (cone_top_z + tip_z) / 2.0) * Cone( - bottom_radius=r_tip, top_radius=r_top, height=cone_h - ) - tip = Pos(0, 0, tip_z) * Sphere(r_tip) - body = barrel + cone + tip - body -= _xcyl(bore_r, w_barrel + 8.0) - pad = None - try: - zone = Pos(*pad_c) * Sphere(pad_r) - cap = body & zone - if cap.volume > 1.0: - body = body - zone - pad = cap - except Exception: - pad = None - return body, pad - - -def _pin(r, h): - pin = _xcyl(r, h) - return _safe_chamfer(pin, pin.edges(), 0.7) - - -def _finger(idx, kx, curl1=21.0, curl2=25.0): - t1 = Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * Rot(curl1, 0, 0) - t2 = t1 * Pos(0, 0, -36.0) * Rot(curl2, 0, 0) - - prox = _link( - length=36.0, width=15.0, r_a=7.5, r_b=7.0, - shaft_w=13.0, shaft_d=13.5, slot_w=9.5, bore_r=3.9, - ) - body, pad = _tip_segment( - r_barrel=6.6, w_barrel=9.0, r_top=6.2, r_tip=4.6, - tip_z=-25.0, cone_top_z=-4.0, - pad_c=(0, 1.9, -24.5), pad_r=5.5, bore_r=3.9, - ) - - solids = [ - styled(t1 * prox, f"finger{idx}_proximal", STRUCT_COLOR), - styled(t2 * body, f"finger{idx}_distal", STRUCT_COLOR), - styled(Pos(kx, KNUCKLE_Y, KNUCKLE_Z) * _pin(3.7, 17.5), - f"finger{idx}_pin_mcp", ALU_COLOR), - styled(t1 * Pos(0, 0, -36.0) * _pin(3.7, 17.5), - f"finger{idx}_pin_pip", ALU_COLOR), - ] - if pad is not None: - solids.append(styled(t2 * pad, f"finger{idx}_tip_pad", ACCENT_COLOR)) - return solids - - -def _thumb(): - seg1 = _link( - length=32.0, width=15.0, r_a=8.5, r_b=7.5, - shaft_w=13.0, shaft_d=13.0, slot_w=9.7, bore_r=4.2, - ) - body, pad = _tip_segment( - r_barrel=6.7, w_barrel=9.2, r_top=6.2, r_tip=4.9, - tip_z=-21.5, cone_top_z=-5.0, - pad_c=(0, 1.8, -21.0), pad_r=5.7, bore_r=3.9, - ) - solids = [ - styled(T_SEG1 * seg1, "thumb_proximal", STRUCT_COLOR), - styled(T_SEG2 * body, "thumb_distal", STRUCT_COLOR), - styled(T_CMC * _pin(4.0, 17.0), "thumb_pin_cmc", ALU_COLOR), - styled(T_SEG1 * Pos(0, 0, -32.0) * _pin(3.7, 16.0), - "thumb_pin_ip", ALU_COLOR), - ] - if pad is not None: - solids.append(styled(T_SEG2 * pad, "thumb_tip_pad", ACCENT_COLOR)) - return solids - - -# ---------------------------------------------------------------- assembly - - -def _right_solids(): - solids = [_wrist_clevis()] - solids += _boss_discs() - solids += _cheek_bolts() - solids.append(_palm_core()) - solids.append(_dorsal_cover()) - solids += _palm_sensor() - for i, kx in enumerate(KNUCKLE_XS): - solids += _finger(i, kx) - solids += _thumb() - return solids - - -def _fallback_solids(): - blk = Pos(4, 0, -100) * Box(80, 36, 200) - return [styled(blk, "hand_fallback", STRUCT_COLOR)] - - -def build_hand(side: str): - try: - solids = _right_solids() - if side == "left": - mirrored = [] - for s in solids: - lbl, col = s.label, s.color - m = mirror(s, about=Plane.XZ) - mirrored.append(styled(m, lbl, col)) - solids = mirrored - for s in solids: - s.label = f"{side}_{s.label}" - except Exception: - solids = _fallback_solids() - return part_compound(f"hand_{side}", solids) - - -def gen_step(): - right = build_hand("right") - left = build_hand("left") - right.location = Pos(0, -125, 0) - left.location = Pos(0, 125, 0) - return part_compound("hand_demo", [right, left]) diff --git a/models/robots/juno/juno_parts/head.py b/models/robots/juno/juno_parts/head.py deleted file mode 100644 index c86e08d7..00000000 --- a/models/robots/juno/juno_parts/head.py +++ /dev/null @@ -1,230 +0,0 @@ -"""juno expressive sensor head. - -Local frame: origin at the neck-pitch joint center, +X face direction, +Z up. -Envelope z +12..+150, |y| <= 69, x -58..+78. Below z=+12 kept clear within -radius 45 (graphite neck guard starts at z=+12 >= +8 allowance). - -Construction notes: -- The helmet is a smooth loft of rounded-rectangle sections built in a frame - centered at HEAD_CTR so a uniform `scale()` (which scales about the origin - for identity-location parts) yields a concentric "inflated" copy. The visor, - chin sensor bar, and rear panel are each (inflated copy) & (band tool) - - helmet, giving thin curved slabs ~2 mm proud that follow the shell surface. -- Ear pods and camera dots are subtracted from the shell before being added as - separate solids, so all contacts are exact face-to-face with no overlap. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - - - -import math - -from build123d import ( - Axis, - Box, - Cone, - Cylinder, - GeomType, - Pos, - RectangleRounded, - Rot, - chamfer, - fillet, - loft, - scale, -) - -from .juno_lib import ( - ACCENT_COLOR, - ALU_COLOR, - EYE_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - VISOR_COLOR, - part_compound, - styled, -) - -# Head-frame point all local modeling is centered on (also the scale center -# for the inflated shell copies used to cut the visor/chin/rear slabs). -HEAD_CTR = (7.0, 0.0, 89.0) - - -def _try_fillet(solid, edges, radius): - for r in (radius, radius * 0.6, radius * 0.35): - try: - return fillet(edges, r) - except Exception: - continue - return solid - - -def _try_chamfer(solid, edges, length): - for c in (length, length * 0.5): - try: - return chamfer(edges, c) - except Exception: - continue - return solid - - -def _helmet_loft(): - """Smooth helmet: fuller cheeks low, widest at sensor band, tapered crown. - - Sections are (z, xc, depth_x, width_y, corner_frac) in LOCAL coords - (head frame minus HEAD_CTR). - """ - sections = [ - (-69.0, -3.0, 100.0, 98.0, 0.40), # jaw line (head z=20) - (-41.0, 0.0, 114.0, 120.0, 0.42), # cheeks (head z=48) - (-7.0, 0.0, 120.0, 130.0, 0.44), # sensor band, widest (head z=82) - (23.0, -3.0, 110.0, 118.0, 0.45), # upper visor (head z=112) - (47.0, -7.0, 88.0, 92.0, 0.46), # crown taper (head z=136) - (57.0, -9.0, 62.0, 64.0, 0.47), - (60.5, -10.0, 34.0, 34.0, 0.48), # crown top (head z=149.5) - ] - profiles = [] - for z, xc, d, w, rf in sections: - profiles.append(Pos(xc, 0, z) * RectangleRounded(d, w, rf * min(d, w))) - return loft(profiles) - - -def build_head(): - """Expressive sensor head; origin at the neck-pitch joint center.""" - helmet = _helmet_loft() - - # Soften jaw line and dome the crown. - helmet = _try_fillet(helmet, helmet.edges().group_by(Axis.Z)[0], 5.0) - helmet = _try_fillet(helmet, helmet.edges().group_by(Axis.Z)[-1], 9.0) - - # Concentric inflated copy (~2 mm proud) for surface-following slabs. - big = scale(helmet, by=1.032) - - # --- aluminum ear pods (dia 36, head z~88, outer face head y=+-67) --- - pods, buttons = [], [] - for s in (1.0, -1.0): - rot = Rot(-90, 0, 0) if s > 0 else Rot(90, 0, 0) - pod = Pos(-7.0, s * 58.5, -1.0) * rot * Cylinder(radius=18.0, height=17.0) - outer = [ - e - for e in pod.edges().filter_by(GeomType.CIRCLE) - if abs(e.arc_center.Y) > 66.0 - ] - pod = _try_chamfer(pod, outer, 2.2) - helmet -= pod - pods.append(pod) - btn = Pos(-7.0, s * 67.8, -1.0) * rot * Cylinder(radius=7.0, height=1.6) - buttons.append(btn) - - # --- tiny camera dots below the visor (head z=51, y=+-13) --- - dots = [] - for s in (1.0, -1.0): - dot = Pos(53.0, s * 13.0, -38.0) * Rot(0, 90, 0) * Cylinder( - radius=2.8, height=12.0 - ) - helmet -= dot - dots.append(dot) - - # --- wraparound gloss visor (head z ~57..117 at front center) --- - # Single sculpted tool: bottom plane tilted forward-down 6 deg, top plane - # raked gently rear-down 4 deg, rear wrap edge raked 15 deg (wraps farther - # back at the temples), corner edges filleted for a sporty - # rounded-trapezoid side profile. - bot_half = Pos(60.0, 0, -32.0) * Rot(0, 6, 0) * Pos(0, 0, 150.0) * Box( - 360.0, 300.0, 300.0 - ) - top_half = Pos(60.0, 0, 28.0) * Rot(0, -4, 0) * Pos(0, 0, -150.0) * Box( - 360.0, 300.0, 300.0 - ) - rear_half = Pos(13.0, 0, 0) * Rot(0, -15, 0) * Pos(150.0, 0, 0) * Box( - 300.0, 320.0, 360.0 - ) - visor_tool = bot_half & top_half & rear_half - corner_edges = [ - e - for e in visor_tool.edges().filter_by(Axis.Y) - if abs(e.center().X) < 45.0 and abs(e.center().Z) < 80.0 - ] - visor_tool = _try_fillet(visor_tool, corner_edges, 12.0) - visor = (big & visor_tool) - helmet - - # --- graphite chin sensor bar: head z 29..45, |y| <= 26, front --- - chin_tool = Pos(53.0, 0, -52.0) * Box(60.0, 52.0, 16.0) - chin_tool = _try_fillet(chin_tool, chin_tool.edges().filter_by(Axis.X), 5.0) - chin = (big & chin_tool) - helmet - - # --- graphite rear vent louvers: three strips, head z 76..118 --- - louvers = [] - for zc in (-8.0, 8.0, 24.0): - tool = Pos(-71.0, 0, zc) * Box(60.0, 220.0, 10.0) - tool = _try_fillet(tool, tool.edges().filter_by(Axis.Y), 4.0) - louvers.append((big & tool) - helmet) - - # --- cute pixelated cyan eyes on the visor (Anki Cozmo style) --- - # Each "pixel" rides the same surface family as the visor: the thin shell - # between the visor outer copy (`big`, 1.032x) and a slightly larger copy - # (`big_eye`), clipped to a small +X-facing square column. This makes the - # tiles follow the curved screen and sit ~1.3 mm proud with exact contact. - big_eye = scale(helmet, by=1.055) - EYE_PITCH = 4.4 # tile center-to-center - EYE_TILE = 3.6 # tile size (gaps read as the dark "screen" grid) - EYE_Z = 6.0 # local z of the eye block (head z ~95) - EYE_DY = 12.0 # half-separation between the two eyes - # 4x4 grid with the four corners dropped -> rounded-square eye. - eye_mask = [ - (c, r) - for c in range(4) - for r in range(4) - if (c, r) not in {(0, 0), (0, 3), (3, 0), (3, 3)} - ] - eye_tiles = [] - for ysign, tag in ((1.0, "l"), (-1.0, "r")): - yc = ysign * EYE_DY - for c, r in eye_mask: - ty = yc + (c - 1.5) * EYE_PITCH - tz = EYE_Z + (r - 1.5) * EYE_PITCH - tool = Pos(70.0, ty, tz) * Box(120.0, EYE_TILE, EYE_TILE) - tool = _try_fillet(tool, tool.edges().filter_by(Axis.X), 0.9) - tile = (big_eye & tool) - big - eye_tiles.append((tile, f"head_eye_{tag}_{c}{r}")) - - # --- graphite neck guard: frustum head z 12..20 under the jaw --- - guard = Pos(-3.0, 0, -73.0) * Cone( - bottom_radius=33.0, top_radius=42.0, height=8.0 - ) - guard = _try_chamfer( - guard, - [ - e - for e in guard.edges().filter_by(GeomType.CIRCLE) - if e.arc_center.Z < -76.0 - ], - 1.2, - ) - - at = Pos(*HEAD_CTR) - children = [ - styled(at * helmet, "head_shell", SHELL_COLOR), - styled(at * visor, "head_visor", VISOR_COLOR), - styled(at * chin, "head_chin_sensor", STRUCT_COLOR), - styled(at * guard, "head_neck_guard", STRUCT_COLOR), - styled(at * pods[0], "head_ear_pod_l", ALU_COLOR), - styled(at * pods[1], "head_ear_pod_r", ALU_COLOR), - styled(at * buttons[0], "head_ear_button_l", ACCENT_COLOR), - styled(at * buttons[1], "head_ear_button_r", ACCENT_COLOR), - styled(at * dots[0], "head_cam_dot_l", VISOR_COLOR), - styled(at * dots[1], "head_cam_dot_r", VISOR_COLOR), - ] - for i, louver in enumerate(louvers): - children.append(styled(at * louver, f"head_vent_louver_{i}", ACCENT_COLOR)) - for tile, name in eye_tiles: - children.append(styled(at * tile, name, EYE_COLOR)) - return part_compound("head", children) - - -def gen_step(): - return build_head() diff --git a/models/robots/juno/juno_parts/joints.py b/models/robots/juno/juno_parts/joints.py deleted file mode 100644 index f637ee2e..00000000 --- a/models/robots/juno/juno_parts/joints.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Connective joint hardware for juno: yokes, carriers, collars. - -These small machined parts link the sculpted body segments and carry the -exposed actuator cans for the joints they own. Frames follow the chain table -in juno.py; all builders return identity-location compounds. -""" - -from __future__ import annotations - -from build123d import Box, Compound, Cylinder, Pos, Rot, chamfer - -from .juno_lib import ( - ACCENT_COLOR, - ALU_COLOR, - STRUCT_COLOR, - actuator_solids, - part_compound, - styled, -) - - -def _s(side: str) -> float: - return 1.0 if side == "left" else -1.0 - - -def build_hip_bracket(side: str) -> Compound: - """Hip yaw can + C-yoke down to the hip-roll axis. Origin: hip-yaw center. - - Compact stack (proportion audit): yaw can dia 70 x 34, roll axis at - z = -64, sized for the carrier's dia-56 roll housing (caps to |x| 30). - """ - children = actuator_solids( - "hip_yaw", 70, 34, center=(0, 0, -17), axis=(0, 0, 1), bolts=False - ) - plates = [] - for sx in (1.0, -1.0): - plates.append(Pos(sx * 38, 0, -65) * Box(12, 46, 54)) - bridge = Pos(0, 0, -38) * Box(88, 46, 8) - yoke = plates[0].fuse(plates[1]).fuse(bridge) - children.append(styled(yoke, "hip_yoke", STRUCT_COLOR)) - for sx in (1.0, -1.0): - boss = Pos(sx * 47, 0, -64) * Rot(0, 90, 0) * Cylinder(radius=25, height=6) - children.append(styled(boss, f"hip_roll_boss_{'f' if sx > 0 else 'b'}", ALU_COLOR)) - return part_compound(f"hip_bracket_{side}", children) - - -def build_hip_carrier(side: str) -> Compound: - """Hip roll housing + medial plate to the hip-pitch axis. Origin: roll center. - - Compact stack: dia-56 roll housing, hip-pitch axis at z = -78 (clears the - thigh's dia-92 pitch can: 78 > 28 + 46). - """ - s = _s(side) - children = [] - housing = Rot(0, 90, 0) * Cylinder(radius=28, height=48) - plate = Pos(0, -s * 44, -42) * Box(76, 8, 110) - bridge = Pos(0, -s * 36, 0) * Box(56, 16, 50) - body = housing.fuse(bridge).fuse(plate) - children.append(styled(body, "hip_carrier_body", STRUCT_COLOR)) - for sx in (1.0, -1.0): - cap = Pos(sx * 27, 0, 0) * Rot(0, 90, 0) * Cylinder(radius=30, height=6) - children.append(styled(cap, f"hip_roll_cap_{'f' if sx > 0 else 'b'}", ALU_COLOR)) - boss = Pos(0, -s * 38.5, -78) * Rot(90, 0, 0) * Cylinder(radius=32, height=3) - children.append(styled(boss, "hip_pitch_boss", ALU_COLOR)) - return part_compound(f"hip_carrier_{side}", children) - - -def build_ankle_link(side: str) -> Compound: - """Fused two-axis ankle block: pitch core at origin, roll core 30 below.""" - children = [] - pitch_core = Rot(90, 0, 0) * Cylinder(radius=27, height=36) - roll_core = Pos(0, 0, -30) * Rot(0, 90, 0) * Cylinder(radius=22, height=28) - neck = Pos(0, 0, -15) * Box(30, 30, 30) - body = pitch_core.fuse(roll_core).fuse(neck) - children.append(styled(body, "ankle_body", STRUCT_COLOR)) - for sy in (1.0, -1.0): - cap = Pos(0, sy * 20.5, 0) * Rot(90, 0, 0) * Cylinder(radius=24, height=5) - children.append(styled(cap, f"ankle_pitch_cap_{'l' if sy > 0 else 'r'}", ALU_COLOR)) - for sx in (1.0, -1.0): - cap = Pos(sx * 16, 0, -30) * Rot(0, 90, 0) * Cylinder(radius=19, height=4) - children.append(styled(cap, f"ankle_roll_cap_{'f' if sx > 0 else 'b'}", ALU_COLOR)) - return part_compound(f"ankle_link_{side}", children) - - -def build_shoulder_pod(side: str) -> Compound: - """Shoulder-pitch can + outboard drum + roll clevis. Origin: pitch center. - - The outboard module is a chamfered cylindrical drum capping the pitch can - (reads as the shoulder actuator housing, per style review), with a blade - dropping to the shoulder-roll clevis. - """ - s = _s(side) - children = actuator_solids( - "shoulder_pitch", 72, 58, center=(0, s * 14, 0), axis=(0, 1, 0) - ) - drum = Pos(0, s * 50, 0) * Rot(90, 0, 0) * Cylinder(radius=40, height=14) - try: - drum = chamfer(drum.edges(), 3.0) - except Exception: - pass - blade = Pos(0, s * 50, -42) * Box(58, 14, 90) - clevis_f = Pos(35, s * 34, -72) * Box(10, 48, 56) - clevis_b = Pos(-35, s * 34, -72) * Box(10, 48, 56) - body = drum.fuse(blade).fuse(clevis_f).fuse(clevis_b) - children.append(styled(body, "pod_body", STRUCT_COLOR)) - face = Pos(0, s * 58.5, 0) * Rot(90, 0, 0) * Cylinder(radius=30, height=3) - children.append(styled(face, "pod_face_ring", ALU_COLOR)) - hub = Pos(0, s * 61, 0) * Rot(90, 0, 0) * Cylinder(radius=12, height=2) - children.append(styled(hub, "pod_face_hub", ACCENT_COLOR)) - for sx in (1.0, -1.0): - boss = ( - Pos(sx * 43, s * 34, -72) * Rot(0, 90, 0) * Cylinder(radius=25, height=6) - ) - children.append(styled(boss, f"roll_boss_{'f' if sx > 0 else 'b'}", ALU_COLOR)) - return part_compound(f"shoulder_pod_{side}", children) - - -def build_yaw_housing(side: str) -> Compound: - """Shoulder-roll can + saddle to the upper-arm yaw ring. Origin: roll center.""" - children = [] - parts = actuator_solids("shoulder_roll", 70, 56, center=(0, 0, 0), axis=(1, 0, 0)) - core = parts[0] - saddle = Pos(0, 0, -14) * Cylinder(radius=26, height=16) - core = styled(core.fuse(saddle), "shoulder_roll_can", STRUCT_COLOR) - children.append(core) - children.extend(parts[1:]) - ring = Pos(0, 0, -22) * Cylinder(radius=28, height=4) - children.append(styled(ring, "yaw_ring", ALU_COLOR)) - return part_compound(f"yaw_housing_{side}", children) - - -def build_wrist_carrier(side: str) -> Compound: - """Wrist-roll disc + wrist-pitch can block. Origin: roll center.""" - children = [] - disc = Pos(0, 0, -2) * Cylinder(radius=22, height=4) - children.append(styled(disc, "wrist_roll_disc", ALU_COLOR)) - parts = actuator_solids( - "wrist_pitch", 48, 40, center=(0, 0, -28), axis=(0, 1, 0), bolts=False - ) - core = parts[0] - block = Pos(0, 0, -10) * Box(34, 34, 14) - core = styled(core.fuse(block), "wrist_pitch_can", STRUCT_COLOR) - children.append(core) - children.extend(parts[1:]) - return part_compound(f"wrist_carrier_{side}", children) - - -def build_neck_collar() -> Compound: - """Neck yaw can + pitch clevis. Origin: neck-yaw joint center.""" - children = actuator_solids( - "neck_yaw", 44, 30, center=(0, 0, 15), axis=(0, 0, 1), bolts=False - ) - for sy in (1.0, -1.0): - plate = Pos(0, sy * 19, 44) * Box(36, 6, 36) - children.append(styled(plate, f"neck_clevis_{'l' if sy > 0 else 'r'}", STRUCT_COLOR)) - boss = Pos(0, sy * 23, 46) * Rot(90, 0, 0) * Cylinder(radius=14, height=3) - children.append(styled(boss, f"neck_boss_{'l' if sy > 0 else 'r'}", ALU_COLOR)) - can = actuator_solids( - "neck_pitch", 40, 28, center=(0, 0, 46), axis=(0, 1, 0), bolts=False - ) - children.extend(can) - return part_compound("neck_collar", children) diff --git a/models/robots/juno/juno_parts/juno_lib.py b/models/robots/juno/juno_parts/juno_lib.py deleted file mode 100644 index 6105684f..00000000 --- a/models/robots/juno/juno_parts/juno_lib.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Shared helpers for the juno humanoid part modules. - -Conventions (all part modules must follow): -- Units mm. Robot faces +X, +Y is robot-left, +Z is up. -- Every part builder returns an identity-location build123d Compound whose - children are closed, labeled, colored solids. Model geometry directly in the - part's local frame (joint/mate origins as documented per part). -- Limb segments: origin at the proximal joint center, segment extends -Z. -- Exposed actuator cans: cylinder axis along the joint axis, centered at the - joint center, built with `actuator_solids()`. - -Verified joint-attach math (see probe_joint.py): for build123d 0.10, -`revolute_attach()` places the child so its joint plane coincides with the -parent joint plane rotated `angle_deg` about the shared axis. -""" - -from __future__ import annotations - -import math - -from build123d import ( - Axis, - Box, - Color, - Compound, - Cylinder, - Location, - Plane, - Pos, - Rot, - Vector, - chamfer, - fillet, -) - -# Palette: warm porcelain + graphite + aluminum, with a coral accent on -# small repeated functional details (hub caps, bumpers, vents). No logos. -SHELL = (0.82, 0.815, 0.79) # warm porcelain composite shells -STRUCT = (0.16, 0.17, 0.19) # graphite structural cores -ALU = (0.88, 0.90, 0.94) # bright machined-aluminum joint rims -VISOR = (0.02, 0.05, 0.11) # gloss midnight-blue sensor glass -RUBBER = (0.08, 0.08, 0.09) # sole / grip rubber -ACCENT = (0.93, 0.38, 0.16) # coral-orange accent details -EYE = (0.32, 0.90, 0.98) # bright cyan display "pixels" (Anki-style) - -SHELL_COLOR = Color(*SHELL) -STRUCT_COLOR = Color(*STRUCT) -ALU_COLOR = Color(*ALU) -VISOR_COLOR = Color(*VISOR) -RUBBER_COLOR = Color(*RUBBER) -ACCENT_COLOR = Color(*ACCENT) -EYE_COLOR = Color(*EYE) - - -def styled(solid, label: str, color: Color): - solid.label = label - solid.color = color - return solid - - -def part_compound(label: str, children) -> Compound: - comp = Compound(children=list(children)) - comp.label = label - return comp - - -def joint_plane(origin, axis_dir, x_ref) -> Location: - return Plane( - origin=Vector(origin), x_dir=Vector(x_ref), z_dir=Vector(axis_dir) - ).location - - -def revolute_attach( - asm, - parent, - child, - name: str, - p_origin, - p_axis, - p_xref, - c_origin, - c_axis, - c_xref, - angle_deg: float, -): - """Author a parent revolute frame + child rigid frame and connect them. - - Joint origins/axes are given in each part's LOCAL modeling frame; the - helper transforms them by each part's current location, so parts that were - already repositioned by upstream connects keep correct joints (connect in - root-to-leaf order). At angle 0 the child's c_xref direction aligns with - the parent's p_xref direction about the shared axis. - """ - j_p_world = parent.location * joint_plane(p_origin, p_axis, p_xref) - z_tip = (j_p_world * Pos(0, 0, 1)).position - axis_world = Axis(j_p_world.position, z_tip - j_p_world.position) - f_parent = asm.revolute_frame( - parent, f"{name}_axis", axis_world, angular_range=(-360.0, 360.0) - ) - # The joint's stored frame may use any x-reference convention; read the - # actual frame back and fold the constant Rz difference into the child - # mount so the child's c_xref aligns with p_xref at angle 0. - joint_obj = parent.joints[f"{name}_axis"] - a_eff_world = parent.location * joint_obj.relative_axis.location - delta = j_p_world.inverse() * a_eff_world # pure Rz about the joint axis - j_c = joint_plane(c_origin, c_axis, c_xref) - l_c_world = child.location * (j_c * delta) - f_child = asm.rigid_frame(child, f"{name}_mount", l_c_world) - asm.revolute(f_parent, f_child, angle=angle_deg, label=name) - - -def _axis_rot(axis_dir) -> Rot: - """Rotation taking +Z to axis_dir (for placing cylinders along a joint axis).""" - d = Vector(axis_dir).normalized() - if abs(d.Z) > 0.999: - return Rot(0, 0, 0) if d.Z > 0 else Rot(180, 0, 0) - if abs(d.Y) > 0.999: - return Rot(-90, 0, 0) if d.Y > 0 else Rot(90, 0, 0) - if abs(d.X) > 0.999: - return Rot(0, 90, 0) if d.X > 0 else Rot(0, -90, 0) - raise ValueError("actuator axis must be a principal axis") - - -def actuator_solids( - name: str, - diameter: float, - length: float, - *, - center=(0.0, 0.0, 0.0), - axis=(0.0, 1.0, 0.0), - bolts: bool = True, -) -> list: - """Exposed cylindrical actuator module centered on a joint. - - Graphite can, recessed aluminum end caps, shallow rim grooves, and an - optional bolt circle on the +axis cap for larger sizes. Returns a list of - labeled, colored solids. - """ - r = diameter / 2.0 - rot = _axis_rot(axis) - at = Pos(*center) * rot - solids = [] - - can_len = length * 0.78 - can = at * Cylinder(radius=r, height=can_len) - can = fillet(can.edges(), min(2.2, r * 0.08)) - solids.append(styled(can, f"{name}_can", STRUCT_COLOR)) - - cap_len = (length - can_len) / 2.0 - rim_r = r * 0.92 - for sign, tag in ((1.0, "out"), (-1.0, "in")): - cap = at * Pos(0, 0, sign * (can_len / 2.0 + cap_len / 2.0)) * Cylinder( - radius=rim_r, height=cap_len - ) - cap = chamfer(cap.edges(), min(1.2, cap_len * 0.3)) - solids.append(styled(cap, f"{name}_cap_{tag}", ALU_COLOR)) - - hub = at * Pos(0, 0, length / 2.0 + 1.0) * Cylinder(radius=r * 0.45, height=2.0) - solids.append(styled(hub, f"{name}_hub", ACCENT_COLOR)) - - if bolts and diameter >= 56.0: - n = 8 if diameter >= 80 else 6 - bc_r = rim_r * 0.74 - for i in range(n): - a = 2.0 * math.pi * i / n - bolt = at * Pos( - bc_r * math.cos(a), bc_r * math.sin(a), length / 2.0 + 0.6 - ) * Cylinder(radius=1.6, height=1.2) - solids.append(styled(bolt, f"{name}_bolt_{i}", STRUCT_COLOR)) - return solids - - -def rounded_box( - label: str, - color: Color, - size, - *, - center=(0.0, 0.0, 0.0), - radius: float = 3.0, - axes: str = "z", -): - """Box with filleted edges along the given axes ("z", "xz", "xyz"...).""" - sx, sy, sz = size - box = Pos(*center) * Box(sx, sy, sz) - edges = [] - for ax_name, ax in (("x", Axis.X), ("y", Axis.Y), ("z", Axis.Z)): - if ax_name in axes: - edges.extend(box.edges().filter_by(ax)) - if edges: - box = fillet(edges, min(radius, min(sx, sy, sz) / 2.0 - 0.4)) - return styled(box, label, color) diff --git a/models/robots/juno/juno_parts/legs.py b/models/robots/juno/juno_parts/legs.py deleted file mode 100644 index eacf6064..00000000 --- a/models/robots/juno/juno_parts/legs.py +++ /dev/null @@ -1,427 +0,0 @@ -"""juno humanoid -- leg segments: thigh, shin, foot. - -Local frames (units mm, robot faces +X, +Y is robot-left, +Z up): -- thigh: origin at hip-pitch joint center; knee axis = Y through (0,0,-245). -- shin: origin at knee joint center; ankle-pitch axis = Y through (0,0,-245). -- foot: origin at ankle-roll joint center, 26 mm above the sole bottom. - -Builders return identity-location Compounds of labeled, colored solids. -Left/right mirror about the XZ plane. -""" - -from __future__ import annotations - -import sys -import traceback -from pathlib import Path - - - -from build123d import ( - Axis, - Box, - Compound, - Cylinder, - GeomType, - Plane, - Pos, - RectangleRounded, - Rot, - chamfer, - fillet, - loft, - mirror, -) - -from .juno_lib import ( - ACCENT_COLOR, - ALU_COLOR, - RUBBER_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - actuator_solids, - part_compound, - rounded_box, - styled, -) - - -# --------------------------------------------------------------- helpers - - -def _solo(shape): - """Largest solid of a boolean result (keeps each child manifold).""" - sols = shape.solids() - if len(sols) == 1: - return sols[0] - return max(sols, key=lambda s: s.volume) - - -def _solids_over(shape, min_volume=800.0): - """All solids of a boolean result above a volume floor, biggest first.""" - sols = sorted(shape.solids(), key=lambda s: s.volume, reverse=True) - return [s for s in sols if s.volume > min_volume] or sols[:1] - - -def _try_fillet(shape, edges, radius): - try: - edges = list(edges) - if edges: - return _solo(fillet(edges, radius)) - except Exception: - pass - return shape - - -def _try_chamfer(shape, edges, length): - try: - edges = list(edges) - if edges: - return _solo(chamfer(edges, length)) - except Exception: - pass - return shape - - -def _cyl_y(radius, length, center=(0.0, 0.0, 0.0)): - return Pos(*center) * Rot(-90, 0, 0) * Cylinder(radius, length) - - -def _cyl_x(radius, length, center=(0.0, 0.0, 0.0)): - return Pos(*center) * Rot(0, 90, 0) * Cylinder(radius, length) - - -def _loft_z(specs): - """Loft rounded-rectangle sections normal to Z. - - specs: iterable of (z, x_size, y_size, x_center, y_center, corner_r). - """ - faces = [ - Pos(xc, yc, z) * RectangleRounded(w, d, r) - for (z, w, d, xc, yc, r) in specs - ] - return _solo(loft(faces)) - - -def _loft_x(specs): - """Loft rounded-rectangle sections normal to X. - - specs: iterable of (x, y_size, z_size, z_center, corner_r). - """ - faces = [ - Plane.YZ.offset(x) * Pos(0, zc, 0) * RectangleRounded(wy, hz, r) - for (x, wy, hz, zc, r) in specs - ] - return _solo(loft(faces)) - - -def _rim_chamfer(shape, radius, length): - """Chamfer circular edges of a given radius (collar rims etc.).""" - try: - edges = [ - e - for e in shape.edges().filter_by(GeomType.CIRCLE) - if abs(e.radius - radius) < 0.6 - ] - return _try_chamfer(shape, edges, length) - except Exception: - return shape - - -def _yoke_fillet(shape, z_lo, z_hi, radius): - """Fillet vertical (Z) edges whose centers lie in a z band.""" - try: - edges = [ - e - for e in shape.edges().filter_by(Axis.Z) - if z_lo < e.center().Z < z_hi and e.length > 20 - ] - return _try_fillet(shape, edges, radius) - except Exception: - return shape - - -def _mirror_children(children): - out = [] - for c in children: - m = mirror(c, about=Plane.XZ) - if m.volume < 0: # reversed orientation from the mirror - try: - m = m.fix() - except Exception: - pass - out.append(styled(m, c.label, c.color)) - return out - - -def _fallback(label, size): - return [styled(Box(*size), f"{label}_fallback", STRUCT_COLOR)] - - -# ----------------------------------------------------------------- thigh - -KNEE_Z = -290.0 # stretched +45 per proportion audit (G1-like leg ratio) - - -def _thigh_children(): - ch = [] - - # Exposed hip-pitch actuator (locked interface). - ch += actuator_solids("hip_pitch", 92, 72, center=(0, 0, 0), axis=(0, 1, 0)) - - # Graphite structural core: quad-like taper + full hip collar ring, - # bored for the actuator can so faces stay clear. - core = _loft_z( - [ - (-30, 92, 72, 2, 0, 18), - (-100, 98, 86, 8, 0, 22), - (-185, 84, 76, 6, 0, 18), - (-225, 60, 68, 2, 0, 14), - ] - ) - core = _solo(core + _cyl_y(55, 34)) # hip collar band |y|<=17 - core = _solo(core - _cyl_y(46.5, 130)) # can clearance bore - core = _rim_chamfer(core, 55.0, 2.0) - ch.append(styled(core, "thigh_core", STRUCT_COLOR)) - - # Ice-gray fairing wrapping front + outer (robot-left) faces, split into - # two shells by a panel reveal that exposes the graphite core. - shell = _loft_z( - [ - (-58, 94, 78, 11, 3, 18), - (-105, 99, 88, 12, 4, 22), - (-185, 84, 80, 11, 3, 18), - (-221, 62, 70, 8, 2, 14), - ] - ) - shell = shell - core - shell = shell - (Pos(0, 0, -140.5) * Box(320, 320, 3)) # panel reveal - panels = _solids_over(shell)[:2] - tags = ("upper", "lower") - for sol, tag in zip(panels, tags): - sol = _try_chamfer(sol, sol.edges(), 0.5) - ch.append(styled(sol, f"thigh_fairing_{tag}", SHELL_COLOR)) - - # Knee fork (locked): yoke + twin plates, inner faces y=+-38, outer - # y=+-48, spanning z -225..-324, bored dia 56 on the knee axis. - fork = Pos(0, 0, -241) * Box(58, 96, 32) - for s in (1.0, -1.0): - fork = fork + Pos(0, s * 43, -273) * Box(60, 10, 34) - fork = fork + _cyl_y(34, 10, (0, s * 43, KNEE_Z)) - fork = _solo(fork - _cyl_y(28, 120, (0, 0, KNEE_Z))) - # relief arch so the shin's knee can (dia 92, |y|<=33) swings clear - fork = _solo(fork - _cyl_y(46.8, 67.6, (0, 0, KNEE_Z))) - fork = _yoke_fillet(fork, -258, -224, 8.0) - fork = _try_chamfer(fork, fork.edges(), 0.8) - ch.append(styled(fork, "knee_fork", STRUCT_COLOR)) - - # Aluminum bearing bosses dia 56 at (0,+-43,-245), through the plates. - for s, tag in ((1.0, "out"), (-1.0, "in")): - boss = _cyl_y(28, 10, (0, s * 43, KNEE_Z)) + _cyl_y( - 30.5, 1.8, (0, s * 48.9, KNEE_Z) - ) - boss = _solo(boss) - boss = _try_chamfer(boss, boss.edges(), 0.6) - ch.append(styled(boss, f"knee_boss_{tag}", ALU_COLOR)) - - return ch - - -def build_thigh(side="left"): - label = f"thigh_{side}" - try: - ch = _thigh_children() - except Exception: - traceback.print_exc() - ch = _fallback(label, (90, 90, 260)) - if side == "right": - ch = _mirror_children(ch) - return part_compound(label, ch) - - -# ------------------------------------------------------------------ shin - -ANKLE_Z = -290.0 # stretched +45 per proportion audit (G1-like leg ratio) - - -def _shin_children(): - ch = [] - - # Exposed knee actuator (locked interface; dia 92 per torque hierarchy). - ch += actuator_solids("knee", 92, 66, center=(0, 0, 0), axis=(0, 1, 0)) - - # Graphite calf core: full upper rear (calf), slim near ankle. - core = _loft_z( - [ - (-26, 82, 60, 0, 0, 16), - (-95, 84, 72, -6, 0, 18), - (-185, 62, 58, -2, 0, 14), - (-225, 44, 50, 0, 0, 11), - ] - ) - core = _solo(core + _cyl_y(53, 30)) # knee collar band |y|<=15 - core = _solo(core - _cyl_y(46.5, 120)) # can clearance bore - core = _rim_chamfer(core, 53.0, 2.0) - ch.append(styled(core, "shin_core", STRUCT_COLOR)) - - # Front shin-guard ridge (ice-gray), split by a panel reveal. - guard = _loft_z( - [ - (-55, 78, 64, 7, 0, 16), - (-100, 78, 64, 6, 0, 16), - (-185, 64, 50, 6, 0, 12), - (-223, 46, 40, 4, 0, 10), - ] - ) - guard = guard - core - guard = guard - (Pos(0, 0, -145.5) * Box(320, 320, 3)) - panels = _solids_over(guard)[:2] - for sol, tag in zip(panels, ("upper", "lower")): - sol = _try_chamfer(sol, sol.edges(), 0.5) - ch.append(styled(sol, f"shin_guard_{tag}", SHELL_COLOR)) - - # Ankle fork (locked): plates inner y=+-26, outer y=+-34, bored dia 40 - # on the ankle-pitch axis through (0,0,-290). - fork = Pos(0, 0, -241) * Box(46, 68, 28) - for s in (1.0, -1.0): - fork = fork + Pos(0, s * 30, -270.5) * Box(54, 8, 39) - fork = fork + _cyl_y(27, 8, (0, s * 30, ANKLE_Z)) - fork = _solo(fork - _cyl_y(20, 100, (0, 0, ANKLE_Z))) - fork = _yoke_fillet(fork, -256, -226, 7.0) - fork = _try_chamfer(fork, fork.edges(), 0.8) - ch.append(styled(fork, "ankle_fork", STRUCT_COLOR)) - - # Aluminum bearing bosses dia 40 at (0,+-30,-245). - for s, tag in ((1.0, "out"), (-1.0, "in")): - boss = _cyl_y(20, 8, (0, s * 30, ANKLE_Z)) + _cyl_y( - 22, 1.8, (0, s * 34.9, ANKLE_Z) - ) - boss = _solo(boss) - boss = _try_chamfer(boss, boss.edges(), 0.6) - ch.append(styled(boss, f"ankle_boss_{tag}", ALU_COLOR)) - - return ch - - -def build_shin(side="left"): - label = f"shin_{side}" - try: - ch = _shin_children() - except Exception: - traceback.print_exc() - ch = _fallback(label, (80, 80, 260)) - if side == "right": - ch = _mirror_children(ch) - return part_compound(label, ch) - - -# ------------------------------------------------------------------ foot - - -def _foot_children(): - ch = [] - - # Clearance for the ankle-roll can (dia 44, |x|<21 keep-clear r<24). - roll_clear = _cyl_x(24.5, 43) - - # Rubber sole plate z -26..-19, x -68..112, width 70, toe-up + heel kick. - sole = rounded_box( - "sole_blank", RUBBER_COLOR, (180, 70, 7), center=(22, 0, -22.5), - radius=16, axes="z", - ) - sole = sole - roll_clear - # toe-up wedge over the last ~30 mm (cut line (82,-26) -> (113,-20.4)) - sole = sole - (Pos(96.5, 0, -29.46) * Rot(0, -10.3, 0) * Box(44, 96, 12)) - # small heel kick (cut line (-58,-26) -> (-69,-22.5)) - sole = sole - (Pos(-63, 0, -29.66) * Rot(0, 17.65, 0) * Box(24, 96, 10)) - for gx in (-54.0, -36.0, -18.0, 36.0, 54.0, 72.0): # tread grooves - sole = sole - (Pos(gx, 0, -25.8) * Box(4.5, 100, 4.5)) - ch.append(styled(_solo(sole), "sole", RUBBER_COLOR)) - - # Sneaker-like wedge upper (ice-gray), lofted along X. Heights raised - # ~+6 mm per the visual review (feet read too thin in profile). - upper = _loft_x( - [ - (-62, 54, 27, -5.5, 6), - (-30, 62, 32, -3.0, 7), - (30, 62, 30, -4.0, 7), - (72, 56, 22, -8.0, 5), - (104, 38, 10, -14.0, 2.4), - ] - ) - recess = rounded_box( - "recess_cut", SHELL_COLOR, (66, 64, 20), center=(0, 0, 2), - radius=9, axes="z", - ) - upper = upper - recess # saddle recess down to z=-8 - upper = _solo(upper - roll_clear) - ch.append(styled(upper, "foot_upper", SHELL_COLOR)) - - # Top saddle (locked): twin plates normal to X at x 22..32 / -32..-22, - # domed around the roll axis, tied by side rails outside the keep-clear. - saddle = None - for s in (1.0, -1.0): - plate = Pos(27 * s, 0, -1) * Box(10, 62, 14) - plate = plate + _cyl_x(19, 10, (27 * s, 0, 0)) - saddle = plate if saddle is None else saddle + plate - saddle = saddle + Pos(0, s * 27.75, -2) * Box(64, 6.5, 12) - saddle = saddle & (Pos(0, 0, 4) * Box(80, 90, 24)) # clip to z -8..16 - saddle = _solo(saddle) - saddle = _try_chamfer(saddle, saddle.edges(), 0.8) - ch.append(styled(saddle, "roll_saddle", STRUCT_COLOR)) - - # Aluminum bearing bosses dia 34, coaxial with X through the origin. - for s, tag in ((1.0, "out"), (-1.0, "in")): - boss = _cyl_x(17, 5.5, (34.75 * s, 0, 0)) + _cyl_x( - 9, 2, (38.5 * s, 0, 0) - ) - boss = _solo(boss) - boss = _try_chamfer(boss, boss.edges(), 0.6) - ch.append(styled(boss, f"roll_boss_{tag}", ALU_COLOR)) - - # Graphite toe bumper wrapping the toe box. - toe = rounded_box( - "toe_blank", STRUCT_COLOR, (13, 50, 10), center=(105.5, 0, -14), - radius=5, axes="z", - ) - toe = _solo(toe - upper) - ch.append(styled(toe, "toe_bumper", ACCENT_COLOR)) - - # Graphite heel bumper. - heel = rounded_box( - "heel_blank", STRUCT_COLOR, (10, 52, 11), center=(-63, 0, -13.5), - radius=4, axes="z", - ) - heel = _solo(heel - upper) - ch.append(styled(heel, "heel_bumper", ACCENT_COLOR)) - - return ch - - -def build_foot(side="left"): - label = f"foot_{side}" - try: - ch = _foot_children() - except Exception: - traceback.print_exc() - ch = _fallback(label, (180, 70, 32)) - if side == "right": - ch = _mirror_children(ch) - return part_compound(label, ch) - - -# ------------------------------------------------------------------ demo - - -def gen_step(): - cols = [] - x = 0.0 - for side in ("left", "right"): - for builder in (build_thigh, build_shin, build_foot): - comp = builder(side) - comp.location = Pos(x, 0, 0) - cols.append(comp) - x += 260.0 - demo = Compound(children=cols) - demo.label = "juno_legs_demo" - return demo diff --git a/models/robots/juno/juno_parts/mass_props.py b/models/robots/juno/juno_parts/mass_props.py deleted file mode 100644 index 7dd8a9b9..00000000 --- a/models/robots/juno/juno_parts/mass_props.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Baked CAD mass properties and bounding boxes for juno links. - -Generated from the juno_parts builders with OCP BRepGProp volume -properties at unit density in each part's local frame (== URDF link -frame); MatrixOfInertia is reported about the center of mass. Units: -mm^3 volume, mm COM/bbox, mm^5 unit-density inertia. Regenerate with a -one-off BRepGProp pass over the builders if part geometry changes. -""" - -from __future__ import annotations - -TOTAL_VOLUME_MM3 = 21995764.7 - -# link: (volume_mm3, com_mm, inertia_com_mm5 (ixx, iyy, izz, ixy, ixz, iyz), -# bbox_min_mm, bbox_max_mm) -LINK_MASS_PROPS = { - "pelvis": ( - 1851739.1, - (0.530, 0.000, -60.660), - (10575555495.5, 2986416959.4, 9648461832.1, -0.0, 27571603.0, 0.0), - (-55.695, -132.500, -120.000), - (55.695, 132.500, 2.000), - ), - "torso": ( - 6108265.6, - (7.794, 0.000, 191.529), - (62794957817.2, 52289642329.9, 24408631078.9, -4456.8, -624909761.4, 2396.0), - (-60.697, -132.000, 2.000), - (78.799, 132.000, 330.000), - ), - "neck_collar": ( - 97687.2, - (0.000, 0.078, 31.736), - (45359820.3, 40671978.2, 25110818.2, -0.0, -0.0, -108895.8), - (-22.000, -24.500, -0.000), - (22.000, 24.500, 66.000), - ), - "head": ( - 1430411.4, - (5.892, 0.000, 78.745), - (3007509481.7, 2878444626.8, 2616214902.5, 400.8, 75520856.9, -277.3), - (-55.025, -68.600, 12.000), - (69.718, 68.600, 149.500), - ), - "hip_bracket_left": ( - 238176.4, - (-0.000, -0.000, -35.975), - (202101382.9, 336364547.2, 246804392.9, 0.0, 0.0, -0.0), - (-50.000, -35.000, -92.000), - (50.000, 35.000, 2.000), - ), - "hip_bracket_right": ( - 238176.4, - (-0.000, -0.000, -35.975), - (202101382.9, 336364547.2, 246804392.9, 0.0, 0.0, -0.0), - (-50.000, -35.000, -92.000), - (50.000, 35.000, 2.000), - ), - "hip_carrier_left": ( - 264972.6, - (0.000, -17.245, -13.249), - (379036437.7, 330576905.5, 231821029.9, -0.0, -0.0, -89888016.9), - (-38.000, -48.000, -110.000), - (38.000, 30.000, 30.000), - ), - "hip_carrier_right": ( - 264972.6, - (-0.000, 17.245, -13.249), - (379036437.7, 330576905.5, 231821029.9, 0.0, -0.0, 89888016.9), - (-38.000, -30.000, -110.000), - (38.000, 48.000, 30.000), - ), - "thigh_left": ( - 2199611.8, - (5.738, 1.309, -111.338), - (18083649732.5, 18275231745.6, 2772515196.8, -32733791.4, 192056640.6, 56844098.9), - (-55.000, -49.800, -324.000), - (61.645, 49.800, 55.000), - ), - "thigh_right": ( - 2199611.8, - (5.738, -1.309, -111.338), - (18083649732.5, 18275231745.6, 2772515196.8, 32733791.4, 192056640.6, -56844098.9), - (-55.000, -49.800, -324.000), - (61.645, 49.800, 55.000), - ), - "shin_left": ( - 1538517.5, - (-0.561, 0.061, -96.071), - (11871561616.6, 12163653962.6, 1389886188.1, -52840.1, 50385797.6, -9043310.6), - (-53.000, -36.030, -317.000), - (53.000, 36.030, 53.000), - ), - "shin_right": ( - 1538517.5, - (-0.561, -0.061, -96.071), - (11871561616.6, 12163653962.6, 1389886188.1, 52840.1, 50385797.6, 9043310.6), - (-53.000, -36.030, -317.000), - (53.000, 36.030, 53.000), - ), - "ankle_link_left": ( - 137561.9, - (-0.000, -0.000, -9.368), - (74241221.1, 74983355.2, 43104451.9, -0.0, 0.0, 0.0), - (-27.000, -23.000, -52.000), - (27.000, 23.000, 27.000), - ), - "ankle_link_right": ( - 137561.9, - (-0.000, -0.000, -9.368), - (74241221.1, 74983355.2, 43104451.9, -0.0, 0.0, 0.0), - (-27.000, -23.000, -52.000), - (27.000, 23.000, 27.000), - ), - "foot_left": ( - 283123.4, - (16.160, 0.000, -9.901), - (123113764.4, 739379797.2, 802702130.6, 10.2, 20480873.7, -7.0), - (-68.000, -35.000, -26.000), - (112.000, 35.000, 17.000), - ), - "foot_right": ( - 283123.4, - (16.160, -0.000, -9.901), - (123113764.4, 739379797.2, 802702130.6, -10.2, 20480873.7, 7.0), - (-68.000, -35.000, -26.000), - (112.000, 35.000, 17.000), - ), - "shoulder_pod_left": ( - 425574.2, - (-0.000, 28.050, -19.091), - (714790487.6, 751008417.9, 407973611.1, 0.0, -0.0, 89264397.4), - (-46.000, -15.000, -100.000), - (46.000, 62.000, 40.000), - ), - "shoulder_pod_right": ( - 425574.2, - (-0.000, -27.809, -19.091), - (717642321.8, 751008417.9, 410825445.2, -0.0, -0.0, -91218689.8), - (-46.000, -62.000, -100.000), - (46.000, 17.000, 40.000), - ), - "yaw_housing_left": ( - 221220.3, - (0.212, 0.000, -1.141), - (129954916.4, 122383446.3, 119370777.6, 0.0, -53460.7, -0.0), - (-28.000, -35.000, -35.000), - (30.000, 35.000, 35.000), - ), - "yaw_housing_right": ( - 221220.3, - (0.212, 0.000, -1.141), - (129954916.4, 122383446.3, 119370777.6, 0.0, -53460.7, -0.0), - (-28.000, -35.000, -35.000), - (30.000, 35.000, 35.000), - ), - "bicep_left": ( - 301963.4, - (0.000, -0.000, -71.359), - (744624663.9, 710292803.0, 140767841.6, 0.1, -4.1, 0.6), - (-28.000, -38.000, -181.000), - (28.000, 38.000, 0.000), - ), - "bicep_right": ( - 301963.4, - (0.000, -0.000, -71.359), - (744624663.9, 710292803.0, 140767841.6, 0.1, -4.1, 0.6), - (-28.000, -38.000, -181.000), - (28.000, 38.000, 0.000), - ), - "forearm_left": ( - 283988.8, - (0.000, 0.117, -40.459), - (762882647.6, 775052016.1, 91527058.5, -74.9, 2642.2, -1346194.6), - (-31.000, -25.000, -150.000), - (31.000, 27.000, 31.000), - ), - "forearm_right": ( - 283988.6, - (0.000, -0.117, -40.459), - (762882302.6, 775051637.8, 91527008.7, 74.2, 2640.4, 1346197.1), - (-31.000, -27.000, -150.000), - (31.000, 25.000, 31.000), - ), - "wrist_carrier_left": ( - 80906.3, - (-0.000, 0.190, -24.780), - (25554672.8, 26183303.0, 21412679.7, -0.0, -0.0, 49549.1), - (-24.000, -22.000, -52.000), - (24.000, 22.000, 0.000), - ), - "wrist_carrier_right": ( - 80906.3, - (-0.000, 0.190, -24.780), - (25554672.8, 26183303.0, 21412679.7, -0.0, -0.0, 49549.1), - (-24.000, -22.000, -52.000), - (24.000, 22.000, 0.000), - ), - "hand_left": ( - 278214.4, - (4.151, -1.046, -70.129), - (533619427.8, 571570852.8, 168975930.2, 1937556.6, 3113956.1, -26275069.1), - (-35.750, -33.485, -167.575), - (61.183, 33.200, 24.000), - ), - "hand_right": ( - 278214.4, - (4.151, 1.046, -70.129), - (533619427.8, 571570852.8, 168975930.2, -1937556.6, 3113956.1, 26275069.1), - (-35.750, -33.200, -167.575), - (61.183, 33.485, 24.000), - ), -} diff --git a/models/robots/juno/juno_parts/pelvis.py b/models/robots/juno/juno_parts/pelvis.py deleted file mode 100644 index a2446a89..00000000 --- a/models/robots/juno/juno_parts/pelvis.py +++ /dev/null @@ -1,260 +0,0 @@ -"""juno pelvis: waist-yaw ring, graphite chassis, hip pods, shell fairings. - -Local frame: origin = waist-yaw joint center at the TOP of the pelvis. -Geometry spans z -120..+2. Robot faces +X, +Y robot-left, +Z up. - -Locked interfaces (do not move): -- Waist ring: aluminum cylinder dia 96, z -2..+2, centered on Z. -- Hip pods: flat round mounting face dia 80 at z=-120 centered (0, +-90, -120); - everything below z=-120 stays clear for the leg hip-yaw actuator (dia 76). -- Envelope: x -65..+65, |y| <= 115 (pod faces at y=+-90 r40 reach 122 by the - locked interface itself). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - - - -import math - -from build123d import ( - Axis, - Box, - Cylinder, - Plane, - Pos, - RectangleRounded, - Rot, - chamfer, - extrude, - fillet, -) - -from .juno_lib import ( - ALU_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - part_compound, - styled, -) - - -def _safe(op, solid): - """Run a fillet/chamfer op; fall back to the input solid on any failure.""" - try: - out = op(solid) - if out is not None and out.volume > 1.0: - return out - except Exception: - pass - return solid - - -def _zband(solid, lo, hi): - return solid.edges().filter_by_position(Axis.Z, lo, hi) - - -# ---------------------------------------------------------------- chassis --- - -def _waist_deck(): - deck = Pos(0, 0, -8) * Box(104, 118, 12) - deck = _safe(lambda s: fillet(s.edges().filter_by(Axis.Z), 14), deck) - deck = _safe(lambda s: chamfer(_zband(s, -2.01, -1.99), 2.0), deck) - deck = _safe(lambda s: chamfer(_zband(s, -14.01, -13.99), 2.0), deck) - return deck - - -def _abdomen_column(): - try: - sk = Plane( - origin=(0, 0, -14), x_dir=(1, 0, 0), z_dir=(0, 0, -1) - ) * RectangleRounded(86, 96, 13) - col = extrude(sk, amount=58, taper=6) - if col.volume > 1.0: - return col - except Exception: - pass - return Pos(0, 0, -43) * Box(80, 90, 58) - - -def _hip_yoke(): - yoke = Pos(0, 0, -86) * Box(76, 164, 36) - yoke = _safe(lambda s: chamfer(s.edges().filter_by(Axis.Y), 5.0), yoke) - return yoke - - -def _pod(side): - return Pos(0, side * 90, -86) * Cylinder(radius=40, height=56) - - -def _chassis(): - chassis = _waist_deck() + _abdomen_column() + _hip_yoke() - chassis = chassis + _pod(+1) + _pod(-1) - - # Recessed service pockets on yoke front/back faces. - for sx in (+1, -1): - for sy in (+1, -1): - chassis -= Pos(sx * 38, sy * 48, -86) * Box(4, 36, 22) - - # Panel seam groove around each pod above the aluminum rim. - for sy in (+1, -1): - ring = Pos(0, sy * 90, -100) * ( - Cylinder(radius=45, height=1.8) - Cylinder(radius=37.5, height=1.8) - ) - chassis -= ring - return styled(chassis, "pelvis_chassis", STRUCT_COLOR) - - -# ----------------------------------------------------------------- accents --- - -def _waist_ring(): - ring = Cylinder(radius=48, height=4) - ring = _safe(lambda s: chamfer(s.edges(), 0.8), ring) - return styled(ring, "pelvis_waist_ring", ALU_COLOR) - - -def _pod_rim(side, tag): - rim = Pos(0, side * 90, -117) * Cylinder(radius=40, height=6) - rim = _safe(lambda s: chamfer(_zband(s, -120.01, -119.9), 1.0), rim) - rim = _safe(lambda s: chamfer(_zband(s, -114.1, -113.9), 0.8), rim) - # machined witness groove - rim -= Pos(0, side * 90, -116) * ( - Cylinder(radius=41, height=1.0) - Cylinder(radius=39, height=1.0) - ) - return styled(rim, f"pelvis_pod_rim_{tag}", ALU_COLOR) - - -def _pod_collar(side, tag): - collar = Pos(0, side * 90, -83) * ( - Cylinder(radius=42.5, height=4) - Cylinder(radius=40, height=4) - ) - collar = _safe(lambda s: chamfer(_zband(s, -85.01, -84.9), 0.7), collar) - return styled(collar, f"pelvis_pod_collar_{tag}", ALU_COLOR) - - -# ---------------------------------------------------------------- fairings --- - -def _fairing(side, tag): - s = side - # Volumetric upper hip shell: y 56..112, z -76..-12, x -58..58. - blank = Pos(0, s * 84, -44) * Box(116, 56, 64) - blank = _safe(lambda b: fillet(b.edges().filter_by(Axis.Z), 14), blank) - - # Plan-view 45 deg facets at the outer-front / outer-rear corners. - blank -= ( - Pos(50, s * 100, -44) * Rot(0, 0, -s * 45.0) * Pos(16, 0, 0) * Box(32, 64, 84) - ) - blank -= ( - Pos(-50, s * 100, -44) * Rot(0, 0, s * 45.0) * Pos(-16, 0, 0) * Box(32, 64, 84) - ) - blank = _safe(lambda b: fillet(b.edges().filter_by(Axis.Z), 5.0), blank) - - # Gentle top-outer slope (hip shoulder). - blank -= ( - Pos(0, s * 72, -12) - * Rot(-18.0 * s, 0, 0) - * Pos(0, 0, 36) - * Box(150, 170, 72) - ) - # Gentle front-top and rear-top slopes. - blank -= Pos(38, s * 84, -13) * Rot(0, 20, 0) * Pos(0, 0, 36) * Box(110, 150, 72) - blank -= Pos(-38, s * 84, -13) * Rot(0, -20, 0) * Pos(0, 0, 36) * Box(110, 150, 72) - - # Mild bottom tucks: lower edge rises toward front and rear. - blank -= ( - Pos(28, s * 84, -76) * Rot(0, -14, 0) * Pos(0, 0, -36) * Box(130, 150, 72) - ) - blank -= ( - Pos(-28, s * 84, -76) * Rot(0, 14, 0) * Pos(0, 0, -36) * Box(130, 150, 72) - ) - # Angled under-edge: bottom rises inboard so the shell lower line meets - # the yoke clearance (-64 at y=56) flush, no stepped notch. - blank -= ( - Pos(0, s * 56, -64) - * Rot(-12.0 * s, 0, 0) - * Pos(0, 0, -36) - * Box(150, 170, 72) - ) - # NOTE: no post-cut fillets here. OCCT's fillet on the sloped-cut edge - # network can throw inside ChFi3d and the unwind wedges the process under - # Rosetta, so the sculpted creases stay crisp by design. - - # Clearance cuts: ride over the chassis with a visible seam gap. - blank -= Pos(0, s * 90, -88) * Cylinder(radius=43.5, height=70) # pod dome - blank -= Pos(0, s * 60, -86) * Box(84, 120, 44) # yoke - blank -= Pos(0, 0, -7) * Box(110, 126, 14) # waist deck - - # Angled vent grooves on the front 45 deg facet. - ux, uy = math.cos(math.radians(45)), math.sin(math.radians(45)) - for d in (-8.0, 0.0, 8.0): - cx = 50.0 + d * ux + 2.5 * ux - cy = s * (100.0 - d * uy + 2.5 * uy) - blank -= Pos(cx, cy, -48) * Rot(0, 0, -s * 45.0) * Box(3.0, 8.0, 20.0) - - # Horizontal panel-line groove across the outer face. - blank -= Pos(0, s * 112.8, -42) * Box(60.0, 4.0, 2.2) - - return styled(blank, f"pelvis_fairing_{tag}", SHELL_COLOR) - - -# ------------------------------------------------------------- crotch guard -- - -def _crotch_guard(): - # Faceted plate built in a local frame, then tilted and pushed to the - # front of the yoke. - p = Box(8, 46, 40) - front_vert = ( - lambda g: chamfer( - g.edges().filter_by(Axis.Z).filter_by_position(Axis.X, 3.9, 4.1), - 6.0, - ) - ) - p = _safe(front_vert, p) - p = _safe( - lambda g: chamfer( - g.edges() - .filter_by(Axis.Y) - .filter_by_position(Axis.Z, -20.1, -19.9) - .filter_by_position(Axis.X, 0.0, 4.2), - 5.0, - ), - p, - ) - p = _safe( - lambda g: chamfer( - g.edges() - .filter_by(Axis.Y) - .filter_by_position(Axis.Z, 19.9, 20.1) - .filter_by_position(Axis.X, 0.0, 4.2), - 3.0, - ), - p, - ) - guard = Pos(50, 0, -86) * Rot(0, 4, 0) * p - # Base wedge filling the gap back to the yoke front face (x=38). - guard += Pos(42.75, 0, -88) * Box(9.5, 40, 32) - return styled(guard, "pelvis_crotch_guard", STRUCT_COLOR) - - -# ------------------------------------------------------------------- part --- - -def build_pelvis(): - children = [ - _chassis(), - _waist_ring(), - _pod_rim(+1, "left"), - _pod_rim(-1, "right"), - _pod_collar(+1, "left"), - _pod_collar(-1, "right"), - _fairing(+1, "left"), - _fairing(-1, "right"), - _crotch_guard(), - ] - return part_compound("pelvis", children) - - -def gen_step(): - return build_pelvis() diff --git a/models/robots/juno/juno_parts/torso.py b/models/robots/juno/juno_parts/torso.py deleted file mode 100644 index f06744c6..00000000 --- a/models/robots/juno/juno_parts/torso.py +++ /dev/null @@ -1,204 +0,0 @@ -"""juno torso/chest. - -Local frame: origin at the waist-yaw joint center (bottom of torso), -+X forward, +Y robot-left, +Z up. Units mm. - -Locked interfaces (do not change): -- nothing below z=+2; graphite waist collar dia 100 spans z 2..36 -- chest volume z 36..320, |y| <= 118, x -78..+82 -- shoulder bosses: ALU cylinders along Y at (0, +-y, 290), |y| 112..132, dia 76 -- neck: ALU collar dia 52, z 318..324; z > 324 kept clear within r < 40 -- top deck <= z 330 - -Construction: a graphite lofted core plus a 5 mm conformal shell ring -(outer loft minus inner loft). Ice-gray armor panels are pieces of that -ring clipped by rounded region boxes, so every seam is a 5 mm-deep -recess exposing the graphite core (G1-style two-tone panel lines). -""" - -from __future__ import annotations - -import sys -from pathlib import Path - - - -import math # noqa: F401 (kept for parity with sibling part modules) - -from build123d import ( - Axis, - Box, - Cylinder, - Plane, - Pos, - RectangleRounded, - Rot, - SortBy, - chamfer, - fillet, - loft, -) - -from .juno_lib import ( - ALU_COLOR, - SHELL_COLOR, - STRUCT_COLOR, - VISOR_COLOR, - part_compound, - styled, -) - -# Loft sections: (z, depth_x, width_y, corner_r, center_x). -# Front line bulges to x=+78 at the chest and pulls back at the top for a -# slightly forward-leaning athletic silhouette; width V-tapers 148 -> 222. -_OUTER = [ - (36.0, 104.0, 132.0, 22.0, 2.0), - (90.0, 112.0, 150.0, 26.0, 5.0), - (160.0, 122.0, 172.0, 30.0, 9.0), - (225.0, 132.0, 196.0, 33.0, 12.0), - (272.0, 138.0, 216.0, 35.0, 9.0), - (300.0, 132.0, 222.0, 36.0, 6.0), - (310.0, 124.0, 210.0, 34.0, 4.0), -] -_SHELL_T = 5.0 -_INNER = [ - (z, d - 2 * _SHELL_T, w - 2 * _SHELL_T, r - _SHELL_T, cx) - for (z, d, w, r, cx) in _OUTER -] - - -def _loft_solid(specs): - sections = [ - Plane.XY.offset(z) * Pos(cx, 0, 0) * RectangleRounded(d, w, r) - for (z, d, w, r, cx) in specs - ] - return loft(sections) - - -def _safe_fillet(solid, edges, radius): - try: - return fillet(edges, radius) - except Exception: - return solid - - -def _safe_chamfer(solid, edges, length): - try: - return chamfer(edges, length) - except Exception: - return solid - - -def _region(size, center, radius): - """Clip volume with rounded Y/Z seam corners (edges along X filleted).""" - box = Pos(*center) * Box(*size) - try: - box = fillet(box.edges().filter_by(Axis.X), radius) - except Exception: - pass - return box - - -def _clip_panel(ring, region): - piece = ring & region - solids = piece.solids() - if len(solids) > 1: - piece = solids.sort_by(SortBy.VOLUME)[-1] - return _safe_fillet(piece, piece.edges(), 1.0) - - -def build_torso(): - children = [] - - # --- graphite waist collar (locked: dia 100, z 2..36) ---------------- - collar = Pos(0, 0, 19) * Cylinder(radius=50, height=34) - collar = _safe_chamfer(collar, collar.edges(), 2.0) - for zc in (9.0, 27.0): - groove = Pos(0, 0, zc) * ( - Cylinder(radius=52, height=2.4) - Cylinder(radius=48, height=2.4) - ) - collar = collar - groove - children.append(styled(collar, "torso_waist_collar", STRUCT_COLOR)) - - # --- graphite core + shoulder gusset barrels -------------------------- - outer = _loft_solid(_OUTER) - inner = _loft_solid(_INNER) - ring = outer - inner - - core = inner - for sgn in (1.0, -1.0): - gusset = Pos(0, sgn * 100.0, 290) * Rot(-90, 0, 0) * Cylinder( - radius=40, height=24 - ) - gusset = _safe_chamfer(gusset, gusset.edges(), 1.5) - core = core + gusset - - # side intake louvres: three slanted grooves cut into each flank - for sgn in (1.0, -1.0): - for xc in (-14.0, 4.0, 22.0): - slot = Pos(xc, sgn * 90.0, 205) * Rot(0, -12, 0) * Box(9, 14, 64) - core = core - slot - - # upper-chest sensor pocket (flat floor at x = 59) - pocket = Pos(69.5, 0, 302) * Box(21, 48, 11) - core = core - pocket - children.append(styled(core, "torso_core", STRUCT_COLOR)) - - # gloss-black sensor bar recessed in the pocket - sensor = Pos(61.5, 0, 301.5) * Box(5, 44, 8) - sensor = _safe_fillet(sensor, sensor.edges().filter_by(Axis.X), 2.0) - children.append(styled(sensor, "torso_chest_sensor", VISOR_COLOR)) - - # --- ice-gray armor panels (conformal ring pieces) -------------------- - # twin pectoral plates with a 10 mm center crease - for sgn, tag in ((1.0, "l"), (-1.0, "r")): - pec = _clip_panel( - ring, _region((95, 68, 146), (47.5, sgn * 39.0, 223), 12) - ) - children.append(styled(pec, f"torso_pec_panel_{tag}", SHELL_COLOR)) - - # abdomen plate below an 8 mm horizontal reveal - abdomen = _clip_panel(ring, _region((95, 90, 92), (47.5, 0, 98), 10)) - children.append(styled(abdomen, "torso_abdomen_panel", SHELL_COLOR)) - - # thin backpack panels either side of the graphite spine column - for sgn, tag in ((1.0, "l"), (-1.0, "r")): - back = _clip_panel( - ring, _region((65, 45, 220), (-62.5, sgn * 35.5, 180), 10) - ) - children.append(styled(back, f"torso_back_panel_{tag}", SHELL_COLOR)) - - # slim graphite spine column, flush with the back panels - spine = _clip_panel(ring, _region((65, 16, 258), (-62.5, 0, 173), 6)) - children.append(styled(spine, "torso_spine_column", STRUCT_COLOR)) - - # --- top deck + neck collar ------------------------------------------- - deck_specs = [ - (310.0, 110.0, 170.0, 30.0, 4.0), - (318.0, 110.0, 170.0, 30.0, 4.0), - ] - deck = _loft_solid(deck_specs) - deck = _safe_chamfer(deck, deck.edges().group_by(Axis.Z)[-1], 2.5) - children.append(styled(deck, "torso_top_deck", SHELL_COLOR)) - - neck = Pos(0, 0, 321) * Cylinder(radius=26, height=6) - neck = _safe_chamfer(neck, neck.edges().group_by(Axis.Z)[-1], 1.0) - children.append(styled(neck, "torso_neck_collar", ALU_COLOR)) - - # --- aluminum shoulder bosses (locked: dia 76, |y| 112..132) ---------- - for sgn, tag in ((1.0, "l"), (-1.0, "r")): - boss = Pos(0, sgn * 122.0, 290) * Rot(-90, 0, 0) * Cylinder( - radius=38, height=20 - ) - boss = _safe_chamfer(boss, boss.edges(), 2.0) - groove = Pos(0, sgn * 126.0, 290) * Rot(-90, 0, 0) * ( - Cylinder(radius=39, height=2.5) - Cylinder(radius=36.5, height=2.5) - ) - boss = boss - groove - children.append(styled(boss, f"torso_shoulder_boss_{tag}", ALU_COLOR)) - - return part_compound("torso", children) - - -def gen_step(): - return build_torso() diff --git a/models/robots/lekiwi/lekiwi.srdf b/models/robots/lekiwi/lekiwi.srdf deleted file mode 100644 index 44f7e748..00000000 --- a/models/robots/lekiwi/lekiwi.srdf +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/lekiwi/lekiwi.urdf b/models/robots/lekiwi/lekiwi.urdf deleted file mode 100644 index c4043cc2..00000000 --- a/models/robots/lekiwi/lekiwi.urdf +++ /dev/null @@ -1,1087 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-1.stl b/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-1.stl deleted file mode 100644 index 17448b62..00000000 --- a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c8e0e2b16340d3fd6738ae0077c1363c719b71ed7a54129103dddbd058b3c50 -size 15712284 diff --git a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-2.stl b/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-2.stl deleted file mode 100644 index 17448b62..00000000 --- a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c8e0e2b16340d3fd6738ae0077c1363c719b71ed7a54129103dddbd058b3c50 -size 15712284 diff --git a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1.stl b/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1.stl deleted file mode 100644 index 17448b62..00000000 --- a/models/robots/lekiwi/meshes/4-Omni-Directional-Wheel_Single_Body-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c8e0e2b16340d3fd6738ae0077c1363c719b71ed7a54129103dddbd058b3c50 -size 15712284 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-1.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-1.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-2.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-2.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-3.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-3.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-4.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-4.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-4.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-5.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-5.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff-5.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff.stl b/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff.stl deleted file mode 100644 index 99af667e..00000000 --- a/models/robots/lekiwi/meshes/94868A713_NO-THREADS_Female-Threaded-Hex-Standoff.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b488075dce6863ecb6b8d03e25b243fda1f7d3d65fae13cfd172e7f8ed686f0e -size 57684 diff --git a/models/robots/lekiwi/meshes/Base_08q-v1.stl b/models/robots/lekiwi/meshes/Base_08q-v1.stl deleted file mode 100644 index 63aa78a6..00000000 --- a/models/robots/lekiwi/meshes/Base_08q-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a05be37db52657615ac69423fe577979efa0cd0634a5e9546bbc85537f003172 -size 856484 diff --git a/models/robots/lekiwi/meshes/Battery---Battery-5.2-Ah-DC5521-Plug-v2.stl b/models/robots/lekiwi/meshes/Battery---Battery-5.2-Ah-DC5521-Plug-v2.stl deleted file mode 100644 index 4f327b45..00000000 --- a/models/robots/lekiwi/meshes/Battery---Battery-5.2-Ah-DC5521-Plug-v2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0cd9200f80ff3a75c8b0447eb2e80cd649a6ac86271ff5003571a9f5d94d42c -size 85084 diff --git a/models/robots/lekiwi/meshes/Bottom-V2-v3.stl b/models/robots/lekiwi/meshes/Bottom-V2-v3.stl deleted file mode 100644 index c48e4974..00000000 --- a/models/robots/lekiwi/meshes/Bottom-V2-v3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb554e74d9240cbcadef4be827eb809637679ab6991e8fbe55a6dfe6d01a28e0 -size 144384 diff --git a/models/robots/lekiwi/meshes/Camera-Model-v3-1.stl b/models/robots/lekiwi/meshes/Camera-Model-v3-1.stl deleted file mode 100644 index 66f0446c..00000000 --- a/models/robots/lekiwi/meshes/Camera-Model-v3-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5ff94543c19e91a6d5ffb540e2d25347d813d191793405ff2e4178230dec21e -size 878284 diff --git a/models/robots/lekiwi/meshes/Camera-Model-v3.stl b/models/robots/lekiwi/meshes/Camera-Model-v3.stl deleted file mode 100644 index 66f0446c..00000000 --- a/models/robots/lekiwi/meshes/Camera-Model-v3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f5ff94543c19e91a6d5ffb540e2d25347d813d191793405ff2e4178230dec21e -size 878284 diff --git a/models/robots/lekiwi/meshes/Camera-Mount-v8.stl b/models/robots/lekiwi/meshes/Camera-Mount-v8.stl deleted file mode 100644 index bdbb121b..00000000 --- a/models/robots/lekiwi/meshes/Camera-Mount-v8.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:631ed680e524b90dcb59dd315830ed1bac842dfd5e77c412ef4c52ff4da2e854 -size 80884 diff --git a/models/robots/lekiwi/meshes/Moving_Jaw_08d-v1.stl b/models/robots/lekiwi/meshes/Moving_Jaw_08d-v1.stl deleted file mode 100644 index 0a163bec..00000000 --- a/models/robots/lekiwi/meshes/Moving_Jaw_08d-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71caabee267376791210950b3b4e2f7968d9b57b92ed3f57711c03f2b5666912 -size 500084 diff --git a/models/robots/lekiwi/meshes/Passive_Horn_01-v1.stl b/models/robots/lekiwi/meshes/Passive_Horn_01-v1.stl deleted file mode 100644 index f55a7df0..00000000 --- a/models/robots/lekiwi/meshes/Passive_Horn_01-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fca61eb1fb63b4bb04ff0c80d395ae10ae69787a7da7e15414234d39b8b0ab7d -size 47684 diff --git a/models/robots/lekiwi/meshes/Rotation_Pitch_08i-v1.stl b/models/robots/lekiwi/meshes/Rotation_Pitch_08i-v1.stl deleted file mode 100644 index c32b040c..00000000 --- a/models/robots/lekiwi/meshes/Rotation_Pitch_08i-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:747a37e0c99c8fa85ad52ccf9fbfbb945468d8f9f590a1b7faadf5188d741ba9 -size 446884 diff --git a/models/robots/lekiwi/meshes/SO_ARM100_08k_116_Square-v1.stl b/models/robots/lekiwi/meshes/SO_ARM100_08k_116_Square-v1.stl deleted file mode 100644 index 52857e0c..00000000 --- a/models/robots/lekiwi/meshes/SO_ARM100_08k_116_Square-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64fdc308759ff58756e3c39a0ea8018d6b37b551bc3d29238b6e86bc5acae666 -size 370984 diff --git a/models/robots/lekiwi/meshes/SO_ARM100_08k_Asym_Mirror_Clip-v1.stl b/models/robots/lekiwi/meshes/SO_ARM100_08k_Asym_Mirror_Clip-v1.stl deleted file mode 100644 index bdb2be5b..00000000 --- a/models/robots/lekiwi/meshes/SO_ARM100_08k_Asym_Mirror_Clip-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f336a7f3940c943554252032ae8e351196353b8aa983827f045c4d7aa4cc52d -size 268384 diff --git a/models/robots/lekiwi/meshes/SO_ARM100_08k_Mirror-v1.stl b/models/robots/lekiwi/meshes/SO_ARM100_08k_Mirror-v1.stl deleted file mode 100644 index 690caa1a..00000000 --- a/models/robots/lekiwi/meshes/SO_ARM100_08k_Mirror-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6a2abf909ddd6e27c6fef5791937497a7686221dc1d877a7b30820b91cc1ca6 -size 713684 diff --git a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-1.stl b/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-1.stl deleted file mode 100644 index b5de5633..00000000 --- a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e5026ea1322832671b8617166503104e98ba185bd5bfc3d037f96b7b4f964cf -size 827784 diff --git a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-2.stl b/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-2.stl deleted file mode 100644 index b5de5633..00000000 --- a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e5026ea1322832671b8617166503104e98ba185bd5bfc3d037f96b7b4f964cf -size 827784 diff --git a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1.stl b/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1.stl deleted file mode 100644 index b5de5633..00000000 --- a/models/robots/lekiwi/meshes/ST3215_Servo_Motor-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e5026ea1322832671b8617166503104e98ba185bd5bfc3d037f96b7b4f964cf -size 827784 diff --git a/models/robots/lekiwi/meshes/STS3215_03a-v1-1.stl b/models/robots/lekiwi/meshes/STS3215_03a-v1-1.stl deleted file mode 100644 index 961dff62..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a-v1-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07695009c2e9ef352787fe98814d85eb936228a1843a0c76d22ab1bb3d37eb92 -size 325384 diff --git a/models/robots/lekiwi/meshes/STS3215_03a-v1-2.stl b/models/robots/lekiwi/meshes/STS3215_03a-v1-2.stl deleted file mode 100644 index 961dff62..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a-v1-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07695009c2e9ef352787fe98814d85eb936228a1843a0c76d22ab1bb3d37eb92 -size 325384 diff --git a/models/robots/lekiwi/meshes/STS3215_03a-v1-3.stl b/models/robots/lekiwi/meshes/STS3215_03a-v1-3.stl deleted file mode 100644 index 961dff62..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a-v1-3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07695009c2e9ef352787fe98814d85eb936228a1843a0c76d22ab1bb3d37eb92 -size 325384 diff --git a/models/robots/lekiwi/meshes/STS3215_03a-v1-4.stl b/models/robots/lekiwi/meshes/STS3215_03a-v1-4.stl deleted file mode 100644 index 961dff62..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a-v1-4.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07695009c2e9ef352787fe98814d85eb936228a1843a0c76d22ab1bb3d37eb92 -size 325384 diff --git a/models/robots/lekiwi/meshes/STS3215_03a-v1.stl b/models/robots/lekiwi/meshes/STS3215_03a-v1.stl deleted file mode 100644 index 961dff62..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:07695009c2e9ef352787fe98814d85eb936228a1843a0c76d22ab1bb3d37eb92 -size 325384 diff --git a/models/robots/lekiwi/meshes/STS3215_03a_Wrist_Roll-v1.stl b/models/robots/lekiwi/meshes/STS3215_03a_Wrist_Roll-v1.stl deleted file mode 100644 index 51a63450..00000000 --- a/models/robots/lekiwi/meshes/STS3215_03a_Wrist_Roll-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b390f54c455a7900687af22432c8f742f4060d3c1afb90091c2fbf1dc3a9b978 -size 282984 diff --git a/models/robots/lekiwi/meshes/Top-V2-v2.stl b/models/robots/lekiwi/meshes/Top-V2-v2.stl deleted file mode 100644 index bcb478d6..00000000 --- a/models/robots/lekiwi/meshes/Top-V2-v2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fbd2b1bc5768d1ccfdeb96796e127715d7f6bee1edcadaf97e586b8b7e641c0e -size 178484 diff --git a/models/robots/lekiwi/meshes/WaveShare_Mounting_Plate_01d-v1.stl b/models/robots/lekiwi/meshes/WaveShare_Mounting_Plate_01d-v1.stl deleted file mode 100644 index dfc42215..00000000 --- a/models/robots/lekiwi/meshes/WaveShare_Mounting_Plate_01d-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abde2af470078f125c6e001e64abcae7c4307bff02f27c3a10dc2a87f7039bed -size 37684 diff --git a/models/robots/lekiwi/meshes/Wrist-Camera-Mount-v11.stl b/models/robots/lekiwi/meshes/Wrist-Camera-Mount-v11.stl deleted file mode 100644 index d39ac17a..00000000 --- a/models/robots/lekiwi/meshes/Wrist-Camera-Mount-v11.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2bb123c754eff42c57c4605a533b94d14e7b51889a813764f798b7878489a98a -size 348784 diff --git a/models/robots/lekiwi/meshes/Wrist_Roll_08c-v1.stl b/models/robots/lekiwi/meshes/Wrist_Roll_08c-v1.stl deleted file mode 100644 index 1376a38b..00000000 --- a/models/robots/lekiwi/meshes/Wrist_Roll_08c-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87507f73f485c2cacb3dc83924a712069fbd68f72b82d4a31a2cfe7f58c9e4c9 -size 543484 diff --git a/models/robots/lekiwi/meshes/Wrist_Roll_Pitch_08i-v1.stl b/models/robots/lekiwi/meshes/Wrist_Roll_Pitch_08i-v1.stl deleted file mode 100644 index 5a16524c..00000000 --- a/models/robots/lekiwi/meshes/Wrist_Roll_Pitch_08i-v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:964cef356d5a120cd7d65018fcbd89a7f2dcd5f7a503d415720f94c452eb5da6 -size 842584 diff --git a/models/robots/lekiwi/meshes/base_plate_layer1-v5.stl b/models/robots/lekiwi/meshes/base_plate_layer1-v5.stl deleted file mode 100644 index ea5c0c61..00000000 --- a/models/robots/lekiwi/meshes/base_plate_layer1-v5.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6304da40d3ee04ec4fc36dd99e2f276febbc6e3770a14b1c2f5b674dfe0bd0f8 -size 290284 diff --git a/models/robots/lekiwi/meshes/base_plate_layer2-v3.stl b/models/robots/lekiwi/meshes/base_plate_layer2-v3.stl deleted file mode 100644 index ce99ff4d..00000000 --- a/models/robots/lekiwi/meshes/base_plate_layer2-v3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:40e6e0bca0c565512d3eb8dca3ddff3e36fa6ae1ef2d9e62aa74d15ab99ac6ee -size 309084 diff --git a/models/robots/lekiwi/meshes/drive_motor_mount-v11-1.stl b/models/robots/lekiwi/meshes/drive_motor_mount-v11-1.stl deleted file mode 100644 index 4990cb77..00000000 --- a/models/robots/lekiwi/meshes/drive_motor_mount-v11-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b46bb245e04a55ff808d00a7f6d4e2341c8e7918956bd3b3d7eaeceb7cfdc84e -size 46284 diff --git a/models/robots/lekiwi/meshes/drive_motor_mount-v11-2.stl b/models/robots/lekiwi/meshes/drive_motor_mount-v11-2.stl deleted file mode 100644 index 4990cb77..00000000 --- a/models/robots/lekiwi/meshes/drive_motor_mount-v11-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b46bb245e04a55ff808d00a7f6d4e2341c8e7918956bd3b3d7eaeceb7cfdc84e -size 46284 diff --git a/models/robots/lekiwi/meshes/drive_motor_mount-v11.stl b/models/robots/lekiwi/meshes/drive_motor_mount-v11.stl deleted file mode 100644 index 4990cb77..00000000 --- a/models/robots/lekiwi/meshes/drive_motor_mount-v11.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b46bb245e04a55ff808d00a7f6d4e2341c8e7918956bd3b3d7eaeceb7cfdc84e -size 46284 diff --git a/models/robots/lekiwi/meshes/lipo_battery_mount-v3.stl b/models/robots/lekiwi/meshes/lipo_battery_mount-v3.stl deleted file mode 100644 index f9a3774b..00000000 --- a/models/robots/lekiwi/meshes/lipo_battery_mount-v3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:722716bedcc94138b48100276a44bb6b707efae506f023ee7308a5c7e108228e -size 13484 diff --git a/models/robots/lekiwi/meshes/omni_wheel_mount-v5-1.stl b/models/robots/lekiwi/meshes/omni_wheel_mount-v5-1.stl deleted file mode 100644 index ad16b069..00000000 --- a/models/robots/lekiwi/meshes/omni_wheel_mount-v5-1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fcebf3d5d9879dc1547fe4a0ff548d64ede092945da1a1edc2597985d376249 -size 399384 diff --git a/models/robots/lekiwi/meshes/omni_wheel_mount-v5-2.stl b/models/robots/lekiwi/meshes/omni_wheel_mount-v5-2.stl deleted file mode 100644 index ad16b069..00000000 --- a/models/robots/lekiwi/meshes/omni_wheel_mount-v5-2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fcebf3d5d9879dc1547fe4a0ff548d64ede092945da1a1edc2597985d376249 -size 399384 diff --git a/models/robots/lekiwi/meshes/omni_wheel_mount-v5.stl b/models/robots/lekiwi/meshes/omni_wheel_mount-v5.stl deleted file mode 100644 index ad16b069..00000000 --- a/models/robots/lekiwi/meshes/omni_wheel_mount-v5.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9fcebf3d5d9879dc1547fe4a0ff548d64ede092945da1a1edc2597985d376249 -size 399384 diff --git a/models/robots/lekiwi/meshes/servo_controller_mount-v3.stl b/models/robots/lekiwi/meshes/servo_controller_mount-v3.stl deleted file mode 100644 index 49e5ec16..00000000 --- a/models/robots/lekiwi/meshes/servo_controller_mount-v3.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47fa5f3c5669a67208f2f59e752d05b4ba0a569641d05cb2f22031944baa7a4a -size 85884 diff --git a/models/robots/lyra/.lyra.step.glb b/models/robots/lyra/.lyra.step.glb deleted file mode 100644 index 8b33d593..00000000 --- a/models/robots/lyra/.lyra.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d375764248c3f9e9e45a89c1185dc126e72315e0fe1213a0694cd460e7c8245 -size 7359324 diff --git a/models/robots/lyra/.lyra.step.js b/models/robots/lyra/.lyra.step.js deleted file mode 100644 index 44a1b0a6..00000000 --- a/models/robots/lyra/.lyra.step.js +++ /dev/null @@ -1,519 +0,0 @@ -// Pose articulation animations for the lyra dexterous-hand STEP assembly. -// -// The STEP geometry is baked in the "relaxed" pose (see lyra.py / -// lyra_parts/chain.py). This module recomputes full-chain FK each frame at -// the target pose and applies, per link, the rigid delta matrix -// T_target * inverse(T_relaxed) in the fixed model frame (mm, RIGHT hand, -// +Z distal, +Y palmar, +X radial, wrist-flange face center at the origin). -// -// All pose tables mirror lyra_parts/chain.py named_poses_deg(); the joint -// list mirrors chain.all_joints() (origins mm in the parent link frame, -// axes: finger flexion about -X, thumb cmc yaw about +Z, thumb flexion -// about -Y). Poses are blended in joint space with smoothstep easing — -// every intermediate state of a serial digit chain is itself a valid pose, -// so blends never break the mechanism. Every loop starts and ends on the -// exact pose it began with. - -const FINGERS = ["index", "middle", "ring", "pinky"]; - -// Chain offsets (mm), mirrored from lyra_parts/chain.py. -const MCP = { - index: [28.5, 0, 99], - middle: [9.5, 0, 103], - ring: [-9.5, 0, 99], - pinky: [-28.5, 0, 90] -}; -const SEG = { index: [44, 26], middle: [48, 29], ring: [44, 27], pinky: [35, 21] }; -const THUMB_CMC = [31, 4, 44]; -const THUMB_BASE_LEN = 13; -const THUMB_METACARPAL_LEN = 46; -const THUMB_PROXIMAL_LEN = 30; - -const NEG_X = [-1, 0, 0]; -const NEG_Y = [0, -1, 0]; -const Z = [0, 0, 1]; - -function buildJoints() { - const joints = []; - for (const finger of FINGERS) { - joints.push( - { name: `${finger}_mcp`, parent: "palm", child: `${finger}_proximal`, origin: MCP[finger], axis: NEG_X }, - { name: `${finger}_pip`, parent: `${finger}_proximal`, child: `${finger}_middle`, origin: [0, 0, SEG[finger][0]], axis: NEG_X }, - { name: `${finger}_dip`, parent: `${finger}_middle`, child: `${finger}_distal`, origin: [0, 0, SEG[finger][1]], axis: NEG_X } - ); - } - joints.push( - { name: "thumb_cmc_yaw", parent: "palm", child: "thumb_base", origin: THUMB_CMC, axis: Z }, - { name: "thumb_cmc_flex", parent: "thumb_base", child: "thumb_metacarpal", origin: [THUMB_BASE_LEN, 0, 0], axis: NEG_Y }, - { name: "thumb_mp", parent: "thumb_metacarpal", child: "thumb_proximal", origin: [THUMB_METACARPAL_LEN, 0, 0], axis: NEG_Y }, - { name: "thumb_ip", parent: "thumb_proximal", child: "thumb_distal", origin: [THUMB_PROXIMAL_LEN, 0, 0], axis: NEG_Y } - ); - return joints; -} - -const JOINTS = buildJoints(); - -// ---------------------------------------------------------------- poses -function fingerPose(curls, thumb) { - const pose = {}; - for (const finger of FINGERS) { - const [mcp, pip, dip] = curls[finger]; - pose[`${finger}_mcp`] = mcp; - pose[`${finger}_pip`] = pip; - pose[`${finger}_dip`] = dip; - } - const [yaw, flex, mp, ip] = thumb; - pose.thumb_cmc_yaw = yaw; - pose.thumb_cmc_flex = flex; - pose.thumb_mp = mp; - pose.thumb_ip = ip; - return pose; -} - -// Mirrored from chain.named_poses_deg(). -const POSES = { - zero: fingerPose( - { index: [0, 0, 0], middle: [0, 0, 0], ring: [0, 0, 0], pinky: [0, 0, 0] }, - [0, 0, 0, 0] - ), - relaxed: fingerPose( - { index: [10, 14, 8], middle: [12, 16, 9], ring: [14, 18, 10], pinky: [16, 20, 12] }, - [34, 26, 14, 12] - ), - fist: fingerPose( - { index: [78, 100, 60], middle: [78, 100, 60], ring: [78, 100, 60], pinky: [78, 100, 60] }, - [92, 13, 58, 74] - ), - precision_pinch: fingerPose( - { index: [40, 48, 30], middle: [66, 92, 55], ring: [66, 92, 55], pinky: [66, 92, 55] }, - [91, 34, 29, 8] - ), - tripod_pinch: fingerPose( - { index: [44, 52, 32], middle: [42, 50, 30], ring: [66, 92, 55], pinky: [66, 92, 55] }, - [100, 39, 0, 14] - ), - point: fingerPose( - { index: [-6, 0, 0], middle: [80, 102, 62], ring: [80, 102, 62], pinky: [80, 102, 62] }, - [62, 30, 42, 45] - ), - ok_sign: fingerPose( - { index: [42, 52, 32], middle: [6, 8, 4], ring: [10, 12, 6], pinky: [14, 16, 8] }, - [91, 26, 35, 20] - ) -}; - -const BAKED_POSE = POSES.relaxed; - -// ------------------------------------------------------------- math kit -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -function smoothstep(u) { - const t = clamp(u, 0, 1); - return t * t * (3 - 2 * t); -} - -// Rodrigues rotation about a unit axis (covers -X, -Y, and Z here). -function rotAxisDeg(axis, deg) { - const rad = (deg * Math.PI) / 180; - const c = Math.cos(rad); - const s = Math.sin(rad); - const t = 1 - c; - const [ax, ay, az] = axis; - return [ - [c + ax * ax * t, ax * ay * t - az * s, ax * az * t + ay * s], - [ay * ax * t + az * s, c + ay * ay * t, ay * az * t - ax * s], - [az * ax * t - ay * s, az * ay * t + ax * s, c + az * az * t] - ]; -} - -function matMul3(a, b) { - const out = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]; - for (let i = 0; i < 3; i += 1) { - for (let j = 0; j < 3; j += 1) { - out[i][j] = (a[i][0] * b[0][j]) + (a[i][1] * b[1][j]) + (a[i][2] * b[2][j]); - } - } - return out; -} - -function matVec3(a, v) { - return [ - (a[0][0] * v[0]) + (a[0][1] * v[1]) + (a[0][2] * v[2]), - (a[1][0] * v[0]) + (a[1][1] * v[1]) + (a[1][2] * v[2]), - (a[2][0] * v[0]) + (a[2][1] * v[1]) + (a[2][2] * v[2]) - ]; -} - -function matTranspose3(a) { - return [ - [a[0][0], a[1][0], a[2][0]], - [a[0][1], a[1][1], a[2][1]], - [a[0][2], a[1][2], a[2][2]] - ]; -} - -const IDENTITY3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; - -function fkFrames(anglesDeg) { - const frames = { palm: { R: IDENTITY3, p: [0, 0, 0] } }; - for (const joint of JOINTS) { - const parent = frames[joint.parent]; - const offset = matVec3(parent.R, joint.origin); - const p = [parent.p[0] + offset[0], parent.p[1] + offset[1], parent.p[2] + offset[2]]; - const R = matMul3(parent.R, rotAxisDeg(joint.axis, finite(anglesDeg[joint.name], 0))); - frames[joint.child] = { R, p }; - } - return frames; -} - -const BAKED_FRAMES = fkFrames(BAKED_POSE); - -// Row-major 4x4 mapping the baked placement of a link onto its target -// placement: M = [R_t R_o^T | p_t - R_t R_o^T p_o]. -function deltaMatrixRowMajor(original, target) { - const Rd = matMul3(target.R, matTranspose3(original.R)); - const moved = matVec3(Rd, original.p); - const t = [target.p[0] - moved[0], target.p[1] - moved[1], target.p[2] - moved[2]]; - return [ - Rd[0][0], Rd[0][1], Rd[0][2], t[0], - Rd[1][0], Rd[1][1], Rd[1][2], t[1], - Rd[2][0], Rd[2][1], Rd[2][2], t[2], - 0, 0, 0, 1 - ]; -} - -function blendPoses(a, b, u) { - const e = smoothstep(u); - const out = {}; - for (const joint of JOINTS) { - out[joint.name] = finite(a[joint.name], 0) + ((finite(b[joint.name], 0) - finite(a[joint.name], 0)) * e); - } - return out; -} - -function addScaled(pose, delta, scale) { - const out = {}; - for (const joint of JOINTS) { - out[joint.name] = finite(pose[joint.name], 0) + (finite(delta[joint.name], 0) * scale); - } - return out; -} - -// ---------------------------------------------------------------- modes -// Keyframe cycle through the showpiece poses; each segment blends for 65% -// of its window and dwells for 35%, and the cycle wraps back to its first -// key so the loop is exact. Key order is chosen so every adjacent blend is -// capsule-verified collision-free (the fist only neighbors tripod/relaxed; -// blending it with pinch/point/ok would sweep the thumb through the index). -const TOUR_KEYS = ["relaxed", "precision_pinch", "ok_sign", "point", "tripod_pinch", "fist"]; -// The tour opens with one finger-ripple wave (starts and ends exactly on -// the relaxed pose, so it splices seamlessly before the first keyframe -// blend) and spends the rest of the cycle on the pose keyframes. -const TOUR_RIPPLE_FRAC = 0.22; - -function tourPose(phase) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - if (p < TOUR_RIPPLE_FRAC) { - return ripplePose(p / TOUR_RIPPLE_FRAC, 1); - } - const q = (p - TOUR_RIPPLE_FRAC) / (1 - TOUR_RIPPLE_FRAC); - const segCount = TOUR_KEYS.length; - const seg = Math.min(Math.floor(q * segCount), segCount - 1); - const u = (q * segCount) - seg; - const from = POSES[TOUR_KEYS[seg]]; - const to = POSES[TOUR_KEYS[(seg + 1) % segCount]]; - return blendPoses(from, to, u / 0.65); -} - -// Open-close power grasp: relaxed -> fist -> relaxed on a raised cosine. -function graspPose(phase, grip) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - const wave = 0.5 * (1 - Math.cos(2 * Math.PI * p)); - return blendPoses(POSES.relaxed, POSES.fist, wave * grip); -} - -// Precision pinch with a double pad tap while closed. -const PINCH_TAP = fingerPose( - { index: [-5, -7, -4], middle: [0, 0, 0], ring: [0, 0, 0], pinky: [0, 0, 0] }, - [0, 0, -7, -5] -); - -function pinchPose(phase) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - if (p < 0.3) { - return blendPoses(POSES.relaxed, POSES.precision_pinch, p / 0.3); - } - if (p < 0.72) { - const u = (p - 0.3) / 0.42; - const tap = Math.sin(2 * Math.PI * 2 * u); - return addScaled(POSES.precision_pinch, PINCH_TAP, Math.max(0, tap)); - } - return blendPoses(POSES.precision_pinch, POSES.relaxed, (p - 0.72) / 0.28); -} - -// Traveling curl wave: each digit pulses inside its own window (raised -// cosine, zero at both ends), thumb last. The windows are wide relative to -// the digit spacing, so each digit starts curling while its neighbor is -// still mid-pulse and the wave reads as one continuous motion. -const RIPPLE_ORDER = ["index", "middle", "ring", "pinky", "thumb"]; -const RIPPLE_CURL = { mcp: 30, pip: 40, dip: 22, thumbFlex: 12, thumbMp: 30, thumbIp: 30 }; -const RIPPLE_WINDOW = 0.45; - -function ripplePose(phase, grip) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - const window = RIPPLE_WINDOW; - const step = (1 - window) / (RIPPLE_ORDER.length - 1); - const pose = {}; - for (const joint of JOINTS) { - pose[joint.name] = BAKED_POSE[joint.name]; - } - RIPPLE_ORDER.forEach((digit, i) => { - const start = i * step; - const u = (p - start) / window; - if (u <= 0 || u >= 1) { - return; - } - const lobe = Math.sin(Math.PI * u); - const amp = lobe * lobe * grip; - if (digit === "thumb") { - pose.thumb_cmc_flex += RIPPLE_CURL.thumbFlex * amp; - pose.thumb_mp += RIPPLE_CURL.thumbMp * amp; - pose.thumb_ip += RIPPLE_CURL.thumbIp * amp; - } else { - pose[`${digit}_mcp`] += RIPPLE_CURL.mcp * amp; - pose[`${digit}_pip`] += RIPPLE_CURL.pip * amp; - pose[`${digit}_dip`] += RIPPLE_CURL.dip * amp; - } - }); - return pose; -} - -// Count 1..5 from a fist (index first, thumb last), then close back. The -// thumb lifts off to a hover clear of the fingers before any finger -// extends, and only re-wraps once the fingers are curled again — every -// adjacent blend is capsule-verified collision-free. -const COUNT_EXTENDED = { index: [2, 2, 1], middle: [2, 2, 1], ring: [4, 4, 2], pinky: [6, 6, 3] }; -const COUNT_THUMB_FIST = [92, 13, 58, 74]; -const COUNT_THUMB_HOVER = [50, 30, 25, 20]; -const COUNT_THUMB_OPEN = [20, 10, 4, 4]; - -function countKey(step, thumb) { - const curls = {}; - FINGERS.forEach((finger, i) => { - curls[finger] = step >= i + 1 ? COUNT_EXTENDED[finger] : [78, 100, 60]; - }); - return fingerPose(curls, thumb); -} - -function countPose(phase) { - const p = ((finite(phase, 0) % 1) + 1) % 1; - // 8 segments: fist -> thumb hover -> 1 -> 2 -> 3 -> 4 -> 5 (thumb opens) - // -> hover (fingers re-curl) -> wrap back to the fist. - const keys = [ - countKey(0, COUNT_THUMB_FIST), - countKey(0, COUNT_THUMB_HOVER), - countKey(1, COUNT_THUMB_HOVER), - countKey(2, COUNT_THUMB_HOVER), - countKey(3, COUNT_THUMB_HOVER), - countKey(4, COUNT_THUMB_HOVER), - countKey(5, COUNT_THUMB_OPEN), - countKey(0, COUNT_THUMB_HOVER) - ]; - const segCount = keys.length; - const seg = Math.min(Math.floor(p * segCount), segCount - 1); - const u = (p * segCount) - seg; - const from = keys[seg]; - const to = seg + 1 < segCount ? keys[seg + 1] : keys[0]; - return blendPoses(from, to, u / 0.6); -} - -const FEATURE_BY_LINK = { - palm: "palm", - index_proximal: "indexProximal", - index_middle: "indexMiddle", - index_distal: "indexDistal", - middle_proximal: "middleProximal", - middle_middle: "middleMiddle", - middle_distal: "middleDistal", - ring_proximal: "ringProximal", - ring_middle: "ringMiddle", - ring_distal: "ringDistal", - pinky_proximal: "pinkyProximal", - pinky_middle: "pinkyMiddle", - pinky_distal: "pinkyDistal", - thumb_base: "thumbBase", - thumb_metacarpal: "thumbMetacarpal", - thumb_proximal: "thumbProximal", - thumb_distal: "thumbDistal" -}; - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/robots/lyra/lyra.step" - }, - label: "lyra dexterous hand", - description: "16-DOF humanoid right hand: pose tour, power grasp, precision pinch with pad taps, finger ripple, and counting, all driven by per-frame chain FK against the baked relaxed pose.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - palm: { ref: "#o1.1", label: "Palm", description: "Root link; static during all hand animations." }, - indexProximal: { ref: "#o1.2", label: "Index proximal" }, - indexMiddle: { ref: "#o1.3", label: "Index middle" }, - indexDistal: { ref: "#o1.4", label: "Index distal" }, - middleProximal: { ref: "#o1.5", label: "Middle proximal" }, - middleMiddle: { ref: "#o1.6", label: "Middle middle" }, - middleDistal: { ref: "#o1.7", label: "Middle distal" }, - ringProximal: { ref: "#o1.8", label: "Ring proximal" }, - ringMiddle: { ref: "#o1.9", label: "Ring middle" }, - ringDistal: { ref: "#o1.10", label: "Ring distal" }, - pinkyProximal: { ref: "#o1.11", label: "Pinky proximal" }, - pinkyMiddle: { ref: "#o1.12", label: "Pinky middle" }, - pinkyDistal: { ref: "#o1.13", label: "Pinky distal" }, - thumbBase: { ref: "#o1.14", label: "Thumb CMC base" }, - thumbMetacarpal: { ref: "#o1.15", label: "Thumb metacarpal" }, - thumbProximal: { ref: "#o1.16", label: "Thumb proximal" }, - thumbDistal: { ref: "#o1.17", label: "Thumb distal" } - }, - parameters: { - phase: { - type: "number", - label: "Cycle phase", - description: "One full cycle of the active mode; the animations drive this 0 -> 1.", - default: 0, - min: 0, - max: 1, - step: 0.001 - }, - mode: { - type: "select", - label: "Mode", - description: "Which articulation cycle the phase scrubs. Each animation drives the matching mode; 'pose' holds the static pose picked below.", - default: "tour", - options: [ - { value: "tour", label: "Pose tour" }, - { value: "grasp", label: "Power grasp" }, - { value: "pinch", label: "Precision pinch" }, - { value: "ripple", label: "Finger ripple" }, - { value: "count", label: "Count to five" }, - { value: "pose", label: "Hold named pose" } - ] - }, - pose: { - type: "select", - label: "Named pose", - description: "SRDF group-state pose held when mode is 'pose' (blend with Grip).", - default: "relaxed", - options: [ - { value: "zero", label: "Zero (flat open)" }, - { value: "relaxed", label: "Relaxed (baked)" }, - { value: "fist", label: "Fist" }, - { value: "precision_pinch", label: "Precision pinch" }, - { value: "tripod_pinch", label: "Tripod pinch" }, - { value: "point", label: "Point" }, - { value: "ok_sign", label: "OK sign" } - ] - }, - grip: { - type: "number", - label: "Grip", - description: "Scales the grasp/ripple amplitude, and blends relaxed -> named pose in 'pose' mode.", - default: 1, - min: 0, - max: 1, - step: 0.01 - } - }, - animations: { - poseTour: { - label: "Pose tour", - description: "Opens with a finger ripple wave, then cycles relaxed -> precision pinch -> OK sign -> point -> tripod pinch -> fist and back, dwelling on each pose.", - duration: 11.5, - loop: true, - update({ cycle, set }) { - set("mode", "tour"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - graspLoop: { - label: "Power grasp", - description: "Full-hand open-close: all four fingers wrap and the thumb locks over them, then releases back to the relaxed pose.", - duration: 2.6, - loop: true, - update({ cycle, set }) { - set("mode", "grasp"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - pinchLoop: { - label: "Precision pinch", - description: "Thumb-index pad opposition with a double tap while closed; the other fingers curl clear.", - duration: 2.8, - loop: true, - update({ cycle, set }) { - set("mode", "pinch"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - rippleLoop: { - label: "Finger ripple", - description: "A curl wave travels index -> pinky and hands off to the thumb, like drumming fingers in the air.", - duration: 2.6, - loop: true, - update({ cycle, set }) { - set("mode", "ripple"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - }, - countLoop: { - label: "Count to five", - description: "From a fist: the thumb lifts clear, then index, middle, ring, pinky extend in turn and the thumb opens to a flat five before the hand closes again.", - duration: 7.0, - loop: true, - update({ cycle, set }) { - set("mode", "count"); - set("phase", ((finite(cycle, 0) % 1) + 1) % 1); - } - } - } - }, - - update({ params, effects }) { - const phase = ((finite(params.phase, 0) % 1) + 1) % 1; - const grip = clamp(params.grip, 0, 1); - const mode = String(params.mode || "tour"); - let target; - if (mode === "grasp") { - target = graspPose(phase, grip); - } else if (mode === "pinch") { - target = pinchPose(phase); - } else if (mode === "ripple") { - target = ripplePose(phase, grip); - } else if (mode === "count") { - target = countPose(phase); - } else if (mode === "pose") { - const named = POSES[String(params.pose || "relaxed")] || POSES.relaxed; - target = blendPoses(POSES.relaxed, named, grip); - } else { - target = tourPose(phase); - } - - const targetFrames = fkFrames(target); - for (const [link, feature] of Object.entries(FEATURE_BY_LINK)) { - effects.transform(feature, { - matrix: deltaMatrixRowMajor(BAKED_FRAMES[link], targetFrames[link]) - }); - } - } -}; diff --git a/models/robots/lyra/3MF/index_distal.3mf b/models/robots/lyra/3MF/index_distal.3mf deleted file mode 100644 index f7f77343..00000000 --- a/models/robots/lyra/3MF/index_distal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:caaf3206bbcfecfa3c007f4ac6b38f6021a3dbd495d89324aba968e843491728 -size 60704 diff --git a/models/robots/lyra/3MF/index_middle.3mf b/models/robots/lyra/3MF/index_middle.3mf deleted file mode 100644 index c4508c4b..00000000 --- a/models/robots/lyra/3MF/index_middle.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:afd4e0d88eecd726eeffa5ef843ec61999179fafa5d422b4ea73b13d3572a074 -size 37962 diff --git a/models/robots/lyra/3MF/index_proximal.3mf b/models/robots/lyra/3MF/index_proximal.3mf deleted file mode 100644 index 796ccaab..00000000 --- a/models/robots/lyra/3MF/index_proximal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e96cceefff3984723e2323c8b8e7f74fad072e3e9873f29f40403f0f65217411 -size 37180 diff --git a/models/robots/lyra/3MF/middle_distal.3mf b/models/robots/lyra/3MF/middle_distal.3mf deleted file mode 100644 index d6e1044d..00000000 --- a/models/robots/lyra/3MF/middle_distal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d36524ba3b4e2696104970b3d1f7a0bb550e29fb8f14780148c6614dc3c54ac -size 61440 diff --git a/models/robots/lyra/3MF/middle_middle.3mf b/models/robots/lyra/3MF/middle_middle.3mf deleted file mode 100644 index 0b00a0f7..00000000 --- a/models/robots/lyra/3MF/middle_middle.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09aba43d6b13562ab58534d549cf255ba0a93fdffcc8c5417523253fa4e54146 -size 37149 diff --git a/models/robots/lyra/3MF/middle_proximal.3mf b/models/robots/lyra/3MF/middle_proximal.3mf deleted file mode 100644 index 44951c2a..00000000 --- a/models/robots/lyra/3MF/middle_proximal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fba3b926f3c24d95e5597498d452855a7acf78e9b2aded1095c191de51009dd1 -size 37418 diff --git a/models/robots/lyra/3MF/palm.3mf b/models/robots/lyra/3MF/palm.3mf deleted file mode 100644 index 5c0a67c2..00000000 --- a/models/robots/lyra/3MF/palm.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:242344e2280faf9578c9bf8fbfce47496aa286716bad0a11878726b18519d044 -size 169634 diff --git a/models/robots/lyra/3MF/pinky_distal.3mf b/models/robots/lyra/3MF/pinky_distal.3mf deleted file mode 100644 index 1cda7e94..00000000 --- a/models/robots/lyra/3MF/pinky_distal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ceeeabd4b2f95e44de6d7b9d1b86c386ab10608587f2c1aad0cce38c2f817f83 -size 60462 diff --git a/models/robots/lyra/3MF/pinky_middle.3mf b/models/robots/lyra/3MF/pinky_middle.3mf deleted file mode 100644 index f67291ea..00000000 --- a/models/robots/lyra/3MF/pinky_middle.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e8d129a44b8a1d270b69d2acb0826544813e664f64f71bd6e5a02f54d5ca153 -size 36750 diff --git a/models/robots/lyra/3MF/pinky_proximal.3mf b/models/robots/lyra/3MF/pinky_proximal.3mf deleted file mode 100644 index 1b2c78d1..00000000 --- a/models/robots/lyra/3MF/pinky_proximal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1b4b17438f81562ff5488d1ed74b106d0555606dccc57d9a77fbe9f37f16bba -size 38307 diff --git a/models/robots/lyra/3MF/ring_distal.3mf b/models/robots/lyra/3MF/ring_distal.3mf deleted file mode 100644 index aab369bd..00000000 --- a/models/robots/lyra/3MF/ring_distal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22e0e74fe7427f77a1893439f9d0e918ce08615a56c5dea7eeaab22b9ccd5852 -size 61690 diff --git a/models/robots/lyra/3MF/ring_middle.3mf b/models/robots/lyra/3MF/ring_middle.3mf deleted file mode 100644 index 09267c02..00000000 --- a/models/robots/lyra/3MF/ring_middle.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a58140d61db4ae3a0cb90b9e84f8479f860eb5399fbff5fc584baa2109d9e6fa -size 38197 diff --git a/models/robots/lyra/3MF/ring_proximal.3mf b/models/robots/lyra/3MF/ring_proximal.3mf deleted file mode 100644 index 45ebe7b8..00000000 --- a/models/robots/lyra/3MF/ring_proximal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca513a2bbf91af874732c9542c543c84ec012bf4b485bc20bf8a571a02362b7f -size 37472 diff --git a/models/robots/lyra/3MF/thumb_base.3mf b/models/robots/lyra/3MF/thumb_base.3mf deleted file mode 100644 index b2a29089..00000000 --- a/models/robots/lyra/3MF/thumb_base.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:481b99e8e7af7d68e4c983aa125958377497ae22ab4deff79555dd780b15a883 -size 35191 diff --git a/models/robots/lyra/3MF/thumb_distal.3mf b/models/robots/lyra/3MF/thumb_distal.3mf deleted file mode 100644 index eda5dff7..00000000 --- a/models/robots/lyra/3MF/thumb_distal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f972e982702ef5d4de1a3671b2ddfeea52045f3b2157dd0ef5f327e76609b5c0 -size 59199 diff --git a/models/robots/lyra/3MF/thumb_metacarpal.3mf b/models/robots/lyra/3MF/thumb_metacarpal.3mf deleted file mode 100644 index b329f415..00000000 --- a/models/robots/lyra/3MF/thumb_metacarpal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af7b47b47e74d95a8c7900158d3f43800ac453dcbd73d746cbf52c7c8bf70fc1 -size 37597 diff --git a/models/robots/lyra/3MF/thumb_proximal.3mf b/models/robots/lyra/3MF/thumb_proximal.3mf deleted file mode 100644 index 103d7021..00000000 --- a/models/robots/lyra/3MF/thumb_proximal.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:df812aba0098873d6ca4fdf8b1813497757cc5353ace69aadd29816b198f7b01 -size 38181 diff --git a/models/robots/lyra/README.md b/models/robots/lyra/README.md deleted file mode 100644 index 91683bf8..00000000 --- a/models/robots/lyra/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# lyra — dexterous humanoid hand concept - -An aesthetically refined five-digit robotic RIGHT hand for an advanced -bipedal robot: slim pearl-composite shell tubes over an exposed graphite -structural spine, machined-aluminum precision knuckle clevises with visible -pivot pins and rim washers, tendon-driven architecture (dorsal tendon -channel grooves fanning to each knuckle plus a five-dial tensioner row at -the wrist), integrated tactile sensing (2x2 palm pad array, per-phalanx -soft-touch strips, palmar fingertip caps, amber-ringed palm sensor), and a -6-bolt wrist flange. Graceful human-like proportions (~198 mm flange to -middle fingertip), clean industrial design, no logos. - -## Degrees of freedom (16) - -| Group | Joints | DOF | -| --- | --- | --- | -| Each finger (x4) | MCP, PIP, DIP flexion (about -X) | 12 | -| Thumb | CMC yaw (opposition swing about +Z), CMC flex, MP, IP (about -Y) | 4 | - -MCP abduction/spread is intentionally omitted (documented tendon-budget -trade in `lyra_parts/chain.py`). Named poses are capsule-tuned to small -positive clearances (`lyra_parts/clearance.py`): `precision_pinch` and -`ok_sign` kiss the thumb-index pads at ~0.7-0.8 mm surface clearance, -`tripod_pinch` chuck-grips a virtual ~12 mm object, and the `fist` thumb -rests on the index middle phalanx — no pose or animation blend -interpenetrates. - -## Files - -- `lyra.py` — build123d generator (`gen_step()`); authoritative source. - Joints are authored as `cadpy.assembly.AssemblyHelper` revolute frames - driven by the baked `relaxed` pose. Also exposes `gen_urdf()` / - `gen_srdf()` for the robot description. -- `lyra_parts/` — part-builder package. `chain.py` is the shared kinematic - chain/pose/limit spec (stdlib-only FK included) used by the CAD assembly, - the URDF/SRDF generators, and the animation sidecar; `lib.py` holds the - palette and the verified `revolute_attach()` joint math; `palm.py` / - `digits.py` build the parts; `description.py` emits the URDF/SRDF XML; - `mass_props.py` holds baked CAD volume/COM/inertia/bbox data; - `clearance.py` is the stdlib capsule-collision self-check (run - `python -m lyra_parts.clearance` from this directory after editing - poses or animation key orders — it sweeps every named pose and every - sidecar blend path). -- `lyra.step` — generated STEP assembly (derived artifact), baked in the - `relaxed` pose. Occurrence refs `#o1.1..#o1.17` follow the `asm.add` - order in `lyra.py` (palm, then index/middle/ring/pinky - proximal/middle/distal, then thumb base/metacarpal/proximal/distal). -- `.lyra.step.js` — CAD Viewer animation sidecar driving per-frame chain FK - deltas against the baked pose. Animations: `poseTour` (a finger-ripple - wave, then relaxed -> precision pinch -> OK sign -> point -> tripod - pinch -> fist; key order chosen so every blend is collision-free), - `graspLoop` (power grasp), `pinchLoop` (pinch with pad double-tap), - `rippleLoop` (traveling finger curl wave with overlapping per-digit - pulses), `countLoop` (count to five from a fist; the thumb lifts to a - hover before any finger extends). - Controls: `phase`, `mode`, `pose` (named SRDF pose hold), `grip`. -- `lyra.urdf` — generated URDF (derived artifact): a frame-only - `wrist_mount` root plus 17 physical links and 16 revolute joints, - per-link 3MF mesh visuals, bbox collisions, CAD-derived inertials at an - assumed 0.62 kg total mass. -- `lyra.srdf` — generated MoveIt2 SRDF (derived artifact): per-digit joint - groups, `fingers`/`hand` unions, a palm-mounted `hand_eef` end effector, - disabled collisions, and hand group states (`zero`, `relaxed`, `fist`, - `precision_pinch`, `tripod_pinch`, `point`, `ok_sign`). -- `STEP/` — per-link wrapper sources (`gen_step()` per link) and their - generated STEP parts; `3MF/` — per-link mm mesh sidecars referenced by - the URDF. - -## Conventions - -Units mm. RIGHT hand: the wrist-flange mount face center is the origin, -+Z distal (fingers up), +Y palmar, +X radial (thumb side). Every link frame -sits at its joint center with axes parallel to the palm at zero angles -(URDF joints are pure translations). Positive joint angles flex/curl. -Regenerate with the CAD skill: `python scripts/step models/robots/lyra/lyra.py`. -Regenerate link meshes per link with the CAD skill, e.g. -`python scripts/step models/robots/lyra/STEP/.py --3mf ../3MF/.3mf`. -Regenerate the robot description with the URDF/SRDF skills: -`python scripts/urdf models/robots/lyra/lyra.py=models/robots/lyra/lyra.urdf` -and `python scripts/srdf models/robots/lyra/lyra.py=models/robots/lyra/lyra.srdf`. diff --git a/models/robots/lyra/STEP/.index_distal.step.glb b/models/robots/lyra/STEP/.index_distal.step.glb deleted file mode 100644 index e55c1d8f..00000000 --- a/models/robots/lyra/STEP/.index_distal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d49b74dbdb182e63333c1571ff19fae65d343d0a898f06cd6674cc92e401844 -size 682188 diff --git a/models/robots/lyra/STEP/.index_middle.step.glb b/models/robots/lyra/STEP/.index_middle.step.glb deleted file mode 100644 index 6fc46bbb..00000000 --- a/models/robots/lyra/STEP/.index_middle.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:331e5dadca75ac02861614bbef2a6cb5786bcae577675e60560c8af926a3a451 -size 708628 diff --git a/models/robots/lyra/STEP/.index_proximal.step.glb b/models/robots/lyra/STEP/.index_proximal.step.glb deleted file mode 100644 index dbd54508..00000000 --- a/models/robots/lyra/STEP/.index_proximal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3ccd05391529404c144b401b45d302593f4ddda2db6be9427df94f5d3625026b -size 694112 diff --git a/models/robots/lyra/STEP/.middle_distal.step.glb b/models/robots/lyra/STEP/.middle_distal.step.glb deleted file mode 100644 index 3e594dc9..00000000 --- a/models/robots/lyra/STEP/.middle_distal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6290418d8c7e71e23d77253b269098f8c4688f6804deb552654dee59bd2c58f5 -size 686224 diff --git a/models/robots/lyra/STEP/.middle_middle.step.glb b/models/robots/lyra/STEP/.middle_middle.step.glb deleted file mode 100644 index 747cc675..00000000 --- a/models/robots/lyra/STEP/.middle_middle.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:83217532d0cab14cfe491dcbc8c0b8765574fc687a9e8b71d3e665e25c9c33a8 -size 698328 diff --git a/models/robots/lyra/STEP/.middle_proximal.step.glb b/models/robots/lyra/STEP/.middle_proximal.step.glb deleted file mode 100644 index 1ffe72f3..00000000 --- a/models/robots/lyra/STEP/.middle_proximal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0547896c8f6617eb881f520825d06807d74eb0b4c98d6ea6f0b40969abb743f0 -size 700116 diff --git a/models/robots/lyra/STEP/.palm.step.glb b/models/robots/lyra/STEP/.palm.step.glb deleted file mode 100644 index 2e06c708..00000000 --- a/models/robots/lyra/STEP/.palm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:243897ac6722f1767f7200d718b86ff2edd21c807884f542f509d81341f5890e -size 3262832 diff --git a/models/robots/lyra/STEP/.pinky_distal.step.glb b/models/robots/lyra/STEP/.pinky_distal.step.glb deleted file mode 100644 index a85edbac..00000000 --- a/models/robots/lyra/STEP/.pinky_distal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:966815d966d625b5836e1fe57893a8e143ef3b45b39fbbe368e7b10907245a57 -size 680684 diff --git a/models/robots/lyra/STEP/.pinky_middle.step.glb b/models/robots/lyra/STEP/.pinky_middle.step.glb deleted file mode 100644 index f64030cb..00000000 --- a/models/robots/lyra/STEP/.pinky_middle.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:618d436ae0769c508b61fbbedf62bf9d118bc74c78f544b5e08b7817c97cc026 -size 673740 diff --git a/models/robots/lyra/STEP/.pinky_proximal.step.glb b/models/robots/lyra/STEP/.pinky_proximal.step.glb deleted file mode 100644 index bd9a62a3..00000000 --- a/models/robots/lyra/STEP/.pinky_proximal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b256ba5a3775a9e9ce51d2b88f94ccff1dfd67a7668505b3c622a712580fe54c -size 708808 diff --git a/models/robots/lyra/STEP/.ring_distal.step.glb b/models/robots/lyra/STEP/.ring_distal.step.glb deleted file mode 100644 index 1d335633..00000000 --- a/models/robots/lyra/STEP/.ring_distal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ca832e4f43916223c5f5c9a9e82b9b034842ac275f86dcd7613d6009fd5792c4 -size 692032 diff --git a/models/robots/lyra/STEP/.ring_middle.step.glb b/models/robots/lyra/STEP/.ring_middle.step.glb deleted file mode 100644 index fe339a31..00000000 --- a/models/robots/lyra/STEP/.ring_middle.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c219c72bfb6a609ceff648397ac0eb6a121577acc858b7c9f3e01e7f9529c061 -size 714812 diff --git a/models/robots/lyra/STEP/.ring_proximal.step.glb b/models/robots/lyra/STEP/.ring_proximal.step.glb deleted file mode 100644 index 5b8640a5..00000000 --- a/models/robots/lyra/STEP/.ring_proximal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e4f86437c6ec1ba68c9be3b741ef032529e683f675ef4a3151fe05f2fe9b161 -size 699948 diff --git a/models/robots/lyra/STEP/.thumb_base.step.glb b/models/robots/lyra/STEP/.thumb_base.step.glb deleted file mode 100644 index e0b029fd..00000000 --- a/models/robots/lyra/STEP/.thumb_base.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31bba9a0c5e1f46a4acb3bd5590ee19847a4b8910f640f750d393512dbcab5c9 -size 589092 diff --git a/models/robots/lyra/STEP/.thumb_distal.step.glb b/models/robots/lyra/STEP/.thumb_distal.step.glb deleted file mode 100644 index 84c1e12e..00000000 --- a/models/robots/lyra/STEP/.thumb_distal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9fb5a93626cfc8c9191b8df193d4ebe01f1222b4fdb8b9a02698211d6d91cd3 -size 714184 diff --git a/models/robots/lyra/STEP/.thumb_metacarpal.step.glb b/models/robots/lyra/STEP/.thumb_metacarpal.step.glb deleted file mode 100644 index 0ae29bb2..00000000 --- a/models/robots/lyra/STEP/.thumb_metacarpal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79447de218a6108b143d997ba322280a6f241bc785d6bbc6dcdbac2f59f2b255 -size 688172 diff --git a/models/robots/lyra/STEP/.thumb_proximal.step.glb b/models/robots/lyra/STEP/.thumb_proximal.step.glb deleted file mode 100644 index 9f9a55b9..00000000 --- a/models/robots/lyra/STEP/.thumb_proximal.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b12a66016caffa30822e6c54d5e10d72b58197002834503190e700f0ed8a2dc5 -size 706992 diff --git a/models/robots/lyra/STEP/index_distal.py b/models/robots/lyra/STEP/index_distal.py deleted file mode 100644 index af9051e9..00000000 --- a/models/robots/lyra/STEP/index_distal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""index_distal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_distal("index")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_distal - - -def gen_step(): - return build_finger_distal("index") diff --git a/models/robots/lyra/STEP/index_distal.step b/models/robots/lyra/STEP/index_distal.step deleted file mode 100644 index 4bf82984..00000000 --- a/models/robots/lyra/STEP/index_distal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21c3c135c30c73c588621a6fc3f5d14de6430f9cfdd0451d8d256d7111278a1e -size 148191 diff --git a/models/robots/lyra/STEP/index_middle.py b/models/robots/lyra/STEP/index_middle.py deleted file mode 100644 index 3d5bbf3d..00000000 --- a/models/robots/lyra/STEP/index_middle.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""index_middle mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_middle("index")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_middle - - -def gen_step(): - return build_finger_middle("index") diff --git a/models/robots/lyra/STEP/index_middle.step b/models/robots/lyra/STEP/index_middle.step deleted file mode 100644 index 45f925c2..00000000 --- a/models/robots/lyra/STEP/index_middle.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e86e1495647b0191bc3a8d356b2b67a1264ab8b57c171a4428f345989944c0fe -size 278139 diff --git a/models/robots/lyra/STEP/index_proximal.py b/models/robots/lyra/STEP/index_proximal.py deleted file mode 100644 index e2436e60..00000000 --- a/models/robots/lyra/STEP/index_proximal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""index_proximal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_proximal("index")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_proximal - - -def gen_step(): - return build_finger_proximal("index") diff --git a/models/robots/lyra/STEP/index_proximal.step b/models/robots/lyra/STEP/index_proximal.step deleted file mode 100644 index 6e1c0ecc..00000000 --- a/models/robots/lyra/STEP/index_proximal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a43d7a15a8c6380715749d1b8cdefb2e41ef0486c75b4e9c2f607dec83f5e4a -size 278439 diff --git a/models/robots/lyra/STEP/middle_distal.py b/models/robots/lyra/STEP/middle_distal.py deleted file mode 100644 index 9ee87ff7..00000000 --- a/models/robots/lyra/STEP/middle_distal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""middle_distal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_distal("middle")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_distal - - -def gen_step(): - return build_finger_distal("middle") diff --git a/models/robots/lyra/STEP/middle_distal.step b/models/robots/lyra/STEP/middle_distal.step deleted file mode 100644 index 53878f06..00000000 --- a/models/robots/lyra/STEP/middle_distal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd028df4353bed14652d891fda0a5bf2dfff84cd31b1695c58a0360aa49cdf6f -size 151716 diff --git a/models/robots/lyra/STEP/middle_middle.py b/models/robots/lyra/STEP/middle_middle.py deleted file mode 100644 index f0385635..00000000 --- a/models/robots/lyra/STEP/middle_middle.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""middle_middle mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_middle("middle")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_middle - - -def gen_step(): - return build_finger_middle("middle") diff --git a/models/robots/lyra/STEP/middle_middle.step b/models/robots/lyra/STEP/middle_middle.step deleted file mode 100644 index f5d14f8b..00000000 --- a/models/robots/lyra/STEP/middle_middle.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3fdeef70c1ce41b0924f38ca2c4d014c241930a1fde969408174f6d3fa1976f2 -size 281556 diff --git a/models/robots/lyra/STEP/middle_proximal.py b/models/robots/lyra/STEP/middle_proximal.py deleted file mode 100644 index 396a29bf..00000000 --- a/models/robots/lyra/STEP/middle_proximal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""middle_proximal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_proximal("middle")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_proximal - - -def gen_step(): - return build_finger_proximal("middle") diff --git a/models/robots/lyra/STEP/middle_proximal.step b/models/robots/lyra/STEP/middle_proximal.step deleted file mode 100644 index 45285ee3..00000000 --- a/models/robots/lyra/STEP/middle_proximal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:66bfbaec95e63a9c1963757e5d050320dc3c4a85d403a03bac65243ae6bb1c19 -size 281629 diff --git a/models/robots/lyra/STEP/palm.py b/models/robots/lyra/STEP/palm.py deleted file mode 100644 index bce918f1..00000000 --- a/models/robots/lyra/STEP/palm.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""palm mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.palm.build_palm()` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.palm import build_palm - - -def gen_step(): - return build_palm() diff --git a/models/robots/lyra/STEP/palm.step b/models/robots/lyra/STEP/palm.step deleted file mode 100644 index 5459da9e..00000000 --- a/models/robots/lyra/STEP/palm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d26f4b6468995bdea587a2798d67c79edb66999881ce8cc89a3bb9e3a1e2729 -size 1430524 diff --git a/models/robots/lyra/STEP/pinky_distal.py b/models/robots/lyra/STEP/pinky_distal.py deleted file mode 100644 index c6d2d237..00000000 --- a/models/robots/lyra/STEP/pinky_distal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""pinky_distal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_distal("pinky")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_distal - - -def gen_step(): - return build_finger_distal("pinky") diff --git a/models/robots/lyra/STEP/pinky_distal.step b/models/robots/lyra/STEP/pinky_distal.step deleted file mode 100644 index c572e37f..00000000 --- a/models/robots/lyra/STEP/pinky_distal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee1ed00bb08c9bc946e86cc8480093f709d859e523677af8dc55aa3e5b971fc9 -size 141715 diff --git a/models/robots/lyra/STEP/pinky_middle.py b/models/robots/lyra/STEP/pinky_middle.py deleted file mode 100644 index 8a2ce6d0..00000000 --- a/models/robots/lyra/STEP/pinky_middle.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""pinky_middle mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_middle("pinky")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_middle - - -def gen_step(): - return build_finger_middle("pinky") diff --git a/models/robots/lyra/STEP/pinky_middle.step b/models/robots/lyra/STEP/pinky_middle.step deleted file mode 100644 index 00d03f75..00000000 --- a/models/robots/lyra/STEP/pinky_middle.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7048d757d4184eb383f0d1d5bb13b6e52f24ede56f0f0cb1cb553bf8fff8b1b3 -size 251581 diff --git a/models/robots/lyra/STEP/pinky_proximal.py b/models/robots/lyra/STEP/pinky_proximal.py deleted file mode 100644 index 1416ee75..00000000 --- a/models/robots/lyra/STEP/pinky_proximal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""pinky_proximal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_proximal("pinky")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_proximal - - -def gen_step(): - return build_finger_proximal("pinky") diff --git a/models/robots/lyra/STEP/pinky_proximal.step b/models/robots/lyra/STEP/pinky_proximal.step deleted file mode 100644 index b11f1d5c..00000000 --- a/models/robots/lyra/STEP/pinky_proximal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71af37c2d84703e2b3c028e90f968fb1e1b335d81bd6b2f276959525d2750f7a -size 267112 diff --git a/models/robots/lyra/STEP/ring_distal.py b/models/robots/lyra/STEP/ring_distal.py deleted file mode 100644 index feb31ae7..00000000 --- a/models/robots/lyra/STEP/ring_distal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""ring_distal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_distal("ring")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_distal - - -def gen_step(): - return build_finger_distal("ring") diff --git a/models/robots/lyra/STEP/ring_distal.step b/models/robots/lyra/STEP/ring_distal.step deleted file mode 100644 index fb5b549d..00000000 --- a/models/robots/lyra/STEP/ring_distal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8cc29818ee707228dc95639c03fb3bef7a6fcb07703a98cd2a0e1e52b16617b -size 142704 diff --git a/models/robots/lyra/STEP/ring_middle.py b/models/robots/lyra/STEP/ring_middle.py deleted file mode 100644 index 7e4b645e..00000000 --- a/models/robots/lyra/STEP/ring_middle.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""ring_middle mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_middle("ring")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_middle - - -def gen_step(): - return build_finger_middle("ring") diff --git a/models/robots/lyra/STEP/ring_middle.step b/models/robots/lyra/STEP/ring_middle.step deleted file mode 100644 index 0daea5d4..00000000 --- a/models/robots/lyra/STEP/ring_middle.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:125b1e7a2998276647e617af514d96e66d6fe28d55905e89727d45a1eaf84672 -size 281464 diff --git a/models/robots/lyra/STEP/ring_proximal.py b/models/robots/lyra/STEP/ring_proximal.py deleted file mode 100644 index 8dbf2320..00000000 --- a/models/robots/lyra/STEP/ring_proximal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""ring_proximal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_finger_proximal("ring")` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_finger_proximal - - -def gen_step(): - return build_finger_proximal("ring") diff --git a/models/robots/lyra/STEP/ring_proximal.step b/models/robots/lyra/STEP/ring_proximal.step deleted file mode 100644 index 314cd793..00000000 --- a/models/robots/lyra/STEP/ring_proximal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5df686d90478c5e3e470215a87e5d8477e54080df51e63953bb82a9c8194f76b -size 281791 diff --git a/models/robots/lyra/STEP/thumb_base.py b/models/robots/lyra/STEP/thumb_base.py deleted file mode 100644 index 3a392775..00000000 --- a/models/robots/lyra/STEP/thumb_base.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""thumb_base mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_thumb_base()` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_thumb_base - - -def gen_step(): - return build_thumb_base() diff --git a/models/robots/lyra/STEP/thumb_base.step b/models/robots/lyra/STEP/thumb_base.step deleted file mode 100644 index 2c1cd2fa..00000000 --- a/models/robots/lyra/STEP/thumb_base.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ddfe2aa9a453d2cb1c42b6d54bb328f4513e1724386d7be9b1352448e0a7f5b8 -size 148971 diff --git a/models/robots/lyra/STEP/thumb_distal.py b/models/robots/lyra/STEP/thumb_distal.py deleted file mode 100644 index 24b1ac65..00000000 --- a/models/robots/lyra/STEP/thumb_distal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""thumb_distal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_thumb_distal()` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_thumb_distal - - -def gen_step(): - return build_thumb_distal() diff --git a/models/robots/lyra/STEP/thumb_distal.step b/models/robots/lyra/STEP/thumb_distal.step deleted file mode 100644 index bd9c5721..00000000 --- a/models/robots/lyra/STEP/thumb_distal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:745fb96505c8556a19a59ad62156a079adcb82909472cbd86a9dda2599b17e12 -size 128727 diff --git a/models/robots/lyra/STEP/thumb_metacarpal.py b/models/robots/lyra/STEP/thumb_metacarpal.py deleted file mode 100644 index ba552bf9..00000000 --- a/models/robots/lyra/STEP/thumb_metacarpal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""thumb_metacarpal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_thumb_metacarpal()` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_thumb_metacarpal - - -def gen_step(): - return build_thumb_metacarpal() diff --git a/models/robots/lyra/STEP/thumb_metacarpal.step b/models/robots/lyra/STEP/thumb_metacarpal.step deleted file mode 100644 index c3f2f154..00000000 --- a/models/robots/lyra/STEP/thumb_metacarpal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95f840419d95abc0e040bc6862980487e3268ca7694476ed6b08fe030c6047f5 -size 289981 diff --git a/models/robots/lyra/STEP/thumb_proximal.py b/models/robots/lyra/STEP/thumb_proximal.py deleted file mode 100644 index e1ab5e8f..00000000 --- a/models/robots/lyra/STEP/thumb_proximal.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -"""thumb_proximal mesh source for the lyra URDF (part-local frame, mm). - -The compound matches `lyra_parts.digits.build_thumb_proximal()` exactly; the -URDF link frame coincides with this part-local frame. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -LYRA_ROOT = Path(__file__).resolve().parents[1] -if str(LYRA_ROOT) not in sys.path: - sys.path.insert(0, str(LYRA_ROOT)) - -from lyra_parts.digits import build_thumb_proximal - - -def gen_step(): - return build_thumb_proximal() diff --git a/models/robots/lyra/STEP/thumb_proximal.step b/models/robots/lyra/STEP/thumb_proximal.step deleted file mode 100644 index b30a0c80..00000000 --- a/models/robots/lyra/STEP/thumb_proximal.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b18f3fc421f1ce239c4c188fa86405b3c30bdd440fe9d6f36b3eda0a80f149fd -size 298326 diff --git a/models/robots/lyra/lyra.py b/models/robots/lyra/lyra.py deleted file mode 100644 index 2f25ad59..00000000 --- a/models/robots/lyra/lyra.py +++ /dev/null @@ -1,107 +0,0 @@ -"""lyra — dexterous humanoid right hand concept for an advanced biped. - -An aesthetically refined five-digit robotic hand: slim pearl-composite -shells over a graphite structural core, machined-aluminum precision -knuckle clevises with visible pivot pins, tendon-driven architecture -(dorsal tendon channels with a tensioner dial row at the wrist), integrated -tactile pads (palm array, per-phalanx strips, soft-touch fingertip caps), -an amber-ringed palm sensor, and a bolt-circle wrist flange. No logos. - -Degrees of freedom (16, statically posed in the baked STEP): - - each finger (index/middle/ring/pinky): MCP, PIP, DIP flexion -> 12 - - thumb: CMC yaw (opposition swing), CMC flex, MP, IP -> 4 - -Coordinates: RIGHT hand; wrist-flange mount face center = origin, -+Z distal (fingers up), +Y palmar, +X radial (thumb side). Units mm. - -Chain offsets, joint limits, and the baked "relaxed" pose live in -lyra_parts/chain.py and are shared with the URDF/SRDF generators -(lyra_parts/description.py) and the CAD Viewer animation sidecar -(.lyra.step.js); edit them there. -""" - -from __future__ import annotations - -from build123d import Compound - -from cadpy.assembly import AssemblyHelper - -from lyra_parts import chain -from lyra_parts.digits import ( - build_finger_distal, - build_finger_middle, - build_finger_proximal, - build_thumb_base, - build_thumb_distal, - build_thumb_metacarpal, - build_thumb_proximal, -) -from lyra_parts.lib import revolute_attach -from lyra_parts.palm import build_palm - - -def _builders(): - builders = {} - for finger in chain.FINGERS: - builders[f"{finger}_proximal"] = lambda f=finger: build_finger_proximal(f) - builders[f"{finger}_middle"] = lambda f=finger: build_finger_middle(f) - builders[f"{finger}_distal"] = lambda f=finger: build_finger_distal(f) - builders["thumb_base"] = build_thumb_base - builders["thumb_metacarpal"] = build_thumb_metacarpal - builders["thumb_proximal"] = build_thumb_proximal - builders["thumb_distal"] = build_thumb_distal - return builders - - -def _xref_for(axis) -> tuple: - return (0.0, 1.0, 0.0) if abs(axis[0]) > 0.9 else (1.0, 0.0, 0.0) - - -def assemble() -> Compound: - """Labeled assembly baked in the chain's relaxed pose. - - Occurrence order (#o1.N in the generated STEP) is palm first, then - chain.all_joints() child order: index, middle, ring, pinky - (proximal/middle/distal each), then thumb base/metacarpal/proximal/ - distal — the animation sidecar relies on this order. - """ - asm = AssemblyHelper(chain.ROBOT_NAME) - builders = _builders() - pose = chain.named_poses_deg()[chain.BAKED_POSE_NAME] - - parts = {"palm": asm.add(build_palm(), "palm")} - for joint in chain.all_joints(): - child = asm.add(builders[joint["child"]](), joint["child"]) - axis = joint["axis"] - xref = _xref_for(axis) - revolute_attach( - asm, - parts[joint["parent"]], - child, - joint["name"], - joint["origin_mm"], - axis, - xref, - (0.0, 0.0, 0.0), - axis, - xref, - pose[joint["name"]], - ) - parts[joint["child"]] = child - return asm.build() - - -def gen_step(): - return assemble() - - -def gen_urdf(): - from lyra_parts.description import build_urdf - - return {"xml": build_urdf()} - - -def gen_srdf(): - from lyra_parts.description import build_srdf - - return {"xml": build_srdf(), "urdf": "lyra.urdf"} diff --git a/models/robots/lyra/lyra.srdf b/models/robots/lyra/lyra.srdf deleted file mode 100644 index 0af88f17..00000000 --- a/models/robots/lyra/lyra.srdf +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/lyra/lyra.step b/models/robots/lyra/lyra.step deleted file mode 100644 index dfef786b..00000000 --- a/models/robots/lyra/lyra.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:12c4ec1a127e2ea73be6e4004b24a442939363a1e69e9bee1b12f0cfd5d854d3 -size 5258666 diff --git a/models/robots/lyra/lyra.urdf b/models/robots/lyra/lyra.urdf deleted file mode 100644 index e9b260bd..00000000 --- a/models/robots/lyra/lyra.urdf +++ /dev/null @@ -1,447 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/lyra/lyra_parts/__init__.py b/models/robots/lyra/lyra_parts/__init__.py deleted file mode 100644 index aee6e7e8..00000000 --- a/models/robots/lyra/lyra_parts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Part-builder package for the lyra dexterous humanoid hand.""" diff --git a/models/robots/lyra/lyra_parts/chain.py b/models/robots/lyra/lyra_parts/chain.py deleted file mode 100644 index bf192ee5..00000000 --- a/models/robots/lyra/lyra_parts/chain.py +++ /dev/null @@ -1,375 +0,0 @@ -"""lyra kinematic chain spec shared by the CAD assembly and URDF/SRDF. - -Stdlib-only (no build123d) so robot-description generators can import it -cheaply. `lyra.py` consumes the same offsets and CAD pose angles, keeping -the STEP assembly, the URDF tree, and the viewer animation sidecar in -lockstep. - -Design ledger: -- Units: chain offsets are millimeters in each parent part's local frame; - URDF emission converts to meters/radians. RIGHT hand frame: origin at the - wrist-flange mount face center, +Z distal (fingers up), +Y palmar (the - palm faces +Y), +X radial (thumb side). The palm link is the root; every - child link frame sits at its joint center with axes parallel to the - parent at zero joint angle (joints are pure translations). -- Fingers (index/middle/ring/pinky): 3 flexion joints each (MCP, PIP, DIP) - about -X, so positive angles curl toward the palm (+Y). Phalanges extend - +Z local. MCP abduction/spread is intentionally omitted: the tendon - routing budget of the compact palm is spent on flexion; documented - design trade, not an oversight. -- Thumb: 4 joints. `thumb_cmc_yaw` about +Z swings the column from radial - (+X, the zero pose) across the palm front toward opposition; - `thumb_cmc_flex`, `thumb_mp`, `thumb_ip` about -Y curl the column toward - distal (+Z). Thumb segments extend +X local with the pad facing +Z, so a - yawed-and-curled thumb meets the curled fingertips pad-to-pad. -- Joint limits, efforts, and velocities are ASSUMED plausible values for a - tendon-driven research-hand concept (documented here, not measured); the - CAD does not model hard stops. -""" - -from __future__ import annotations - -import math - -X_AXIS = (1.0, 0.0, 0.0) -Y_AXIS = (0.0, 1.0, 0.0) -Z_AXIS = (0.0, 0.0, 1.0) -NEG_X_AXIS = (-1.0, 0.0, 0.0) -NEG_Y_AXIS = (0.0, -1.0, 0.0) - -ROBOT_NAME = "lyra" - -FINGERS = ("index", "middle", "ring", "pinky") - -# MCP joint centers in the palm frame (mm): a gentle knuckle arch, the -# middle finger proudest, the pinky lower and slightly palmar. -MCP_ORIGIN_MM = { - "index": (28.5, 0.0, 99.0), - "middle": (9.5, 0.0, 103.0), - "ring": (-9.5, 0.0, 99.0), - "pinky": (-28.5, 0.0, 90.0), -} - -# Segment lengths (mm): proximal (MCP->PIP), middle (PIP->DIP), and the -# distal tip length (DIP->fingertip surface) used for pose design. -SEGMENT_MM = { - "index": (44.0, 26.0, 23.0), - "middle": (48.0, 29.0, 24.5), - "ring": (44.0, 27.0, 23.5), - "pinky": (35.0, 21.0, 20.5), -} - -# Thumb column offsets (mm). -THUMB_CMC_ORIGIN_MM = (31.0, 4.0, 44.0) # turret center on the radial palm corner -THUMB_BASE_LEN_MM = 13.0 # cmc_yaw -> cmc_flex along the column -THUMB_METACARPAL_LEN_MM = 46.0 # cmc_flex -> MP -THUMB_PROXIMAL_LEN_MM = 30.0 # MP -> IP -THUMB_TIP_LEN_MM = 24.0 # IP -> thumb-pad surface (pose design) - -# ---------------------------------------------------------------- limits -# ASSUMED tendon-driven actuator budget for a research-hand concept. -FINGER_MCP_EFFORT_NM = 3.0 -FINGER_PIP_EFFORT_NM = 2.0 -FINGER_DIP_EFFORT_NM = 1.5 -THUMB_CMC_EFFORT_NM = 4.0 -THUMB_MP_EFFORT_NM = 2.5 -THUMB_IP_EFFORT_NM = 2.0 -FINGER_VELOCITY_RAD_S = 8.0 -THUMB_VELOCITY_RAD_S = 6.0 - -# ASSUMED joint travel (degrees); positive = flexion (curl toward palm). -MCP_RANGE_DEG = (-15.0, 95.0) -PIP_RANGE_DEG = (-5.0, 105.0) -DIP_RANGE_DEG = (-5.0, 95.0) -THUMB_CMC_YAW_RANGE_DEG = (-10.0, 100.0) # positive sweeps toward opposition -THUMB_CMC_FLEX_RANGE_DEG = (-10.0, 55.0) # positive lifts the column distal -THUMB_MP_RANGE_DEG = (-10.0, 75.0) -THUMB_IP_RANGE_DEG = (-15.0, 85.0) - - -def finger_joints(finger: str) -> list[dict]: - prox, mid, _tip = SEGMENT_MM[finger] - return [ - { - "name": f"{finger}_mcp", - "parent": "palm", - "child": f"{finger}_proximal", - "origin_mm": MCP_ORIGIN_MM[finger], - "axis": NEG_X_AXIS, - "range_deg": MCP_RANGE_DEG, - "effort_nm": FINGER_MCP_EFFORT_NM, - "velocity_rad_s": FINGER_VELOCITY_RAD_S, - }, - { - "name": f"{finger}_pip", - "parent": f"{finger}_proximal", - "child": f"{finger}_middle", - "origin_mm": (0.0, 0.0, prox), - "axis": NEG_X_AXIS, - "range_deg": PIP_RANGE_DEG, - "effort_nm": FINGER_PIP_EFFORT_NM, - "velocity_rad_s": FINGER_VELOCITY_RAD_S, - }, - { - "name": f"{finger}_dip", - "parent": f"{finger}_middle", - "child": f"{finger}_distal", - "origin_mm": (0.0, 0.0, mid), - "axis": NEG_X_AXIS, - "range_deg": DIP_RANGE_DEG, - "effort_nm": FINGER_DIP_EFFORT_NM, - "velocity_rad_s": FINGER_VELOCITY_RAD_S, - }, - ] - - -def thumb_joints() -> list[dict]: - return [ - { - "name": "thumb_cmc_yaw", - "parent": "palm", - "child": "thumb_base", - "origin_mm": THUMB_CMC_ORIGIN_MM, - "axis": Z_AXIS, - "range_deg": THUMB_CMC_YAW_RANGE_DEG, - "effort_nm": THUMB_CMC_EFFORT_NM, - "velocity_rad_s": THUMB_VELOCITY_RAD_S, - }, - { - "name": "thumb_cmc_flex", - "parent": "thumb_base", - "child": "thumb_metacarpal", - "origin_mm": (THUMB_BASE_LEN_MM, 0.0, 0.0), - "axis": NEG_Y_AXIS, - "range_deg": THUMB_CMC_FLEX_RANGE_DEG, - "effort_nm": THUMB_CMC_EFFORT_NM, - "velocity_rad_s": THUMB_VELOCITY_RAD_S, - }, - { - "name": "thumb_mp", - "parent": "thumb_metacarpal", - "child": "thumb_proximal", - "origin_mm": (THUMB_METACARPAL_LEN_MM, 0.0, 0.0), - "axis": NEG_Y_AXIS, - "range_deg": THUMB_MP_RANGE_DEG, - "effort_nm": THUMB_MP_EFFORT_NM, - "velocity_rad_s": THUMB_VELOCITY_RAD_S, - }, - { - "name": "thumb_ip", - "parent": "thumb_proximal", - "child": "thumb_distal", - "origin_mm": (THUMB_PROXIMAL_LEN_MM, 0.0, 0.0), - "axis": NEG_Y_AXIS, - "range_deg": THUMB_IP_RANGE_DEG, - "effort_nm": THUMB_IP_EFFORT_NM, - "velocity_rad_s": THUMB_VELOCITY_RAD_S, - }, - ] - - -def all_joints() -> list[dict]: - """All 16 movable joints in root-to-leaf order (fingers, then thumb).""" - joints: list[dict] = [] - for finger in FINGERS: - joints.extend(finger_joints(finger)) - joints.extend(thumb_joints()) - return joints - - -def all_links() -> list[str]: - """All 17 link names, root first, in joint emission order.""" - links = ["palm"] - for joint in all_joints(): - links.append(joint["child"]) - return links - - -# -------------------------------------------------------------------- FK -def _rot_axis(axis, deg: float): - """3x3 rotation about a principal-ish axis (unit vector), row-major.""" - ax, ay, az = axis - rad = math.radians(deg) - c, s = math.cos(rad), math.sin(rad) - t = 1.0 - c - return [ - [c + ax * ax * t, ax * ay * t - az * s, ax * az * t + ay * s], - [ay * ax * t + az * s, c + ay * ay * t, ay * az * t - ax * s], - [az * ax * t - ay * s, az * ay * t + ax * s, c + az * az * t], - ] - - -def _mat_mul(a, b): - return [ - [sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] - for i in range(3) - ] - - -def _mat_vec(a, v): - return tuple(sum(a[i][k] * v[k] for k in range(3)) for i in range(3)) - - -def fk_frames(pose_deg: dict[str, float]) -> dict[str, tuple]: - """Link frames {link: (R 3x3, p mm)} in the palm frame for a pose.""" - identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]] - frames = {"palm": (identity, (0.0, 0.0, 0.0))} - for joint in all_joints(): - rot_parent, pos_parent = frames[joint["parent"]] - offset = _mat_vec(rot_parent, joint["origin_mm"]) - pos = tuple(pos_parent[i] + offset[i] for i in range(3)) - rot = _mat_mul( - rot_parent, _rot_axis(joint["axis"], pose_deg.get(joint["name"], 0.0)) - ) - frames[joint["child"]] = (rot, pos) - return frames - - -def tip_world_mm(pose_deg: dict[str, float], digit: str) -> tuple: - """Pad-surface point of a digit tip in the palm frame (pose design aid).""" - frames = fk_frames(pose_deg) - if digit == "thumb": - rot, pos = frames["thumb_distal"] - local = (THUMB_TIP_LEN_MM, 0.0, 0.0) - else: - rot, pos = frames[f"{digit}_distal"] - local = (0.0, 0.0, SEGMENT_MM[digit][2]) - offset = _mat_vec(rot, local) - return tuple(pos[i] + offset[i] for i in range(3)) - - -# ----------------------------------------------------------------- poses -def _zero_deg() -> dict[str, float]: - return {joint["name"]: 0.0 for joint in all_joints()} - - -def _relaxed_deg() -> dict[str, float]: - """Gentle natural curl; this is the pose baked into lyra.step.""" - pose = _zero_deg() - curl = { - "index": (10.0, 14.0, 8.0), - "middle": (12.0, 16.0, 9.0), - "ring": (14.0, 18.0, 10.0), - "pinky": (16.0, 20.0, 12.0), - } - for finger, (mcp, pip_deg, dip) in curl.items(): - pose[f"{finger}_mcp"] = mcp - pose[f"{finger}_pip"] = pip_deg - pose[f"{finger}_dip"] = dip - pose.update( - { - "thumb_cmc_yaw": 34.0, - "thumb_cmc_flex": 26.0, - "thumb_mp": 14.0, - "thumb_ip": 12.0, - } - ) - return pose - - -def _fist_deg() -> dict[str, float]: - pose = _zero_deg() - for finger in FINGERS: - pose[f"{finger}_mcp"] = 78.0 - pose[f"{finger}_pip"] = 100.0 - pose[f"{finger}_dip"] = 60.0 - # Capsule-tuned wrap: the thumb pad rests on the index middle phalanx - # with ~0.8 mm surface clearance (no interpenetration at any digit). - pose.update( - { - "thumb_cmc_yaw": 92.0, - "thumb_cmc_flex": 13.0, - "thumb_mp": 58.0, - "thumb_ip": 74.0, - } - ) - return pose - - -def _precision_pinch_deg() -> dict[str, float]: - """Thumb-index pad opposition, capsule-tuned so the tip pads kiss with - ~0.8 mm surface clearance (no tip-sphere interpenetration).""" - pose = _zero_deg() - pose.update({"index_mcp": 40.0, "index_pip": 48.0, "index_dip": 30.0}) - for finger in ("middle", "ring", "pinky"): - pose[f"{finger}_mcp"] = 66.0 - pose[f"{finger}_pip"] = 92.0 - pose[f"{finger}_dip"] = 55.0 - pose.update( - { - "thumb_cmc_yaw": 91.0, - "thumb_cmc_flex": 34.0, - "thumb_mp": 29.0, - "thumb_ip": 8.0, - } - ) - return pose - - -def _tripod_pinch_deg() -> dict[str, float]: - """Thumb opposed between index and middle pads, chuck-gripping a - virtual object (capsule-tuned: ~12 mm pad-surface gap to both).""" - pose = _precision_pinch_deg() - pose.update({"index_mcp": 44.0, "index_pip": 52.0, "index_dip": 32.0}) - pose.update({"middle_mcp": 42.0, "middle_pip": 50.0, "middle_dip": 30.0}) - pose.update( - { - "thumb_cmc_yaw": 100.0, - "thumb_cmc_flex": 39.0, - "thumb_mp": 0.0, - "thumb_ip": 14.0, - } - ) - return pose - - -def _point_deg() -> dict[str, float]: - pose = _zero_deg() - pose.update({"index_mcp": -6.0, "index_pip": 0.0, "index_dip": 0.0}) - for finger in ("middle", "ring", "pinky"): - pose[f"{finger}_mcp"] = 80.0 - pose[f"{finger}_pip"] = 102.0 - pose[f"{finger}_dip"] = 62.0 - pose.update( - { - "thumb_cmc_yaw": 62.0, - "thumb_cmc_flex": 30.0, - "thumb_mp": 42.0, - "thumb_ip": 45.0, - } - ) - return pose - - -def _ok_sign_deg() -> dict[str, float]: - """Index-thumb ring closed, remaining fingers extended with a stagger.""" - pose = _zero_deg() - pose.update({"index_mcp": 42.0, "index_pip": 52.0, "index_dip": 32.0}) - pose.update({"middle_mcp": 6.0, "middle_pip": 8.0, "middle_dip": 4.0}) - pose.update({"ring_mcp": 10.0, "ring_pip": 12.0, "ring_dip": 6.0}) - pose.update({"pinky_mcp": 14.0, "pinky_pip": 16.0, "pinky_dip": 8.0}) - pose.update( - { - "thumb_cmc_yaw": 91.0, - "thumb_cmc_flex": 26.0, - "thumb_mp": 35.0, - "thumb_ip": 20.0, - } - ) - return pose - - -BAKED_POSE_NAME = "relaxed" - - -def named_poses_deg() -> dict[str, dict[str, float]]: - """SRDF group-state poses (degrees) for the full hand.""" - return { - "zero": _zero_deg(), - "relaxed": _relaxed_deg(), - "fist": _fist_deg(), - "precision_pinch": _precision_pinch_deg(), - "tripod_pinch": _tripod_pinch_deg(), - "point": _point_deg(), - "ok_sign": _ok_sign_deg(), - } diff --git a/models/robots/lyra/lyra_parts/clearance.py b/models/robots/lyra/lyra_parts/clearance.py deleted file mode 100644 index b78e3f4f..00000000 --- a/models/robots/lyra/lyra_parts/clearance.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Capsule-clearance self-check for lyra digit poses and animation paths. - -Stdlib-only. Each digit segment is approximated by a conservative capsule -(segment + radius) in its link frame; a pose is collision-free when every -thumb capsule clears every finger capsule. The named grip poses are tuned -to small POSITIVE clearances (pads kiss at ~+0.7..0.8 mm, never overlap), -and the animation key orders in `.lyra.step.js` are chosen so every -adjacent smoothstep blend stays clear: - -- pose tour: relaxed -> precision_pinch -> ok_sign -> point -> - tripod_pinch -> fist (the fist only neighbors tripod/relaxed; blending - it with pinch/point/ok sweeps the thumb through the index). -- count: the thumb lifts to a hover key before any finger extends and only - re-wraps after the fingers curl back. - -Run after editing poses: - - ./.venv/bin/python -m lyra_parts.clearance (from models/robots/lyra) - -Exits nonzero if any named pose or animation path dips below zero. -""" - -from __future__ import annotations - -import math - -from lyra_parts import chain - -# Conservative capsule radii (mm at finger scale 1; see digits.py widths). -FINGER_SCALE = {"index": 1.0, "middle": 1.03, "ring": 0.97, "pinky": 0.88} -PROX_R, MID_R, DIST_R = 7.0, 6.3, 5.2 -THUMB_META_R, THUMB_PROX_R, THUMB_DIST_R = 6.8, 6.2, 5.2 -FINGER_TIP_SPHERE_R = 4.4 # scaled; distal capsule ends at the tip-sphere center -THUMB_TIP_SPHERE_R = 4.8 - - -def _seg_seg_dist(p1, q1, p2, q2): - def sub(a, b): - return [a[i] - b[i] for i in range(3)] - - def dot(a, b): - return sum(a[i] * b[i] for i in range(3)) - - d1, d2, r = sub(q1, p1), sub(q2, p2), sub(p1, p2) - a, e, f = dot(d1, d1), dot(d2, d2), dot(d2, r) - if a <= 1e-12 and e <= 1e-12: - return math.dist(p1, p2) - if a <= 1e-12: - s, t = 0.0, max(0.0, min(1.0, f / e)) - else: - c = dot(d1, r) - if e <= 1e-12: - t, s = 0.0, max(0.0, min(1.0, -c / a)) - else: - b = dot(d1, d2) - denom = a * e - b * b - s = max(0.0, min(1.0, (b * f - c * e) / denom)) if denom > 1e-12 else 0.0 - t = (b * s + f) / e - if t < 0: - t, s = 0.0, max(0.0, min(1.0, -c / a)) - elif t > 1: - t, s = 1.0, max(0.0, min(1.0, (b - c) / a)) - c1 = [p1[i] + d1[i] * s for i in range(3)] - c2 = [p2[i] + d2[i] * t for i in range(3)] - return math.dist(c1, c2) - - -def _mv(rot, v): - return [sum(rot[i][k] * v[k] for k in range(3)) for i in range(3)] - - -def digit_capsules(pose_deg: dict[str, float]) -> dict[str, tuple]: - """{name: (p0, p1, radius)} capsules in the palm frame for a pose.""" - frames = chain.fk_frames(pose_deg) - caps = {} - for finger in chain.FINGERS: - s = FINGER_SCALE[finger] - prox, mid, tip = chain.SEGMENT_MM[finger] - spans = ( - ("prox", f"{finger}_proximal", prox, PROX_R * s), - ("mid", f"{finger}_middle", mid, MID_R * s), - ("dist", f"{finger}_distal", tip - FINGER_TIP_SPHERE_R * s, DIST_R * s), - ) - for seg, link, length, radius in spans: - rot, pos = frames[link] - tip_pt = [pos[i] + _mv(rot, [0, 0, length])[i] for i in range(3)] - caps[f"{finger}.{seg}"] = (list(pos), tip_pt, radius) - spans = ( - ("meta", "thumb_metacarpal", chain.THUMB_METACARPAL_LEN_MM, THUMB_META_R), - ("prox", "thumb_proximal", chain.THUMB_PROXIMAL_LEN_MM, THUMB_PROX_R), - ("dist", "thumb_distal", chain.THUMB_TIP_LEN_MM - THUMB_TIP_SPHERE_R, THUMB_DIST_R), - ) - for seg, link, length, radius in spans: - rot, pos = frames[link] - tip_pt = [pos[i] + _mv(rot, [length, 0, 0])[i] for i in range(3)] - caps[f"thumb.{seg}"] = (list(pos), tip_pt, radius) - return caps - - -def worst_clearance(pose_deg: dict[str, float]) -> tuple[float, str, str]: - """Smallest thumb-vs-finger capsule clearance (mm; negative = overlap).""" - caps = digit_capsules(pose_deg) - worst = (math.inf, "", "") - for tname in ("thumb.meta", "thumb.prox", "thumb.dist"): - p1, q1, r1 = caps[tname] - for fname, (p2, q2, r2) in caps.items(): - if fname.startswith("thumb."): - continue - d = _seg_seg_dist(p1, q1, p2, q2) - r1 - r2 - if d < worst[0]: - worst = (d, tname, fname) - return worst - - -def _smoothstep(u: float) -> float: - t = max(0.0, min(1.0, u)) - return t * t * (3 - 2 * t) - - -def path_worst(pose_a, pose_b, steps: int = 120) -> tuple[float, str, str, float]: - """Worst clearance along the smoothstep blend a -> b (the sidecar path).""" - names = [joint["name"] for joint in chain.all_joints()] - worst = (math.inf, "", "", 0.0) - for i in range(steps + 1): - e = _smoothstep(i / steps) - pose = {n: pose_a[n] + (pose_b[n] - pose_a[n]) * e for n in names} - d, ca, cb = worst_clearance(pose) - if d < worst[0]: - worst = (d, ca, cb, i / steps) - return worst - - -# Animation key orders and ripple-wave constants mirrored from .lyra.step.js. -TOUR_KEYS = ("relaxed", "precision_pinch", "ok_sign", "point", "tripod_pinch", "fist") -RIPPLE_ORDER = ("index", "middle", "ring", "pinky", "thumb") -RIPPLE_CURL = {"mcp": 30.0, "pip": 40.0, "dip": 22.0, "thumbFlex": 12.0, "thumbMp": 30.0, "thumbIp": 30.0} -RIPPLE_WINDOW = 0.45 - - -def ripple_pose(phase: float, grip: float = 1.0) -> dict[str, float]: - """Mirror of the sidecar ripplePose: overlapping per-digit curl pulses - on top of the baked relaxed pose (the tour's opening wave).""" - p = phase % 1.0 - step = (1.0 - RIPPLE_WINDOW) / (len(RIPPLE_ORDER) - 1) - pose = dict(chain.named_poses_deg()[chain.BAKED_POSE_NAME]) - for i, digit in enumerate(RIPPLE_ORDER): - u = (p - i * step) / RIPPLE_WINDOW - if u <= 0.0 or u >= 1.0: - continue - lobe = math.sin(math.pi * u) - amp = lobe * lobe * grip - if digit == "thumb": - pose["thumb_cmc_flex"] += RIPPLE_CURL["thumbFlex"] * amp - pose["thumb_mp"] += RIPPLE_CURL["thumbMp"] * amp - pose["thumb_ip"] += RIPPLE_CURL["thumbIp"] * amp - else: - pose[f"{digit}_mcp"] += RIPPLE_CURL["mcp"] * amp - pose[f"{digit}_pip"] += RIPPLE_CURL["pip"] * amp - pose[f"{digit}_dip"] += RIPPLE_CURL["dip"] * amp - return pose -COUNT_THUMB_FIST = (92.0, 13.0, 58.0, 74.0) -COUNT_THUMB_HOVER = (50.0, 30.0, 25.0, 20.0) -COUNT_THUMB_OPEN = (20.0, 10.0, 4.0, 4.0) -COUNT_EXTENDED = { - "index": (2.0, 2.0, 1.0), - "middle": (2.0, 2.0, 1.0), - "ring": (4.0, 4.0, 2.0), - "pinky": (6.0, 6.0, 3.0), -} - - -def count_key(step: int, thumb) -> dict[str, float]: - pose = {joint["name"]: 0.0 for joint in chain.all_joints()} - for i, finger in enumerate(chain.FINGERS): - mcp, pip_deg, dip = COUNT_EXTENDED[finger] if step >= i + 1 else (78.0, 100.0, 60.0) - pose[f"{finger}_mcp"] = mcp - pose[f"{finger}_pip"] = pip_deg - pose[f"{finger}_dip"] = dip - yaw, flex, mp, ip = thumb - pose.update( - {"thumb_cmc_yaw": yaw, "thumb_cmc_flex": flex, "thumb_mp": mp, "thumb_ip": ip} - ) - return pose - - -def count_keys() -> list[dict[str, float]]: - return [ - count_key(0, COUNT_THUMB_FIST), - count_key(0, COUNT_THUMB_HOVER), - count_key(1, COUNT_THUMB_HOVER), - count_key(2, COUNT_THUMB_HOVER), - count_key(3, COUNT_THUMB_HOVER), - count_key(4, COUNT_THUMB_HOVER), - count_key(5, COUNT_THUMB_OPEN), - count_key(0, COUNT_THUMB_HOVER), - ] - - -def run_checks(verbose: bool = True) -> list[str]: - """Return a list of failure strings (empty = all poses/paths clear).""" - failures = [] - poses = chain.named_poses_deg() - for name, pose in poses.items(): - d, ca, cb = worst_clearance(pose) - if verbose: - print(f"pose {name:18s} clearance {d:7.2f} mm ({ca}|{cb})") - if d < 0: - failures.append(f"pose {name}: {d:.2f} mm overlap ({ca}|{cb})") - for i in range(len(TOUR_KEYS)): - a, b = TOUR_KEYS[i], TOUR_KEYS[(i + 1) % len(TOUR_KEYS)] - d, ca, cb, u = path_worst(poses[a], poses[b]) - if verbose: - print(f"tour {a} -> {b}: worst {d:7.2f} mm at u={u:.2f}") - if d < 0: - failures.append(f"tour {a}->{b}: {d:.2f} mm overlap at u={u:.2f} ({ca}|{cb})") - keys = count_keys() - for i in range(len(keys)): - a, b = keys[i], keys[(i + 1) % len(keys)] - d, ca, cb, u = path_worst(a, b) - if verbose: - print(f"count seg {i}: worst {d:7.2f} mm at u={u:.2f}") - if d < 0: - failures.append(f"count seg {i}: {d:.2f} mm overlap at u={u:.2f} ({ca}|{cb})") - for a, b in (("relaxed", "fist"), ("relaxed", "precision_pinch")): - d, ca, cb, u = path_worst(poses[a], poses[b]) - if verbose: - print(f"loop {a} <-> {b}: worst {d:7.2f} mm at u={u:.2f}") - if d < 0: - failures.append(f"loop {a}<->{b}: {d:.2f} mm overlap ({ca}|{cb})") - worst = (math.inf, "", "", 0.0) - for i in range(241): - u = i / 240.0 - d, ca, cb = worst_clearance(ripple_pose(u)) - if d < worst[0]: - worst = (d, ca, cb, u) - if verbose: - print(f"ripple wave: worst {worst[0]:7.2f} mm at u={worst[3]:.2f}") - if worst[0] < 0: - failures.append(f"ripple wave: {worst[0]:.2f} mm overlap at u={worst[3]:.2f} ({worst[1]}|{worst[2]})") - return failures - - -if __name__ == "__main__": - import sys - - fails = run_checks() - if fails: - print("\nFAILED:") - for failure in fails: - print(" ", failure) - sys.exit(1) - print("\nall poses and animation paths are collision-free") diff --git a/models/robots/lyra/lyra_parts/description.py b/models/robots/lyra/lyra_parts/description.py deleted file mode 100644 index 396650b4..00000000 --- a/models/robots/lyra/lyra_parts/description.py +++ /dev/null @@ -1,266 +0,0 @@ -"""URDF and SRDF emission for the lyra dexterous hand. - -Stdlib-only builders consumed by `lyra.py` `gen_urdf()` / `gen_srdf()`. - -Design ledger (in addition to the chain ledger in `lyra_parts/chain.py`): -- Units: URDF output is meters/kilograms/radians; chain offsets and baked - mass properties are millimeters (mesh sources are mm, hence the 0.001 - mesh scale). Frames: RIGHT hand, +Z distal, +Y palmar, +X radial; the - palm link is the root body with the wrist-flange mount face center at - its origin, plus a frame-only `wrist_mount` link at the same point for - attaching the hand to a robot forearm. -- Link frames: every link frame coincides with its parent joint frame and - with the part-local modeling frame of the matching `lyra_parts` builder, - so all visual mesh origins are identity. -- Visual geometry: per-link 3MF meshes under `3MF/` exported from the - `STEP/.py` wrapper sources (XCAF colors preserved). -- Collision geometry: axis-aligned bounding boxes of each part in its - link frame — intentionally coarse, for viewer/planning sanity rather - than contact-accurate simulation. -- Inertials: CAD volume properties (OCP BRepGProp) at an ASSUMED uniform - density chosen so the whole hand masses ASSUMED_TOTAL_MASS_KG. COM and - the full inertia tensor (about the COM, link frame) come from the CAD - solids; only the density is assumed. Confidence: estimated. -- SRDF: per-digit chain groups, a `fingers` union, and a whole-`hand` - joint group; group states are the named poses from - `chain.named_poses_deg()` converted to radians. Disabled collisions: - URDF-adjacent pairs (reason Adjacent) plus pairs whose collision boxes - already overlap in the zero pose (reason Default), computed from the - chain offsets at import time. -""" - -from __future__ import annotations - -import math -import xml.etree.ElementTree as ET - -from lyra_parts import chain -from lyra_parts.mass_props import LINK_MASS_PROPS, TOTAL_VOLUME_MM3 - -ROBOT_NAME = chain.ROBOT_NAME - -# Tendon-driven research-hand concept: shells, spines, and pins only; the -# CAD does not model actuators, so total mass is an assumption distributed -# by CAD volume. -ASSUMED_TOTAL_MASS_KG = 0.62 -UNIFORM_DENSITY_KG_PER_MM3 = ASSUMED_TOTAL_MASS_KG / TOTAL_VOLUME_MM3 - -MM_TO_M = 0.001 -# Unit-density inertia integrates r^2 dV in mm^5; kg*m^2 = density * mm^5 * 1e-6. -INERTIA_MM5_TO_KG_M2 = UNIFORM_DENSITY_KG_PER_MM3 * 1e-6 -MESH_DIR = "3MF" -MESH_SCALE_FROM_MM = "0.001 0.001 0.001" - -HEADER_NOTE = ( - " Generated by lyra.py (lyra_parts/description.py) for CAD Viewer and " - "external URDF consumers. Link frames coincide with lyra_parts part-local " - "frames; meshes are mm 3MF exports of the same builders. Inertials use CAD " - "volume properties at a uniform assumed density totalling 0.62 kg; " - "collision geometry is per-link CAD bounding boxes; joint limits/efforts " - "are assumed tendon-drive values documented in lyra_parts/chain.py. " -) - - -def _fmt(value: float) -> str: - text = f"{value:.9f}".rstrip("0").rstrip(".") - if text in ("-0", ""): - return "0" - return text - - -def _vec(values, scale: float = 1.0) -> str: - return " ".join(_fmt(v * scale) for v in values) - - -def _link_element(robot: ET.Element, name: str) -> None: - volume_mm3, com_mm, inertia_mm5, bbox_min_mm, bbox_max_mm = LINK_MASS_PROPS[name] - link = ET.SubElement(robot, "link", {"name": name}) - - inertial = ET.SubElement(link, "inertial") - ET.SubElement(inertial, "origin", {"xyz": _vec(com_mm, MM_TO_M), "rpy": "0 0 0"}) - ET.SubElement(inertial, "mass", {"value": _fmt(volume_mm3 * UNIFORM_DENSITY_KG_PER_MM3)}) - ixx, iyy, izz, ixy, ixz, iyz = (v * INERTIA_MM5_TO_KG_M2 for v in inertia_mm5) - ET.SubElement( - inertial, - "inertia", - { - "ixx": _fmt(ixx), "ixy": _fmt(ixy), "ixz": _fmt(ixz), - "iyy": _fmt(iyy), "iyz": _fmt(iyz), "izz": _fmt(izz), - }, - ) - - visual = ET.SubElement(link, "visual") - ET.SubElement(visual, "origin", {"xyz": "0 0 0", "rpy": "0 0 0"}) - geometry = ET.SubElement(visual, "geometry") - ET.SubElement( - geometry, - "mesh", - {"filename": f"{MESH_DIR}/{name}.3mf", "scale": MESH_SCALE_FROM_MM}, - ) - - bbox_center_mm = [(lo + hi) / 2.0 for lo, hi in zip(bbox_min_mm, bbox_max_mm)] - bbox_size_mm = [hi - lo for lo, hi in zip(bbox_min_mm, bbox_max_mm)] - collision = ET.SubElement(link, "collision") - ET.SubElement(collision, "origin", {"xyz": _vec(bbox_center_mm, MM_TO_M), "rpy": "0 0 0"}) - geometry = ET.SubElement(collision, "geometry") - ET.SubElement(geometry, "box", {"size": _vec(bbox_size_mm, MM_TO_M)}) - - -def _joint_element(robot: ET.Element, joint: dict) -> None: - element = ET.SubElement(robot, "joint", {"name": joint["name"], "type": "revolute"}) - ET.SubElement(element, "parent", {"link": joint["parent"]}) - ET.SubElement(element, "child", {"link": joint["child"]}) - ET.SubElement(element, "origin", {"xyz": _vec(joint["origin_mm"], MM_TO_M), "rpy": "0 0 0"}) - ET.SubElement(element, "axis", {"xyz": _vec(joint["axis"])}) - lower_deg, upper_deg = joint["range_deg"] - ET.SubElement( - element, - "limit", - { - "lower": _fmt(math.radians(lower_deg)), - "upper": _fmt(math.radians(upper_deg)), - "effort": _fmt(joint["effort_nm"]), - "velocity": _fmt(joint["velocity_rad_s"]), - }, - ) - - -def build_urdf() -> ET.Element: - robot = ET.Element("robot", {"name": ROBOT_NAME}) - robot.append(ET.Comment(HEADER_NOTE)) - # Frame-only mounting root (no inertial/visual/collision by intent): - # external consumers attach the hand at the wrist-flange face frame. - ET.SubElement(robot, "link", {"name": "wrist_mount"}) - for name in chain.all_links(): - _link_element(robot, name) - mount_joint = ET.SubElement( - robot, "joint", {"name": "wrist_mount_to_palm", "type": "fixed"} - ) - ET.SubElement(mount_joint, "parent", {"link": "wrist_mount"}) - ET.SubElement(mount_joint, "child", {"link": "palm"}) - ET.SubElement(mount_joint, "origin", {"xyz": "0 0 0", "rpy": "0 0 0"}) - for joint in chain.all_joints(): - _joint_element(robot, joint) - return robot - - -# ------------------------------------------------------------------ SRDF -def _link_origins_zero_mm() -> dict[str, tuple[float, float, float]]: - """Link-frame origins in the palm frame at the all-zero pose. - - At zero joint angles every chain transform is a pure translation, so - origins are cumulative offset sums. - """ - origins = {"palm": (0.0, 0.0, 0.0)} - for joint in chain.all_joints(): - px, py, pz = origins[joint["parent"]] - ox, oy, oz = joint["origin_mm"] - origins[joint["child"]] = (px + ox, py + oy, pz + oz) - return origins - - -def _zero_pose_overlapping_pairs() -> list[tuple[str, str]]: - """Collision-box pairs that already overlap in the zero pose.""" - origins = _link_origins_zero_mm() - boxes = {} - for name in chain.all_links(): - _, _, _, bbox_min_mm, bbox_max_mm = LINK_MASS_PROPS[name] - origin = origins[name] - boxes[name] = ( - [o + lo for o, lo in zip(origin, bbox_min_mm)], - [o + hi for o, hi in zip(origin, bbox_max_mm)], - ) - names = chain.all_links() - pairs = [] - for i, a in enumerate(names): - a_min, a_max = boxes[a] - for b in names[i + 1 :]: - b_min, b_max = boxes[b] - if all(a_min[k] < b_max[k] and a_max[k] > b_min[k] for k in range(3)): - pairs.append((a, b)) - return pairs - - -def _adjacent_pairs() -> list[tuple[str, str]]: - return [(joint["parent"], joint["child"]) for joint in chain.all_joints()] - - -def build_srdf() -> ET.Element: - robot = ET.Element("robot", {"name": ROBOT_NAME}) - robot.append( - ET.Comment( - " MoveIt2 planning semantics for lyra.urdf. Groups: per-digit " - "chains, a fingers union, and the whole-hand joint group. Group " - "states are named hand poses (radians) from lyra_parts/chain.py. " - "Disabled collisions: URDF-adjacent pairs plus zero-pose " - "bounding-box overlaps (reason Default). " - ) - ) - - ET.SubElement( - robot, - "virtual_joint", - { - "name": "world_to_wrist", - "type": "fixed", - "parent_frame": "world", - "child_link": "wrist_mount", - }, - ) - - # Per-digit groups own their flexion joints, so digit links are in the - # group but the palm is not: the hand end effector can then attach at - # the palm (adjacent to, not inside, the group). - digits = list(chain.FINGERS) + ["thumb"] - digit_joints = {finger: chain.finger_joints(finger) for finger in chain.FINGERS} - digit_joints["thumb"] = chain.thumb_joints() - for digit in digits: - group = ET.SubElement(robot, "group", {"name": digit}) - for joint in digit_joints[digit]: - ET.SubElement(group, "joint", {"name": joint["name"]}) - - fingers_group = ET.SubElement(robot, "group", {"name": "fingers"}) - for finger in chain.FINGERS: - ET.SubElement(fingers_group, "group", {"name": finger}) - - hand_group = ET.SubElement(robot, "group", {"name": "hand"}) - for digit in digits: - ET.SubElement(hand_group, "group", {"name": digit}) - - joint_order = [joint["name"] for joint in chain.all_joints()] - for pose_name, pose_deg in chain.named_poses_deg().items(): - state = ET.SubElement(robot, "group_state", {"name": pose_name, "group": "hand"}) - for joint_name in joint_order: - ET.SubElement( - state, - "joint", - {"name": joint_name, "value": _fmt(math.radians(pose_deg[joint_name]))}, - ) - - ET.SubElement( - robot, - "end_effector", - { - "name": "hand_eef", - "parent_link": "palm", - "group": "hand", - }, - ) - - adjacent = _adjacent_pairs() - adjacent_set = {frozenset(pair) for pair in adjacent} - for link1, link2 in adjacent: - ET.SubElement( - robot, - "disable_collisions", - {"link1": link1, "link2": link2, "reason": "Adjacent"}, - ) - for link1, link2 in _zero_pose_overlapping_pairs(): - if frozenset((link1, link2)) in adjacent_set: - continue - ET.SubElement( - robot, - "disable_collisions", - {"link1": link1, "link2": link2, "reason": "Default"}, - ) - return robot diff --git a/models/robots/lyra/lyra_parts/digits.py b/models/robots/lyra/lyra_parts/digits.py deleted file mode 100644 index 41a4e2ce..00000000 --- a/models/robots/lyra/lyra_parts/digits.py +++ /dev/null @@ -1,422 +0,0 @@ -"""lyra digit links: finger phalanges and the thumb column. - -Local frames (see lib.py): -- Finger phalanges: origin at the proximal joint center, +Z distal along - the segment, +Y palmar, flexion axis -X. -- Thumb segments: origin at the proximal joint center, +X distal along the - column, +Z palmar pad direction, flexion axis -Y (the thumb_base yaw - turret tang spins about +Z). - -Each phalanx is a pearl composite shell TUBE over an exposed graphite -structural spine: graphite tang barrel -> center spine bar -> fork -crossbar -> clevis cheeks, one connected solid, with the spine visible at -both joint ends (the "visible precision joints" design language). The -soft-touch tactile strip rides the tube's palmar face. - -Articulation-safety rules (hold for every joint at any angle in range): -- Full-width child geometry (the shell tube) starts beyond parent lobe - radius + 0.4 from the joint axis; only the circular tang barrel and the - narrow center spine (which never shares an x/y band with the parent - cheeks, and stays clear of the pin bore radius) sit closer. -- Fork crossbars sit >= 9 mm proximal of their own joint axis, outside the - +/-43 deg corner sweep of the child spine at full flexion. - -Joint hardware ownership: the parent link owns clevis cheeks, aluminum rim -washers, and the pivot pin; the child owns the bored tang barrel. -""" - -from __future__ import annotations - -from build123d import Axis, Box, Cone, Pos, Rot, Sphere - -from .chain import ( - SEGMENT_MM, - THUMB_METACARPAL_LEN_MM, - THUMB_PROXIMAL_LEN_MM, - THUMB_BASE_LEN_MM, -) -from .lib import ( - ALU_COLOR, - GRAPHITE_COLOR, - PAD_COLOR, - PEARL_COLOR, - part_compound, - pin, - safe_chamfer, - safe_fillet, - styled, - xcyl, - ycyl, - zcyl, -) - -# Per-finger width scale (lengths come from chain.SEGMENT_MM). -FINGER_SCALE = {"index": 1.0, "middle": 1.03, "ring": 0.97, "pinky": 0.88} - -# Knuckle hardware (mm at scale 1). Tang widths match the parent fork gap -# minus 0.6 clearance; bores give the parent-owned pin a 0.4 running fit. -PALM_LOBE_R = 8.0 # palm knuckle lobe radius (palm.py uses this too) -PROX_TANG_R, PROX_TANG_W, PROX_BORE_R = 7.2, 10.4, 3.0 -PROX_FORK_GAP, PROX_CHEEK_T, PROX_LOBE_R = 9.6, 2.6, 6.5 -PROX_PIN_R, PROX_RIM_R = 2.4, 4.4 -MID_TANG_R, MID_TANG_W, MID_BORE_R = 6.4, 9.0, 2.8 -MID_FORK_GAP, MID_CHEEK_T, MID_LOBE_R = 8.4, 2.4, 5.8 -MID_PIN_R, MID_RIM_R = 2.1, 3.8 -DIST_TANG_R, DIST_TANG_W, DIST_BORE_R = 5.6, 7.8, 2.5 - -MCP_PIN_R = 2.6 # palm-owned; phalanx bore must clear it -RIM_T = 1.2 - - -def _bored_xbarrel(r, w, bore_r): - return xcyl(r, w) - xcyl(bore_r, w + 4.0) - - -def _bored_ybarrel(r, w, bore_r): - return ycyl(r, w) - ycyl(bore_r, w + 4.0) - - -def _finger_fork(z, gap, cheek_t, lobe_r, pin_r, rim_r, body_d): - """Distal clevis of a finger phalanx: cheeks + lobes (one solid, bored), - rim washers, and the pivot pin.""" - half = gap / 2.0 - fork = None - for sx in (1.0, -1.0): - xc = sx * (half + cheek_t / 2.0) - plate = Pos(xc, 0.2, z - 6.0) * Box(cheek_t, body_d, 12.0) - lobe = Pos(xc, 0, z) * xcyl(lobe_r, cheek_t) - cheek = plate + lobe - fork = cheek if fork is None else fork + cheek - fork = safe_chamfer( - fork, [e for e in fork.edges().filter_by(Axis.X)], 0.8 - ) - fork -= Pos(0, 0, z) * xcyl(pin_r + 0.4, gap + 2 * cheek_t + 8.0) - rims = [] - for sx in (1.0, -1.0): - xr = sx * (half + cheek_t + RIM_T / 2.0) - rim = Pos(xr, 0, z) * ( - xcyl(rim_r, RIM_T) - xcyl(pin_r + 0.4, RIM_T + 2.0) - ) - rims.append(rim) - pin_len = gap + 2 * cheek_t + 2 * RIM_T + 1.2 - pivot = Pos(0, 0, z) * pin("x", pin_r, pin_len) - return fork, rims, pivot - - -def _finger_phalanx( - label_prefix, - pin_name, - s, - length, - tang_r, - tang_w, - bore_r, - fork_gap, - cheek_t, - lobe_r, - pin_r, - rim_r, - parent_lobe_r, - body_w, - body_d, - y_shift, -): - """Pearl shell tube over an exposed graphite spine, plus fork hardware.""" - tube_z0 = parent_lobe_r + 0.4 - tube_z1 = length - 13.2 * s - bar_w = 6.5 * s - chan_w = bar_w + 0.5 - - # Graphite spine: tang barrel + center bar + fork crossbar + clevis. - core = _bored_xbarrel(tang_r, tang_w, bore_r) - bar = Pos(0, y_shift, (4.5 * s + length - 9.0 * s) / 2.0) * Box( - bar_w, bar_w, length - 9.0 * s - 4.5 * s - ) - core += bar - crossbar = Pos(0, 0.2, length - 11.0 * s) * Box( - fork_gap + 2 * cheek_t, bar_w, 4.0 * s - ) - core += crossbar - fork, rims, pivot = _finger_fork( - length, fork_gap, cheek_t, lobe_r, pin_r, rim_r, body_d - ) - core += fork - - # Pearl composite shell tube around the spine. - tube = Pos(0, y_shift, (tube_z0 + tube_z1) / 2.0) * Box( - body_w, body_d, tube_z1 - tube_z0 - ) - tube = safe_fillet(tube, tube.edges().filter_by(Axis.Z), min(2.6, body_w / 4.5)) - tube -= Pos(0, y_shift, (tube_z0 + tube_z1) / 2.0) * Box( - chan_w, chan_w, tube_z1 - tube_z0 + 4.0 - ) - tube = safe_chamfer( - tube, [e for e in tube.edges().filter_by(Axis.X)], 0.7 - ) - - # Soft-touch tactile strip on the palmar tube face. - pad = Pos(0, y_shift + body_d / 2.0 + 0.5, (tube_z0 + tube_z1) / 2.0) * Box( - body_w - 4.0 * s, 1.6, max((tube_z1 - tube_z0) * 0.72, 2.5) - ) - pad = safe_fillet(pad, pad.edges().filter_by(Axis.Y), min(1.8 * s, 2.0)) - - solids = [ - styled(core, f"{label_prefix}_spine", GRAPHITE_COLOR), - styled(tube, f"{label_prefix}_shell", PEARL_COLOR), - styled(pad, f"{label_prefix}_pad", PAD_COLOR), - styled(pivot, f"{pin_name}_pin", ALU_COLOR), - ] - for i, rim in enumerate(rims): - solids.append(styled(rim, f"{pin_name}_rim_{i}", ALU_COLOR)) - return solids - - -def build_finger_proximal(finger: str): - s = FINGER_SCALE[finger] - solids = _finger_phalanx( - f"{finger}_proximal", f"{finger}_pip", s, SEGMENT_MM[finger][0], - PROX_TANG_R * s, PROX_TANG_W * s, PROX_BORE_R * s, - PROX_FORK_GAP * s, PROX_CHEEK_T * s, PROX_LOBE_R * s, - PROX_PIN_R * s, PROX_RIM_R * s, PALM_LOBE_R * s, - 13.4 * s, 11.4 * s, 0.7 * s, - ) - return part_compound(f"{finger}_proximal", solids) - - -def build_finger_middle(finger: str): - s = FINGER_SCALE[finger] - solids = _finger_phalanx( - f"{finger}_middle", f"{finger}_dip", s, SEGMENT_MM[finger][1], - MID_TANG_R * s, MID_TANG_W * s, MID_BORE_R * s, - MID_FORK_GAP * s, MID_CHEEK_T * s, MID_LOBE_R * s, - MID_PIN_R * s, MID_RIM_R * s, PROX_LOBE_R * s, - 12.2 * s, 10.6 * s, 0.6 * s, - ) - return part_compound(f"{finger}_middle", solids) - - -def build_finger_distal(finger: str): - s = FINGER_SCALE[finger] - length = SEGMENT_MM[finger][2] - tip_r = 4.4 * s - cone_z0 = MID_LOBE_R * s + 0.4 - tip_c = length - tip_r - - # Tang + connection collar (graphite, one solid). The collar is - # center-band only, so it clears the parent pin bore and never shares - # an x band with the parent cheeks. - tang = _bored_xbarrel(DIST_TANG_R * s, DIST_TANG_W * s, DIST_BORE_R * s) - collar_r = 4.0 * s - collar_len = cone_z0 + 0.5 - 4.4 - tang += Pos(0, 0.4 * s, 4.4 + collar_len / 2.0) * zcyl(collar_r, collar_len) - - cone_h = tip_c - cone_z0 - body = Pos(0, 0.4 * s, cone_z0 + cone_h / 2.0) * Cone( - bottom_radius=5.3 * s, top_radius=tip_r, height=cone_h - ) - body += Pos(0, 0.4 * s, tip_c) * Sphere(tip_r) - # Seat the pearl body on the collar shoulder (face contact, no overlap). - body -= Pos(0, 0.4 * s, (cone_z0 + 0.5) / 2.0) * zcyl(collar_r + 0.05, cone_z0 + 0.5) - - # Soft-touch fingertip pad: split a palmar cap off the body with a - # boolean sphere so the seam is face-to-face with no overlap. - # Palmar-only soft cap: the zone center sits palmar of the tip sphere so - # the apex and dorsal face stay pearl. - pad = None - try: - zone = Pos(0, 3.6 * s, tip_c + 0.2) * Sphere(4.7 * s) - cap = body & zone - if cap.volume > 1.0: - body = body - zone - pad = cap - except Exception: - pad = None - - solids = [ - styled(tang, f"{finger}_distal_tang", GRAPHITE_COLOR), - styled(body, f"{finger}_distal_body", PEARL_COLOR), - ] - if pad is not None: - solids.append(styled(pad, f"{finger}_tip_pad", PAD_COLOR)) - return part_compound(f"{finger}_distal", solids) - - -# ----------------------------------------------------------------- thumb - -T_PROX_LOBE_R = 7.0 # thumb_base flex-fork lobe radius (palm relief uses it) -TB_TANG_R, TB_TANG_H, TB_BORE_R = 8.8, 9.5, 3.3 -TM_TANG_R, TM_TANG_W, TM_BORE_R = 6.8, 9.8, 2.9 -TM_FORK_GAP, TM_CHEEK_T, TM_LOBE_R = 9.8, 2.5, 6.4 -TM_PIN_R, TM_RIM_R = 2.3, 4.2 -TP_TANG_R, TP_TANG_W, TP_BORE_R = 6.2, 9.2, 2.7 -TP_FORK_GAP, TP_CHEEK_T, TP_LOBE_R = 8.6, 2.4, 5.6 -TP_PIN_R, TP_RIM_R = 2.1, 3.7 -TD_TANG_R, TD_TANG_W, TD_BORE_R = 5.2, 8.0, 2.5 -THUMB_TIP_R = 4.8 -THUMB_TIP_TOTAL_MM = 24.0 # matches chain.THUMB_TIP_LEN_MM - - -def _thumb_fork(x, gap, cheek_t, lobe_r, pin_r, rim_r, body_d): - """Distal clevis of a thumb segment: cheeks normal to Y.""" - half = gap / 2.0 - fork = None - for sy in (1.0, -1.0): - yc = sy * (half + cheek_t / 2.0) - plate = Pos(x - 6.0, yc, 0.2) * Box(12.0, cheek_t, body_d) - lobe = Pos(x, yc, 0) * ycyl(lobe_r, cheek_t) - cheek = plate + lobe - fork = cheek if fork is None else fork + cheek - fork = safe_chamfer( - fork, [e for e in fork.edges().filter_by(Axis.Y)], 0.8 - ) - fork -= Pos(x, 0, 0) * ycyl(pin_r + 0.4, gap + 2 * cheek_t + 8.0) - rims = [] - for sy in (1.0, -1.0): - yr = sy * (half + cheek_t + RIM_T / 2.0) - rim = Pos(x, yr, 0) * ( - ycyl(rim_r, RIM_T) - ycyl(pin_r + 0.4, RIM_T + 2.0) - ) - rims.append(rim) - pin_len = gap + 2 * cheek_t + 2 * RIM_T + 1.2 - pivot = Pos(x, 0, 0) * pin("y", pin_r, pin_len) - return fork, rims, pivot - - -def _thumb_phalanx( - label_prefix, - pin_name, - length, - tang_r, - tang_w, - bore_r, - fork_gap, - cheek_t, - lobe_r, - pin_r, - rim_r, - parent_lobe_r, - body_w, - body_d, - z_shift, -): - """Thumb segment along +X: pearl tube over graphite spine, Y-axis forks.""" - tube_x0 = parent_lobe_r + 0.4 - tube_x1 = length - 13.2 - bar_w = 6.5 - chan_w = bar_w + 0.5 - - core = _bored_ybarrel(tang_r, tang_w, bore_r) - bar = Pos((4.5 + length - 9.0) / 2.0, 0, z_shift) * Box( - length - 9.0 - 4.5, bar_w, bar_w - ) - core += bar - # Fork cheeks are stacked in Y, so the crossbar spans the Y width. - crossbar = Pos(length - 11.0, 0, 0.2) * Box(4.0, fork_gap + 2 * cheek_t, bar_w) - core += crossbar - fork, rims, pivot = _thumb_fork( - length, fork_gap, cheek_t, lobe_r, pin_r, rim_r, body_d - ) - core += fork - - tube = Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift) * Box( - tube_x1 - tube_x0, body_w, body_d - ) - tube = safe_fillet(tube, tube.edges().filter_by(Axis.X), min(2.6, body_w / 4.5)) - tube -= Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift) * Box( - tube_x1 - tube_x0 + 4.0, chan_w, chan_w - ) - tube = safe_chamfer( - tube, [e for e in tube.edges().filter_by(Axis.Y)], 0.7 - ) - - pad = Pos((tube_x0 + tube_x1) / 2.0, 0, z_shift + body_d / 2.0 + 0.5) * Box( - max((tube_x1 - tube_x0) * 0.72, 2.5), body_w - 4.0, 1.6 - ) - pad = safe_fillet(pad, pad.edges().filter_by(Axis.Z), 1.8) - - solids = [ - styled(core, f"{label_prefix}_spine", GRAPHITE_COLOR), - styled(tube, f"{label_prefix}_shell", PEARL_COLOR), - styled(pad, f"{label_prefix}_pad", PAD_COLOR), - styled(pivot, f"{pin_name}_pin", ALU_COLOR), - ] - for i, rim in enumerate(rims): - solids.append(styled(rim, f"{pin_name}_rim_{i}", ALU_COLOR)) - return solids - - -def build_thumb_base(): - """CMC turret tang (spins about +Z on the palm spindle) + flex clevis.""" - disc = zcyl(TB_TANG_R, TB_TANG_H) - zcyl(TB_BORE_R, TB_TANG_H + 4.0) - disc = safe_chamfer(disc, disc.edges(), 0.8) - neck = Pos(8.6, 0, 0) * Box(8.6, 12.6, 9.0) - neck = safe_fillet(neck, neck.edges().filter_by(Axis.Z), 2.0) - core = disc + neck - fork, rims, pivot = _thumb_fork( - THUMB_BASE_LEN_MM, TM_TANG_W + 0.6, 2.6, T_PROX_LOBE_R, 2.5, 4.5, 11.0 - ) - core += fork - solids = [ - styled(core, "thumb_base_link", GRAPHITE_COLOR), - styled(pivot, "thumb_cmc_flex_pin", ALU_COLOR), - ] - for i, rim in enumerate(rims): - solids.append(styled(rim, f"thumb_cmc_flex_rim_{i}", ALU_COLOR)) - return part_compound("thumb_base", solids) - - -def build_thumb_metacarpal(): - solids = _thumb_phalanx( - "thumb_metacarpal", "thumb_mp", THUMB_METACARPAL_LEN_MM, - TM_TANG_R, TM_TANG_W, TM_BORE_R, - TM_FORK_GAP, TM_CHEEK_T, TM_LOBE_R, TM_PIN_R, TM_RIM_R, - T_PROX_LOBE_R, 12.6, 13.0, 0.5, - ) - return part_compound("thumb_metacarpal", solids) - - -def build_thumb_proximal(): - solids = _thumb_phalanx( - "thumb_proximal", "thumb_ip", THUMB_PROXIMAL_LEN_MM, - TP_TANG_R, TP_TANG_W, TP_BORE_R, - TP_FORK_GAP, TP_CHEEK_T, TP_LOBE_R, TP_PIN_R, TP_RIM_R, - TM_LOBE_R, 11.6, 11.8, 0.4, - ) - return part_compound("thumb_proximal", solids) - - -def build_thumb_distal(): - length = THUMB_TIP_TOTAL_MM - tip_r = THUMB_TIP_R - cone_x0 = TP_LOBE_R + 0.4 - tip_c = length - tip_r - - tang = _bored_ybarrel(TD_TANG_R, TD_TANG_W, TD_BORE_R) - collar_r = 4.0 - collar_len = cone_x0 + 0.5 - 4.4 - tang += Pos(4.4 + collar_len / 2.0, 0, 0.4) * xcyl(collar_r, collar_len) - - cone_h = tip_c - cone_x0 - body = Pos(cone_x0 + cone_h / 2.0, 0, 0.4) * Rot(0, 90, 0) * Cone( - bottom_radius=5.4, top_radius=tip_r, height=cone_h - ) - body += Pos(tip_c, 0, 0.4) * Sphere(tip_r) - body -= Pos((cone_x0 + 0.5) / 2.0, 0, 0.4) * xcyl(collar_r + 0.05, cone_x0 + 0.5) - - pad = None - try: - zone = Pos(tip_c + 0.2, 0, 3.4) * Sphere(5.0) - cap = body & zone - if cap.volume > 1.0: - body = body - zone - pad = cap - except Exception: - pad = None - - solids = [ - styled(tang, "thumb_distal_tang", GRAPHITE_COLOR), - styled(body, "thumb_distal_body", PEARL_COLOR), - ] - if pad is not None: - solids.append(styled(pad, "thumb_tip_pad", PAD_COLOR)) - return part_compound("thumb_distal", solids) diff --git a/models/robots/lyra/lyra_parts/lib.py b/models/robots/lyra/lyra_parts/lib.py deleted file mode 100644 index 172a7b82..00000000 --- a/models/robots/lyra/lyra_parts/lib.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Shared helpers for the lyra dexterous-hand part modules. - -Conventions (all part modules must follow): -- Units mm. RIGHT hand: origin at the wrist-flange mount face center, - +Z distal (fingers up), +Y palmar (palm faces +Y), +X radial (thumb side). -- Every part builder returns an identity-location build123d Compound whose - children are closed, labeled, colored solids, modeled in the part's local - frame (origin at the part's proximal joint center). -- Finger phalanges: segment extends +Z local, flexion axis -X local. -- Thumb segments: segment extends +X local, palmar pad faces +Z local, - flexion axis -Y local. -- Joint hardware: the PARENT link owns the clevis cheeks and the aluminum - pivot pin; the child owns the tang barrel. Everything touching a joint - axis is a surface of revolution about that axis, so articulation never - opens a visual gap. -""" - -from __future__ import annotations - -from build123d import ( - Axis, - Box, - Color, - Compound, - Cylinder, - Location, - Plane, - Pos, - Rot, - Vector, - chamfer, - fillet, -) - -# Palette: warm pearl composite shells over a graphite structural core, -# machined-aluminum joint hardware, near-black soft-touch tactile pads, and -# a restrained amber accent on small functional details. No logos. -PEARL = (0.91, 0.905, 0.89) # warm-white composite shells -GRAPHITE = (0.15, 0.16, 0.18) # structural cores / phalanx links -ALU = (0.87, 0.89, 0.93) # machined-aluminum rims, pins, flange -PAD = (0.10, 0.10, 0.11) # soft-touch tactile pad rubber -AMBER = (0.83, 0.50, 0.16) # amber accent (sensor ring, tensioners) -SENSOR = (0.03, 0.06, 0.12) # gloss midnight sensor glass - -PEARL_COLOR = Color(*PEARL) -GRAPHITE_COLOR = Color(*GRAPHITE) -ALU_COLOR = Color(*ALU) -PAD_COLOR = Color(*PAD) -AMBER_COLOR = Color(*AMBER) -SENSOR_COLOR = Color(*SENSOR) - - -def styled(solid, label: str, color: Color): - solid.label = label - solid.color = color - return solid - - -def part_compound(label: str, children) -> Compound: - comp = Compound(children=list(children)) - comp.label = label - return comp - - -def safe_chamfer(part, edges, length, length2=None): - try: - edges = list(edges) - if not edges: - return part - if length2 is not None: - return chamfer(edges, length, length2) - return chamfer(edges, length) - except Exception: - return part - - -def safe_fillet(part, edges, radius): - try: - edges = list(edges) - if not edges: - return part - return fillet(edges, radius) - except Exception: - return part - - -def xcyl(r, h): - """Cylinder along +X centered at the origin.""" - return Rot(0, 90, 0) * Cylinder(radius=r, height=h) - - -def ycyl(r, h): - """Cylinder along +Y centered at the origin.""" - return Rot(-90, 0, 0) * Cylinder(radius=r, height=h) - - -def zcyl(r, h): - """Cylinder along +Z centered at the origin.""" - return Cylinder(radius=r, height=h) - - -AXIS_CYL = {"x": xcyl, "y": ycyl, "z": zcyl} - - -def pin(axis: str, r: float, h: float): - """Chamfered aluminum pivot pin along a principal axis.""" - p = AXIS_CYL[axis](r, h) - return safe_chamfer(p, p.edges(), 0.6) - - -def joint_plane(origin, axis_dir, x_ref) -> Location: - return Plane( - origin=Vector(origin), x_dir=Vector(x_ref), z_dir=Vector(axis_dir) - ).location - - -def revolute_attach( - asm, - parent, - child, - name: str, - p_origin, - p_axis, - p_xref, - c_origin, - c_axis, - c_xref, - angle_deg: float, -): - """Author a parent revolute frame + child rigid frame and connect them. - - Joint origins/axes are given in each part's LOCAL modeling frame; the - helper transforms them by each part's current location, so parts that - were already repositioned by upstream connects keep correct joints - (connect in root-to-leaf order). At angle 0 the child's c_xref direction - aligns with the parent's p_xref direction about the shared axis. - - (Same verified build123d-0.10 joint-attach math as the juno humanoid.) - """ - j_p_world = parent.location * joint_plane(p_origin, p_axis, p_xref) - z_tip = (j_p_world * Pos(0, 0, 1)).position - axis_world = Axis(j_p_world.position, z_tip - j_p_world.position) - f_parent = asm.revolute_frame( - parent, f"{name}_axis", axis_world, angular_range=(-360.0, 360.0) - ) - # The joint's stored frame may use any x-reference convention; read the - # actual frame back and fold the constant Rz difference into the child - # mount so the child's c_xref aligns with p_xref at angle 0. - joint_obj = parent.joints[f"{name}_axis"] - a_eff_world = parent.location * joint_obj.relative_axis.location - delta = j_p_world.inverse() * a_eff_world # pure Rz about the joint axis - j_c = joint_plane(c_origin, c_axis, c_xref) - l_c_world = child.location * (j_c * delta) - f_child = asm.rigid_frame(child, f"{name}_mount", l_c_world) - asm.revolute(f_parent, f_child, angle=angle_deg, label=name) diff --git a/models/robots/lyra/lyra_parts/mass_props.py b/models/robots/lyra/lyra_parts/mass_props.py deleted file mode 100644 index 7b68d94e..00000000 --- a/models/robots/lyra/lyra_parts/mass_props.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Baked CAD mass properties and bounding boxes for lyra links. - -Generated from the lyra_parts builders with OCP BRepGProp volume -properties at unit density in each part's local frame (== URDF link -frame); MatrixOfInertia is reported about the center of mass. Units: -mm^3 volume, mm COM/bbox, mm^5 unit-density inertia. Regenerate with a -one-off BRepGProp pass over the builders if part geometry changes. -""" - -from __future__ import annotations - -TOTAL_VOLUME_MM3 = 183433.2 - -# link: (volume_mm3, com_mm, inertia_com_mm5 (ixx, iyy, izz, ixy, ixz, iyz), -# bbox_min_mm, bbox_max_mm) -LINK_MASS_PROPS = { - "palm": ( - 123973.6, - (0.993, -0.096, 53.213), - (98442492.1, 132133998.1, 45650708.9, -180950.0, -4422274.8, 267642.8), - (-37.800, -23.000, 0.000), - (42.200, 23.000, 111.240), - ), - "index_proximal": ( - 6607.3, - (0.000, 0.620, 20.936), - (1583385.9, 1622711.3, 195769.4, -0.0, -0.0, 2966.7), - (-9.200, -7.200, -7.200), - (9.200, 7.700, 50.500), - ), - "index_middle": ( - 3115.9, - (-0.000, 0.306, 12.322), - (358262.5, 380099.2, 79965.1, -0.0, -0.0, 1146.0), - (-8.400, -6.400, -6.400), - (8.400, 7.200, 31.800), - ), - "index_distal": ( - 1761.6, - (-0.000, 0.268, 8.476), - (110817.2, 107993.2, 21753.1, 0.0, 0.0, -2040.6), - (-5.300, -5.600, -5.600), - (5.300, 5.700, 23.000), - ), - "middle_proximal": ( - 7645.0, - (0.000, 0.662, 22.857), - (2101192.9, 2147398.6, 238274.3, 0.0, 0.0, 3717.1), - (-9.422, -7.416, -7.416), - (9.422, 7.892, 54.695), - ), - "middle_middle": ( - 3697.0, - (-0.000, 0.371, 13.725), - (489757.6, 515013.3, 99289.4, -0.0, 0.0, 1847.4), - (-8.598, -6.592, -6.592), - (8.598, 7.377, 34.974), - ), - "middle_distal": ( - 1989.4, - (-0.000, 0.281, 9.129), - (139234.0, 135954.3, 26039.1, -0.0, -0.0, -2463.6), - (-5.459, -5.768, -5.768), - (5.459, 5.871, 24.500), - ), - "ring_proximal": ( - 6221.7, - (0.000, 0.624, 20.993), - (1463887.2, 1497956.0, 173277.2, 0.0, 0.0, 2780.8), - (-8.978, -6.984, -6.984), - (8.978, 7.508, 50.305), - ), - "ring_middle": ( - 3059.6, - (-0.000, 0.350, 12.820), - (355245.3, 374190.6, 73452.1, -0.0, 0.0, 1337.2), - (-8.202, -6.208, -6.208), - (8.202, 7.023, 32.626), - ), - "ring_distal": ( - 1689.9, - (0.000, 0.265, 8.812), - (107760.6, 105339.2, 19599.9, 0.0, 0.0, -1880.2), - (-5.141, -5.432, -5.432), - (5.141, 5.529, 23.500), - ), - "pinky_proximal": ( - 4097.4, - (0.000, 0.519, 16.725), - (666458.6, 687445.5, 96382.6, -0.0, 0.0, 1444.0), - (-8.312, -6.336, -6.336), - (8.312, 6.932, 40.720), - ), - "pinky_middle": ( - 1978.8, - (0.000, 0.233, 10.001), - (160299.1, 172443.0, 40658.8, 0.0, -0.0, 412.6), - (-7.608, -5.632, -5.632), - (7.608, 6.492, 26.104), - ), - "pinky_distal": ( - 1211.2, - (0.000, 0.233, 7.600), - (60213.8, 58744.3, 11570.5, -0.0, 0.0, -1111.7), - (-4.664, -4.928, -4.928), - (4.664, 5.016, 20.500), - ), - "thumb_base": ( - 3735.7, - (5.759, 0.000, 0.024), - (133267.4, 225133.8, 298293.0, -0.0, 80.7, 0.0), - (-8.800, -9.600, -7.000), - (20.000, 9.600, 7.000), - ), - "thumb_metacarpal": ( - 7046.8, - (22.244, -0.000, 0.572), - (214637.6, 1669731.3, 1691338.0, -0.0, 4697.0, -0.0), - (-6.800, -9.200, -6.800), - (52.400, 9.200, 8.300), - ), - "thumb_proximal": ( - 3711.2, - (14.338, -0.000, 0.347), - (97250.4, 480703.7, 500682.0, -0.0, 1249.7, -0.0), - (-6.200, -8.500, -6.200), - (35.600, 8.500, 7.600), - ), - "thumb_distal": ( - 1891.1, - (9.782, 0.000, 0.294), - (23945.1, 120446.4, 118749.4, 0.0, -2004.1, 0.0), - (-5.200, -5.400, -5.200), - (24.000, 5.400, 5.800), - ), -} diff --git a/models/robots/lyra/lyra_parts/palm.py b/models/robots/lyra/lyra_parts/palm.py deleted file mode 100644 index 834516ee..00000000 --- a/models/robots/lyra/lyra_parts/palm.py +++ /dev/null @@ -1,301 +0,0 @@ -"""lyra palm: wrist flange, carpal core, palm slab, knuckle row, thumb turret. - -Local frame = robot root frame: origin at the wrist-flange mount face -center, +Z distal, +Y palmar, +X radial. The palm owns all MCP clevis -cheeks, knuckle pins, the thumb CMC turret pedestal/spindle, and the -tactile palm pad. - -Articulation clearances baked into the shape: -- Knuckle slots are open from y -14 to +15 so proximal phalanges sweep - -15..+95 deg without touching the covers. -- The palmar plate's top edge stays >= 8 mm proximal of each MCP axis, - outside the swept envelope of the flexing proximal shaft. -- A 15 mm radius relief is turned around the thumb turret so the yawing - thumb base clevis never clips the radial palm corner. -""" - -from __future__ import annotations - -from build123d import ( - Axis, - Box, - Plane, - Polyline, - Pos, - Rot, - RectangleRounded, - extrude, - loft, - make_face, -) - -from .chain import FINGERS, MCP_ORIGIN_MM, THUMB_CMC_ORIGIN_MM -from .digits import ( - FINGER_SCALE, - MCP_PIN_R, - PALM_LOBE_R, - PROX_TANG_W, - TB_TANG_R, - TB_TANG_H, -) -from .lib import ( - ALU_COLOR, - AMBER_COLOR, - GRAPHITE_COLOR, - PAD_COLOR, - PEARL_COLOR, - SENSOR_COLOR, - part_compound, - pin, - safe_chamfer, - safe_fillet, - styled, - xcyl, - ycyl, - zcyl, -) - -SLAB_HALF_Y = 9.5 # graphite core half-depth -COVER_Y0, COVER_Y1 = -12.1, -9.5 # pearl dorsal cover -PLATE_Y0, PLATE_Y1 = 9.5, 11.5 # pearl palmar plate -MCP_PIN_LEN = 17.0 - -# Thumb turret (axis +Z through chain.THUMB_CMC_ORIGIN_MM). -TURRET_R = 10.5 -TURRET_RELIEF_R = 15.0 -SPINDLE_R = 2.7 - -CX, CY, CZ = THUMB_CMC_ORIGIN_MM - -# Palm silhouette in the XZ plane (clockwise; +X radial, +Z distal). The -# knuckle arch follows the MCP line (index 99 / middle 103 / ring 99 / -# pinky 90); lobes added per knuckle crown above it. -CORE_PTS = [ - (-25.0, 33.0), (-33.0, 50.0), (-36.5, 72.0), (-36.5, 87.0), - (-30.0, 90.0), (-19.0, 95.0), (-9.5, 99.0), (0.0, 101.0), - (9.5, 103.0), (19.0, 101.0), (28.5, 99.0), (34.0, 97.5), - (37.5, 95.5), (37.5, 70.0), (30.0, 46.0), (25.0, 33.0), -] -COVER_PTS = [ - (-23.5, 35.0), (-31.3, 51.0), (-34.8, 72.0), (-34.8, 85.6), - (-29.0, 88.4), (-18.6, 93.0), (-9.5, 97.0), (0.0, 99.0), - (9.5, 101.0), (18.6, 99.0), (28.5, 97.0), (33.0, 95.6), - (35.8, 93.8), (35.8, 71.0), (28.6, 47.5), (23.4, 35.0), -] -PLATE_PTS = [ - (-23.5, 35.0), (-31.0, 51.0), (-34.5, 72.0), (-34.5, 80.0), - (-28.6, 82.5), (-18.6, 86.0), (-9.5, 91.0), (0.0, 93.0), - (9.5, 95.0), (19.0, 93.0), (28.5, 91.0), (35.5, 87.5), - (35.5, 71.0), (28.6, 47.5), (23.4, 35.0), -] - - -def _outline_solid(pts, y0, depth): - pts3 = [(x, y0, z) for x, z in pts] - face = make_face(Polyline(*pts3, close=True)) - return extrude(face, amount=depth, dir=(0, 1, 0)) - - -def _slot_gap(finger: str) -> float: - return PROX_TANG_W * FINGER_SCALE[finger] + 0.7 - - -def _knuckle_cuts(solid): - """Open the four MCP clevis slots and pin bores in a palm solid.""" - for finger in FINGERS: - kx, _, kz = MCP_ORIGIN_MM[finger] - gap = _slot_gap(finger) - solid -= Pos(kx, 0.5, kz + 2.0) * Box(gap, 29.0, 26.0) - solid -= Pos(kx, 0, kz) * xcyl(MCP_PIN_R + 0.4, 26.0) - return solid - - -def _thumb_relief(solid): - """Turned clearance around the thumb turret for the yawing base clevis.""" - disc_z0 = CZ - TB_TANG_H / 2.0 - return solid - Pos(CX, CY, disc_z0 + 5.6) * zcyl(TURRET_RELIEF_R, 11.6) - - -def _wrist_flange(): - flange = Pos(0, 0, 3.0) * zcyl(23.0, 6.0) - flange = safe_chamfer(flange, flange.edges(), 1.2) - heads = [] - for i in range(6): - from math import cos, pi, sin - - a = 2.0 * pi * i / 6.0 - bx, by = 16.5 * cos(a), 16.5 * sin(a) - flange -= Pos(bx, by, 1.5) * zcyl(3.6, 3.0) - flange -= Pos(bx, by, 3.0) * zcyl(2.3, 8.0) - head = Pos(bx, by, 1.4) * zcyl(3.3, 2.2) - head = safe_chamfer(head, head.edges(), 0.5) - heads.append(styled(head, f"flange_bolt_{i}", GRAPHITE_COLOR)) - return styled(flange, "wrist_flange", ALU_COLOR), heads - - -def _carpal_core(): - try: - f1 = Plane.XY.offset(6.0) * RectangleRounded(40.0, 17.5, 7.0) - f2 = Plane.XY.offset(33.0) * RectangleRounded(50.0, 19.0, 8.0) - carpal = loft([f1, f2]) - except Exception: - carpal = Pos(0, 0, 19.5) * Box(46.0, 18.2, 27.0) - carpal = safe_fillet(carpal, carpal.edges().filter_by(Axis.Z), 7.0) - return styled(carpal, "carpal_core", GRAPHITE_COLOR) - - -def _palm_core(): - core = _outline_solid(CORE_PTS, -SLAB_HALF_Y, 2 * SLAB_HALF_Y) - core = safe_fillet(core, core.edges().filter_by(Axis.Y), 2.2) - - # Knuckle lobe crowns (cut by the clevis slots into cheek arcs). - for finger in FINGERS: - kx, _, kz = MCP_ORIGIN_MM[finger] - s = FINGER_SCALE[finger] - core += Pos(kx, 0, kz) * xcyl(PALM_LOBE_R * s, _slot_gap(finger) + 7.0) - - # Thumb turret pedestal (bored for the spindle), merged into the - # radial-palmar corner. - disc_z0 = CZ - TB_TANG_H / 2.0 - pedestal = Pos(CX, CY, disc_z0 - 3.4) * (zcyl(TURRET_R, 6.4) - zcyl(SPINDLE_R + 0.7, 9.0)) - pedestal = safe_chamfer(pedestal, pedestal.edges(), 1.0) - core += pedestal - - core = _knuckle_cuts(core) - core = _thumb_relief(core) - return styled(core, "palm_core", GRAPHITE_COLOR) - - -def _dorsal_cover(): - cover = _outline_solid(COVER_PTS, COVER_Y0, COVER_Y1 - COVER_Y0) - cover = safe_fillet(cover, cover.edges().filter_by(Axis.Y), 2.0) - try: - outer = cover.faces().sort_by(Axis.Y)[0] - cover = safe_chamfer(cover, outer.edges(), 1.0) - except Exception: - pass - # Engraved tendon-channel lines running from the carpal exit to each - # knuckle slot (clean covered cable routing, shown as shadow grooves). - from math import atan2, degrees, hypot - - for finger in FINGERS: - kx, _, kz = MCP_ORIGIN_MM[finger] - ax, az = kx * 0.42, 40.0 - bx, bz = kx, kz - 12.0 - mx, mz = (ax + bx) / 2.0, (az + bz) / 2.0 - ang = degrees(atan2(bx - ax, bz - az)) - glen = hypot(bx - ax, bz - az) - groove = Pos(mx, COVER_Y0 + 0.25, mz) * Rot(0, ang, 0) * Box(1.6, 1.0, glen) - try: - cover -= groove - except Exception: - pass - cover = _knuckle_cuts(cover) - cover = _thumb_relief(cover) - return styled(cover, "dorsal_cover", PEARL_COLOR) - - -def _palmar_plate(): - plate = _outline_solid(PLATE_PTS, PLATE_Y0, PLATE_Y1 - PLATE_Y0) - plate = safe_fillet(plate, plate.edges().filter_by(Axis.Y), 2.0) - try: - outer = plate.faces().sort_by(Axis.Y)[-1] - plate = safe_chamfer(plate, outer.edges(), 0.8) - except Exception: - pass - # Pocket for the tactile pad (pad sits 0.4 into the plate). - plate -= Pos(0, PLATE_Y1, 65.0) * Box(46.4, 2.0, 30.4) - plate = _knuckle_cuts(plate) - plate = _thumb_relief(plate) - return styled(plate, "palmar_plate", PEARL_COLOR) - - -def _palm_pad(): - pad = Pos(0, PLATE_Y1 - 0.4 + 0.85, 65.0) * Box(46.0, 1.7, 30.0) - pad = safe_fillet(pad, pad.edges().filter_by(Axis.Y), 5.0) - # Tactile zone split grooves (2 x 2 sensor array read as fine lines). - for cut in ( - Pos(0, PLATE_Y1 + 1.3, 65.0) * Box(1.2, 1.0, 30.5), - Pos(0, PLATE_Y1 + 1.3, 65.0) * Box(46.5, 1.0, 1.2), - ): - try: - pad -= cut - except Exception: - pass - pad = _thumb_relief(pad) - return styled(pad, "palm_pad", PAD_COLOR) - - -def _palm_sensor(): - ring = Pos(0, PLATE_Y1 + 0.3, 87.0) * (ycyl(5.0, 0.6) - ycyl(4.0, 3.0)) - lens = Pos(0, PLATE_Y1 + 0.2, 87.0) * ycyl(3.9, 0.8) - return [ - styled(ring, "palm_sensor_ring", AMBER_COLOR), - styled(lens, "palm_sensor_lens", SENSOR_COLOR), - ] - - -def _tensioners(): - """Tendon tensioner dial row on the dorsal carpal face.""" - out = [] - for i, dx in enumerate((-12.0, -6.0, 0.0, 6.0, 12.0)): - face_y = -(8.75 + 0.75 * (20.0 - 6.0) / 27.0) - dial = Pos(dx, face_y - 1.1, 20.0) * ycyl(2.6, 2.4) - dial = safe_chamfer(dial, dial.edges(), 0.5) - out.append(styled(dial, f"tensioner_dial_{i}", ALU_COLOR)) - dot = Pos(dx, face_y - 2.5, 20.0) * ycyl(1.0, 0.7) - out.append(styled(dot, f"tensioner_dot_{i}", AMBER_COLOR)) - return out - - -def _knuckle_hardware(): - """Palm-owned MCP pins plus rim washers on the outer index/pinky cheeks.""" - out = [] - for finger in FINGERS: - kx, _, kz = MCP_ORIGIN_MM[finger] - out.append( - styled( - Pos(kx, 0, kz) * pin("x", MCP_PIN_R, MCP_PIN_LEN), - f"{finger}_mcp_pin", - ALU_COLOR, - ) - ) - for finger, xr in (("index", 38.2), ("pinky", -37.2)): - kx, _, kz = MCP_ORIGIN_MM[finger] - rim = Pos(xr, 0, kz) * (xcyl(4.6, 1.2) - xcyl(MCP_PIN_R + 0.4, 3.0)) - out.append(styled(rim, f"{finger}_mcp_rim", ALU_COLOR)) - return out - - -def _turret_hardware(): - """Bearing collar + spindle/cap for the thumb CMC turret (palm side).""" - disc_z0 = CZ - TB_TANG_H / 2.0 - disc_z1 = CZ + TB_TANG_H / 2.0 - collar = Pos(CX, CY, disc_z0 - 0.95) * (zcyl(TURRET_R + 0.7, 1.5) - zcyl(TB_TANG_R + 0.8, 3.0)) - collar = safe_chamfer(collar, collar.edges(), 0.4) - spindle = Pos(CX, CY, (disc_z0 - 5.0 + disc_z1 + 0.4) / 2.0) * zcyl( - SPINDLE_R, disc_z1 + 0.4 - (disc_z0 - 5.0) - ) - cap = Pos(CX, CY, disc_z1 + 0.4 + 0.8) * zcyl(4.8, 1.6) - cap = safe_chamfer(cap, cap.edges(), 0.5) - spindle += cap - return [ - styled(collar, "thumb_turret_collar", ALU_COLOR), - styled(spindle, "thumb_turret_spindle", ALU_COLOR), - ] - - -def build_palm(): - flange, bolt_heads = _wrist_flange() - solids = [flange] - solids += bolt_heads - solids.append(_carpal_core()) - solids.append(_palm_core()) - solids.append(_dorsal_cover()) - solids.append(_palmar_plate()) - solids.append(_palm_pad()) - solids += _palm_sensor() - solids += _tensioners() - solids += _knuckle_hardware() - solids += _turret_hardware() - return part_compound("palm", solids) diff --git a/models/robots/openarm/meshes/arm/v10/collision/link0_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link0_symp.stl deleted file mode 100644 index 303161fd..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link0_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:baf52578e1d9e6225f3818cae82b6074a0b948d3cef8e9a3e6dfafca78507590 -size 40284 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link1_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link1_symp.stl deleted file mode 100644 index f4cb78f2..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link1_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:066113d13d5cc85098609003bc7ebb73c570015350877f5ed7162ef1b6601852 -size 17784 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link2_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link2_symp.stl deleted file mode 100644 index 85d0cde7..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link2_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:382ab32e4ae0880e8a1512e7a6ca6ce1f478a6c125db4efa977429ffb1d6b02a -size 13384 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link3_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link3_symp.stl deleted file mode 100644 index 48d72950..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link3_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00c908cefab152c00416a570a48bf9aafed1549085f19ff2d882dc3f355d9f59 -size 156984 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link4_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link4_symp.stl deleted file mode 100644 index cb96fbfc..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link4_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b54883b8c7c96268a68a5879f95998a53ad0b0c4fe74325fad63a6caef669c73 -size 1139984 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link5_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link5_symp.stl deleted file mode 100644 index d165f462..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link5_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:678a2802906eff7b45a836d2f34a2d8e51def50b6599376968f888e05c72739e -size 751484 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link6_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link6_symp.stl deleted file mode 100644 index 16e23366..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link6_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:95529bec23733476dfdbbb266c7db0d25a473a568de73c8337a82440fe4a9ac3 -size 30084 diff --git a/models/robots/openarm/meshes/arm/v10/collision/link7_symp.stl b/models/robots/openarm/meshes/arm/v10/collision/link7_symp.stl deleted file mode 100644 index 96f68d28..00000000 --- a/models/robots/openarm/meshes/arm/v10/collision/link7_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:434f207f21f75f5f0bd604e390b8e5bc7b62b619265222846770e06b3f9b5cfb -size 23884 diff --git a/models/robots/openarm/meshes/body/v10/collision/body_link0_symp.stl b/models/robots/openarm/meshes/body/v10/collision/body_link0_symp.stl deleted file mode 100644 index dad87333..00000000 --- a/models/robots/openarm/meshes/body/v10/collision/body_link0_symp.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c18bbf7e86b03e3faf802e61e8eb438b38dcbcf146d97cffe6e808c65e9a72a -size 293284 diff --git a/models/robots/openarm/meshes/ee/openarm_hand/collision/finger.stl b/models/robots/openarm/meshes/ee/openarm_hand/collision/finger.stl deleted file mode 100644 index ad504060..00000000 --- a/models/robots/openarm/meshes/ee/openarm_hand/collision/finger.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e96e1314618cf434908f70df78f68dd2b049c03538964e8d41fc99abe41564d -size 13284 diff --git a/models/robots/openarm/meshes/ee/openarm_hand/collision/hand.stl b/models/robots/openarm/meshes/ee/openarm_hand/collision/hand.stl deleted file mode 100644 index 381740b7..00000000 --- a/models/robots/openarm/meshes/ee/openarm_hand/collision/hand.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e5d373ebbd3fd001b506058644062ad71a68f1ced5ca5d5ed0f6de20137956b -size 18284 diff --git a/models/robots/openarm/openarm-bimanual.srdf b/models/robots/openarm/openarm-bimanual.srdf deleted file mode 100644 index 130e0e89..00000000 --- a/models/robots/openarm/openarm-bimanual.srdf +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/openarm/openarm-bimanual.urdf b/models/robots/openarm/openarm-bimanual.urdf deleted file mode 100644 index 86eee1fd..00000000 --- a/models/robots/openarm/openarm-bimanual.urdf +++ /dev/null @@ -1,649 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/models/robots/qdd_actuator/.qdd_actuator.step.glb b/models/robots/qdd_actuator/.qdd_actuator.step.glb deleted file mode 100644 index 2bafb97e..00000000 --- a/models/robots/qdd_actuator/.qdd_actuator.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b76dab2c1193026f65adf708a29963fb68665871a16be9f55fd3b2450ed52938 -size 10321136 diff --git a/models/robots/qdd_actuator/.qdd_actuator.step.js b/models/robots/qdd_actuator/.qdd_actuator.step.js deleted file mode 100644 index 90e8db22..00000000 --- a/models/robots/qdd_actuator/.qdd_actuator.step.js +++ /dev/null @@ -1,345 +0,0 @@ -const Z_AXIS = [0, 0, 1]; -const ORIGIN = [0, 0, 0]; - -// Gear-stage constants mirrored from qdd_actuator.py (module 1.0, fixed ring). -const SUN_TEETH = 24; -const PLANET_TEETH = 30; -const RING_TEETH = 84; -// Fixed-ring planetary reduction: carrier = sun / (1 + Nr/Ns) = sun / 4.5. -const REDUCTION_RATIO = 1 + (RING_TEETH / SUN_TEETH); -const PLANET_CENTER_R = 27; -const PLANET_ANGLES_DEG = [90, 210, 330]; - -// 4.5 sun revolutions per cycle returns the carrier to exactly 360 deg. The -// rotor/sun end the cycle 180 deg from start, which is invisible: every -// rotating part is 180-deg symmetric (28 alternating magnets = 14 pole pairs, -// 24/30-tooth gears, 6x lightening + 4x coupling holes), so the loop is -// seamless at tooth/pole phase. -const DRIVE_CYCLE_DEG = REDUCTION_RATIO * 360; - -// Rotor-bearing ball cage runs near half the inner-race speed; 4/9 (~0.444 vs -// the physical ~0.452 for this geometry) makes the 10-ball ring land exactly -// on its 36-deg symmetry at the end of the drive cycle. -const BALL_CAGE_RATIO = 4 / 9; -// Cross-roller cage: outer race fixed, inner race at carrier speed -> cage at -// half carrier speed. 16 alternating-tilt rollers repeat every 45 deg, and -// (360 / 2) = 180 deg per cycle closes on that symmetry. -const ROLLER_CAGE_RATIO = 0.5; - -// Documented static exploded layout from qdd_actuator.py EXPLODE_OFFSETS -// (mm along Z at explode = 1), verified pairwise non-overlapping there. -const EXPLODE_OFFSETS_MM = { - connectorPower: -82, - connectorSignal: -82, - rearCover: -64, - driverPcb: -46, - encoderPcb: -32, - housing: 0, - encoderMagnetRing: 70, - rearBearing: 78, - stator: 94, - rotor: 128, - frontBearing: 154, - ringGear: 168, - sunGear: 196, - planetGear: 222, - carrier: 250, - crossRollerBearing: 280, - frontRetainer: 298, - retainerScrews: 312, - torqueSensor: 330, - outputFlange: 348, - cableTube: 360 -}; -const PLANET_EXPLODE_RADIAL_MM = 14; - -// The viewer floor sits at the assembled model's lowest geometry (the rear -// connectors). Rebase the explosion so the lowest station is 0: every part -// explodes upward or stays put, relative spacing is unchanged, and nothing -// sinks through the floor. The rear connectors keep their assembled height. -const FLOOR_LIFT_MM = -Math.min(...Object.values(EXPLODE_OFFSETS_MM)); - -// "Keep gear mesh" station: ring, sun, and planets explode together as one -// meshed cluster so the tooth engagement stays watchable; the carrier still -// lifts to its own documented station so its plate does not cover the mesh. -// The cluster spans z 6.8..20.5 mm; 210 keeps >60 mm clearance to the front -// bearing station (154) below and ~28 mm to the carrier station (250) above. -const GEAR_STAGE_CLUSTER_MM = 210; - -const PLANETS = [ - { id: "planet1", angleDeg: PLANET_ANGLES_DEG[0] }, - { id: "planet2", angleDeg: PLANET_ANGLES_DEG[1] }, - { id: "planet3", angleDeg: PLANET_ANGLES_DEG[2] } -]; - -const SHELL_PARTS = [ - "housing", - "frontRetainer", - "retainerScrews", - "rearCover", - "connectorPower", - "connectorSignal" -]; -const ELECTRONICS_PARTS = ["driverPcb", "encoderPcb"]; -const OUTPUT_STACK_PARTS = [ - "xrollerInner", - "xrollerOuter", - "xrollerRollers", - "torqueSensor", - "outputFlange" -]; -const DRIVE_TRAIN_PARTS = ["sunGear", "planet1", "planet2", "planet3", "carrier"]; - -function finite(value, fallback = 0) { - const numericValue = Number(value); - return Number.isFinite(numericValue) ? numericValue : fallback; -} - -function clamp(value, min, max) { - return Math.min(Math.max(finite(value, min), min), max); -} - -function clamp01(value) { - return clamp(value, 0, 1); -} - -function wrap01(cycle) { - return ((finite(cycle, 0) % 1) + 1) % 1; -} - -function planetCenter(angleDeg) { - const angleRad = angleDeg * Math.PI / 180; - return [ - PLANET_CENTER_R * Math.cos(angleRad), - PLANET_CENTER_R * Math.sin(angleRad), - 14.5 - ]; -} - -function radialUnit(angleDeg) { - const angleRad = angleDeg * Math.PI / 180; - return [Math.cos(angleRad), Math.sin(angleRad), 0]; -} - -function applyVisibility(effects, params) { - effects.visible("*", true); - effects.visible(SHELL_PARTS, params.showShell !== false); - effects.visible(ELECTRONICS_PARTS, params.showElectronics !== false); - effects.visible(OUTPUT_STACK_PARTS, params.showOutputStack !== false); -} - -export default { - manifest: { - schemaVersion: 1, - step: { - path: "models/robots/qdd_actuator/qdd_actuator.step" - }, - label: "QDD actuator", - description: "Quasi-direct-drive actuator with viewer-time drivetrain animation: the outrunner rotor spins the sun gear, three planets walk the fixed ring at the derived 4.5:1 reduction, and the carrier drives the torque sensor and output flange.", - units: { - length: "mm", - angle: "deg", - time: "s" - }, - features: { - housing: { ref: "#o1.1", label: "Housing", description: "Fixed finned aluminum housing." }, - frontRetainer: { ref: "#o1.2", label: "Front retainer ring" }, - rearCover: { ref: "#o1.3", label: "Rear cover" }, - retainerScrews: { ref: "#o1.4", label: "Retainer screws" }, - connectorPower: { ref: "#o1.5", label: "Power connector" }, - connectorSignal: { ref: "#o1.6", label: "Signal connector" }, - driverPcb: { ref: "#o1.7", label: "Driver PCB" }, - encoderPcb: { ref: "#o1.8", label: "Encoder PCB" }, - encoderMagnetRing: { ref: "#o1.9", label: "Encoder magnet ring", description: "Spins with the rotor for the rear magnetic encoder." }, - stator: { ref: "#o1.10", label: "Stator", description: "Fixed 24-slot stator and windings." }, - rotor: { ref: "#o1.11", label: "Rotor", description: "Outrunner rotor bell and 28-magnet ring; the animation input." }, - rearBearingInner: { ref: "#o1.12.1", label: "Rear bearing inner race" }, - rearBearingOuter: { ref: "#o1.12.2", label: "Rear bearing outer race" }, - rearBearingBalls: { ref: "#o1.12.3", label: "Rear bearing balls" }, - frontBearingInner: { ref: "#o1.13.1", label: "Front bearing inner race" }, - frontBearingOuter: { ref: "#o1.13.2", label: "Front bearing outer race" }, - frontBearingBalls: { ref: "#o1.13.3", label: "Front bearing balls" }, - sunGear: { ref: "#o1.14", label: "Sun gear (24T)", description: "Bolted to the rotor flange; rotates at input speed.", axis: Z_AXIS, origin: ORIGIN }, - ringGear: { ref: "#o1.15", label: "Ring gear (84T)", description: "Fixed internal gear pressed into the housing bore." }, - planet1: { ref: "#o1.16", label: "Planet gear 1 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[0]) }, - planet2: { ref: "#o1.17", label: "Planet gear 2 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[1]) }, - planet3: { ref: "#o1.18", label: "Planet gear 3 (30T)", axis: Z_AXIS, origin: planetCenter(PLANET_ANGLES_DEG[2]) }, - carrier: { ref: "#o1.19", label: "Planet carrier", description: "Output member at 1/4.5 of input speed.", axis: Z_AXIS, origin: ORIGIN }, - xrollerInner: { ref: "#o1.20.1", label: "Cross-roller inner ring", description: "Rotates with the output stage." }, - xrollerOuter: { ref: "#o1.20.2", label: "Cross-roller outer ring", description: "Fixed in the housing seat." }, - xrollerRollers: { ref: "#o1.20.3", label: "Cross-roller rollers" }, - torqueSensor: { ref: "#o1.21", label: "Torque sensor flexure", description: "Strain-gauge flexure between carrier and flange." }, - outputFlange: { ref: "#o1.22", label: "Output flange" }, - cableTube: { ref: "#o1.23", label: "Cable tube", description: "Static hollow-shaft cable conduit." } - }, - parameters: { - drive: { - type: "number", - label: "Drive", - description: "Rotor/sun input angle over one closed mesh cycle (4.5 input revolutions = 1 carrier revolution).", - default: 0, - min: 0, - max: DRIVE_CYCLE_DEG, - step: 1, - unit: "deg" - }, - explode: { - type: "number", - label: "Explode", - description: "Axial technical explosion from the documented build123d layout, applied after the gear motion.", - default: 0, - min: 0, - max: 1, - step: 0.01 - }, - keepGearMesh: { - type: "boolean", - label: "Keep gear mesh", - description: "Explode the ring, sun, and planets together as one meshed cluster instead of separate stations; the carrier still lifts clear of the mesh.", - default: true - }, - showShell: { type: "boolean", label: "Housing shell", default: true }, - showElectronics: { type: "boolean", label: "Electronics", default: true }, - showOutputStack: { type: "boolean", label: "Output stack", default: true }, - highlightDrive: { type: "boolean", label: "Highlight gear train", default: false } - }, - animations: { - driveLoop: { - label: "Drive 4.5:1 reduction", - description: "Continuous rotor input: sun at input speed, planets walking the fixed ring, carrier and output flange at 1/4.5 speed.", - duration: 12, - loop: true, - update({ cycle, set }) { - set("drive", wrap01(cycle) * DRIVE_CYCLE_DEG); - } - }, - inspectExplode: { - label: "Exploded drive inspection", - description: "Same gear cycle while the stack separates into exploded stations and returns; the planetary stage stays meshed while 'Keep gear mesh' is on.", - duration: 12, - loop: true, - update({ cycle, set }) { - const phase = wrap01(cycle); - set("drive", phase * DRIVE_CYCLE_DEG); - set("explode", Math.sin(phase * Math.PI)); - } - } - } - }, - - update({ params, effects }) { - const drive = finite(params.drive); - const explode = clamp01(params.explode); - const carrierAngle = drive / REDUCTION_RATIO; - // Mesh-consistent planet spin about its own moving axis, relative to the - // carrier frame: external sun/planet mesh reverses the relative rotation. - const planetSpinRelativeToCarrier = -(SUN_TEETH / PLANET_TEETH) * (drive - carrierAngle); - const ballOrbitAngle = drive * BALL_CAGE_RATIO; - const rollerOrbitAngle = carrierAngle * ROLLER_CAGE_RATIO; - const lift = (key) => (EXPLODE_OFFSETS_MM[key] + FLOOR_LIFT_MM) * explode; - const keepGearMesh = params.keepGearMesh !== false; - const gearLift = (key) => (keepGearMesh ? (GEAR_STAGE_CLUSTER_MM + FLOOR_LIFT_MM) * explode : lift(key)); - const planetRadialMm = keepGearMesh ? 0 : PLANET_EXPLODE_RADIAL_MM; - - applyVisibility(effects, params); - - if (params.highlightDrive === true) { - effects.highlight(DRIVE_TRAIN_PARTS, true); - } - - // Input group: rotor bell + magnets, encoder target ring, sun gear. - effects.transform("rotor", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive }, - translate: [0, 0, lift("rotor")] - }); - effects.transform("encoderMagnetRing", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive }, - translate: [0, 0, lift("encoderMagnetRing")] - }); - effects.transform("sunGear", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive }, - translate: [0, 0, gearLift("sunGear")] - }); - - // Rotor support bearings: inner races spin with the hub, ball rings orbit - // at cage speed, outer races stay seated in the housing sleeve. - for (const [inner, balls, outer, key] of [ - ["rearBearingInner", "rearBearingBalls", "rearBearingOuter", "rearBearing"], - ["frontBearingInner", "frontBearingBalls", "frontBearingOuter", "frontBearing"] - ]) { - effects.transform(inner, { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: drive }, - translate: [0, 0, lift(key)] - }); - effects.transform(balls, { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: ballOrbitAngle }, - translate: [0, 0, lift(key)] - }); - effects.transform(outer, { translate: [0, 0, lift(key)] }); - } - - // Planets: spin about their own (initial) axis, separate radially in the - // exploded view, then orbit the sun axis with the carrier. Steps compose - // in order in the fixed model frame, so the radial offset and orbit keep - // the planets pointing outward at the current carrier angle. - for (const planet of PLANETS) { - const radial = radialUnit(planet.angleDeg); - effects.transform(planet.id, { - transforms: [ - { - rotate: { - axis: Z_AXIS, - origin: planetCenter(planet.angleDeg), - angleDeg: planetSpinRelativeToCarrier - } - }, - { - translate: [ - radial[0] * planetRadialMm * explode, - radial[1] * planetRadialMm * explode, - gearLift("planetGear") - ] - }, - { rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle } } - ] - }); - } - - // Output group at carrier speed: carrier, torque sensor, flange, and the - // cross-roller inner ring; the roller ring orbits at cage speed. - effects.transform("carrier", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle }, - translate: [0, 0, lift("carrier")] - }); - effects.transform("torqueSensor", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle }, - translate: [0, 0, lift("torqueSensor")] - }); - effects.transform("outputFlange", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle }, - translate: [0, 0, lift("outputFlange")] - }); - effects.transform("xrollerInner", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: carrierAngle }, - translate: [0, 0, lift("crossRollerBearing")] - }); - effects.transform("xrollerRollers", { - rotate: { axis: Z_AXIS, origin: ORIGIN, angleDeg: rollerOrbitAngle }, - translate: [0, 0, lift("crossRollerBearing")] - }); - effects.transform("xrollerOuter", { translate: [0, 0, lift("crossRollerBearing")] }); - - // Static parts only take their exploded-station offsets. - effects.transform("housing", { translate: [0, 0, lift("housing")] }); - effects.transform("ringGear", { translate: [0, 0, gearLift("ringGear")] }); - effects.transform("stator", { translate: [0, 0, lift("stator")] }); - effects.transform("frontRetainer", { translate: [0, 0, lift("frontRetainer")] }); - effects.transform("retainerScrews", { translate: [0, 0, lift("retainerScrews")] }); - effects.transform("rearCover", { translate: [0, 0, lift("rearCover")] }); - effects.transform("connectorPower", { translate: [0, 0, lift("connectorPower")] }); - effects.transform("connectorSignal", { translate: [0, 0, lift("connectorSignal")] }); - effects.transform("driverPcb", { translate: [0, 0, lift("driverPcb")] }); - effects.transform("encoderPcb", { translate: [0, 0, lift("encoderPcb")] }); - effects.transform("cableTube", { translate: [0, 0, lift("cableTube")] }); - } -}; diff --git a/models/robots/qdd_actuator/qdd_actuator.py b/models/robots/qdd_actuator/qdd_actuator.py deleted file mode 100644 index 724ace96..00000000 --- a/models/robots/qdd_actuator/qdd_actuator.py +++ /dev/null @@ -1,912 +0,0 @@ -"""Compact quasi-direct-drive (QDD) actuator concept for a humanoid robot joint. - -Functional concept layout, manufacturable industrial design: -- high-torque-density outrunner BLDC motor (24-slot stator, 28-magnet rotor) -- low-ratio single-stage planetary reduction (sun 24T / planets 30T / ring 84T, - module 1.0, fixed ring -> 4.5:1) -- integrated rear magnetic encoder (rotor magnet ring + encoder PCB) -- output torque sensing (4-spoke strain-gauge flexure between carrier and flange) -- cross-roller output bearing (alternating +/-45 deg rollers in V-groove races) -- hollow shaft cable routing (Ø14/Ø11 static tube through the full stack) -- compact annular motor-driver PCB (FETs, bus capacitors, MCU) -- aluminum housing with axial heat fins, bolted front retainer ring -- sealed rear circular connectors (power + signal, o-ring glands) - -Coordinate convention: -- Units mm. Origin on the actuator centerline (Z = rotation axis). -- +Z is the output direction; the gear stage and output flange are at +Z, - the driver electronics and sealed connectors are at -Z. - -The same parameterized builder produces the assembled model (explode=0) and an -exploded technical layout (explode=1) used by qdd_actuator_exploded.py. The -exploded translations are documented per-part static Locations along +/-Z. -""" - -from __future__ import annotations - -import math - -from build123d import * # noqa: F401,F403 -from cadpy.assembly import AssemblyHelper - -# --------------------------------------------------------------------------- -# Gear stage parameters (module-1.0 concept teeth, trapezoidal with backlash) -# --------------------------------------------------------------------------- -GEAR_MODULE = 1.0 -SUN_TEETH = 24 -PLANET_TEETH = 30 -RING_TEETH = 84 # = SUN_TEETH + 2 * PLANET_TEETH -PLANET_COUNT = 3 -REDUCTION_RATIO = 1.0 + RING_TEETH / SUN_TEETH # 4.5:1, fixed ring gear - -SUN_PITCH_R = GEAR_MODULE * SUN_TEETH / 2 # 12.0 -PLANET_PITCH_R = GEAR_MODULE * PLANET_TEETH / 2 # 15.0 -RING_PITCH_R = GEAR_MODULE * RING_TEETH / 2 # 42.0 -PLANET_CENTER_R = SUN_PITCH_R + PLANET_PITCH_R # 27.0 -PLANET_ANGLES_DEG = (90.0, 210.0, 330.0) - -ADDENDUM = 1.0 * GEAR_MODULE -DEDENDUM = 1.25 * GEAR_MODULE -GEAR_WIDTH = 12.0 -GEAR_Z = (8.5, 20.5) - -SUN_ROOT_R = SUN_PITCH_R - DEDENDUM # 10.75 -SUN_TIP_R = SUN_PITCH_R + ADDENDUM # 13.0 -PLANET_ROOT_R = PLANET_PITCH_R - DEDENDUM # 13.75 -PLANET_TIP_R = PLANET_PITCH_R + ADDENDUM # 16.0 -RING_TIP_R = RING_PITCH_R - ADDENDUM # 41.0 (internal teeth point inward) -RING_ROOT_R = RING_PITCH_R + DEDENDUM # 43.25 -RING_RIM_R = 46.0 -PLANET_BORE_R = 4.0 - -# --------------------------------------------------------------------------- -# Hollow shaft cable routing -# --------------------------------------------------------------------------- -TUBE_OUTER_R = 7.0 # Ø14 conduit -TUBE_INNER_R = 5.5 # Ø11 cable bore -# The static tube protrudes past the rear cover and the output flange so the -# hollow-shaft cable path visibly enters and exits the actuator. -TUBE_Z = (-36.5, 35.5) - -# --------------------------------------------------------------------------- -# Outrunner BLDC motor -# --------------------------------------------------------------------------- -STATOR_SLOTS = 24 -STATOR_YOKE_IR = 28.0 # seats on the housing center sleeve -STATOR_YOKE_OR = 33.0 -STATOR_TOOTH_TIP_R = 40.0 -STATOR_TOOTH_W = 5.0 # tangential tooth width -STATOR_SHOE_W = 7.6 # tangential pole-shoe width -STATOR_SHOE_IR = 38.8 -STATOR_STACK_Z = (-13.0, 1.0) - -WINDING_RADIAL = (33.4, 38.6) # clears yoke OD and pole shoes -WINDING_TANGENTIAL = 8.0 -WINDING_AXIAL = 17.0 # end turns overhang the stack -WINDING_HOLE_TANGENTIAL = 5.1 -WINDING_HOLE_AXIAL = 14.1 - -MAGNET_COUNT = 28 -MAGNET_IR = 40.75 # 0.75 mm airgap over the stator tooth tips -MAGNET_OR = 43.75 -MAGNET_W = 8.0 -MAGNET_Z = (-13.0, 1.0) -BACK_IRON_R = (43.75, 45.5) -BACK_IRON_Z = (-13.5, 2.5) - -ROTOR_HUB_R = (7.5, 11.0) # hollow hub over the cable tube -ROTOR_HUB_Z = (-20.0, 5.5) -ROTOR_WEB_Z = (2.5, 5.5) # front web (output-side bell) -ROTOR_FLANGE_R = (7.5, 17.0) # sun-gear coupling flange -ROTOR_FLANGE_Z = (5.5, 6.8) -COUPLING_HOLE_R = 11.25 # 4x M3 rotor/sun coupling circle -AIRGAP = MAGNET_IR - STATOR_TOOTH_TIP_R - -# Rotor support ball bearings (inner race on hub, outer race in sleeve bore) -ROTOR_BRG_IR = (11.0, 12.4) -ROTOR_BRG_OR = (13.6, 15.0) -ROTOR_BRG_BALL_R = 1.25 -ROTOR_BRG_BALL_COUNT = 10 -ROTOR_BRG_REAR_Z = (-17.0, -12.0) -ROTOR_BRG_FRONT_Z = (-3.5, 1.5) - -# --------------------------------------------------------------------------- -# Housing (aluminum, axial heat fins) and front retainer -# --------------------------------------------------------------------------- -BARREL_R = (46.0, 50.0) -BARREL_Z = (-31.0, 29.0) -# Dense thin-fin array over the motor/electronics zone; deliberately many, -# thin, and shallow so the array reads as heat-sink fins, not gear teeth. -# Fins stop short of BOTH end faces so each rim keeps a machined solid band. -FIN_COUNT = 64 -FIN_RADIAL = (49.5, 52.5) -FIN_W = 1.4 -FIN_Z = (-26.5, 12.0) -REAR_WALL_R = (13.0, 46.0) -REAR_WALL_Z = (-22.0, -18.0) -SLEEVE_R = (15.0, 28.0) -SLEEVE_Z = (-18.0, 1.5) -RETAINER_R = (41.5, 50.0) -RETAINER_Z = (29.0, 31.0) -FRONT_SCREW_CIRCLE_R = 48.0 # 8x M3 retainer screws into the barrel face -FRONT_SCREW_COUNT = 8 -RETAINER_SCREW_HEAD_R = 2.75 # visible M3 socket-head fasteners in the ring -RETAINER_SCREW_HEAD_H = 2.2 -RETAINER_SCREW_SOCKET_R = 1.45 -REAR_SCREW_CIRCLE_R = 48.0 # 8x M3 rear cover screws -REAR_SCREW_COUNT = 8 -PHASE_WIRE_HOLE_R = 2.5 # 3x stator phase-wire passages through the rear wall -PHASE_WIRE_CIRCLE_R = 34.0 -PHASE_WIRE_ANGLES = (15.0, 135.0, 255.0) - -ENC_STANDOFF_R = 2.5 -ENC_STANDOFF_CIRCLE_R = 17.5 -ENC_STANDOFF_Z = (-24.4, -22.0) -ENC_STANDOFF_ANGLES = (90.0, 210.0, 330.0) -DRV_STANDOFF_R = 3.0 -DRV_STANDOFF_CIRCLE_R = 40.0 -DRV_STANDOFF_Z = (-28.8, -22.0) -DRV_STANDOFF_ANGLES = (30.0, 150.0, 270.0) - -# --------------------------------------------------------------------------- -# Rear cover and sealed connectors -# --------------------------------------------------------------------------- -COVER_PLATE_Z = (-34.0, -31.0) -COVER_R = 50.0 -COVER_LIP_R = (43.8, 45.8) -COVER_LIP_Z = (-31.0, -29.5) -COVER_BOSS_R = (7.05, 8.5) -COVER_BOSS_Z = (-31.0, -29.0) -CONNECTOR_CIRCLE_R = 28.0 -CONNECTOR_ANGLES = {"power": 135.0, "signal": 225.0} -CONNECTOR_SCALE = {"power": 1.0, "signal": 0.82} -ORING_MAJOR_R = 8.2 # scaled per connector -ORING_MINOR_R = 0.6 -ORING_GLAND_MINOR_R = 0.7 - -# --------------------------------------------------------------------------- -# Electronics -# --------------------------------------------------------------------------- -DRIVER_BOARD_R = (9.0, 43.0) -DRIVER_BOARD_Z = (-30.4, -28.8) -FET_COUNT = 6 -FET_SIZE = (9.0, 7.0, 2.5) # radial, tangential, height -FET_CIRCLE_R = 38.0 -CAP_COUNT = 6 -CAP_R = 4.0 -CAP_H = 5.0 -CAP_CIRCLE_R = 33.0 -MCU_SIZE = (10.0, 10.0, 1.5) -MCU_CENTER = (0.0, 22.0) # az 90 deg - -ENCODER_BOARD_R = (8.0, 20.0) -ENCODER_BOARD_Z = (-26.0, -24.4) -ENCODER_CHIP_SIZE = (5.0, 5.0, 1.0) -ENCODER_CHIP_CENTER = (0.0, -10.0) # az 270 deg, faces the rotor magnet ring -ENC_MAGNET_R = (7.5, 12.0) -ENC_MAGNET_Z = (-22.0, -20.0) - -# --------------------------------------------------------------------------- -# Output: carrier, cross-roller bearing, torque sensor, flange -# --------------------------------------------------------------------------- -CARRIER_PIN_R = 3.75 -CARRIER_PIN_Z = (8.5, 21.0) -CARRIER_PLATE_R = (8.0, 31.0) -CARRIER_PLATE_Z = (21.0, 24.5) -CARRIER_HUB_R = (8.0, 25.0) -CARRIER_HUB_Z = (24.5, 26.0) -CARRIER_LIGHTEN_HOLE_R = 4.0 -CARRIER_LIGHTEN_CIRCLE_R = 19.0 -CARRIER_LIGHTEN_ANGLES = (30.0, 150.0, 270.0) - -XROLLER_INNER_R = (33.0, 37.5) -XROLLER_OUTER_R = (41.5, 46.0) -XROLLER_Z = (21.0, 29.0) -XROLLER_GROOVE_SQ = 5.65 # rotated-45deg square raceway profile -XROLLER_PITCH_R = 39.5 -XROLLER_ROLLER_R = 2.75 -XROLLER_ROLLER_L = 5.5 -XROLLER_COUNT = 16 - -FLEXURE_HUB_R = (8.0, 14.0) -FLEXURE_HUB_Z = (26.0, 28.5) -FLEXURE_SPOKE_COUNT = 4 -FLEXURE_SPOKE_L = 13.0 -FLEXURE_SPOKE_W = 6.0 -FLEXURE_SPOKE_Z = (26.5, 28.5) -FLEXURE_RIM_R = (26.0, 32.0) -FLEXURE_RIM_Z = (26.5, 29.0) -GAUGE_SIZE = (8.0, 4.0, 0.3) -GAUGE_Z = 28.5 - -FLANGE_PLATE_R = (7.6, 36.0) -FLANGE_PLATE_Z = (29.0, 33.0) -FLANGE_BOSS_R = (14.0, 20.0) -FLANGE_BOSS_Z = (33.0, 34.5) -FLANGE_BOLT_CIRCLE_R = 28.0 # 6x M5 output bolt circle -FLANGE_BOLT_R = 2.75 -FLANGE_BOLT_COUNT = 6 -FLANGE_PILOT_HOLE_R = 1.7 # 4x M3 on the pilot boss -FLANGE_PILOT_CIRCLE_R = 17.0 - -# --------------------------------------------------------------------------- -# Exploded-view static translations (mm at explode=1.0), documented per part. -# Pure +/-Z technical explosion with two-tier spacing rhythm: ~10-17 mm air -# gaps between parts inside a subsystem and ~20-25 mm gaps at subsystem -# boundaries (electronics | housing | motor | gear stage | output stage), so -# every part owns a readable station. Verified pairwise non-overlapping at -# factor 1.0. -# --------------------------------------------------------------------------- -EXPLODE_OFFSETS = { - # rear: sealed connectors / cover / electronics (~12-18 mm internal gaps) - "connector_power": -82.0, - "connector_signal": -82.0, - "rear_cover": -64.0, - "driver_pcb": -46.0, - "encoder_pcb": -32.0, - # fixed root (housing | electronics boundary ~24 mm) - "housing": 0.0, - # motor group (housing | motor boundary ~19 mm, ~11-16 mm internal) - "encoder_magnet_ring": 70.0, - "rotor_bearing_rear": 78.0, - "stator": 94.0, - "rotor": 128.0, - "rotor_bearing_front": 154.0, - # gear stage (motor | gears boundary ~21 mm, ~14-16 mm internal) - "ring_gear": 168.0, - "sun_gear": 196.0, - "planet_gear": 222.0, - "planet_carrier": 250.0, - # output stage (gears | output boundary ~25 mm, ~11-18 mm internal) - "cross_roller_bearing": 280.0, - "front_retainer": 298.0, - "retainer_screws": 312.0, - "torque_sensor": 330.0, - "output_flange": 348.0, - "cable_tube": 360.0, -} -# Planets additionally translate radially outward so the three gears separate -# in projection instead of stacking behind each other. -PLANET_EXPLODE_RADIAL = 14.0 - -# --------------------------------------------------------------------------- -# Concept material colors (production-actuator palette: dark anodized body, -# machined-aluminum output hardware, one anodized accent ring) -# --------------------------------------------------------------------------- -AL_BODY = Color(0.13, 0.135, 0.15) # black-anodized housing -AL_COVER = Color(0.115, 0.12, 0.13) # black-anodized rear cover -AL_BRIGHT = Color(0.78, 0.79, 0.81) # machined aluminum (flange, carrier) -AL_RETAINER = Color(0.85, 0.20, 0.01) # saturated anodized-orange accent ring -STEEL = Color(0.62, 0.63, 0.66) -STEEL_POLISHED = Color(0.76, 0.77, 0.79) -STEEL_RACE = Color(0.24, 0.25, 0.29) # dark bearing-steel races vs aluminum -STEEL_ROLLER = Color(0.85, 0.86, 0.88) -LAMINATION = Color(0.30, 0.31, 0.34) -COPPER = Color(0.80, 0.45, 0.18) -ROTOR_DARK = Color(0.24, 0.25, 0.28) -MAGNET_N = Color(0.10, 0.10, 0.12) # alternating pole colors: dark / nickel -MAGNET_S = Color(0.68, 0.69, 0.71) -FERRITE = Color(0.25, 0.08, 0.10) -PCB_GREEN = Color(0.05, 0.30, 0.14) -IC_BLACK = Color(0.08, 0.08, 0.09) -CAP_GRAY = Color(0.22, 0.24, 0.30) -PLASTIC_BLACK = Color(0.07, 0.07, 0.08) -GUNMETAL = Color(0.34, 0.35, 0.38) # connector coupling nuts -PIN_GOLD = Color(0.80, 0.64, 0.25) # connector contacts -BLACK_OXIDE = Color(0.20, 0.20, 0.22) # socket-head fasteners -RUBBER = Color(0.04, 0.04, 0.04) -TITANIUM = Color(0.60, 0.59, 0.64) -GAUGE_GOLD = Color(0.85, 0.72, 0.35) -TUBE_BLACK = Color(0.32, 0.33, 0.35) # dark gunmetal, reads against the cover - -# --------------------------------------------------------------------------- -# Consistency checks (concept-level fit, not tolerance engineering) -# --------------------------------------------------------------------------- -assert RING_TEETH == SUN_TEETH + 2 * PLANET_TEETH, "planetary tooth-count identity" -assert (SUN_TEETH + RING_TEETH) % PLANET_COUNT == 0, "equal planet spacing condition" -assert AIRGAP >= 0.5, "outrunner magnetic airgap" -assert SUN_ROOT_R - TUBE_OUTER_R - 0.5 >= 2.5, "sun gear wall over hollow tube" -assert MAGNET_OR <= BACK_IRON_R[0] + 1e-9, "magnets sit inside the back iron" -assert BACK_IRON_R[1] < BARREL_R[0], "rotor bell clears the housing bore" -assert XROLLER_INNER_R[1] < XROLLER_PITCH_R - 0.5, "roller pitch sits in the race gap" -assert XROLLER_OUTER_R[0] > XROLLER_PITCH_R + 0.5, "roller pitch sits in the race gap" -assert RING_RIM_R == BARREL_R[0], "ring gear rim seats in the housing bore" -assert FLEXURE_RIM_R[1] < XROLLER_INNER_R[0], "torque flexure clears the bearing" - - -def _tube_z(outer_r: float, inner_r: float, z: tuple[float, float]) -> Part: - """Annular ring solid between z[0]..z[1].""" - return Pos(0, 0, z[0]) * extrude(Circle(outer_r) - Circle(inner_r), amount=z[1] - z[0]) - - -def _cyl_z(r: float, z: tuple[float, float]) -> Part: - return Pos(0, 0, z[0]) * extrude(Circle(r), amount=z[1] - z[0]) - - -def _external_gear(teeth: int, root_r: float, tip_r: float, bore_r: float) -> Part: - """Concept gear: root cylinder + trapezoidal teeth with visible backlash.""" - half_arc = math.pi * GEAR_MODULE / 2 - wt = half_arc * 0.34 # half-width at tip (thinned for visible backlash) - wr = half_arc * 0.55 # half-width at root - h = tip_r - root_r - rm = (tip_r + root_r) / 2 - tooth = Polygon( - (-h / 2 - 0.4, -wr), (h / 2, -wt), (h / 2, wt), (-h / 2 - 0.4, wr), align=None - ) - teeth_sk = [loc * tooth for loc in PolarLocations(rm, teeth)] - sketch = Circle(root_r) + teeth_sk - Circle(bore_r) - return extrude(sketch, amount=GEAR_WIDTH) - - -def _internal_ring_gear() -> Part: - """Internal ring gear with inward trapezoidal teeth and press-in rim.""" - half_arc = math.pi * GEAR_MODULE / 2 - wt = half_arc * 0.34 - wr = half_arc * 0.55 - h = RING_ROOT_R - RING_TIP_R - rm = (RING_ROOT_R + RING_TIP_R) / 2 - # counter-clockwise point order so the face normal is +Z and the teeth - # extrude in the same direction as the rim - tooth = Polygon( - (h / 2 + 0.4, -wr), (h / 2 + 0.4, wr), (-h / 2, wt), (-h / 2, -wt), align=None - ) - teeth_sk = [loc * tooth for loc in PolarLocations(rm, RING_TEETH)] - sketch = Circle(RING_RIM_R) - Circle(RING_ROOT_R) + teeth_sk - return extrude(sketch, amount=GEAR_WIDTH) - - -def make_housing() -> Part: - """Finned aluminum housing: barrel, rear wall, stator sleeve, standoffs.""" - barrel = _tube_z(BARREL_R[1], BARREL_R[0], BARREL_Z) - rear_wall = _tube_z(REAR_WALL_R[1], REAR_WALL_R[0], REAR_WALL_Z) - sleeve = _tube_z(SLEEVE_R[1], SLEEVE_R[0], SLEEVE_Z) - - fin = Box( - FIN_RADIAL[1] - FIN_RADIAL[0] + 0.5, # overlap into the barrel for fusing - FIN_W, - FIN_Z[1] - FIN_Z[0], - ) - fin_loc = Pos((FIN_RADIAL[0] - 0.5 + FIN_RADIAL[1]) / 2, 0, (FIN_Z[0] + FIN_Z[1]) / 2) - fins = [rot * (fin_loc * fin) for rot in PolarLocations(0, FIN_COUNT)] - - standoffs = [] - for az in ENC_STANDOFF_ANGLES: - standoffs.append( - Rotation(0, 0, az) - * Pos(ENC_STANDOFF_CIRCLE_R, 0, 0) - * _cyl_z(ENC_STANDOFF_R, ENC_STANDOFF_Z) - ) - for az in DRV_STANDOFF_ANGLES: - standoffs.append( - Rotation(0, 0, az) - * Pos(DRV_STANDOFF_CIRCLE_R, 0, 0) - * _cyl_z(DRV_STANDOFF_R, DRV_STANDOFF_Z) - ) - - housing = barrel + [rear_wall, sleeve, *fins, *standoffs] - - cuts = [] - # 8x M3 retainer screw holes in the front barrel face - for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5): - cuts.append(loc * _cyl_z(1.7, (BARREL_Z[1] - 8.0, BARREL_Z[1] + 0.1))) - # 8x M3 rear cover screw holes in the rear barrel face - for loc in PolarLocations(REAR_SCREW_CIRCLE_R, REAR_SCREW_COUNT, start_angle=22.5): - cuts.append(loc * _cyl_z(1.4, (BARREL_Z[0] - 0.1, BARREL_Z[0] + 5.0))) - # 3x stator phase-wire passages through the rear wall - for az in PHASE_WIRE_ANGLES: - cuts.append( - Rotation(0, 0, az) - * Pos(PHASE_WIRE_CIRCLE_R, 0, 0) - * _cyl_z(PHASE_WIRE_HOLE_R, (REAR_WALL_Z[0] - 0.1, REAR_WALL_Z[1] + 0.1)) - ) - return housing - cuts - - -def make_front_retainer() -> Part: - """Bolted bearing retainer ring (makes the front stack assemblable).""" - ring = _tube_z(RETAINER_R[1], RETAINER_R[0], RETAINER_Z) - holes = [ - loc * _cyl_z(1.7, (RETAINER_Z[0] - 0.1, RETAINER_Z[1] + 0.1)) - for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5) - ] - return ring - holes - - -def make_retainer_screws() -> list[Part]: - """8x M3 socket-head fasteners populating the retainer screw circle.""" - screws = [] - socket = extrude(RegularPolygon(RETAINER_SCREW_SOCKET_R, 6), amount=1.2) - for loc in PolarLocations(FRONT_SCREW_CIRCLE_R, FRONT_SCREW_COUNT, start_angle=22.5): - head = _cyl_z( - RETAINER_SCREW_HEAD_R, (RETAINER_Z[1], RETAINER_Z[1] + RETAINER_SCREW_HEAD_H) - ) - shank = _cyl_z(1.55, (RETAINER_Z[0] + 0.5, RETAINER_Z[1])) - screw = (head + shank) - Pos( - 0, 0, RETAINER_Z[1] + RETAINER_SCREW_HEAD_H - 1.1 - ) * socket - screws.append(loc * screw) - return screws - - -def make_rear_cover() -> Part: - """Sealed rear cover: plate, locating lip, tube boss, o-ring glands.""" - plate = _cyl_z(COVER_R, COVER_PLATE_Z) - lip = _tube_z(COVER_LIP_R[1], COVER_LIP_R[0], COVER_LIP_Z) - boss = _tube_z(COVER_BOSS_R[1], COVER_BOSS_R[0], COVER_BOSS_Z) - cover = plate + [lip, boss] - - cuts = [_cyl_z(COVER_BOSS_R[0], (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1))] - # connector pass-throughs + o-ring glands on the rear face - for name, az in CONNECTOR_ANGLES.items(): - hole_r = 5.0 if name == "power" else 4.0 - conn_loc = Rotation(0, 0, az) * Pos(CONNECTOR_CIRCLE_R, 0, 0) - cuts.append(conn_loc * _cyl_z(hole_r, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1))) - cuts.append( - conn_loc - * Pos(0, 0, COVER_PLATE_Z[0]) - * Torus(ORING_MAJOR_R * CONNECTOR_SCALE[name], ORING_GLAND_MINOR_R) - ) - # 8x M3 counterbored cover screws - for loc in PolarLocations(REAR_SCREW_CIRCLE_R, REAR_SCREW_COUNT, start_angle=22.5): - cuts.append(loc * _cyl_z(1.7, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[1] + 0.1))) - cuts.append(loc * _cyl_z(3.0, (COVER_PLATE_Z[0] - 0.1, COVER_PLATE_Z[0] + 1.8))) - return cover - cuts - - -def make_connector(kind: str) -> list[Part]: - """Sealed circular connector at the cover face. - - Body with shrouded keying cup, 12-flat coupling nut (knurl read), recessed - insert floor with a gold pin array, and an o-ring seal at the flange. - """ - az = CONNECTOR_ANGLES[kind] - scale = CONNECTOR_SCALE[kind] - z0 = COVER_PLATE_Z[0] # mounting face, body protrudes in -Z - flange = _cyl_z(9.0 * scale, (z0 - 1.5, z0)) - barrel = _cyl_z(7.5 * scale, (z0 - 6.5, z0 - 1.5)) - core = _cyl_z(5.5 * scale, (z0 - 7.8, z0 - 1.5)) - shroud = _tube_z(6.0 * scale, 5.0 * scale, (z0 - 10.0, z0 - 7.5)) - insert_floor = _cyl_z(5.0 * scale, (z0 - 8.4, z0 - 7.5)) - body = flange + [barrel, core, shroud, insert_floor] - # cut the gland into the connector flange as well so the o-ring seats cleanly - body = body - Pos(0, 0, z0) * Torus(ORING_MAJOR_R * scale, ORING_GLAND_MINOR_R) - collar = Pos(0, 0, z0 - 9.0) * extrude( - RegularPolygon(8.5 * scale, 12) - Circle(6.2 * scale), amount=2.5 - ) - o_ring = Pos(0, 0, z0) * Torus(ORING_MAJOR_R * scale, ORING_MINOR_R) - pin_count = 4 if kind == "power" else 6 - pin_r = 0.8 * scale if kind == "power" else 0.55 - pins = Compound( - children=[ - loc * _cyl_z(pin_r, (z0 - 9.7, z0 - 8.4)) - for loc in PolarLocations(2.6 * scale, pin_count) - ] - ) - loc = Rotation(0, 0, az) * Pos(CONNECTOR_CIRCLE_R, 0, 0) - return [loc * body, loc * collar, loc * o_ring, loc * pins] - - -def make_driver_pcb() -> dict[str, object]: - """Annular motor-driver PCB with FETs, bus capacitors, and MCU.""" - board = _tube_z(DRIVER_BOARD_R[1], DRIVER_BOARD_R[0], DRIVER_BOARD_Z) - z_face = DRIVER_BOARD_Z[1] - fets = [] - for i, loc in enumerate(PolarLocations(FET_CIRCLE_R, FET_COUNT, start_angle=15.0)): - fets.append( - loc * Pos(0, 0, z_face + FET_SIZE[2] / 2) * Box(FET_SIZE[0], FET_SIZE[1], FET_SIZE[2]) - ) - caps = [ - loc * Pos(0, 0, z_face + CAP_H / 2) * Cylinder(CAP_R, CAP_H) - for loc in PolarLocations(CAP_CIRCLE_R, CAP_COUNT, start_angle=0.0) - ] - mcu = Pos(MCU_CENTER[0], MCU_CENTER[1], z_face + MCU_SIZE[2] / 2) * Box(*MCU_SIZE) - return {"board": board, "fets": fets, "caps": caps, "mcu": mcu} - - -def make_encoder_pcb() -> dict[str, object]: - """Encoder PCB behind the rotor magnet ring.""" - board = _tube_z(ENCODER_BOARD_R[1], ENCODER_BOARD_R[0], ENCODER_BOARD_Z) - chip = Pos( - ENCODER_CHIP_CENTER[0], ENCODER_CHIP_CENTER[1], ENCODER_BOARD_Z[1] + ENCODER_CHIP_SIZE[2] / 2 - ) * Box(*ENCODER_CHIP_SIZE) - return {"board": board, "chip": chip} - - -def make_stator() -> dict[str, object]: - """24-slot outrunner stator: laminated yoke/teeth and copper windings.""" - yoke = _tube_z(STATOR_YOKE_OR, STATOR_YOKE_IR, STATOR_STACK_Z) - z_mid = (STATOR_STACK_Z[0] + STATOR_STACK_Z[1]) / 2 - stack_h = STATOR_STACK_Z[1] - STATOR_STACK_Z[0] - - tooth = Pos((STATOR_YOKE_OR - 0.5 + STATOR_TOOTH_TIP_R) / 2, 0, z_mid) * Box( - STATOR_TOOTH_TIP_R - STATOR_YOKE_OR + 0.5, STATOR_TOOTH_W, stack_h - ) - shoe = Pos((STATOR_SHOE_IR + STATOR_TOOTH_TIP_R) / 2, 0, z_mid) * Box( - STATOR_TOOTH_TIP_R - STATOR_SHOE_IR, STATOR_SHOE_W, stack_h - ) - tooth_unit = tooth + shoe - teeth = [rot * tooth_unit for rot in PolarLocations(0, STATOR_SLOTS)] - stack = yoke + teeth - - # one rectangular coil loop around a tooth, then a polar pattern of copies - wind_r_mid = (WINDING_RADIAL[0] + WINDING_RADIAL[1]) / 2 - coil_outer = Pos(wind_r_mid, 0, z_mid) * Box( - WINDING_RADIAL[1] - WINDING_RADIAL[0], WINDING_TANGENTIAL, WINDING_AXIAL - ) - coil_hole = Pos(wind_r_mid, 0, z_mid) * Box( - WINDING_RADIAL[1] - WINDING_RADIAL[0] + 1.0, WINDING_HOLE_TANGENTIAL, WINDING_HOLE_AXIAL - ) - coil = coil_outer - coil_hole - windings = [rot * coil for rot in PolarLocations(0, STATOR_SLOTS)] - return {"stack": stack, "windings": windings} - - -def make_rotor() -> dict[str, object]: - """Outrunner rotor bell: hollow hub, front web, back iron, magnet ring.""" - hub = _tube_z(ROTOR_HUB_R[1], ROTOR_HUB_R[0], ROTOR_HUB_Z) - web = _tube_z(BACK_IRON_R[1], ROTOR_HUB_R[0], ROTOR_WEB_Z) - iron = _tube_z(BACK_IRON_R[1], BACK_IRON_R[0], BACK_IRON_Z) - flange = _tube_z(ROTOR_FLANGE_R[1], ROTOR_FLANGE_R[0], ROTOR_FLANGE_Z) - body = hub + [web, iron, flange] - - cuts = [] - # 6x web lightening holes - for loc in PolarLocations(28.0, 6, start_angle=30.0): - cuts.append(loc * _cyl_z(5.0, (ROTOR_WEB_Z[0] - 0.1, ROTOR_WEB_Z[1] + 0.1))) - # 4x M3 sun-gear coupling holes - for loc in PolarLocations(COUPLING_HOLE_R, 4, start_angle=45.0): - cuts.append(loc * _cyl_z(1.6, (ROTOR_FLANGE_Z[0] - 0.1, ROTOR_FLANGE_Z[1] + 0.1))) - body = body - cuts - - z_mid = (MAGNET_Z[0] + MAGNET_Z[1]) / 2 - - # Two independent prototypes so the alternating pole groups keep distinct - # exported materials (copies of one shape share a single STEP style). - def magnet_proto() -> Part: - return Pos((MAGNET_IR + MAGNET_OR) / 2, 0, z_mid) * Box( - MAGNET_OR - MAGNET_IR, MAGNET_W, MAGNET_Z[1] - MAGNET_Z[0] - ) - - north_proto, south_proto = magnet_proto(), magnet_proto() - locs = list(PolarLocations(0, MAGNET_COUNT)) - magnets_north = [locs[i] * north_proto for i in range(0, MAGNET_COUNT, 2)] - magnets_south = [locs[i] * south_proto for i in range(1, MAGNET_COUNT, 2)] - return {"body": body, "magnets_north": magnets_north, "magnets_south": magnets_south} - - -def make_ball_bearing(z: tuple[float, float]) -> dict[str, object]: - """Simplified deep-groove rotor support bearing: races + ball ring.""" - z_mid = (z[0] + z[1]) / 2 - ball_pitch_r = (ROTOR_BRG_IR[1] + ROTOR_BRG_OR[0]) / 2 - groove = Pos(0, 0, z_mid) * Torus(ball_pitch_r, ROTOR_BRG_BALL_R + 0.05) - inner = _tube_z(ROTOR_BRG_IR[1], ROTOR_BRG_IR[0], z) - groove - outer = _tube_z(ROTOR_BRG_OR[1], ROTOR_BRG_OR[0], z) - groove - balls = [ - loc * Pos(0, 0, z_mid) * Sphere(ROTOR_BRG_BALL_R) - for loc in PolarLocations(ball_pitch_r, ROTOR_BRG_BALL_COUNT) - ] - return {"inner": inner, "outer": outer, "balls": balls} - - -def make_sun_gear() -> Part: - """Sun gear with hollow bore and rotor coupling flange.""" - gear = Pos(0, 0, GEAR_Z[0]) * _external_gear(SUN_TEETH, SUN_ROOT_R, SUN_TIP_R, TUBE_OUTER_R + 0.5) - neck = _tube_z(SUN_ROOT_R, TUBE_OUTER_R + 0.5, (ROTOR_FLANGE_Z[1], GEAR_Z[0])) - flange = _tube_z(15.0, TUBE_OUTER_R + 0.5, (ROTOR_FLANGE_Z[1], ROTOR_FLANGE_Z[1] + 1.5)) - sun = gear + [neck, flange] - holes = [ - loc * _cyl_z(1.6, (ROTOR_FLANGE_Z[1] - 0.1, ROTOR_FLANGE_Z[1] + 1.6)) - for loc in PolarLocations(COUPLING_HOLE_R, 4, start_angle=45.0) - ] - return sun - holes - - -def make_planet_gear() -> Part: - """One planet gear at the origin; placed with mesh-phase rotation.""" - return _external_gear(PLANET_TEETH, PLANET_ROOT_R, PLANET_TIP_R, PLANET_BORE_R) - - -def planet_location(psi_deg: float) -> Location: - """Planet placement: position on the carrier circle plus mesh phasing. - - With sun/ring tooth patterns starting at azimuth 0, a tooth of each faces - the planet center whenever psi is a multiple of the sun/ring angular pitch - (true for 90/210/330 with 24 and 84 teeth). Rotating the planet by - psi + half a planet tooth pitch puts a tooth gap on the line of centers. - """ - phase = psi_deg + 180.0 / PLANET_TEETH - px = PLANET_CENTER_R * math.cos(math.radians(psi_deg)) - py = PLANET_CENTER_R * math.sin(math.radians(psi_deg)) - return Pos(px, py, GEAR_Z[0]) * Rotation(0, 0, phase) - - -def make_carrier() -> Part: - """Single-sided planet carrier: pins, plate, and torque-sensor hub.""" - plate = _tube_z(CARRIER_PLATE_R[1], CARRIER_PLATE_R[0], CARRIER_PLATE_Z) - hub = _tube_z(CARRIER_HUB_R[1], CARRIER_HUB_R[0], CARRIER_HUB_Z) - pins = [ - loc * _cyl_z(CARRIER_PIN_R, CARRIER_PIN_Z) - for loc in PolarLocations(PLANET_CENTER_R, PLANET_COUNT, start_angle=90.0) - ] - carrier = plate + [hub, *pins] - lighten = [ - Rotation(0, 0, az) - * Pos(CARRIER_LIGHTEN_CIRCLE_R, 0, 0) - * _cyl_z(CARRIER_LIGHTEN_HOLE_R, (CARRIER_PLATE_Z[0] - 0.1, CARRIER_PLATE_Z[1] + 0.1)) - for az in CARRIER_LIGHTEN_ANGLES - ] - return carrier - lighten - - -def make_cross_roller_bearing() -> dict[str, object]: - """Cross-roller output bearing: V-groove races, alternating 45-deg rollers.""" - z_mid = (XROLLER_Z[0] + XROLLER_Z[1]) / 2 - groove_profile = Plane.XZ * ( - Pos(XROLLER_PITCH_R, z_mid) * Rotation(0, 0, 45) * Rectangle(XROLLER_GROOVE_SQ, XROLLER_GROOVE_SQ) - ) - groove = revolve(groove_profile, axis=Axis.Z) - inner = _tube_z(XROLLER_INNER_R[1], XROLLER_INNER_R[0], XROLLER_Z) - groove - outer = _tube_z(XROLLER_OUTER_R[1], XROLLER_OUTER_R[0], XROLLER_Z) - groove - - rollers = [] - for i in range(XROLLER_COUNT): - az = 360.0 * i / XROLLER_COUNT - tilt = 45.0 if i % 2 == 0 else -45.0 - rollers.append( - Rotation(0, 0, az) - * Pos(XROLLER_PITCH_R, 0, z_mid) - * Rotation(0, tilt, 0) - * Cylinder(XROLLER_ROLLER_R, XROLLER_ROLLER_L) - ) - return {"inner": inner, "outer": outer, "rollers": rollers} - - -def make_torque_sensor() -> dict[str, object]: - """Spoked torque-sensing flexure with strain-gauge pads.""" - hub = _tube_z(FLEXURE_HUB_R[1], FLEXURE_HUB_R[0], FLEXURE_HUB_Z) - rim = _tube_z(FLEXURE_RIM_R[1], FLEXURE_RIM_R[0], FLEXURE_RIM_Z) - spoke_mid_r = (FLEXURE_HUB_R[1] + FLEXURE_RIM_R[0]) / 2 - spoke = Pos(spoke_mid_r, 0, (FLEXURE_SPOKE_Z[0] + FLEXURE_SPOKE_Z[1]) / 2) * Box( - FLEXURE_SPOKE_L, FLEXURE_SPOKE_W, FLEXURE_SPOKE_Z[1] - FLEXURE_SPOKE_Z[0] - ) - spokes = [rot * spoke for rot in PolarLocations(0, FLEXURE_SPOKE_COUNT)] - flexure = hub + [rim, *spokes] - gauges = [ - rot * Pos(spoke_mid_r, 0, GAUGE_Z + GAUGE_SIZE[2] / 2) * Box(*GAUGE_SIZE) - for rot in PolarLocations(0, FLEXURE_SPOKE_COUNT) - ] - return {"flexure": flexure, "gauges": gauges} - - -def make_output_flange() -> Part: - """Output flange: bolt circle, pilot boss, hollow bore.""" - plate = _tube_z(FLANGE_PLATE_R[1], FLANGE_PLATE_R[0], FLANGE_PLATE_Z) - boss = _tube_z(FLANGE_BOSS_R[1], FLANGE_BOSS_R[0], FLANGE_BOSS_Z) - flange = plate + boss - cuts = [ - loc * _cyl_z(FLANGE_BOLT_R, (FLANGE_PLATE_Z[0] - 0.1, FLANGE_PLATE_Z[1] + 0.1)) - for loc in PolarLocations(FLANGE_BOLT_CIRCLE_R, FLANGE_BOLT_COUNT) - ] - cuts += [ - loc * _cyl_z(FLANGE_PILOT_HOLE_R, (FLANGE_PLATE_Z[0] + 0.5, FLANGE_BOSS_Z[1] + 0.1)) - for loc in PolarLocations(FLANGE_PILOT_CIRCLE_R, 4, start_angle=45.0) - ] - return flange - cuts - - -def make_cable_tube() -> Part: - """Static hollow cable-routing tube through the full actuator stack.""" - return _tube_z(TUBE_OUTER_R, TUBE_INNER_R, TUBE_Z) - - -def build_actuator(explode: float = 0.0) -> Compound: - """Build the labeled actuator assembly. - - explode=0 returns the assembled model with source mate frames/joints. - explode>0 translates each part along Z by EXPLODE_OFFSETS * explode as a - documented static exploded layout (joints are omitted in that mode). - """ - asm = AssemblyHelper("qdd_actuator") - - housing = asm.add(make_housing(), "housing", color=AL_BODY) - retainer = asm.add(make_front_retainer(), "front_retainer", color=AL_RETAINER) - cover = asm.add(make_rear_cover(), "rear_cover", color=AL_COVER) - - screws = make_retainer_screws() - retainer_screws = asm.add( - Compound(children=screws), "retainer_screws", color=BLACK_OXIDE - ) - - connectors = {} - for kind in CONNECTOR_ANGLES: - body, collar, o_ring, pins = make_connector(kind) - asm.feature(body, "connector_body", kind, color=PLASTIC_BLACK) - asm.feature(collar, "connector_collar", kind, color=GUNMETAL) - asm.feature(o_ring, "connector_o_ring", kind, color=RUBBER) - asm.feature(pins, "connector_pins", kind, color=PIN_GOLD) - connectors[kind] = asm.add_module("connector", [body, collar, o_ring, pins], kind) - - drv = make_driver_pcb() - asm.feature(drv["board"], "driver_board", color=PCB_GREEN) - for i, fet in enumerate(drv["fets"], start=1): - asm.feature(fet, "fet", f"q{i}", color=IC_BLACK) - for i, cap in enumerate(drv["caps"], start=1): - asm.feature(cap, "bus_capacitor", f"c{i}", color=CAP_GRAY) - asm.feature(drv["mcu"], "mcu", color=IC_BLACK) - driver_pcb = asm.add_module( - "driver_pcb", [drv["board"], *drv["fets"], *drv["caps"], drv["mcu"]] - ) - - enc = make_encoder_pcb() - asm.feature(enc["board"], "encoder_board", color=PCB_GREEN) - asm.feature(enc["chip"], "encoder_ic", color=IC_BLACK) - encoder_pcb = asm.add_module("encoder_pcb", [enc["board"], enc["chip"]]) - - enc_magnet = asm.add( - _tube_z(ENC_MAGNET_R[1], ENC_MAGNET_R[0], ENC_MAGNET_Z), - "encoder_magnet_ring", - color=FERRITE, - ) - - st = make_stator() - asm.feature(st["stack"], "lamination_stack", color=LAMINATION) - winding_compound = asm.feature(Compound(children=st["windings"]), "windings", color=COPPER) - stator = asm.add_module("stator", [st["stack"], winding_compound]) - - rt = make_rotor() - asm.feature(rt["body"], "rotor_bell", color=ROTOR_DARK) - mag_n = asm.feature(Compound(children=rt["magnets_north"]), "magnets_north", color=MAGNET_N) - mag_s = asm.feature(Compound(children=rt["magnets_south"]), "magnets_south", color=MAGNET_S) - rotor = asm.add_module("rotor", [rt["body"], mag_n, mag_s]) - - bearings = {} - for name, z in (("rear", ROTOR_BRG_REAR_Z), ("front", ROTOR_BRG_FRONT_Z)): - brg = make_ball_bearing(z) - asm.feature(brg["inner"], "inner_race", name, color=STEEL_POLISHED) - asm.feature(brg["outer"], "outer_race", name, color=STEEL_POLISHED) - balls = asm.feature(Compound(children=brg["balls"]), "balls", name, color=STEEL_ROLLER) - bearings[name] = asm.add_module( - "rotor_bearing", [brg["inner"], brg["outer"], balls], name - ) - - sun = asm.add(make_sun_gear(), "sun_gear", color=STEEL) - ring = asm.add(Pos(0, 0, GEAR_Z[0]) * _internal_ring_gear(), "ring_gear", color=STEEL) - - planet_proto = make_planet_gear() - planets = [] - for i, psi in enumerate(PLANET_ANGLES_DEG, start=1): - planet = planet_location(psi) * planet_proto - planets.append(asm.add(planet, "planet_gear", f"p{i}", color=STEEL)) - - carrier = asm.add(make_carrier(), "planet_carrier", color=AL_BRIGHT) - - xr = make_cross_roller_bearing() - asm.feature(xr["inner"], "inner_ring", color=STEEL_RACE) - asm.feature(xr["outer"], "outer_ring", color=STEEL_RACE) - rollers = asm.feature(Compound(children=xr["rollers"]), "rollers", color=STEEL_ROLLER) - xroller = asm.add_module("cross_roller_bearing", [xr["inner"], xr["outer"], rollers]) - - ts = make_torque_sensor() - asm.feature(ts["flexure"], "flexure_disc", color=TITANIUM) - for i, gauge in enumerate(ts["gauges"], start=1): - asm.feature(gauge, "strain_gauge", f"g{i}", color=GAUGE_GOLD) - torque_sensor = asm.add_module("torque_sensor", [ts["flexure"], *ts["gauges"]]) - - flange = asm.add(make_output_flange(), "output_flange", color=AL_BRIGHT) - tube = asm.add(make_cable_tube(), "cable_tube", color=TUBE_BLACK) - - by_name = { - "housing": housing, - "front_retainer": retainer, - "retainer_screws": retainer_screws, - "rear_cover": cover, - "connector_power": connectors["power"], - "connector_signal": connectors["signal"], - "driver_pcb": driver_pcb, - "encoder_pcb": encoder_pcb, - "encoder_magnet_ring": enc_magnet, - "stator": stator, - "rotor": rotor, - "rotor_bearing_rear": bearings["rear"], - "rotor_bearing_front": bearings["front"], - "sun_gear": sun, - "ring_gear": ring, - "planet_carrier": carrier, - "cross_roller_bearing": xroller, - "torque_sensor": torque_sensor, - "output_flange": flange, - "cable_tube": tube, - } - - if explode: - for name, part in by_name.items(): - offset = EXPLODE_OFFSETS.get(name, 0.0) * explode - if offset: - part.move(Location((0, 0, offset))) - for psi, planet in zip(PLANET_ANGLES_DEG, planets): - dx = PLANET_EXPLODE_RADIAL * explode * math.cos(math.radians(psi)) - dy = PLANET_EXPLODE_RADIAL * explode * math.sin(math.radians(psi)) - planet.move(Location((dx, dy, EXPLODE_OFFSETS["planet_gear"] * explode))) - else: - # Source mate frames + rigid connections (all identity by construction; - # connect() re-derives placement from the named datums and records the - # relationships for the exported topology). - seat = asm.rigid_frame(housing, "stator_seat", Location((0, 0, STATOR_STACK_Z[0]))) - asm.face_to_face(seat, asm.rigid_frame(stator, "stack_rear", Location((0, 0, STATOR_STACK_Z[0])))) - - ring_seat = asm.rigid_frame(housing, "ring_gear_seat", Location((0, 0, GEAR_Z[0]))) - asm.face_to_face(ring_seat, asm.rigid_frame(ring, "rim_rear", Location((0, 0, GEAR_Z[0])))) - - cover_seat = asm.rigid_frame(housing, "cover_seat", Location((0, 0, BARREL_Z[0]))) - asm.face_to_face(cover_seat, asm.rigid_frame(cover, "lip_face", Location((0, 0, BARREL_Z[0])))) - - retainer_seat = asm.rigid_frame(housing, "retainer_seat", Location((0, 0, RETAINER_Z[0]))) - asm.face_to_face( - retainer_seat, asm.rigid_frame(retainer, "ring_rear", Location((0, 0, RETAINER_Z[0]))) - ) - screw_seat = asm.rigid_frame(retainer, "screw_seats", Location((0, 0, RETAINER_Z[1]))) - asm.face_to_face( - screw_seat, - asm.rigid_frame(retainer_screws, "head_seats", Location((0, 0, RETAINER_Z[1]))), - ) - - for kind, module in connectors.items(): - az = math.radians(CONNECTOR_ANGLES[kind]) - mount = Location( - ( - CONNECTOR_CIRCLE_R * math.cos(az), - CONNECTOR_CIRCLE_R * math.sin(az), - COVER_PLATE_Z[0], - ) - ) - pad = asm.rigid_frame(cover, f"{kind}_pad", mount) - asm.face_to_face(pad, asm.rigid_frame(module, "mount_face", mount)) - - for name, module, z_seat in ( - ("driver_pcb", driver_pcb, DRV_STANDOFF_Z[0]), - ("encoder_pcb", encoder_pcb, ENC_STANDOFF_Z[0]), - ): - seat_frame = asm.rigid_frame(housing, f"{name}_seat", Location((0, 0, z_seat))) - asm.face_to_face( - seat_frame, asm.rigid_frame(module, "mount_plane", Location((0, 0, z_seat))) - ) - - coupling = asm.rigid_frame(rotor, "sun_coupling", Location((0, 0, ROTOR_FLANGE_Z[1]))) - asm.face_to_face( - coupling, asm.rigid_frame(sun, "flange_rear", Location((0, 0, ROTOR_FLANGE_Z[1]))) - ) - magnet_seat = asm.rigid_frame(rotor, "encoder_magnet_seat", Location((0, 0, ENC_MAGNET_Z[0]))) - asm.face_to_face( - magnet_seat, - asm.rigid_frame(enc_magnet, "ring_rear", Location((0, 0, ENC_MAGNET_Z[0]))), - ) - - sensor_seat = asm.rigid_frame(carrier, "sensor_seat", Location((0, 0, FLEXURE_HUB_Z[0]))) - asm.face_to_face( - sensor_seat, - asm.rigid_frame(torque_sensor, "hub_rear", Location((0, 0, FLEXURE_HUB_Z[0]))), - ) - rim_face = asm.rigid_frame(torque_sensor, "rim_face", Location((0, 0, FLEXURE_RIM_Z[1]))) - asm.face_to_face( - rim_face, asm.rigid_frame(flange, "mount_rear", Location((0, 0, FLEXURE_RIM_Z[1]))) - ) - - xr_seat = asm.rigid_frame(housing, "bearing_seat", Location((0, 0, XROLLER_Z[0]))) - asm.face_to_face( - xr_seat, asm.rigid_frame(xroller, "outer_ring_rear", Location((0, 0, XROLLER_Z[0]))) - ) - - tube_bore = asm.rigid_frame(cover, "tube_bore", Location((0, 0, COVER_PLATE_Z[0]))) - asm.face_to_face( - tube_bore, asm.rigid_frame(tube, "rear_end", Location((0, 0, COVER_PLATE_Z[0]))) - ) - - # Motion datums (joint frames only; static pose is authored explicitly): - asm.revolute_frame(housing, "rotor_axis", Axis((0, 0, 0), (0, 0, 1))) - asm.revolute_frame(housing, "output_axis", Axis((0, 0, XROLLER_Z[1]), (0, 0, 1))) - - return asm.build() - - -def gen_step(): - return build_actuator(explode=0.0) diff --git a/models/robots/qdd_actuator/qdd_actuator.step b/models/robots/qdd_actuator/qdd_actuator.step deleted file mode 100644 index ce1f5b1d..00000000 --- a/models/robots/qdd_actuator/qdd_actuator.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:717f22d6991f88bdbb59900cc7917eb0d71e743d3a895df490749e6b4dc2ffdc -size 6216601 diff --git a/models/robots/so101/meshes/base_motor_holder_so101_v1.stl b/models/robots/so101/meshes/base_motor_holder_so101_v1.stl deleted file mode 100644 index ac9c3807..00000000 --- a/models/robots/so101/meshes/base_motor_holder_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cd2f241037ea377af1191fffe0dd9d9006beea6dcc48543660ed41647072424 -size 1877084 diff --git a/models/robots/so101/meshes/base_so101_v2.stl b/models/robots/so101/meshes/base_so101_v2.stl deleted file mode 100644 index 503d30be..00000000 --- a/models/robots/so101/meshes/base_so101_v2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb12b7026575e1f70ccc7240051f9d943553bf34e5128537de6cd86fae33924d -size 471584 diff --git a/models/robots/so101/meshes/motor_holder_so101_base_v1.stl b/models/robots/so101/meshes/motor_holder_so101_base_v1.stl deleted file mode 100644 index f8e3d75c..00000000 --- a/models/robots/so101/meshes/motor_holder_so101_base_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:31242ae6fb59d8b15c66617b88ad8e9bded62d57c35d11c0c43a70d2f4caa95b -size 1129384 diff --git a/models/robots/so101/meshes/motor_holder_so101_wrist_v1.stl b/models/robots/so101/meshes/motor_holder_so101_wrist_v1.stl deleted file mode 100644 index e55b7194..00000000 --- a/models/robots/so101/meshes/motor_holder_so101_wrist_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:887f92e6013cb64ea3a1ab8675e92da1e0beacfd5e001f972523540545e08011 -size 1052184 diff --git a/models/robots/so101/meshes/moving_jaw_so101_v1.stl b/models/robots/so101/meshes/moving_jaw_so101_v1.stl deleted file mode 100644 index eb17d253..00000000 --- a/models/robots/so101/meshes/moving_jaw_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:785a9dded2f474bc1d869e0d3dae398a3dcd9c0c345640040472210d2861fa9d -size 1413584 diff --git a/models/robots/so101/meshes/rotation_pitch_so101_v1.stl b/models/robots/so101/meshes/rotation_pitch_so101_v1.stl deleted file mode 100644 index b536cb41..00000000 --- a/models/robots/so101/meshes/rotation_pitch_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9be900cc2a2bf718102841ef82ef8d2873842427648092c8ed2ca1e2ef4ffa34 -size 883684 diff --git a/models/robots/so101/meshes/sts3215_03a_no_horn_v1.stl b/models/robots/so101/meshes/sts3215_03a_no_horn_v1.stl deleted file mode 100644 index 18e93356..00000000 --- a/models/robots/so101/meshes/sts3215_03a_no_horn_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75ef3781b752e4065891aea855e34dc161a38a549549cd0970cedd07eae6f887 -size 865884 diff --git a/models/robots/so101/meshes/sts3215_03a_v1.stl b/models/robots/so101/meshes/sts3215_03a_v1.stl deleted file mode 100644 index a14c57b9..00000000 --- a/models/robots/so101/meshes/sts3215_03a_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a37c871fb502483ab96c256baf457d36f2e97afc9205313d9c5ab275ef941cd0 -size 954084 diff --git a/models/robots/so101/meshes/under_arm_so101_v1.stl b/models/robots/so101/meshes/under_arm_so101_v1.stl deleted file mode 100644 index 47b611ef..00000000 --- a/models/robots/so101/meshes/under_arm_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d01d1f2de365651dcad9d6669e94ff87ff7652b5bb2d10752a66a456a86dbc71 -size 1975884 diff --git a/models/robots/so101/meshes/upper_arm_so101_v1.stl b/models/robots/so101/meshes/upper_arm_so101_v1.stl deleted file mode 100644 index 8832740f..00000000 --- a/models/robots/so101/meshes/upper_arm_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:475056e03a17e71919b82fd88ab9a0b898ab50164f2a7943652a6b2941bb2d4f -size 1303484 diff --git a/models/robots/so101/meshes/waveshare_mounting_plate_so101_v2.stl b/models/robots/so101/meshes/waveshare_mounting_plate_so101_v2.stl deleted file mode 100644 index e0d90d5b..00000000 --- a/models/robots/so101/meshes/waveshare_mounting_plate_so101_v2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e197e24005a07d01bbc06a8c42311664eaeda415bf859f68fa247884d0f1a6e9 -size 62784 diff --git a/models/robots/so101/meshes/wrist_roll_follower_so101_v1.stl b/models/robots/so101/meshes/wrist_roll_follower_so101_v1.stl deleted file mode 100644 index 9a5fa8fe..00000000 --- a/models/robots/so101/meshes/wrist_roll_follower_so101_v1.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b17b410a12d64ec39554abc3e8054d8a97384b2dc4a8d95a5ecb2a93670f5f4 -size 1439884 diff --git a/models/robots/so101/meshes/wrist_roll_pitch_so101_v2.stl b/models/robots/so101/meshes/wrist_roll_pitch_so101_v2.stl deleted file mode 100644 index 2f531712..00000000 --- a/models/robots/so101/meshes/wrist_roll_pitch_so101_v2.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c7ec5525b4d8b9e397a30ab4bb0037156a5d5f38a4adf2c7d943d6c56eda5ae -size 2699784 diff --git a/models/robots/so101/so101.srdf b/models/robots/so101/so101.srdf deleted file mode 100644 index 7602e22f..00000000 --- a/models/robots/so101/so101.srdf +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/so101/so101.urdf b/models/robots/so101/so101.urdf deleted file mode 100644 index 78db31cb..00000000 --- a/models/robots/so101/so101.urdf +++ /dev/null @@ -1,395 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - - - - - - - - - transmission_interface/SimpleTransmission - - hardware_interface/PositionJointInterface - - - hardware_interface/PositionJointInterface - 1 - - - diff --git a/models/robots/tom/robot_common/__init__.py b/models/robots/tom/robot_common/__init__.py deleted file mode 100644 index c0a98808..00000000 --- a/models/robots/tom/robot_common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Shared CAD generator helpers.""" diff --git a/models/robots/tom/robot_common/keyed_connection.py b/models/robots/tom/robot_common/keyed_connection.py deleted file mode 100644 index 1c169afc..00000000 --- a/models/robots/tom/robot_common/keyed_connection.py +++ /dev/null @@ -1,232 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -import math - -import build123d - - -# Extracted from the legacy keyed plate profile. -# Coordinates are relative to the cut center in an arbitrary in-plane basis `(u, v)`. - - -@dataclass(frozen=True) -class KeyedConnectionArcSpec: - start_u: float - start_v: float - mid_u: float - mid_v: float - end_u: float - end_v: float - - -KEYED_CONNECTION_REF = "legacy keyed plate profile" -KEYED_CONNECTION_ROTATION_DEG = 45.0 - -KEYED_CONNECTION_ARC_SPECS_MM: tuple[KeyedConnectionArcSpec, ...] = ( - KeyedConnectionArcSpec(-2.634358, -4.828056, 0.0, -5.499997, 2.634358, -4.828056), - KeyedConnectionArcSpec(2.634358, -4.828056, 3.095745, -4.818908, 3.368107, -5.19144), - KeyedConnectionArcSpec(3.368107, -5.19144, 6.081117, -6.081114, 5.191444, -3.368104), - KeyedConnectionArcSpec(5.191444, -3.368104, 4.818911, -3.095741, 4.82806, -2.634354), - KeyedConnectionArcSpec(4.82806, -2.634354, 5.5, 0.000004, 4.82806, 2.634361), - KeyedConnectionArcSpec(4.82806, 2.634361, 4.818911, 3.095748, 5.191444, 3.36811), - KeyedConnectionArcSpec(5.191444, 3.36811, 6.081117, 6.08112, 3.368107, 5.191447), - KeyedConnectionArcSpec(3.368107, 5.191447, 3.095745, 4.818915, 2.634358, 4.828063), - KeyedConnectionArcSpec(2.634358, 4.828063, 0.0, 5.500003, -2.634358, 4.828063), - KeyedConnectionArcSpec(-2.634358, 4.828063, -3.095745, 4.818915, -3.368107, 5.191447), - KeyedConnectionArcSpec(-3.368107, 5.191447, -6.081117, 6.08112, -5.191444, 3.36811), - KeyedConnectionArcSpec(-5.191444, 3.36811, -4.818911, 3.095748, -4.82806, 2.634361), - KeyedConnectionArcSpec(-4.82806, 2.634361, -5.5, 0.000004, -4.82806, -2.634354), - KeyedConnectionArcSpec(-4.82806, -2.634354, -4.818911, -3.095741, -5.191444, -3.368104), - KeyedConnectionArcSpec(-5.191444, -3.368104, -6.081117, -6.081114, -3.368107, -5.19144), - KeyedConnectionArcSpec(-3.368107, -5.19144, -3.095745, -4.818908, -2.634358, -4.828056), -) - -# Conservative exact-profile half-span for bend-clearance checks. The exported -# helper rotates this profile 45 degrees by default to match the plate cut -# orientation used across the current parts. -KEYED_CONNECTION_HALF_SPAN_MM = 6.549746 - - -def _to_global_point( - *, - origin: build123d.Vector, - u_dir: build123d.Vector, - v_dir: build123d.Vector, - u: float, - v: float, -) -> build123d.Vector: - return build123d.Vector( - origin.X + (u * u_dir.X) + (v * v_dir.X), - origin.Y + (u * u_dir.Y) + (v * v_dir.Y), - origin.Z + (u * u_dir.Z) + (v * v_dir.Z), - ) - - -def _rotated_in_plane_dirs( - *, - u_dir: build123d.Vector, - v_dir: build123d.Vector, - rotation_degrees: float, -) -> tuple[build123d.Vector, build123d.Vector]: - if abs(rotation_degrees) <= 1e-9: - return u_dir, v_dir - - rotation_rad = math.radians(rotation_degrees) - cos_theta = math.cos(rotation_rad) - sin_theta = math.sin(rotation_rad) - rotated_u = (u_dir * cos_theta) + (v_dir * sin_theta) - rotated_v = (v_dir * cos_theta) - (u_dir * sin_theta) - return rotated_u, rotated_v - - -def make_keyed_connection_wire( - *, - origin: build123d.Vector, - u_dir: build123d.Vector, - v_dir: build123d.Vector, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Wire: - rotated_u_dir, rotated_v_dir = _rotated_in_plane_dirs( - u_dir=u_dir, - v_dir=v_dir, - rotation_degrees=rotation_degrees, - ) - edges = [ - build123d.Edge.make_three_point_arc( - _to_global_point(origin=origin, u_dir=rotated_u_dir, v_dir=rotated_v_dir, u=spec.start_u, v=spec.start_v), - _to_global_point(origin=origin, u_dir=rotated_u_dir, v_dir=rotated_v_dir, u=spec.mid_u, v=spec.mid_v), - _to_global_point(origin=origin, u_dir=rotated_u_dir, v_dir=rotated_v_dir, u=spec.end_u, v=spec.end_v), - ) - for spec in KEYED_CONNECTION_ARC_SPECS_MM - ] - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single keyed-connection wire, found {len(wires)}") - return wires[0] - - -def make_keyed_connection_face( - *, - origin: build123d.Vector, - u_dir: build123d.Vector, - v_dir: build123d.Vector, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Face: - return build123d.Face( - make_keyed_connection_wire( - origin=origin, - u_dir=u_dir, - v_dir=v_dir, - rotation_degrees=rotation_degrees, - ) - ) - - -def make_keyed_connection_y_aligned_cutter( - *, - center: build123d.Vector, - start_y: float, - direction_y: float, - thickness_mm: float, - cut_extension_mm: float, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Solid: - length = thickness_mm + (2.0 * cut_extension_mm) - face = make_keyed_connection_face( - origin=build123d.Vector(center.X, start_y, center.Z), - u_dir=build123d.Vector(1.0, 0.0, 0.0), - v_dir=build123d.Vector(0.0, 0.0, 1.0), - rotation_degrees=rotation_degrees, - ) - return build123d.Solid.extrude(face, build123d.Vector(0.0, direction_y * length, 0.0)) - - -def make_keyed_connection_x_aligned_cutter( - *, - center: build123d.Vector, - start_x: float, - direction_x: float, - thickness_mm: float, - cut_extension_mm: float, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Solid: - length = thickness_mm + (2.0 * cut_extension_mm) - face = make_keyed_connection_face( - origin=build123d.Vector(start_x, center.Y, center.Z), - u_dir=build123d.Vector(0.0, 1.0, 0.0), - v_dir=build123d.Vector(0.0, 0.0, 1.0), - rotation_degrees=rotation_degrees, - ) - return build123d.Solid.extrude(face, build123d.Vector(direction_x * length, 0.0, 0.0)) - - -def cut_keyed_connection_y_aligned( - shape: build123d.Shape, - *, - center: build123d.Vector, - start_y: float, - direction_y: float, - thickness_mm: float, - cut_extension_mm: float, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Shape: - return shape.cut( - make_keyed_connection_y_aligned_cutter( - center=center, - start_y=start_y, - direction_y=direction_y, - thickness_mm=thickness_mm, - cut_extension_mm=cut_extension_mm, - rotation_degrees=rotation_degrees, - ) - ) - - -def cut_keyed_connection_x_aligned( - shape: build123d.Shape, - *, - center: build123d.Vector, - start_x: float, - direction_x: float, - thickness_mm: float, - cut_extension_mm: float, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> build123d.Shape: - return shape.cut( - make_keyed_connection_x_aligned_cutter( - center=center, - start_x=start_x, - direction_x=direction_x, - thickness_mm=thickness_mm, - cut_extension_mm=cut_extension_mm, - rotation_degrees=rotation_degrees, - ) - ) - - -def sample_keyed_connection_outline( - *, - center_u: float, - center_v: float, - points_per_arc: int = 16, - rotation_degrees: float = KEYED_CONNECTION_ROTATION_DEG, -) -> list[tuple[float, float]]: - if points_per_arc < 2: - raise ValueError("points_per_arc must be at least 2") - - wire = make_keyed_connection_wire( - origin=build123d.Vector(center_u, center_v, 0.0), - u_dir=build123d.Vector(1.0, 0.0, 0.0), - v_dir=build123d.Vector(0.0, 1.0, 0.0), - rotation_degrees=rotation_degrees, - ) - points: list[tuple[float, float]] = [] - for edge_index, edge in enumerate(wire.edges()): - start_step = 0 if edge_index == 0 else 1 - for step in range(start_step, points_per_arc): - point = edge.position_at(step / (points_per_arc - 1)) - points.append((point.X, point.Y)) - if len(points) > 1 and math.dist(points[0], points[-1]) <= 1e-9: - points.pop() - return points diff --git a/models/robots/tom/robot_common/materials.py b/models/robots/tom/robot_common/materials.py deleted file mode 100644 index 135a1aea..00000000 --- a/models/robots/tom/robot_common/materials.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import build123d - - -BLACK_ALUMINUM_RGBA = (0.168627, 0.184314, 0.2, 1.0) -GRAY_ALUMINUM_RGBA = (0.643137, 0.670588, 0.701961, 1.0) -MOUNT_PLATE_RGBA = (0.831373, 0.517647, 0.329412, 1.0) - -BLACK_ALUMINUM_COLOR = build123d.Color(*BLACK_ALUMINUM_RGBA) -GRAY_ALUMINUM_COLOR = build123d.Color(*GRAY_ALUMINUM_RGBA) -MOUNT_PLATE_COLOR = build123d.Color(*MOUNT_PLATE_RGBA) diff --git a/models/robots/tom/robot_common/robot_arm.py b/models/robots/tom/robot_common/robot_arm.py deleted file mode 100644 index b0bbdbb6..00000000 --- a/models/robots/tom/robot_common/robot_arm.py +++ /dev/null @@ -1,2135 +0,0 @@ -from __future__ import annotations - -from copy import deepcopy -import math -from pathlib import PurePosixPath -import xml.etree.ElementTree as ET - -from robot_common.materials import BLACK_ALUMINUM_RGBA -from v1.assemblies.base_link import ( - STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM as BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, -) - - -EXTRUSION_2020_LENGTH_MM = 100.0 -SECONDARY_EXTRUSION_2020_LENGTH_MM = 150.0 -SECONDARY_EXTRUSION_EXTENSION_MM = SECONDARY_EXTRUSION_2020_LENGTH_MM - EXTRUSION_2020_LENGTH_MM -EXTRUSION_BRACKET_INSERTION_THICKNESS_MM = 1.6 -HORN_YOKE_SWEEP_LIMIT_DEG = 135.0 -HORN_YOKE_SWEEP_LIMIT_RAD = math.radians(HORN_YOKE_SWEEP_LIMIT_DEG) -BASE_PLATE_BOTTOM_LOCAL_Y_MM = -32.9752 -ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM = 2.7308 -KGF_CM_TO_NM = 0.0980665 - -# Mass and effort source values verified against online vendor/project data on -# 2026-04-27: -# - STS3250: https://www.feetechrc.com/en/562636.html -# - STS3215: https://www.feetechrc.com/525603.html -# - HFS5-2020: https://misumi.partcommunity.com/3d-cad-models/hfsmg5-2020-hfsfmg5-2020-hfs5-series-curved-extrusions-misumi -# - 5052-H32: https://sendcutsend.com/materials/5052-aluminum/ -# - Robonine gripper: https://github.com/roboninecom/SO-ARM100-101-Parallel-Gripper -STS3250_MASS_KG = 0.0745 -STS3215_MASS_KG = 0.055 -STS3250_RATED_TORQUE_NM = 16.0 * KGF_CM_TO_NM -STS3215_RATED_TORQUE_NM = 10.0 * KGF_CM_TO_NM -STS3250_PEAK_STALL_TORQUE_NM = 50.0 * KGF_CM_TO_NM -STS3215_STALL_TORQUE_NM = 30.0 * KGF_CM_TO_NM -HFS5_2020_MASS_KG_PER_M = 0.5 -HFS5_2020_100_MASS_KG = HFS5_2020_MASS_KG_PER_M * 0.100 -HFS5_2020_150_MASS_KG = HFS5_2020_MASS_KG_PER_M * 0.150 -ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 = 0.097 * 0.45359237 / (0.0254**3) / 1e9 -BASE_PLATE_MASS_KG = 5745.825053237387 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 -SERVO_END_MOUNT_MASS_KG = 1934.588542178479 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 -SERVO_HORN_YOKE_MASS_KG = 4126.128540235039 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 -EXTRUSION_BRACKET_2020_MASS_KG = 2335.413008330946 * ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 -GRIPPER_ASSEMBLY_MASS_KG = 0.250 -STS3250_JOINT_EFFORT_TEXT = f"{STS3250_RATED_TORQUE_NM:.6f}" -STS3215_JOINT_EFFORT_TEXT = f"{STS3215_RATED_TORQUE_NM:.6f}" -GRIPPER_SERVO_EFFORT_TEXT = f"{STS3215_STALL_TORQUE_NM:.6f}" - - -def _translate_instance_world_vector( - instance: dict[str, object], - *, - vector_xyz: tuple[float, float, float], - distance_mm: float, -) -> dict[str, object]: - transform = [float(value) for value in instance["transform"]] - transform[3] += vector_xyz[0] * distance_mm - transform[7] += vector_xyz[1] * distance_mm - transform[11] += vector_xyz[2] * distance_mm - return {**instance, "transform": transform} - - -def _instance_local_axis_world_vector(instance: dict[str, object], *, axis: str) -> tuple[float, float, float]: - transform = [float(value) for value in instance["transform"]] - return _transform_local_axis_world_vector(transform, axis=axis) - - -def _transform_local_axis_world_vector(transform: list[float], *, axis: str) -> tuple[float, float, float]: - axis_column = {"x": 0, "y": 1, "z": 2}[axis] - return ( - transform[axis_column], - transform[4 + axis_column], - transform[8 + axis_column], - ) - - -def _find_named_instance(instances: list[dict[str, object]], name: str) -> dict[str, object]: - for instance in instances: - if str(instance["name"]) == name: - return instance - raise RuntimeError(f"Missing assembly instance {name!r}") - - -def _translate_named_instances_world_vector( - instances: list[dict[str, object]], - names: frozenset[str], - *, - vector_xyz: tuple[float, float, float], - distance_mm: float, -) -> None: - for index, instance in enumerate(instances): - if str(instance["name"]) in names: - instances[index] = _translate_instance_world_vector( - instance, - vector_xyz=vector_xyz, - distance_mm=distance_mm, - ) - - -def _replace_named_instance( - instances: list[dict[str, object]], - *, - name: str, - replacement: dict[str, object], -) -> None: - for index, instance in enumerate(instances): - if str(instance["name"]) == name: - instances[index] = replacement - return - raise RuntimeError(f"Missing assembly instance {name!r}") - - -def _base_ground_y_offset_mm() -> float: - base_plate = _find_named_instance(ASSEMBLY_INSTANCES, "base_plate") - base_plate_transform = [float(value) for value in base_plate["transform"]] - return -(base_plate_transform[7] + BASE_PLATE_BOTTOM_LOCAL_Y_MM) - - -def _grounded_top_level_transform( - transform: list[float], - *, - y_offset_mm: float = 0.0, -) -> list[float]: - grounded = [float(value) for value in transform] - grounded[7] += _base_ground_y_offset_mm() + y_offset_mm - return grounded - - -def _y_up_to_z_up_transform(transform: list[float]) -> list[float]: - # Rotate exported top-level assembly placements so old +Y becomes standard +Z. - source = [float(value) for value in transform] - return [ - source[0], source[1], source[2], source[3], - -source[8], -source[9], -source[10], -source[11], - source[4], source[5], source[6], source[7], - 0.0, 0.0, 0.0, 1.0, - ] - - -def _standard_ground_top_level_transform( - transform: list[float], - *, - y_offset_mm: float = 0.0, -) -> list[float]: - return _y_up_to_z_up_transform( - _grounded_top_level_transform(transform, y_offset_mm=y_offset_mm) - ) - - -def _translate_transform_world_offset( - transform: list[float], - offset_xyz_mm: tuple[float, float, float], -) -> list[float]: - translated = [float(value) for value in transform] - translated[3] += offset_xyz_mm[0] - translated[7] += offset_xyz_mm[1] - translated[11] += offset_xyz_mm[2] - return translated - - -def _insert_named_instance_before( - instances: list[dict[str, object]], - *, - before_name: str, - instance: dict[str, object], -) -> None: - for index, current in enumerate(instances): - if str(current["name"]) == before_name: - instances.insert(index, instance) - return - raise RuntimeError(f"Missing assembly instance {before_name!r}") - - -def _extrusion_bracket_transform_from_extrusion( - extrusion_instance: dict[str, object], -) -> list[float]: - transform = [float(value) for value in extrusion_instance["transform"]] - # The wrap bracket uses the extrusion's frame, flipped 180 degrees around its - # local x axis so the horn plate sits at the extrusion start plane. - for index in (1, 5, 9, 2, 6, 10): - transform[index] = -transform[index] - return transform - - -def _extrusion_end_bracket_transform_from_extrusion( - extrusion_instance: dict[str, object], - *, - length_mm: float, -) -> list[float]: - transform = [float(value) for value in extrusion_instance["transform"]] - # Match the extrusion frame at the downstream end plane so the bracket wraps - # the far end without rotating the existing downstream chain. - extrusion_z_world = _instance_local_axis_world_vector(extrusion_instance, axis="z") - transform[3] += extrusion_z_world[0] * -length_mm - transform[7] += extrusion_z_world[1] * -length_mm - transform[11] += extrusion_z_world[2] * -length_mm - return transform - - -ASSEMBLY_INSTANCES = [{'path': 'imports/sts3250.step', - 'name': 'sts3250_1', - 'transform': [0.0, - 1.0, - 0.0, - -1.264258, - -1.0, - 0.0, - 0.0, - 52.610399203721, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_horn_yoke.step', - 'name': 'servo_horn_yoke', - 'transform': [0.0, - 1.0, - 0.0, - -1.264258, - -1.0, - 0.0, - 0.0, - 52.610399203721, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3250.step', - 'name': 'sts3250_2', - 'transform': [1.0, - 0.0, - 0.0, - 15.135742000006, - 0.0, - 1.0, - 0.0, - 134.620798778311, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'extrusion_bracket_2020.step', - 'name': 'secondary_servo_2020_connector', - 'transform': [1.0, - 0.0, - 0.0, - -10.364257999994, - 0.0, - 1.0, - 0.0, - 145.850798778311, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/hfs5_2020_150.step', - 'name': 'secondary_servo_hfs5_2020', - 'transform': [1.0, - 0.0, - 0.0, - -10.364257999994, - 0.0, - 0.0, - -1.0, - 147.450798778311, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_end_mount.step', - 'name': 'quaternary_servo_end_mount', - 'transform': [0.0, - 1.0, - 0.0, - -0.764257999994, - -1.0, - 0.0, - 0.0, - 261.060997778311, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3250.step', - 'name': 'sts3250_4', - 'transform': [0.0, - 1.0, - 0.0, - -0.764257999994, - -1.0, - 0.0, - 0.0, - 261.060997778311, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_horn_yoke.step', - 'name': 'quinary_horn_yoke', - 'transform': [-0.000376789973259, - 0.999999858029296, - 0.000376790000006, - -0.777330838358056, - -1.41970704e-07, - 0.000376789973259, - -0.999999929014646, - 286.55752815855016, - -0.999999929014646, - -0.000376790000006, - 0.0, - -25.496532190119495, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3215.step', - 'name': 'sts3215_5', - 'transform': [0.999999858029296, - 0.000376789973259, - 0.000376790000006, - 15.653567529745356, - 0.000376789973259, - 1.41970704e-07, - -0.999999929014646, - 286.56371915718586, - -0.000376790000006, - 0.999999929014646, - 0.0, - 56.5076826323431, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'extrusion_bracket_2020.step', - 'name': 'quinary_servo_2020_connector', - 'transform': [0.999999858029296, - 0.000376789973259, - 0.000376790000006, - -9.838731499094065, - 0.000376789973259, - 1.41970704e-07, - -0.999999929014646, - 286.55411391315283, - -0.000376790000006, - 0.999999929014646, - 0.0, - 67.74728867422357, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/hfs5_2020_100.step', - 'name': 'quinary_servo_hfs5_2020', - 'transform': [0.999999858029296, - 0.000376790000006, - -0.000376789973259, - -9.83812863513685, - 0.000376789973259, - -0.999999929014646, - -1.41970704e-07, - 286.55411414030596, - -0.000376790000006, - 0.0, - -0.999999929014646, - 69.347288560647, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_end_mount.step', - 'name': 'senary_servo_end_mount', - 'transform': [-0.000376789973259, - 0.999999858029296, - 0.000376790000006, - -0.198788813720378, - -1.41970704e-07, - 0.000376789973259, - -0.999999929014646, - 286.5577461474151, - -0.999999929014646, - -0.000376790000006, - 0.0, - 182.95386361794084, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3215.step', - 'name': 'sts3215_6', - 'transform': [-0.000376789973259, - 0.999999858029296, - 0.000376790000006, - -0.198788813720378, - -1.41970704e-07, - 0.000376789973259, - -0.999999929014646, - 286.5577461474151, - -0.999999929014646, - -0.000376790000006, - 0.0, - 182.95386361794084, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_horn_yoke.step', - 'name': 'septenary_horn_yoke', - 'transform': [-0.000376789973259, - 0.999999858029296, - 0.000376790000006, - -0.198788813720376, - -1.41970704e-07, - 0.000376789973259, - -0.999999929014646, - 286.5577461474151, - -0.999999929014646, - -0.000376790000006, - 0.0, - 182.95386361794084, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3215.step', - 'name': 'sts3215_7', - 'transform': [0.999999858029296, - 0.000376789973259, - 0.000376790000006, - 16.232109554383037, - 0.000376789973259, - 1.41970704e-07, - -0.999999929014646, - 286.5639371460508, - -0.000376790000006, - 0.999999929014646, - 0.0, - 264.9580784404034, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'servo_end_mount.step', - 'name': 'servo_end_mount', - 'transform': [0.0, - 1.0, - 0.0, - -1.264258, - -1.0, - 0.0, - 0.0, - 52.610399203721, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'imports/sts3250.step', - 'name': 'sts3250_3', - 'transform': [1.0, - 0.0, - 0.0, - 14.635742, - 0.0, - 1.0, - 0.0, - 27.7702, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'base_plate.step', - 'name': 'base_plate', - 'transform': [1.0, - 0.0, - 0.0, - 14.635742, - 0.0, - 1.0, - 0.0, - 27.7702, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0]}, - {'path': 'gripper/rb9_01_061_000_gripper.step', - 'name': 'parallel_gripper', - 'transform': [0.999999929014645, - 0.0, - 0.00037679, - -9.277696917667, - 0.0, - 1.0, - 0.0, - 286.552253, - -0.00037679, - 0.0, - 0.999999929014645, - 326.167694693849, - 0.0, - 0.0, - 0.0, - 1.0]}] - -REMOVED_ROBOT_ARM_INSTANCE_NAMES = frozenset() -SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES = frozenset( - { - "quaternary_servo_end_mount", - "sts3250_4", - "quinary_horn_yoke", - "sts3215_5", - "quinary_servo_2020_connector", - "quinary_servo_hfs5_2020", - "quinary_servo_end_bracket", - "senary_servo_end_mount", - "sts3215_6", - "septenary_horn_yoke", - "sts3215_7", - "parallel_gripper", - } -) -QUINARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES = frozenset( - { - "senary_servo_end_mount", - "sts3215_6", - "septenary_horn_yoke", - "sts3215_7", - "parallel_gripper", - } -) - -for connector_name, extrusion_name in ( - ("secondary_servo_2020_connector", "secondary_servo_hfs5_2020"), - ("quinary_servo_2020_connector", "quinary_servo_hfs5_2020"), -): - connector_instance = _find_named_instance(ASSEMBLY_INSTANCES, connector_name) - extrusion_instance = _find_named_instance(ASSEMBLY_INSTANCES, extrusion_name) - _replace_named_instance( - ASSEMBLY_INSTANCES, - name=connector_name, - replacement={ - **connector_instance, - "path": "extrusion_bracket_2020.step", - "transform": _extrusion_bracket_transform_from_extrusion(extrusion_instance), - }, - ) - -for bracket_name, extrusion_name, before_name in ( - ("secondary_servo_end_bracket", "secondary_servo_hfs5_2020", "quaternary_servo_end_mount"), - ("quinary_servo_end_bracket", "quinary_servo_hfs5_2020", "senary_servo_end_mount"), -): - extrusion_instance = _find_named_instance(ASSEMBLY_INSTANCES, extrusion_name) - bracket_length_mm = ( - SECONDARY_EXTRUSION_2020_LENGTH_MM - if extrusion_name == "secondary_servo_hfs5_2020" - else EXTRUSION_2020_LENGTH_MM - ) - _insert_named_instance_before( - ASSEMBLY_INSTANCES, - before_name=before_name, - instance={ - "path": "extrusion_bracket_2020.step", - "name": bracket_name, - "transform": _extrusion_end_bracket_transform_from_extrusion( - extrusion_instance, - length_mm=bracket_length_mm, - ), - }, - ) - -_translate_named_instances_world_vector( - ASSEMBLY_INSTANCES, - SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES, - vector_xyz=tuple( - -value - for value in _instance_local_axis_world_vector( - _find_named_instance(ASSEMBLY_INSTANCES, "secondary_servo_hfs5_2020"), - axis="z", - ) - ), - distance_mm=SECONDARY_EXTRUSION_EXTENSION_MM, -) -_translate_named_instances_world_vector( - ASSEMBLY_INSTANCES, - SECONDARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES, - vector_xyz=tuple( - -value - for value in _instance_local_axis_world_vector( - _find_named_instance(ASSEMBLY_INSTANCES, "secondary_servo_hfs5_2020"), - axis="z", - ) - ), - distance_mm=EXTRUSION_BRACKET_INSERTION_THICKNESS_MM, -) -_translate_named_instances_world_vector( - ASSEMBLY_INSTANCES, - QUINARY_END_BRACKET_DOWNSTREAM_INSTANCE_NAMES, - vector_xyz=tuple( - -value - for value in _instance_local_axis_world_vector( - _find_named_instance(ASSEMBLY_INSTANCES, "quinary_servo_hfs5_2020"), - axis="z", - ) - ), - distance_mm=EXTRUSION_BRACKET_INSERTION_THICKNESS_MM, -) - -# Robonine gripper uses a module 0.5, 26-tooth pinion and a 76 mm full stroke. -GRIPPER_CLAW_TRAVEL_M = 0.038 -GRIPPER_SERVO_CLOSE_DEG = 334.96 -GRIPPER_SERVO_CLOSE_RAD = math.radians(GRIPPER_SERVO_CLOSE_DEG) -GRIPPER_RACK_TRAVEL_PER_RAD_M = GRIPPER_CLAW_TRAVEL_M / GRIPPER_SERVO_CLOSE_RAD -GRIPPER_SERVO_VELOCITY_RAD_PER_S = 0.03 / GRIPPER_RACK_TRAVEL_PER_RAD_M - -# Mesh-derived gripper inertials. The Robonine gripper source gives a -# 250 g assembly mass, but not per-link mass properties. These values -# distribute that mass over the generated gripper mesh volume estimates. -GRIPPER_STATIC_MESH_VOLUME_MM3 = 69789.45759247476 -GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3 = 1118.1904419098348 -GRIPPER_CLAW_MESH_VOLUME_MM3 = 19529.73092604228 -GRIPPER_MESH_VOLUME_SUM_MM3 = ( - GRIPPER_STATIC_MESH_VOLUME_MM3 - + GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3 - + (2.0 * GRIPPER_CLAW_MESH_VOLUME_MM3) -) -GRIPPER_MESH_DENSITY_KG_PER_MM3 = GRIPPER_ASSEMBLY_MASS_KG / GRIPPER_MESH_VOLUME_SUM_MM3 - - -def _mesh_estimated_mass_kg(volume_mm3: float) -> float: - return volume_mm3 * GRIPPER_MESH_DENSITY_KG_PER_MM3 - - -def _mesh_center_m(center_xyz_mm: tuple[float, float, float]) -> tuple[float, float, float]: - return tuple(value * 0.001 for value in center_xyz_mm) - - -def _mesh_inertia_kg_m2(inertia_mm5: tuple[float, float, float]) -> tuple[float, float, float]: - return tuple(value * GRIPPER_MESH_DENSITY_KG_PER_MM3 * 1e-6 for value in inertia_mm5) - - -URDF_LINK_INERTIALS = { - "base_link": { - "mass": STS3250_MASS_KG + BASE_PLATE_MASS_KG, - "origin_xyz": (-0.013065695, 0.018748416, -0.000001347), - "inertia": (2.00286469315e-05, 3.27226966744e-05, 3.64056286856e-05), - }, - "shoulder_yaw_link": { - "mass": STS3250_MASS_KG + SERVO_END_MOUNT_MASS_KG, - "origin_xyz": (0.001212643, 0.046188702, -0.000001520), - "inertia": (2.19822684157e-05, 1.33477342495e-05, 2.72133948517e-05), - }, - "shoulder_pitch_link": { - "mass": STS3250_MASS_KG + SERVO_HORN_YOKE_MASS_KG, - "origin_xyz": (-0.046864415, 0.009984643, -0.000001415), - "inertia": (2.61354708556e-05, 5.11017950126e-05, 6.8521371663e-05), - }, - "shoulder_roll_link": { - "mass": ( - STS3250_MASS_KG - + SERVO_END_MOUNT_MASS_KG - + HFS5_2020_150_MASS_KG - + 2 * EXTRUSION_BRACKET_2020_MASS_KG - ), - "origin_xyz": (0.000577873, 0.144167410, -0.000000724), - "inertia": (0.000595051077045, 2.80096804783e-05, 0.0006060283843), - }, - "elbow_pitch_link": { - "mass": STS3215_MASS_KG + SERVO_HORN_YOKE_MASS_KG, - "origin_xyz": (0.000001711, 0.009998836, 0.046867544), - "inertia": (6.85659210126e-05, 5.11223026952e-05, 2.61742086504e-05), - }, - "elbow_roll_link": { - "mass": ( - STS3215_MASS_KG - + SERVO_END_MOUNT_MASS_KG - + HFS5_2020_100_MASS_KG - + 2 * EXTRUSION_BRACKET_2020_MASS_KG - ), - "origin_xyz": (0.000680967, 0.113970513, -0.000000851), - "inertia": (0.000292565038128, 2.38225119977e-05, 0.000301901347194), - }, - "wrist_pitch_link": { - "mass": STS3215_MASS_KG + SERVO_HORN_YOKE_MASS_KG, - "origin_xyz": (-0.046864415, 0.009984643, -0.000001415), - "inertia": (2.61354708556e-05, 5.11017950126e-05, 6.8521371663e-05), - }, - "gripper_base_link": { - "mass": _mesh_estimated_mass_kg(GRIPPER_STATIC_MESH_VOLUME_MM3), - "origin_xyz": _mesh_center_m((-0.0126298648, 15.5220681, -11.3782697)), - "inertia": _mesh_inertia_kg_m2((72377447.4, 94420670.5, 126870002.0)), - }, - "gripper_servo_link": { - "mass": _mesh_estimated_mass_kg(GRIPPER_SERVO_GEAR_MESH_VOLUME_MM3), - "origin_xyz": _mesh_center_m((0.0000000329379318, -0.00000015359035, -0.668503477)), - "inertia": _mesh_inertia_kg_m2((28356.08228658, 28356.0947249, 43883.87103322)), - }, - "gripper_right_claw_link": { - "mass": _mesh_estimated_mass_kg(GRIPPER_CLAW_MESH_VOLUME_MM3), - "origin_xyz": _mesh_center_m((45.56599565, -1.48478733, 22.03617984)), - "inertia": _mesh_inertia_kg_m2((10873354.67241446, 9338011.90258315, 5226795.70594811)), - }, - "gripper_left_claw_link": { - "mass": _mesh_estimated_mass_kg(GRIPPER_CLAW_MESH_VOLUME_MM3), - "origin_xyz": _mesh_center_m((-45.56599565, 1.48478733, 22.03617984)), - "inertia": _mesh_inertia_kg_m2((10873354.67241446, 9338011.90258318, 5226795.70594813)), - }, -} - - -def _urdf_numeric_text(value: float) -> str: - numeric = float(value) - if abs(numeric) < 0.5e-9: - numeric = 0.0 - return f"{numeric:.9f}".rstrip("0").rstrip(".") - - -def _urdf_link_inertial(link_name: str) -> dict[str, object]: - inertial = URDF_LINK_INERTIALS[link_name] - ixx, iyy, izz = inertial["inertia"] - return { - "origin_xyz": " ".join(_urdf_numeric_text(value) for value in inertial["origin_xyz"]), - "origin_rpy": "0 0 0", - "mass": _urdf_numeric_text(float(inertial["mass"])), - "inertia": { - "ixx": _urdf_numeric_text(ixx), - "ixy": "0", - "ixz": "0", - "iyy": _urdf_numeric_text(iyy), - "iyz": "0", - "izz": _urdf_numeric_text(izz), - }, - } - - -URDF_LINKS = ( - {"name": "base_footprint"}, - { - "name": "base_link", - "inertial": _urdf_link_inertial("base_link"), - "visuals": ( - { - "instance_name": "sts3250_3", - "origin_xyz": "0 0.030575 0", - "origin_rpy": "0 0 0", - }, - { - "instance_name": "base_plate", - "origin_xyz": "0 0.0329752 0", - "origin_rpy": "0 0 0", - }, - ), - }, - { - "name": "shoulder_yaw_link", - "inertial": _urdf_link_inertial("shoulder_yaw_link"), - "visuals": ( - { - "instance_name": "sts3250_1", - "origin_xyz": "0.0096 0.033940199 0", - "origin_rpy": "0 0 -1.570796327", - }, - { - "instance_name": "servo_end_mount", - "origin_xyz": "0.0096 0.033940199 0", - "origin_rpy": "0 0 -1.570796327", - }, - ), - }, - { - "name": "shoulder_pitch_link", - "inertial": _urdf_link_inertial("shoulder_pitch_link"), - "visuals": ( - { - "instance_name": "servo_horn_yoke", - "origin_xyz": "0.0255 0.0091 0", - "origin_rpy": "0 0 0", - }, - { - "instance_name": "sts3250_2", - "origin_xyz": "-0.0594 0.0255 0", - "origin_rpy": "0 0 1.570796327", - }, - ), - }, - { - "name": "shoulder_roll_link", - "inertial": _urdf_link_inertial("shoulder_roll_link"), - "visuals": ( - { - "instance_name": "secondary_servo_2020_connector", - "origin_xyz": "0 0.0190404 0", - "origin_rpy": "-1.570796327 0 0", - }, - { - "instance_name": "secondary_servo_hfs5_2020", - "origin_xyz": "0 0.0190404 0", - "origin_rpy": "1.570796327 0 0", - }, - { - "instance_name": "secondary_servo_end_bracket", - "origin_xyz": "0 0.1690404 0", - "origin_rpy": "1.570796327 0 0", - }, - { - "instance_name": "quaternary_servo_end_mount", - "origin_xyz": "0.0096 0.186981399 0", - "origin_rpy": "0 0 -1.570796327", - }, - { - "instance_name": "sts3250_4", - "origin_xyz": "0.0096 0.186981399 0", - "origin_rpy": "0 0 -1.570796327", - }, - ), - }, - { - "name": "elbow_pitch_link", - "inertial": _urdf_link_inertial("elbow_pitch_link"), - "visuals": ( - { - "instance_name": "quinary_horn_yoke", - "origin_xyz": "0.00000347 0.009086927 -0.025496532", - "origin_rpy": "-1.570796327 1.570419537 -1.570419537", - }, - { - "instance_name": "sts3215_5", - "origin_xyz": "-0.000002722 0.025518914 0.059397282", - "origin_rpy": "1.570796327 0.00037679 1.571173117", - }, - ), - }, - { - "name": "elbow_roll_link", - "inertial": _urdf_link_inertial("elbow_roll_link"), - "visuals": ( - { - "instance_name": "quinary_servo_2020_connector", - "origin_xyz": "0.000003466 0.0190404 0", - "origin_rpy": "-1.570796327 0 0", - }, - { - "instance_name": "quinary_servo_hfs5_2020", - "origin_xyz": "0.000003466 0.0190404 0", - "origin_rpy": "1.570796327 0 0", - }, - { - "instance_name": "quinary_servo_end_bracket", - "origin_xyz": "0.000003466 0.1190404 0", - "origin_rpy": "1.570796327 0 0", - }, - { - "instance_name": "senary_servo_end_mount", - "origin_xyz": "0.0096 0.136981399 0", - "origin_rpy": "0 0 -1.570796327", - }, - { - "instance_name": "sts3215_6", - "origin_xyz": "0.0096 0.136981399 0", - "origin_rpy": "0 0 -1.570796327", - }, - ), - }, - { - "name": "wrist_pitch_link", - "inertial": _urdf_link_inertial("wrist_pitch_link"), - "visuals": ( - { - "instance_name": "septenary_horn_yoke", - "origin_xyz": "0.0255 0.0091 0", - "origin_rpy": "0 0 0", - }, - { - "instance_name": "sts3215_7", - "origin_xyz": "-0.0594 0.0255 0", - "origin_rpy": "0 0 1.570796327", - }, - ), - }, - {"name": "wrist_roll_link"}, - { - "name": "gripper_base_link", - "inertial": _urdf_link_inertial("gripper_base_link"), - "visuals": ( - { - "mesh_filename": "3MF/gripper_static.3mf", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - }, - ), - }, - { - "name": "gripper_servo_link", - "inertial": _urdf_link_inertial("gripper_servo_link"), - "visuals": ( - { - "mesh_filename": "3MF/gripper_servo_gear.3mf", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - }, - ), - }, - { - "name": "gripper_right_claw_link", - "inertial": _urdf_link_inertial("gripper_right_claw_link"), - "visuals": ( - { - "mesh_filename": "3MF/gripper_right_claw.3mf", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - }, - ), - }, - { - "name": "gripper_left_claw_link", - "inertial": _urdf_link_inertial("gripper_left_claw_link"), - "visuals": ( - { - "mesh_filename": "3MF/gripper_left_claw.3mf", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - }, - ), - }, -) - -URDF_JOINTS = ( - { - "name": "base_footprint_to_base_link", - "joint_type": "fixed", - "parent": "base_footprint", - "child": "base_link", - "origin_xyz": "0.014635742 0 0", - "origin_rpy": "1.570796327 0 0", - }, - { - "name": "base_yaw", - "joint_type": "continuous", - "parent": "base_link", - "child": "shoulder_yaw_link", - "origin_xyz": "-0.0255 0.021475 0", - "origin_rpy": "0 0 0", - "axis_xyz": "0 1 0", - "limit": { - "effort": STS3250_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "shoulder_pitch", - "joint_type": "revolute", - "parent": "shoulder_yaw_link", - "child": "shoulder_pitch_link", - "origin_xyz": "0.0005 0.059440199 0", - "origin_rpy": "0 0 -1.570796327", - "axis_xyz": "0 1 0", - "limit": { - "lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "effort": STS3250_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "shoulder_roll", - "joint_type": "continuous", - "parent": "shoulder_pitch_link", - "child": "shoulder_roll_link", - "origin_xyz": "-0.0503 0 0", - "origin_rpy": "0 0 1.570796327", - "axis_xyz": "0 1 0", - "limit": { - "effort": STS3250_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "elbow_pitch", - "joint_type": "revolute", - "parent": "shoulder_roll_link", - "child": "elbow_pitch_link", - "origin_xyz": "0.0005 0.212481399 0", - "origin_rpy": "0 0 -1.570796327", - "axis_xyz": "0 1 0", - "limit": { - "lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "effort": STS3250_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "elbow_roll", - "joint_type": "continuous", - "parent": "elbow_pitch_link", - "child": "elbow_roll_link", - "origin_xyz": "0.000006888 0.000015489 0.050306891", - "origin_rpy": "1.570796327 0.00037679 1.571173117", - "axis_xyz": "0 1 0", - "limit": { - "effort": STS3215_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "wrist_pitch", - "joint_type": "revolute", - "parent": "elbow_roll_link", - "child": "wrist_pitch_link", - "origin_xyz": "0.0005 0.162481399 0", - "origin_rpy": "0 0 -1.570796327", - "axis_xyz": "0 1 0", - "limit": { - "lower": f"{-HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "upper": f"{HORN_YOKE_SWEEP_LIMIT_RAD:.9f}", - "effort": STS3215_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "wrist_roll", - "joint_type": "continuous", - "parent": "wrist_pitch_link", - "child": "wrist_roll_link", - "origin_xyz": "-0.0503 0 0", - "origin_rpy": "0 0 1.570796327", - "axis_xyz": "0 1 0", - "limit": { - "effort": STS3215_JOINT_EFFORT_TEXT, - "velocity": "4.000000", - }, - }, - { - "name": "wrist_roll_to_gripper_base", - "joint_type": "fixed", - "parent": "wrist_roll_link", - "child": "gripper_base_link", - "origin_xyz": "-0.00003287 0.0674104 0.000002072", - "origin_rpy": "-1.570796185 -0.00037679 0", - }, - { - "name": "gripper_servo", - "joint_type": "revolute", - "parent": "gripper_base_link", - "child": "gripper_servo_link", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - "axis_xyz": "0 0 1", - "attrs": {"default_deg": "0"}, - "limit": { - "lower": "0", - "upper": f"{GRIPPER_SERVO_CLOSE_RAD:.9f}", - "effort": GRIPPER_SERVO_EFFORT_TEXT, - "velocity": f"{GRIPPER_SERVO_VELOCITY_RAD_PER_S:.6f}", - }, - }, - { - "name": "gripper_right_claw_slide", - "joint_type": "prismatic", - "parent": "gripper_base_link", - "child": "gripper_right_claw_link", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - "axis_xyz": "-1 0 0", - "limit": { - "lower": "0", - "upper": f"{GRIPPER_CLAW_TRAVEL_M:.6f}", - "effort": "150", - "velocity": "0.03", - }, - "mimic": { - "joint": "gripper_servo", - "multiplier": f"{GRIPPER_RACK_TRAVEL_PER_RAD_M:.12f}", - "offset": "0", - }, - }, - { - "name": "gripper_left_claw_slide", - "joint_type": "prismatic", - "parent": "gripper_base_link", - "child": "gripper_left_claw_link", - "origin_xyz": "0 0 0", - "origin_rpy": "0 0 0", - "axis_xyz": "1 0 0", - "limit": { - "lower": "0", - "upper": f"{GRIPPER_CLAW_TRAVEL_M:.6f}", - "effort": "150", - "velocity": "0.03", - }, - "mimic": { - "joint": "gripper_servo", - "multiplier": f"{GRIPPER_RACK_TRAVEL_PER_RAD_M:.12f}", - "offset": "0", - }, - }, -) - -URDF_VISUAL_INSTANCE_NAMES = tuple( - str(visual["instance_name"]) - for link in URDF_LINKS - for visual in link.get("visuals", ()) - if "instance_name" in visual and str(visual["instance_name"]) not in REMOVED_ROBOT_ARM_INSTANCE_NAMES -) - -URDF_MATERIALS = { - "black_aluminum": BLACK_ALUMINUM_RGBA, -} - -URDF_STEP_MATERIALS = { - "imports/sts3250.step": "black_aluminum", - "imports/sts3215.step": "black_aluminum", -} - -PRESERVE_SOURCE_COLOR_STEP_PATHS = frozenset( - { - "imports/sts3250.step", - "imports/sts3215.step", - "gripper/rb9_01_061_000_gripper.step", - } -) -EXPLORER_POSES = ( - { - "name": "home", - "joint_values": { - "base_yaw": 0, - "shoulder_pitch": 0, - "shoulder_roll": 0, - "elbow_pitch": 0, - "elbow_roll": 0, - "wrist_pitch": 0, - "wrist_roll": 0, - "gripper_servo": 0, - }, - }, - { - "name": "reach", - "joint_values": { - "base_yaw": 0, - "shoulder_pitch": 90, - "shoulder_roll": 0, - "elbow_pitch": -90, - "elbow_roll": 0, - "wrist_pitch": 0, - "wrist_roll": 0, - "gripper_servo": 90, - }, - }, - { - "name": "action", - "joint_values": { - "base_yaw": 95, - "shoulder_pitch": 32, - "shoulder_roll": 35, - "elbow_pitch": -20, - "elbow_roll": -103, - "wrist_pitch": 65, - "wrist_roll": -71, - "gripper_servo": 185, - }, - }, -) -ROBOT_ARM_LINK_ASSEMBLIES = ( - { - "path": "assemblies/base_link.step", - "name": "base_link", - "anchor_instance_name": "base_plate", - "top_level_y_offset_mm": BASE_PLATE_BOTTOM_LOCAL_Y_MM, - "instance_names": frozenset( - { - "sts3250_3", - "base_plate", - } - ), - }, - { - "path": "assemblies/shoulder_yaw_link.step", - "name": "shoulder_yaw_link", - "anchor_instance_name": "sts3250_1", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "sts3250_1", - "servo_end_mount", - } - ), - }, - { - "path": "assemblies/pitch_link_sts3250.step", - "name": "shoulder_pitch_link", - "anchor_instance_name": "servo_horn_yoke", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "servo_horn_yoke", - "sts3250_2", - } - ), - }, - { - "path": "assemblies/shoulder_roll_link.step", - "name": "shoulder_roll_link", - "anchor_instance_name": "secondary_servo_2020_connector", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "secondary_servo_2020_connector", - "secondary_servo_hfs5_2020", - "secondary_servo_end_bracket", - "quaternary_servo_end_mount", - "sts3250_4", - } - ), - }, - { - "path": "assemblies/pitch_link_sts3215.step", - "name": "elbow_pitch_link", - "anchor_instance_name": "quinary_horn_yoke", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "quinary_horn_yoke", - "sts3215_5", - } - ), - }, - { - "path": "assemblies/elbow_roll_link.step", - "name": "elbow_roll_link", - "anchor_instance_name": "quinary_servo_2020_connector", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "quinary_servo_2020_connector", - "quinary_servo_hfs5_2020", - "quinary_servo_end_bracket", - "senary_servo_end_mount", - "sts3215_6", - } - ), - }, - { - "path": "assemblies/pitch_link_sts3215.step", - "name": "wrist_pitch_link", - "anchor_instance_name": "septenary_horn_yoke", - "top_level_y_offset_mm": BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - "instance_names": frozenset( - { - "septenary_horn_yoke", - "sts3215_7", - } - ), - }, -) - - -def _xml(tag: str, attrs: dict[str, object] | None = None, children: tuple[ET.Element, ...] = ()) -> ET.Element: - element = ET.Element(tag, {key: str(value) for key, value in (attrs or {}).items()}) - for child in children: - element.append(child) - return element - - -def _urdf_inertial_element(inertial: dict[str, object]) -> ET.Element: - return _xml( - "inertial", - children=( - _xml("origin", {"xyz": inertial["origin_xyz"], "rpy": inertial["origin_rpy"]}), - _xml("mass", {"value": inertial["mass"]}), - _xml("inertia", dict(inertial["inertia"])), - ), - ) - - -def _urdf_mesh_filename_for_entry(entry: dict[str, object]) -> str: - mesh_filename = entry.get("mesh_filename") - if mesh_filename is None: - mesh_filename = _urdf_mesh_filename(str(entry["instance_name"])) - return str(mesh_filename) - - -def _urdf_mesh_geometry_element(entry: dict[str, object]) -> ET.Element: - mesh_filename = _urdf_mesh_filename_for_entry(entry) - return _xml( - "geometry", - children=( - _xml( - "mesh", - { - "filename": mesh_filename, - "scale": "0.001 0.001 0.001", - }, - ), - ), - ) - - -def _urdf_visual_element(visual: dict[str, object]) -> ET.Element: - children = [ - _xml("origin", {"xyz": visual["origin_xyz"], "rpy": visual["origin_rpy"]}), - _urdf_mesh_geometry_element(visual), - ] - material_name = _urdf_visual_material_name(visual) - if material_name: - children.append(_xml("material", {"name": material_name})) - return _xml( - "visual", - children=tuple(children), - ) - - -def _urdf_collision_element(collision: dict[str, object]) -> ET.Element: - return _xml( - "collision", - children=( - _xml("origin", {"xyz": collision["origin_xyz"], "rpy": collision["origin_rpy"]}), - _urdf_mesh_geometry_element(collision), - ), - ) - - -def _urdf_entry_is_removed(entry: dict[str, object]) -> bool: - return "instance_name" in entry and str(entry["instance_name"]) in REMOVED_ROBOT_ARM_INSTANCE_NAMES - - -def _urdf_link_element(link: dict[str, object]) -> ET.Element: - children: list[ET.Element] = [] - inertial = link.get("inertial") - if inertial is not None: - children.append(_urdf_inertial_element(dict(inertial))) - for visual in link.get("visuals", ()): - if _urdf_entry_is_removed(dict(visual)): - continue - children.append(_urdf_visual_element(dict(visual))) - collision_specs = link.get("collisions") - if collision_specs is None: - collision_specs = link.get("visuals", ()) - for collision in collision_specs: - if _urdf_entry_is_removed(dict(collision)): - continue - children.append(_urdf_collision_element(dict(collision))) - return _xml("link", {"name": link["name"]}, tuple(children)) - - -def _urdf_joint_element(joint: dict[str, object]) -> ET.Element: - attrs = {"name": joint["name"], "type": joint["joint_type"]} - attrs.update(dict(joint.get("urdf_attrs", {}))) - children = [ - _xml("parent", {"link": joint["parent"]}), - _xml("child", {"link": joint["child"]}), - _xml("origin", {"xyz": joint["origin_xyz"], "rpy": joint["origin_rpy"]}), - ] - axis_xyz = joint.get("axis_xyz") - if axis_xyz is not None: - children.append(_xml("axis", {"xyz": axis_xyz})) - limit = joint.get("limit") - if limit is not None: - children.append(_xml("limit", dict(limit))) - mimic = joint.get("mimic") - if mimic is not None: - children.append(_xml("mimic", dict(mimic))) - return _xml("joint", attrs, tuple(children)) - - -def _explorer_numeric_value(value: float) -> float | int: - numeric = float(value) - return int(numeric) if numeric.is_integer() else numeric - - -def _urdf_explorer_metadata( - *, - joints: tuple[dict[str, object], ...] = URDF_JOINTS, - poses: tuple[dict[str, object], ...] = EXPLORER_POSES, -) -> dict[str, object]: - joint_defaults_by_name = {} - for joint in joints: - attrs = dict(joint.get("attrs", {})) - if "default_deg" in attrs: - joint_defaults_by_name[str(joint["name"])] = _explorer_numeric_value(float(attrs["default_deg"])) - return { - "schemaVersion": 3, - "kind": "texttocad-urdf-explorer", - "jointDefaultsByName": joint_defaults_by_name, - "poses": [ - { - "name": str(pose["name"]), - "jointValuesByName": { - str(joint_name): _explorer_numeric_value(float(joint_value)) - for joint_name, joint_value in dict(pose["joint_values"]).items() - }, - } - for pose in poses - ], - } - - -def _urdf_root_element( - *, - links: tuple[dict[str, object], ...] = URDF_LINKS, - joints: tuple[dict[str, object], ...] = URDF_JOINTS, - robot_name: str = "tom", - source_name: str = "tom.py", -) -> ET.Element: - root = _xml("robot", {"name": robot_name}) - root.append(ET.Comment(f" Generated by {source_name} for CAD Explorer and external URDF consumers. ")) - root.append( - ET.Comment( - " Link inertials use STS3250 mass 74.5 g and STS3215 mass 55 g from Feetech, HFS5-2020 profile " - "mass 0.5 kg/m from MISUMI, 5052-H32 density 0.097 lb/in^3 from " - "SendCutSend, and Robonine gripper assembly mass 250 g distributed " - "across generated gripper mesh volume estimates. Arm joint effort " - "uses each servo family's rated torque: STS3250 16 kg-cm " - "and STS3215 10 kg-cm at 12 V. " - ) - ) - used_material_names = sorted( - { - material_name - for link in links - for visual in link.get("visuals", ()) - if (material_name := _urdf_visual_material_name(visual)) - } - ) - for material_name in used_material_names: - rgba = URDF_MATERIALS[material_name] - root.append( - _xml( - "material", - {"name": material_name}, - (_xml("color", {"rgba": _urdf_rgba_text(rgba)}),), - ) - ) - for link in links: - root.append(_urdf_link_element(link)) - root.append( - ET.Comment( - " Joint frames use the horn-center line resolved from " - "@cad[STEP/imports/sts3250#o1.4.f2,o1.5.f3]. " - ) - ) - for joint in joints: - root.append(_urdf_joint_element(joint)) - return root - - -def _serialize_urdf(root: ET.Element) -> str: - ET.indent(root, space=" ") - return '\n' + ET.tostring(root, encoding="unicode") - - -def _assembly_instances() -> list[dict[str, object]]: - instances = [] - for instance in ASSEMBLY_INSTANCES: - step_path = PurePosixPath(str(instance["path"])).as_posix() - instances.append( - { - "path": step_path, - "name": str(instance["name"]), - "transform": [float(value) for value in instance["transform"]], - "use_source_colors": step_path in PRESERVE_SOURCE_COLOR_STEP_PATHS, - } - ) - return instances - - -def _assembly_children() -> list[dict[str, object]]: - source_instances = _assembly_instances() - source_by_name = {str(instance["name"]): instance for instance in source_instances} - missing_anchors = [ - str(spec["anchor_instance_name"]) - for spec in ROBOT_ARM_LINK_ASSEMBLIES - if str(spec["anchor_instance_name"]) not in source_by_name - ] - if missing_anchors: - raise RuntimeError(f"Missing robot arm link assembly anchors: {missing_anchors!r}") - - children = [] - consumed_instance_names = set(REMOVED_ROBOT_ARM_INSTANCE_NAMES) - downstream_offset_xyz_mm = (0.0, 0.0, 0.0) - for link_spec in ROBOT_ARM_LINK_ASSEMBLIES: - anchor = source_by_name[str(link_spec["anchor_instance_name"])] - child_transform = _translate_transform_world_offset( - _standard_ground_top_level_transform( - anchor["transform"], - y_offset_mm=float(link_spec["top_level_y_offset_mm"]), - ), - downstream_offset_xyz_mm, - ) - children.append( - { - "path": str(link_spec["path"]), - "name": str(link_spec["name"]), - "transform": child_transform, - "use_source_colors": True, - } - ) - consumed_instance_names.update(link_spec["instance_names"]) - if str(link_spec["name"]) in {"shoulder_roll_link", "elbow_roll_link"}: - local_z_world = _transform_local_axis_world_vector(child_transform, axis="z") - downstream_offset_xyz_mm = ( - downstream_offset_xyz_mm[0] + (local_z_world[0] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM), - downstream_offset_xyz_mm[1] + (local_z_world[1] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM), - downstream_offset_xyz_mm[2] + (local_z_world[2] * ROLL_LINK_SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM), - ) - - for instance in source_instances: - instance_name = str(instance["name"]) - if instance_name in consumed_instance_names: - continue - child_transform = _standard_ground_top_level_transform( - instance["transform"], - y_offset_mm=BASE_LINK_STS3250_SOURCE_Y_OFFSET_MM, - ) - if instance_name == "parallel_gripper": - child_transform = _translate_transform_world_offset(child_transform, downstream_offset_xyz_mm) - children.append( - { - **instance, - "transform": child_transform, - } - ) - return children - - -def _assembly_instance_by_name() -> dict[str, dict[str, object]]: - instances = _assembly_instances() - by_name = {str(instance["name"]): instance for instance in instances} - if len(by_name) != len(instances): - raise RuntimeError("assembly instance names must be unique") - return by_name - - -def _urdf_rgba_text(rgba: tuple[float, float, float, float]) -> str: - return " ".join(f"{float(value):.6f}".rstrip("0").rstrip(".") for value in rgba) - - -def _urdf_visual_material_name(visual: dict[str, object]) -> str: - if str(_urdf_mesh_filename_for_entry(visual)).lower().endswith(".3mf"): - return "" - material_name = visual.get("material") - if material_name is not None: - return str(material_name) - if "instance_name" not in visual: - return "" - by_name = _assembly_instance_by_name() - instance_name = str(visual["instance_name"]) - try: - instance = by_name[instance_name] - except KeyError as exc: - raise RuntimeError(f"URDF visual references missing assembly instance {instance_name!r}") from exc - step_path = PurePosixPath(str(instance["path"])).as_posix() - return URDF_STEP_MATERIALS.get(step_path, "") - - -def _urdf_mesh_filename(instance_name: str) -> str: - by_name = _assembly_instance_by_name() - try: - instance = by_name[instance_name] - except KeyError as exc: - raise RuntimeError(f"URDF visual references missing assembly instance {instance_name!r}") from exc - step_path = PurePosixPath(str(instance["path"])) - if step_path.suffix.lower() != ".step": - raise RuntimeError(f"URDF visual instance {instance_name!r} does not reference a .step file") - return f"3MF/{step_path.stem}.3mf" - - -ROBOT_ARM_MOTION_JOINT_NAMES = [ - "base_yaw", - "shoulder_pitch", - "shoulder_roll", - "elbow_pitch", - "elbow_roll", - "wrist_pitch", - "wrist_roll", -] - -ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS = ( - ("wrist_pitch_link", "gripper_base_link"), - ("elbow_pitch_link", "shoulder_roll_link"), -) - -ROBOT_ARM_SRDF_INSPECTION_ARM_GROUP_STATE_DEG = { - "base_yaw": 45.0, - "shoulder_pitch": -27.0, - "shoulder_roll": -53.001, - "elbow_pitch": 3.0, - "elbow_roll": 29.969, - "wrist_pitch": 58.004, - "wrist_roll": -56.0, -} - -ROBOT_ARM_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG = { - "base_yaw": 45.0, - "shoulder_pitch": -27.0, - "shoulder_roll": 53.001, - "elbow_pitch": 3.0, - "elbow_roll": -29.969, - "wrist_pitch": 58.004, - "wrist_roll": 56.0, -} - -ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG = ( - ( - "home", - { - "base_yaw": 0.0, - "shoulder_pitch": 0.0, - "shoulder_roll": 0.0, - "elbow_pitch": 0.0, - "elbow_roll": 0.0, - "wrist_pitch": 0.0, - "wrist_roll": 0.0, - }, - ), - ( - "reach_forward", - { - "base_yaw": 0.0, - "shoulder_pitch": 60.0, - "shoulder_roll": 0.0, - "elbow_pitch": -90.0, - "elbow_roll": 0.0, - "wrist_pitch": 30.0, - "wrist_roll": 0.0, - }, - ), - ( - "inspection", - ROBOT_ARM_SRDF_INSPECTION_ARM_GROUP_STATE_DEG, - ), - ( - "inspection_mirrored", - ROBOT_ARM_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG, - ), -) - -ROBOT_ARM_SRDF_ARM_WITH_GRIPPER_GROUP_STATES_DEG = () - -ROBOT_ARM_SRDF_GRIPPER_GROUP_STATES_RAD = ( - ("open", {"gripper_servo": 0.0}), - ("half_closed", {"gripper_servo": GRIPPER_SERVO_CLOSE_RAD * 0.5}), - ("closed", {"gripper_servo": GRIPPER_SERVO_CLOSE_RAD}), -) - - -def _joint_degrees_to_radians(joint_values_deg: dict[str, float]) -> dict[str, float]: - return { - joint_name: math.radians(float(value_deg)) - for joint_name, value_deg in joint_values_deg.items() - } - - -def _srdf_group_state_element( - *, - name: str, - group: str, - joint_values_rad: dict[str, float], -) -> ET.Element: - return _xml( - "group_state", - {"name": name, "group": group}, - tuple( - _xml("joint", {"name": joint_name, "value": _urdf_numeric_text(value_rad)}) - for joint_name, value_rad in joint_values_rad.items() - ), - ) - - -def _srdf_adjacent_collision_pairs() -> tuple[tuple[str, str], ...]: - pairs = [] - seen: set[tuple[str, str]] = set() - for joint in URDF_JOINTS: - parent = str(joint["parent"]) - child = str(joint["child"]) - if "base_footprint" in {parent, child}: - continue - key = tuple(sorted((parent, child))) - if key in seen: - continue - seen.add(key) - pairs.append((parent, child)) - return tuple(pairs) - - -def _srdf_root_element(*, robot_name: str = "tom") -> ET.Element: - root = _xml("robot", {"name": robot_name}) - root.append(ET.Comment(" Generated by tom.py for MoveIt2 and CAD Viewer test poses. ")) - root.append( - _xml( - "virtual_joint", - { - "name": "fixed_base", - "type": "fixed", - "parent_frame": "world", - "child_link": "base_footprint", - }, - ) - ) - - arm_group = _xml("group", {"name": "arm"}) - arm_group.append(_xml("chain", {"base_link": "base_link", "tip_link": "wrist_roll_link"})) - root.append(arm_group) - - tcp_group = _xml("group", {"name": "tcp"}) - tcp_group.append(_xml("link", {"name": "gripper_base_link"})) - root.append(tcp_group) - - gripper_group = _xml("group", {"name": "gripper"}) - gripper_group.append(_xml("joint", {"name": "gripper_servo"})) - root.append(gripper_group) - - arm_with_gripper_group = _xml("group", {"name": "arm_with_gripper"}) - arm_with_gripper_group.append(_xml("group", {"name": "arm"})) - arm_with_gripper_group.append(_xml("group", {"name": "gripper"})) - root.append(arm_with_gripper_group) - - root.append( - _xml( - "end_effector", - { - "name": "gripper_tcp", - "parent_link": "wrist_roll_link", - "group": "tcp", - "parent_group": "arm", - }, - ) - ) - - for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG: - root.append( - _srdf_group_state_element( - name=name, - group="arm", - joint_values_rad=_joint_degrees_to_radians(joint_values_deg), - ) - ) - for name, joint_values_rad in ROBOT_ARM_SRDF_GRIPPER_GROUP_STATES_RAD: - root.append( - _srdf_group_state_element( - name=name, - group="gripper", - joint_values_rad=joint_values_rad, - ) - ) - for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_WITH_GRIPPER_GROUP_STATES_DEG: - root.append( - _srdf_group_state_element( - name=name, - group="arm_with_gripper", - joint_values_rad=_joint_degrees_to_radians(joint_values_deg), - ) - ) - for link1, link2 in _srdf_adjacent_collision_pairs(): - root.append(_xml("disable_collisions", {"link1": link1, "link2": link2, "reason": "Adjacent"})) - return root - -ROBOT_ARM_2X_ARM_1_BASE_YAW_DEG = 90.0 -ROBOT_ARM_2X_ARM_2_BASE_YAW_DEG = -90.0 -ROBOT_ARM_2X_BASE_OFFSET_X_M = 0.395 -ROBOT_ARM_2X_HOLDING_HANDS_POSE_JOINT_VALUES = { - "arm_1_base_yaw": 90, - "arm_1_shoulder_pitch": 0, - "arm_1_shoulder_roll": 0, - "arm_1_elbow_pitch": 90, - "arm_1_elbow_roll": 0, - "arm_1_wrist_pitch": 0, - "arm_1_wrist_roll": -24, - "arm_1_gripper_servo": 208, - "arm_2_base_yaw": -90, - "arm_2_shoulder_pitch": 0, - "arm_2_shoulder_roll": 0, - "arm_2_elbow_pitch": 90, - "arm_2_elbow_roll": 0, - "arm_2_wrist_pitch": 0, - "arm_2_wrist_roll": -45, - "arm_2_gripper_servo": 334.95, -} - -ROBOT_ARM_2X_ARM_SPECS = ( - { - "name": "arm_1", - "prefix": "arm_1_", - "base_origin_xyz": (-ROBOT_ARM_2X_BASE_OFFSET_X_M, 0.0, 0.0), - "base_origin_rpy": (0.0, 0.0, 0.0), - "default_joint_degrees": {"base_yaw": ROBOT_ARM_2X_ARM_1_BASE_YAW_DEG}, - }, - { - "name": "arm_2", - "prefix": "arm_2_", - "base_origin_xyz": (ROBOT_ARM_2X_BASE_OFFSET_X_M, 0.0, 0.0), - "base_origin_rpy": (0.0, 0.0, 0.0), - "default_joint_degrees": {"base_yaw": ROBOT_ARM_2X_ARM_2_BASE_YAW_DEG}, - }, -) - -ROBOT_ARM_2X_MOTION_JOINT_NAMES = [ - f"{spec['prefix']}{joint_name}" - for spec in ROBOT_ARM_2X_ARM_SPECS - for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES -] - - -def _urdf_xyz_values(text: str) -> tuple[float, float, float]: - values = tuple(float(value) for value in text.split()) - if len(values) != 3: - raise RuntimeError(f"URDF xyz value must have exactly 3 components: {text!r}") - return values - - -def _urdf_xyz_text(values: tuple[float, float, float]) -> str: - return " ".join(_urdf_numeric_text(value) for value in values) - - -def _prefixed_robot_arm_link(link: dict[str, object], *, prefix: str) -> dict[str, object]: - prefixed = deepcopy(link) - prefixed["name"] = f"{prefix}{link['name']}" - return prefixed - - -def _prefixed_robot_arm_joint( - joint: dict[str, object], - *, - prefix: str, - base_origin_xyz: tuple[float, float, float], - base_origin_rpy: tuple[float, float, float], - default_joint_degrees: dict[str, float], -) -> dict[str, object]: - prefixed = deepcopy(joint) - parent_name = str(joint["parent"]) - child_name = str(joint["child"]) - joint_name = str(joint["name"]) - prefixed["name"] = f"{prefix}{joint['name']}" - prefixed["parent"] = "base_footprint" if parent_name == "base_footprint" else f"{prefix}{parent_name}" - prefixed["child"] = f"{prefix}{child_name}" - if parent_name == "base_footprint": - prefixed["origin_xyz"] = _urdf_xyz_text(base_origin_xyz) - prefixed["origin_rpy"] = _urdf_xyz_text(base_origin_rpy) - if joint_name in default_joint_degrees: - attrs = dict(prefixed.get("attrs", {})) - attrs["default_deg"] = _urdf_numeric_text(float(default_joint_degrees[joint_name])) - prefixed["attrs"] = attrs - if "mimic" in prefixed: - mimic = dict(prefixed["mimic"]) - mimic["joint"] = f"{prefix}{mimic['joint']}" - prefixed["mimic"] = mimic - return prefixed - - -def _robot_arm_2x_links() -> tuple[dict[str, object], ...]: - links = [{"name": "base_footprint"}] - for spec in ROBOT_ARM_2X_ARM_SPECS: - prefix = str(spec["prefix"]) - links.extend( - _prefixed_robot_arm_link(link, prefix=prefix) - for link in URDF_LINKS - if str(link["name"]) != "base_footprint" - ) - return tuple(links) - - -def _robot_arm_2x_joints() -> tuple[dict[str, object], ...]: - joints = [] - for spec in ROBOT_ARM_2X_ARM_SPECS: - prefix = str(spec["prefix"]) - base_origin_xyz = tuple(float(value) for value in spec["base_origin_xyz"]) - base_origin_rpy = tuple(float(value) for value in spec["base_origin_rpy"]) - default_joint_degrees = { - str(name): float(value) - for name, value in dict(spec.get("default_joint_degrees", {})).items() - } - joints.extend( - _prefixed_robot_arm_joint( - joint, - prefix=prefix, - base_origin_xyz=base_origin_xyz, - base_origin_rpy=base_origin_rpy, - default_joint_degrees=default_joint_degrees, - ) - for joint in URDF_JOINTS - ) - return tuple(joints) - - -def _explorer_pose_by_name(name: str) -> dict[str, object]: - for pose in EXPLORER_POSES: - if str(pose["name"]) == name: - return pose - raise RuntimeError(f"Missing explorer pose {name!r}") - - -def _robot_arm_2x_default_joint_values() -> dict[str, object]: - joint_values = {} - home_pose = _explorer_pose_by_name("home") - for spec in ROBOT_ARM_2X_ARM_SPECS: - prefix = str(spec["prefix"]) - base_yaw_degrees = float(dict(spec.get("default_joint_degrees", {})).get("base_yaw", 0.0)) - joint_values.update( - { - f"{prefix}{joint_name}": ( - float(joint_value) + base_yaw_degrees - if str(joint_name) == "base_yaw" - else joint_value - ) - for joint_name, joint_value in dict(home_pose["joint_values"]).items() - } - ) - return joint_values - - -def _robot_arm_2x_explorer_pose( - pose: dict[str, object], -) -> dict[str, object]: - joint_values = {} - pose_joint_values = dict(pose["joint_values"]) - for spec in ROBOT_ARM_2X_ARM_SPECS: - prefix = str(spec["prefix"]) - base_yaw_degrees = float(dict(spec.get("default_joint_degrees", {})).get("base_yaw", 0.0)) - joint_values.update( - { - f"{prefix}{joint_name}": ( - float(joint_value) + base_yaw_degrees - if str(joint_name) == "base_yaw" - else joint_value - ) - for joint_name, joint_value in pose_joint_values.items() - } - ) - return { - "name": str(pose["name"]), - "joint_values": joint_values, - } - - -def _robot_arm_2x_explorer_poses() -> tuple[dict[str, object], ...]: - return ( - *(_robot_arm_2x_explorer_pose(pose) for pose in EXPLORER_POSES), - { - "name": "holding_hands", - "joint_values": ROBOT_ARM_2X_HOLDING_HANDS_POSE_JOINT_VALUES, - }, - ) - - -def _prefixed_disabled_collision_pairs(*, prefix: str) -> list[list[str]]: - return [ - [f"{prefix}{left}", f"{prefix}{right}"] - for left, right in ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS - ] - - -def _robot_arm_2x_disabled_collision_pairs() -> list[list[str]]: - pairs = [] - for spec in ROBOT_ARM_2X_ARM_SPECS: - pairs.extend(_prefixed_disabled_collision_pairs(prefix=str(spec["prefix"]))) - return pairs - - -def _robot_arm_2x_motion_joint_names(*, prefix: str) -> list[str]: - return [f"{prefix}{joint_name}" for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES] - - -def _robot_arm_2x_motion_planning_groups() -> list[dict[str, object]]: - return [ - { - "name": str(spec["name"]), - "jointNames": _robot_arm_2x_motion_joint_names(prefix=str(spec["prefix"])), - } - for spec in ROBOT_ARM_2X_ARM_SPECS - ] - - -def _robot_arm_2x_default_motion_joint_values_rad(*, prefix: str) -> dict[str, float]: - default_joint_values = _robot_arm_2x_default_joint_values() - return { - name: math.radians(float(default_joint_values[name])) - for name in _robot_arm_2x_motion_joint_names(prefix=prefix) - } - - -def robot_arm_assembly_children() -> list[dict[str, object]]: - return _assembly_children() - - -def robot_arm_step_envelope() -> dict[str, object]: - return { - "children": robot_arm_assembly_children(), - } - - -def robot_arm_urdf_envelope(*, urdf_output: str = "tom.urdf") -> dict[str, object]: - return { - "xml": _serialize_urdf(_urdf_root_element()), - "urdf_output": urdf_output, - "explorer_metadata": _urdf_explorer_metadata(), - } - - -def robot_arm_2x_urdf_envelope(*, urdf_output: str = "robot_arm_2x.urdf") -> dict[str, object]: - links = _robot_arm_2x_links() - joints = _robot_arm_2x_joints() - return { - "xml": _serialize_urdf( - _urdf_root_element( - links=links, - joints=joints, - robot_name="robot_arm_2x", - source_name="robot_arm_2x_urdf.py", - ) - ), - "urdf_output": urdf_output, - "explorer_metadata": _urdf_explorer_metadata( - joints=joints, - poses=_robot_arm_2x_explorer_poses(), - ), - } - - -def robot_arm_srdf_envelope(*, urdf: str = "tom.urdf") -> dict[str, object]: - return { - "xml": _serialize_urdf(_srdf_root_element()), - "urdf": urdf, - } - - -def robot_arm_motion_envelope(*, urdf: str = "tom.urdf") -> dict[str, object]: - return { - "urdf": urdf, - "provider": "moveit_py", - "commands": ["urdf.solvePose", "urdf.planToPose"], - "planningGroup": "arm", - "jointNames": ROBOT_ARM_MOTION_JOINT_NAMES, - "endEffectors": [ - { - "name": "gripper", - "link": "gripper_base_link", - "frame": "base_link", - "parentLink": "wrist_roll_link", - "positionTolerance": 0.002, - } - ], - "planner": { - "pipeline": "ompl", - "plannerId": "RRTConnectkConfigDefault", - "planningTime": 1.0, - }, - "disabledCollisionPairs": [list(pair) for pair in ROBOT_ARM_MOTION_DISABLED_COLLISION_PAIRS], - "groupStates": [ - { - "name": name, - "jointValuesByNameRad": { - joint_name: joint_values_rad[joint_name] - for joint_name in ROBOT_ARM_MOTION_JOINT_NAMES - }, - } - for name, joint_values_deg in ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG - for joint_values_rad in (_joint_degrees_to_radians(joint_values_deg),) - ], - } - - -def robot_arm_2x_motion_envelope(*, urdf: str = "robot_arm_2x.urdf") -> dict[str, object]: - default_group = str(ROBOT_ARM_2X_ARM_SPECS[0]["name"]) - default_prefix = str(ROBOT_ARM_2X_ARM_SPECS[0]["prefix"]) - return { - "urdf": urdf, - "provider": "moveit_py", - "commands": ["urdf.solvePose", "urdf.planToPose"], - "planningGroup": default_group, - "jointNames": _robot_arm_2x_motion_joint_names(prefix=default_prefix), - "planningGroups": _robot_arm_2x_motion_planning_groups(), - "endEffectors": [ - { - "name": f"{spec['name']}_gripper", - "link": f"{spec['prefix']}gripper_base_link", - "frame": f"{spec['prefix']}base_link", - "parentLink": f"{spec['prefix']}wrist_roll_link", - "planningGroup": str(spec["name"]), - "jointNames": _robot_arm_2x_motion_joint_names(prefix=str(spec["prefix"])), - "positionTolerance": 0.002, - } - for spec in ROBOT_ARM_2X_ARM_SPECS - ], - "planner": { - "pipeline": "ompl", - "plannerId": "RRTConnectkConfigDefault", - "planningTime": 1.0, - }, - "disabledCollisionPairs": _robot_arm_2x_disabled_collision_pairs(), - "groupStates": [ - { - "name": "home", - "planningGroup": str(spec["name"]), - "jointValuesByNameRad": _robot_arm_2x_default_motion_joint_values_rad(prefix=str(spec["prefix"])), - } - for spec in ROBOT_ARM_2X_ARM_SPECS - ], - } diff --git a/models/robots/tom/robot_common/step_import.py b/models/robots/tom/robot_common/step_import.py deleted file mode 100644 index 43c25a5d..00000000 --- a/models/robots/tom/robot_common/step_import.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import build123d - - -def _compound_from_shapes( - shapes: list[build123d.Shape], - *, - label: str = "", -) -> build123d.Compound: - return build123d.Compound(obj=shapes, children=shapes, label=label) - - -def import_as_shape(step_path: Path) -> build123d.Shape: - imported = build123d.import_step(step_path) - solids = imported.solids() - if solids: - return _compound_from_shapes(solids) - - if isinstance(imported, build123d.Shape): - return imported - - if not imported.children: - raise RuntimeError(f"No CAD shapes found in {step_path}") - return _compound_from_shapes(imported.children) diff --git a/models/robots/tom/v1/.tom.step.glb b/models/robots/tom/v1/.tom.step.glb deleted file mode 100644 index 8fdd9133..00000000 --- a/models/robots/tom/v1/.tom.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f0668093bb0ca1fd4af4adc3677b802d4f7b469a730da68f83bce0feda865cf -size 34586788 diff --git a/models/robots/tom/v1/3MF/base_clamp.3mf b/models/robots/tom/v1/3MF/base_clamp.3mf deleted file mode 100644 index a1d2e35b..00000000 --- a/models/robots/tom/v1/3MF/base_clamp.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5f14c94226a85e85725599fa2ccde838fdcf81312657ccdbdbd3e915839bee10 -size 16127 diff --git a/models/robots/tom/v1/3MF/base_plate.3mf b/models/robots/tom/v1/3MF/base_plate.3mf deleted file mode 100644 index dd7a0b46..00000000 --- a/models/robots/tom/v1/3MF/base_plate.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f2fcaacf4da98988cb3605022cdd7f5690c59371713d92c55c4eff82bcbcfd24 -size 10096 diff --git a/models/robots/tom/v1/3MF/extrusion_bracket_2020.3mf b/models/robots/tom/v1/3MF/extrusion_bracket_2020.3mf deleted file mode 100644 index 4964c7a6..00000000 --- a/models/robots/tom/v1/3MF/extrusion_bracket_2020.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:050c3a2bddecca375212f45792d6c5714de3c6fecf2815c1419d60159c8845de -size 11913 diff --git a/models/robots/tom/v1/3MF/gripper_left_claw.3mf b/models/robots/tom/v1/3MF/gripper_left_claw.3mf deleted file mode 100644 index e7a32695..00000000 --- a/models/robots/tom/v1/3MF/gripper_left_claw.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7cbffd50496caa60895ffad0087c1a612d3a7bad8de86c77aff9fe609b38de6d -size 65609 diff --git a/models/robots/tom/v1/3MF/gripper_right_claw.3mf b/models/robots/tom/v1/3MF/gripper_right_claw.3mf deleted file mode 100644 index 0c2049b9..00000000 --- a/models/robots/tom/v1/3MF/gripper_right_claw.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8736b551c1f5ac3a484bcf48c6ce89fd6a878f2c134c8da5ef76d758b26bbbc -size 65591 diff --git a/models/robots/tom/v1/3MF/gripper_servo_gear.3mf b/models/robots/tom/v1/3MF/gripper_servo_gear.3mf deleted file mode 100644 index 504fb818..00000000 --- a/models/robots/tom/v1/3MF/gripper_servo_gear.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfbcdbde32ff26d41f99ad0513afe3d9ea4baa10e5ef8012a9efc8f4adb34bee -size 30673 diff --git a/models/robots/tom/v1/3MF/gripper_static.3mf b/models/robots/tom/v1/3MF/gripper_static.3mf deleted file mode 100644 index bbd4b0f9..00000000 --- a/models/robots/tom/v1/3MF/gripper_static.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c961e3d07e07cb6bfe5a5544edcbc9238c2a4681e955e08c9ea172558052295f -size 353321 diff --git a/models/robots/tom/v1/3MF/hfs5_2020_100.3mf b/models/robots/tom/v1/3MF/hfs5_2020_100.3mf deleted file mode 100644 index 647fc7b8..00000000 --- a/models/robots/tom/v1/3MF/hfs5_2020_100.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:81eed3935722c0323ac35547c246e2be92d7fb5ae3525fa39a53d0cf70933e09 -size 7926 diff --git a/models/robots/tom/v1/3MF/hfs5_2020_150.3mf b/models/robots/tom/v1/3MF/hfs5_2020_150.3mf deleted file mode 100644 index 12bfe851..00000000 --- a/models/robots/tom/v1/3MF/hfs5_2020_150.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb11621ac38064448744401fb045cd1fc3f41303c8aa618c3f27bf2880b5afb6 -size 7923 diff --git a/models/robots/tom/v1/3MF/servo_end_mount.3mf b/models/robots/tom/v1/3MF/servo_end_mount.3mf deleted file mode 100644 index 2108b8ba..00000000 --- a/models/robots/tom/v1/3MF/servo_end_mount.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a41614cdbc960916decc88da22f93d515e1f24b80e716fff12087c3b328d9ada -size 13655 diff --git a/models/robots/tom/v1/3MF/servo_horn_yoke.3mf b/models/robots/tom/v1/3MF/servo_horn_yoke.3mf deleted file mode 100644 index fc3e9397..00000000 --- a/models/robots/tom/v1/3MF/servo_horn_yoke.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:490d8ad5b1872d57e7befdde32b426199365cb8029258f4b4e1ee58763bc6ad2 -size 27265 diff --git a/models/robots/tom/v1/3MF/sts3215.3mf b/models/robots/tom/v1/3MF/sts3215.3mf deleted file mode 100644 index 6f0fe173..00000000 --- a/models/robots/tom/v1/3MF/sts3215.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65a10d52c1d5f61706436b335014c6f7f6f9b869e07d2fff8ccc30b95f8b5ba4 -size 647698 diff --git a/models/robots/tom/v1/3MF/sts3250.3mf b/models/robots/tom/v1/3MF/sts3250.3mf deleted file mode 100644 index 182b3656..00000000 --- a/models/robots/tom/v1/3MF/sts3250.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0453772f80a60de27c057b562180585f5160f1cee61e987fc651c46be4290a41 -size 115734 diff --git a/models/robots/tom/v1/DXF/base_clamp.dxf b/models/robots/tom/v1/DXF/base_clamp.dxf deleted file mode 100644 index 0cf0854f..00000000 --- a/models/robots/tom/v1/DXF/base_clamp.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f219765d1f9a48b75686b10c2105fce95a90897049de4ed3136e5155f2853fc -size 38950 diff --git a/models/robots/tom/v1/DXF/extrusion_bracket_2020.dxf b/models/robots/tom/v1/DXF/extrusion_bracket_2020.dxf deleted file mode 100644 index cda39b32..00000000 --- a/models/robots/tom/v1/DXF/extrusion_bracket_2020.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a76f7e025e07001eb3ad3d55e6b51727c56aa6e2b6d9ffc1aafe6bcc9cc6bdca -size 48409 diff --git a/models/robots/tom/v1/DXF/servo_end_mount.dxf b/models/robots/tom/v1/DXF/servo_end_mount.dxf deleted file mode 100644 index c18f00ed..00000000 --- a/models/robots/tom/v1/DXF/servo_end_mount.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c6482e15502b2e38702d5c30bd3774f01731a014d50136c535bc3445ed431a38 -size 56819 diff --git a/models/robots/tom/v1/DXF/servo_horn_yoke.dxf b/models/robots/tom/v1/DXF/servo_horn_yoke.dxf deleted file mode 100644 index 1aa40c83..00000000 --- a/models/robots/tom/v1/DXF/servo_horn_yoke.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e40210f438d07158b20d3143c2bca624e4e63233f41054c4a6fa436b741c3dd5 -size 69929 diff --git a/models/robots/tom/v1/assemblies/.base_link.step.glb b/models/robots/tom/v1/assemblies/.base_link.step.glb deleted file mode 100644 index 0f0acff6..00000000 --- a/models/robots/tom/v1/assemblies/.base_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4284409a0648c7a9f8c4bf8f46315aae6b8dd6e101dc864ab519411bda6e5a7c -size 3564456 diff --git a/models/robots/tom/v1/assemblies/.elbow_roll_link.step.glb b/models/robots/tom/v1/assemblies/.elbow_roll_link.step.glb deleted file mode 100644 index d074c498..00000000 --- a/models/robots/tom/v1/assemblies/.elbow_roll_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:57e545a1b94b5a30ffdd710a9e4d960e039b6c2b88ab12cf9bce4668519577b0 -size 12377116 diff --git a/models/robots/tom/v1/assemblies/.pitch_link_sts3215.step.glb b/models/robots/tom/v1/assemblies/.pitch_link_sts3215.step.glb deleted file mode 100644 index 4c527907..00000000 --- a/models/robots/tom/v1/assemblies/.pitch_link_sts3215.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:866b8876ad0cef18a1259c4e1dd6eb9f6a1854040b77cb73f4ad361771ba15ec -size 11926388 diff --git a/models/robots/tom/v1/assemblies/.pitch_link_sts3250.step.glb b/models/robots/tom/v1/assemblies/.pitch_link_sts3250.step.glb deleted file mode 100644 index f0a3cb3e..00000000 --- a/models/robots/tom/v1/assemblies/.pitch_link_sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:479700fc9e6b1bb080639d5ab7ad2722fec19d901407ff219759d38875379094 -size 4005564 diff --git a/models/robots/tom/v1/assemblies/.shoulder_roll_link.step.glb b/models/robots/tom/v1/assemblies/.shoulder_roll_link.step.glb deleted file mode 100644 index b8a9cd34..00000000 --- a/models/robots/tom/v1/assemblies/.shoulder_roll_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8a48d1ca8c1a3ca3b62972b8e46f83b1e7142799a17b5aba942f00fe9c36836 -size 3261028 diff --git a/models/robots/tom/v1/assemblies/.shoulder_yaw_link.step.glb b/models/robots/tom/v1/assemblies/.shoulder_yaw_link.step.glb deleted file mode 100644 index b8aaa8bf..00000000 --- a/models/robots/tom/v1/assemblies/.shoulder_yaw_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cc622639a1bbe8242b8ffe7970a89ba98fe8082ff556d2cf0cc296142534982c -size 3648636 diff --git a/models/robots/tom/v1/assemblies/base_link.py b/models/robots/tom/v1/assemblies/base_link.py deleted file mode 100644 index d7678a33..00000000 --- a/models/robots/tom/v1/assemblies/base_link.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.19]" - -BASE_PLATE_BOTTOM_Y_MM = -32.9752 -BASE_PLATE_TOP_Y_MM = -29.8002 -BASE_LINK_FLOOR_Y_OFFSET_MM = -BASE_PLATE_BOTTOM_Y_MM -STS3250_BASE_PLATE_FACE_LOCAL_Y_MM = -27.4 -STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM = ( - BASE_PLATE_TOP_Y_MM - - STS3250_BASE_PLATE_FACE_LOCAL_Y_MM -) - -STS3250_BASE_PLATE_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, BASE_LINK_FLOOR_Y_OFFSET_MM + STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - -BASE_PLATE_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, BASE_LINK_FLOOR_Y_OFFSET_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_3", - "transform": STS3250_BASE_PLATE_TRANSFORM, - }, - { - "path": "../parts/base_plate.step", - "name": "base_plate", - "transform": BASE_PLATE_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v1/assemblies/base_link.step b/models/robots/tom/v1/assemblies/base_link.step deleted file mode 100644 index e3600e72..00000000 --- a/models/robots/tom/v1/assemblies/base_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6655c5419185aee436de6dc130a2f6da08bc2d1d27f65da1c36c0a84269413b -size 2499702 diff --git a/models/robots/tom/v1/assemblies/elbow_roll_link.py b/models/robots/tom/v1/assemblies/elbow_roll_link.py deleted file mode 100644 index 45bf4917..00000000 --- a/models/robots/tom/v1/assemblies/elbow_roll_link.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from v1.assemblies.robot_arm_link_common import ( - rebased_robot_arm_instances, - translated_instances, -) - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.11,o1.12,o1.13,o1.14,o1.15]" -ANCHOR_INSTANCE_NAME = "quinary_servo_2020_connector" -EXTRACTED_INSTANCE_NAMES = ( - "quinary_servo_2020_connector", - "quinary_servo_hfs5_2020", - "quinary_servo_end_bracket", - "senary_servo_end_mount", - "sts3215_6", -) -SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM = 2.7308 -SERVO_END_MOUNT_INSTANCE_NAMES = frozenset( - { - "senary_servo_end_mount", - "sts3215_6", - } -) - - -def gen_step() -> dict[str, object]: - instances = rebased_robot_arm_instances( - source_ref=SOURCE_REF, - anchor_instance_name=ANCHOR_INSTANCE_NAME, - extracted_instance_names=EXTRACTED_INSTANCE_NAMES, - ) - return { - "instances": translated_instances( - instances, - names=SERVO_END_MOUNT_INSTANCE_NAMES, - vector_xyz=(0.0, 0.0, 1.0), - distance_mm=SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM, - ), - } diff --git a/models/robots/tom/v1/assemblies/elbow_roll_link.step b/models/robots/tom/v1/assemblies/elbow_roll_link.step deleted file mode 100644 index 1edf7854..00000000 --- a/models/robots/tom/v1/assemblies/elbow_roll_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af32ba6e66cd88ceccfa5455d2a9d945c4b5e1c0fde22de078be5904b7db9790 -size 22535991 diff --git a/models/robots/tom/v1/assemblies/pitch_link_sts3215.py b/models/robots/tom/v1/assemblies/pitch_link_sts3215.py deleted file mode 100644 index dfd0f9e6..00000000 --- a/models/robots/tom/v1/assemblies/pitch_link_sts3215.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - - -YOKE_PLATE_FACE_X_MM = -57.5 -STS3215_YOKE_PLATE_FACE_LOCAL_Y_MM = -27.4 - -STS3215_TRANSFORM = [ - 0.0, - -1.0, - 0.0, - ( - YOKE_PLATE_FACE_X_MM - + STS3215_YOKE_PLATE_FACE_LOCAL_Y_MM - ), - 1.0, 0.0, 0.0, 16.4, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - ], - }, - { - "path": "../parts/imports/sts3215.step", - "name": "sts3215", - "transform": STS3215_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v1/assemblies/pitch_link_sts3215.step b/models/robots/tom/v1/assemblies/pitch_link_sts3215.step deleted file mode 100644 index e4167d43..00000000 --- a/models/robots/tom/v1/assemblies/pitch_link_sts3215.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9e9abf3ac5cc6312a1b1757e4c2c4097d09555222b97df30ade66a0ca73c4c82 -size 22241228 diff --git a/models/robots/tom/v1/assemblies/pitch_link_sts3250.py b/models/robots/tom/v1/assemblies/pitch_link_sts3250.py deleted file mode 100644 index 1eefcf0c..00000000 --- a/models/robots/tom/v1/assemblies/pitch_link_sts3250.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - - -YOKE_PLATE_FACE_X_MM = -57.5 -STS3250_YOKE_PLATE_FACE_LOCAL_Y_MM = -27.4 - -STS3250_TRANSFORM = [ - 0.0, - -1.0, - 0.0, - ( - YOKE_PLATE_FACE_X_MM - + STS3250_YOKE_PLATE_FACE_LOCAL_Y_MM - ), - 1.0, 0.0, 0.0, 16.4, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - ], - }, - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250", - "transform": STS3250_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v1/assemblies/pitch_link_sts3250.step b/models/robots/tom/v1/assemblies/pitch_link_sts3250.step deleted file mode 100644 index 5d3b0c12..00000000 --- a/models/robots/tom/v1/assemblies/pitch_link_sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5cc6af5fca5f345655e28ead6bd27ed9f7bfc3d7c51722f178034ba12c25e85b -size 2726671 diff --git a/models/robots/tom/v1/assemblies/robot_arm_link_common.py b/models/robots/tom/v1/assemblies/robot_arm_link_common.py deleted file mode 100644 index 47c88362..00000000 --- a/models/robots/tom/v1/assemblies/robot_arm_link_common.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path -from pathlib import PurePosixPath - -TOM_DIR = Path(__file__).resolve().parents[2] -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common import robot_arm - - -def _matrix(transform: list[float]) -> list[list[float]]: - return [transform[index : index + 4] for index in range(0, 16, 4)] - - -def _flatten(matrix: list[list[float]]) -> list[float]: - return [value for row in matrix for value in row] - - -def _matmul(left: list[list[float]], right: list[list[float]]) -> list[list[float]]: - return [ - [ - sum(left[row][inner] * right[inner][col] for inner in range(4)) - for col in range(4) - ] - for row in range(4) - ] - - -def _invert_rigid_transform(transform: list[float]) -> list[list[float]]: - matrix = _matrix(transform) - rotation = [row[:3] for row in matrix[:3]] - translation = [matrix[row][3] for row in range(3)] - inverse_rotation = [[rotation[col][row] for col in range(3)] for row in range(3)] - inverse_translation = [ - -sum(inverse_rotation[row][col] * translation[col] for col in range(3)) - for row in range(3) - ] - return [ - [*inverse_rotation[0], inverse_translation[0]], - [*inverse_rotation[1], inverse_translation[1]], - [*inverse_rotation[2], inverse_translation[2]], - [0.0, 0.0, 0.0, 1.0], - ] - - -def _relative_step_path(path: str) -> str: - step_path = PurePosixPath(path) - if step_path.parts and step_path.parts[0] in {"imports", "gripper"}: - return (PurePosixPath("..") / "parts" / step_path).as_posix() - if step_path.parts and step_path.parts[0] == "assemblies": - return (PurePosixPath("..") / step_path).as_posix() - return (PurePosixPath("..") / "parts" / step_path.name).as_posix() - - -def translated_instances( - instances: list[dict[str, object]], - *, - names: frozenset[str], - vector_xyz: tuple[float, float, float], - distance_mm: float, -) -> list[dict[str, object]]: - translated: list[dict[str, object]] = [] - for instance in instances: - transform = [float(value) for value in instance["transform"]] - if str(instance["name"]) in names: - transform[3] += vector_xyz[0] * distance_mm - transform[7] += vector_xyz[1] * distance_mm - transform[11] += vector_xyz[2] * distance_mm - translated.append({**instance, "transform": transform}) - return translated - - -def rebased_robot_arm_instances( - *, - source_ref: str, - anchor_instance_name: str, - extracted_instance_names: tuple[str, ...], -) -> list[dict[str, object]]: - source_instances = { - str(instance["name"]): instance - for instance in robot_arm._assembly_instances() - } - missing = [ - name for name in extracted_instance_names if name not in source_instances - ] - if missing: - raise RuntimeError(f"{source_ref} references missing source instances: {missing!r}") - - anchor = source_instances[anchor_instance_name] - anchor_inverse = _invert_rigid_transform( - [float(value) for value in anchor["transform"]] - ) - - instances: list[dict[str, object]] = [] - for name in extracted_instance_names: - source = source_instances[name] - relative_transform = _matmul( - anchor_inverse, - _matrix([float(value) for value in source["transform"]]), - ) - instances.append( - { - "path": _relative_step_path(str(source["path"])), - "name": str(source["name"]), - "transform": _flatten(relative_transform), - "use_source_colors": bool(source.get("use_source_colors", True)), - } - ) - return instances diff --git a/models/robots/tom/v1/assemblies/shoulder_roll_link.py b/models/robots/tom/v1/assemblies/shoulder_roll_link.py deleted file mode 100644 index 302cb766..00000000 --- a/models/robots/tom/v1/assemblies/shoulder_roll_link.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from v1.assemblies.robot_arm_link_common import ( - rebased_robot_arm_instances, - translated_instances, -) - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.4,o1.5,o1.6,o1.7,o1.8]" -ANCHOR_INSTANCE_NAME = "secondary_servo_2020_connector" -EXTRACTED_INSTANCE_NAMES = ( - "secondary_servo_2020_connector", - "secondary_servo_hfs5_2020", - "secondary_servo_end_bracket", - "quaternary_servo_end_mount", - "sts3250_4", -) -SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM = 2.7308 -SERVO_END_MOUNT_INSTANCE_NAMES = frozenset( - { - "quaternary_servo_end_mount", - "sts3250_4", - } -) - - -def gen_step() -> dict[str, object]: - instances = rebased_robot_arm_instances( - source_ref=SOURCE_REF, - anchor_instance_name=ANCHOR_INSTANCE_NAME, - extracted_instance_names=EXTRACTED_INSTANCE_NAMES, - ) - return { - "instances": translated_instances( - instances, - names=SERVO_END_MOUNT_INSTANCE_NAMES, - vector_xyz=(0.0, 0.0, 1.0), - distance_mm=SERVO_END_MOUNT_FLUSH_Z_ADJUST_MM, - ), - } diff --git a/models/robots/tom/v1/assemblies/shoulder_roll_link.step b/models/robots/tom/v1/assemblies/shoulder_roll_link.step deleted file mode 100644 index f64a61db..00000000 --- a/models/robots/tom/v1/assemblies/shoulder_roll_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05048892ad27c6e42f4acd73a387ce9daec6a83512109d5c69075450c08976b5 -size 3008817 diff --git a/models/robots/tom/v1/assemblies/shoulder_yaw_link.py b/models/robots/tom/v1/assemblies/shoulder_yaw_link.py deleted file mode 100644 index d6caccef..00000000 --- a/models/robots/tom/v1/assemblies/shoulder_yaw_link.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from v1.assemblies.robot_arm_link_common import rebased_robot_arm_instances - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.2]" -ANCHOR_INSTANCE_NAME = "sts3250_1" -EXTRACTED_INSTANCE_NAMES = ( - "sts3250_1", - "servo_end_mount", -) - - -def gen_step() -> dict[str, object]: - return { - "instances": rebased_robot_arm_instances( - source_ref=SOURCE_REF, - anchor_instance_name=ANCHOR_INSTANCE_NAME, - extracted_instance_names=EXTRACTED_INSTANCE_NAMES, - ), - } diff --git a/models/robots/tom/v1/assemblies/shoulder_yaw_link.step b/models/robots/tom/v1/assemblies/shoulder_yaw_link.step deleted file mode 100644 index 5f0297be..00000000 --- a/models/robots/tom/v1/assemblies/shoulder_yaw_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e0eb7caba749f82b62f1e7ff424884658855b65015bdf7212fa4a1ae133dbc51 -size 2601700 diff --git a/models/robots/tom/v1/parts/.base_clamp.step.glb b/models/robots/tom/v1/parts/.base_clamp.step.glb deleted file mode 100644 index 6be7f029..00000000 --- a/models/robots/tom/v1/parts/.base_clamp.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1e7f8e2c2500f6a32759186b5f0e0968420cb82036b2bd36f7a0ad996359e576 -size 694864 diff --git a/models/robots/tom/v1/parts/.base_plate.step.glb b/models/robots/tom/v1/parts/.base_plate.step.glb deleted file mode 100644 index 86a591ef..00000000 --- a/models/robots/tom/v1/parts/.base_plate.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a2058816b11c12a77025f326f389d7e414b1022792652a8e973b2226c52a467 -size 440740 diff --git a/models/robots/tom/v1/parts/.extrusion_bracket_2020.step.glb b/models/robots/tom/v1/parts/.extrusion_bracket_2020.step.glb deleted file mode 100644 index da872c34..00000000 --- a/models/robots/tom/v1/parts/.extrusion_bracket_2020.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e8ac4efa0103b4b59d7c78f7446c66e50a78fd2130d3e4e960b17d66b2a6c19a -size 525348 diff --git a/models/robots/tom/v1/parts/.servo_end_mount.step.glb b/models/robots/tom/v1/parts/.servo_end_mount.step.glb deleted file mode 100644 index 5f7492f1..00000000 --- a/models/robots/tom/v1/parts/.servo_end_mount.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfbca60ceff9b42adc31cbe0e18a70a37323196ddc4e16c4a02d1d845fdad9a0 -size 558624 diff --git a/models/robots/tom/v1/parts/.servo_horn_yoke.step.glb b/models/robots/tom/v1/parts/.servo_horn_yoke.step.glb deleted file mode 100644 index 8006e7ee..00000000 --- a/models/robots/tom/v1/parts/.servo_horn_yoke.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6136b17f95f5327732307d3c0124d90a2e1647839d2fe33a885aea07452c48fe -size 1078356 diff --git a/models/robots/tom/v1/parts/base_clamp.py b/models/robots/tom/v1/parts/base_clamp.py deleted file mode 100644 index 43301f7b..00000000 --- a/models/robots/tom/v1/parts/base_clamp.py +++ /dev/null @@ -1,765 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate a bent base clamp that wraps under the base servo footprint. - -The upper plate stays in the servo base frame, a side wall rises from the -positive-x side, and a lower return closes the U so the part can clamp to a -desk or fixture from below. -""" - -from __future__ import annotations - -from dataclasses import dataclass -import math -import os -import sys -from pathlib import Path - -from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet - -V1_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V1_DIR.parent -_CACHE_HOME = V1_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import build123d -import ezdxf -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - - -DISPLAY_NAME = "Base Clamp" -PLATE_THICKNESS_MM = 25.4 * 0.125 -SADDLE_FOOTPRINT_X_BUFFER_MM = 2.0 -SIDE_WALL_THICKNESS_MM = PLATE_THICKNESS_MM -BOOLEAN_OVERLAP_MM = 0.05 -CUT_EXTENSION_MM = 0.5 -CORNER_RADIUS_MM = 3.0 -RETURN_FACE_LENGTH_RATIO = 2.0 / 3.0 -M3_CLEARANCE_RADIUS_MM = 1.6 -BASE_CENTER_HORN_FACE_RADIUS_MM = 2.8 -BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM = 0.25 -BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM = ( - BASE_CENTER_HORN_FACE_RADIUS_MM + BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM -) -M8_CLEARANCE_DIAMETER_MM = 8.5 -M8_CLEARANCE_RADIUS_MM = 0.5 * M8_CLEARANCE_DIAMETER_MM -SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.125 -SENDCUTSEND_K_FACTOR = 0.44 -SENDCUTSEND_BEND_DEDUCTION_90_MM = 25.4 * 0.216 -SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * (25.4 * 0.630) -SENDCUTSEND_MIN_FLANGE_FORMED_MM = 25.4 * 0.476 -SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM = 0.1 - -# Base servo footprint bounds in robot-arm local coordinates. -SADDLE_BASE_FACE_Y_MM = -29.8002 -SADDLE_BASE_X_MIN_MM = -35.61 -SADDLE_BASE_X_MAX_MM = 6.338516 -SADDLE_BASE_Z_MIN_MM = -15.960226 -SADDLE_BASE_Z_MAX_MM = 15.960224 - -# 5x M3 lower-horn clearance pattern on the base plate. -BASE_HORN_CONNECTION_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = ( - (-32.5, 0.0, M3_CLEARANCE_RADIUS_MM), - (-25.5, -7.0, M3_CLEARANCE_RADIUS_MM), - (-25.5, 0.0, BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM), - (-25.5, 7.0, M3_CLEARANCE_RADIUS_MM), - (-18.5, 0.0, M3_CLEARANCE_RADIUS_MM), -) - -# Resolved from: -# @cad[STEP/imports/sts3250#o1.2.e920,o1.2.e922] -# @cad[STEP/imports/sts3250#o1.2.e921,o1.2.e923] -M2_CLEARANCE_RADIUS_MM = 1.1 -BASE_SERVO_M2_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = ( - (-17.2, -10.25, M2_CLEARANCE_RADIUS_MM), - (7.25, -10.25, M2_CLEARANCE_RADIUS_MM), - (-17.2, 10.25, M2_CLEARANCE_RADIUS_MM), - (7.25, 10.25, M2_CLEARANCE_RADIUS_MM), -) -BASE_ATTACHMENT_HOLES_MM: tuple[tuple[float, float, float], ...] = ( - BASE_HORN_CONNECTION_HOLE_SPECS_MM + BASE_SERVO_M2_HOLE_SPECS_MM -) - -# Resolved from the four PCB spacer hole centers in the original SO-101 bus board. -SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM: tuple[tuple[float, float], ...] = ( - (2.4930065, 2.4999875), - (2.4930065, 30.4999925), - (39.493009, 2.4999875), - (39.505008000000004, 30.4999925), -) -SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM = M3_CLEARANCE_RADIUS_MM -SERVO_BUS_MOUNT_LOCAL_X_MIN_MM = min(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_X_MAX_MM = max(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_X_MIN_MM + SERVO_BUS_MOUNT_LOCAL_X_MAX_MM) -SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM = min(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM = max(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM + SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM) -SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM = 0.97 -BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM = 0.53 -BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM = ( - SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM + BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM -) -BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM = ( - (SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM - SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM) - + (2.0 * (SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM + BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM)) -) - -# SendCutSend's current .125" 5052 guideline uses a 0.630" die width for -# bending; cut features should stay outside half of that die width from the -# bend line. We size the wall from the mount-hole span plus that offset. -SENDCUTSEND_5052_125_DIE_WIDTH_MM = 0.630 * 25.4 -SENDCUTSEND_FEATURE_TO_BEND_MIN_MM = (0.5 * SENDCUTSEND_5052_125_DIE_WIDTH_MM) + SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM -U_BRACKET_CLEARANCE_MM = ( - (SERVO_BUS_MOUNT_LOCAL_X_MAX_MM - SERVO_BUS_MOUNT_LOCAL_X_MIN_MM) - + (2.0 * SENDCUTSEND_FEATURE_TO_BEND_MIN_MM) - - (2.0 * PLATE_THICKNESS_MM) -) - -# Resolved from @cad[STEP/imports/servo_bus]. -SERVO_BUS_LOCAL_X_MIN_MM = -0.390013 -SERVO_BUS_LOCAL_X_MAX_MM = 42.401989 -SERVO_BUS_LOCAL_Y_MIN_MM = -0.012095 -SERVO_BUS_LOCAL_Y_MAX_MM = 33.012075 -SERVO_BUS_LOCAL_Z_MIN_MM = -6.0 -SERVO_BUS_LOCAL_Z_MAX_MM = 1.599999 -BASE_PLATE_Z_WIDTH_MM = max( - SERVO_BUS_LOCAL_Y_MAX_MM - SERVO_BUS_LOCAL_Y_MIN_MM, - BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM, -) - - -@dataclass(frozen=True) -class BracketLayout: - x_min: float - x_max: float - y_min: float - y_max: float - z_min: float - z_max: float - width: float - height: float - return_width: float - return_x_min: float - return_y_min: float - return_y_max: float - side_x_min: float - side_x_max: float - overall_y_min: float - face_y: float - attachment_holes: tuple[tuple[float, float, float], ...] - return_mount_holes: tuple[tuple[float, float, float], ...] - servo_bus_mount_holes_yzr: tuple[tuple[float, float, float], ...] - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - top_flange_flat_length_mm: float - return_flange_flat_length_mm: float - web_flat_start_mm: float - web_flat_end_mm: float - bend_line_positions_mm: tuple[float, float] - cut_circles: tuple[tuple[float, float, float], ...] - rounded_corner_radius_mm: float - min_feature_to_bend_mm: float - - -def _make_box( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _sendcutsend_outside_setback_90_mm() -> float: - return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + PLATE_THICKNESS_MM - - -def _sendcutsend_bend_allowance_90_mm() -> float: - return (math.pi * 0.5) * ( - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * PLATE_THICKNESS_MM) - ) - - -def _validate_sendcutsend_bend_rule() -> None: - bend_deduction_error = abs( - SENDCUTSEND_BEND_DEDUCTION_90_MM - - ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm()) - ) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _cast_shape(obj: object) -> build123d.Shape: - from build123d.topology import downcast - from build123d.topology.shape_core import shapetype - - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _build_bracket_layout() -> BracketLayout: - x_min = SADDLE_BASE_X_MIN_MM - SADDLE_FOOTPRINT_X_BUFFER_MM - x_max = SADDLE_BASE_X_MAX_MM + SADDLE_FOOTPRINT_X_BUFFER_MM - required_x_max_for_mount_holes = max( - x - + radius - + SENDCUTSEND_HALF_DIE_WIDTH_MM - + SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM - for x, _z, radius in BASE_ATTACHMENT_HOLES_MM - ) - x_max = max(x_max, required_x_max_for_mount_holes) - z_center = 0.5 * (SADDLE_BASE_Z_MIN_MM + SADDLE_BASE_Z_MAX_MM) - z_min = z_center - (0.5 * BASE_PLATE_Z_WIDTH_MM) - z_max = z_center + (0.5 * BASE_PLATE_Z_WIDTH_MM) - y_min = SADDLE_BASE_FACE_Y_MM - PLATE_THICKNESS_MM - y_max = SADDLE_BASE_FACE_Y_MM - return_y_max = y_min - U_BRACKET_CLEARANCE_MM - return_y_min = return_y_max - PLATE_THICKNESS_MM - side_x_min = x_max - BOOLEAN_OVERLAP_MM - side_x_max = x_max + SIDE_WALL_THICKNESS_MM - return_x_min = side_x_min - (RETURN_FACE_LENGTH_RATIO * (side_x_max - x_min)) - return_mount_holes = ( - ( - 0.5 * (return_x_min + side_x_min), - 0.5 * (z_min + z_max), - M8_CLEARANCE_RADIUS_MM, - ), - ) - - servo_bus_mount_holes_yzr = _servo_bus_mount_holes_yzr( - y_min=return_y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - ) - - return BracketLayout( - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - width=x_max - x_min, - height=z_max - z_min, - return_width=side_x_max - return_x_min, - return_x_min=return_x_min, - return_y_min=return_y_min, - return_y_max=return_y_max, - side_x_min=side_x_min, - side_x_max=side_x_max, - overall_y_min=return_y_min, - face_y=y_max, - attachment_holes=BASE_ATTACHMENT_HOLES_MM, - return_mount_holes=return_mount_holes, - servo_bus_mount_holes_yzr=servo_bus_mount_holes_yzr, - ) - - -def _fillet_plan_corners( - plate: build123d.Solid, - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, - include_min_x: bool = True, - include_max_x: bool = True, -) -> build123d.Shape: - matches: list[build123d.Edge] = [] - for edge in plate.edges(): - if _geom_type_name(edge) != "LINE": - continue - bb = edge.bounding_box() - if abs((bb.max.Y - bb.min.Y) - (y_max - y_min)) > 1e-3: - continue - if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Z - bb.min.Z) > 1e-3: - continue - center = edge.center() - if abs(abs(center.X - ((x_min + x_max) * 0.5)) - ((x_max - x_min) * 0.5)) > 1e-3: - continue - if abs(abs(center.Z - ((z_min + z_max) * 0.5)) - ((z_max - z_min) * 0.5)) > 1e-3: - continue - at_min_x = abs(center.X - x_min) <= 1e-3 - at_max_x = abs(center.X - x_max) <= 1e-3 - if at_min_x and not include_min_x: - continue - if at_max_x and not include_max_x: - continue - matches.append(edge) - - expected_count = (2 if include_min_x else 0) + (2 if include_max_x else 0) - if len(matches) != expected_count: - raise RuntimeError(f"Expected {expected_count} plan-view corner edges to fillet, found {len(matches)}") - - mk = BRepFilletAPI_MakeFillet(plate.wrapped) - for edge in matches: - mk.Add(CORNER_RADIUS_MM, edge.wrapped) - mk.Build() - if not mk.IsDone(): - raise RuntimeError("Failed to round U-bracket plate corners") - return _cast_shape(mk.Shape()) - - -def _cut_y_holes( - plate: build123d.Shape, - *, - holes_xzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - hole_depth = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - for x, z, radius in holes_xzr: - if not (x_min + radius <= x <= x_max - radius): - raise RuntimeError(f"Hole at x={x:.3f} mm does not fit within the bracket x span") - if not (z_min + radius <= z <= z_max - radius): - raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span") - plate = plate.cut( - _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(x, y_min - CUT_EXTENSION_MM, z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - return plate - - -def _cut_x_holes( - bracket: build123d.Shape, - *, - holes_yzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - hole_depth = (x_max - x_min) + (2.0 * CUT_EXTENSION_MM) - for y, z, radius in holes_yzr: - if not (y_min + radius <= y <= y_max - radius): - raise RuntimeError(f"Hole at y={y:.3f} mm does not fit within the bracket y span") - if not (z_min + radius <= z <= z_max - radius): - raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span") - bracket = bracket.cut( - _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(x_min - CUT_EXTENSION_MM, y, z), - direction=build123d.Vector(1.0, 0.0, 0.0), - ) - ) - return bracket - - -def _cut_base_plate_mount_features( - plate: build123d.Shape, - *, - holes_xzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - return _cut_y_holes( - plate, - holes_xzr=holes_xzr, - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - ) - - -def _as_single_solid(shape: build123d.Shape) -> build123d.Solid: - solids = list(shape.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected a single solid, found {len(solids)}") - return solids[0] - - -def _make_lower_plate(layout: BracketLayout) -> build123d.Shape: - plate = _make_box( - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _fillet_plan_corners( - plate, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - include_min_x=True, - include_max_x=False, - ) - plate = _cut_base_plate_mount_features( - plate, - holes_xzr=layout.attachment_holes, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - return _as_single_solid(plate) - - -def _servo_bus_mount_holes_yzr( - *, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> tuple[tuple[float, float, float], ...]: - target_center_y = 0.5 * (y_min + y_max) - target_center_z = 0.5 * (z_min + z_max) - bus_to_bracket_y_offset = target_center_y - SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM - bus_to_bracket_z_offset = target_center_z + SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM - - return tuple( - ( - bus_x + bus_to_bracket_y_offset, - bus_to_bracket_z_offset - bus_y, - SERVO_BUS_SPACER_CLEARANCE_RADIUS_MM, - ) - for bus_x, bus_y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM - ) - - -def _make_return_plate(layout: BracketLayout) -> build123d.Shape: - plate = _make_box( - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _fillet_plan_corners( - plate, - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - include_min_x=True, - include_max_x=False, - ) - plate = _cut_y_holes( - plate, - holes_xzr=layout.return_mount_holes, - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - return _as_single_solid(plate) - - -def _make_side_wall(layout: BracketLayout) -> build123d.Shape: - return _make_box( - x_min=layout.side_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - - -def build_bracket() -> build123d.Solid: - layout = _build_bracket_layout() - - bracket = _make_lower_plate(layout) - bracket = _as_single_solid(bracket.fuse(_make_side_wall(layout))) - bracket = _as_single_solid(bracket.fuse(_make_return_plate(layout))) - bracket = _as_single_solid( - _cut_x_holes( - bracket, - holes_yzr=layout.servo_bus_mount_holes_yzr, - x_min=layout.side_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - ) - - print( - f"{DISPLAY_NAME}: " - f"lower_width={layout.width:.3f} mm, " - f"return_width={layout.return_width:.3f} mm, " - f"lower_height={layout.height:.3f} mm, " - f"plate_thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"clearance={U_BRACKET_CLEARANCE_MM:.3f} mm, " - f"overall_y_min={layout.overall_y_min:.4f} mm, " - f"horn_m3_holes={len(BASE_HORN_CONNECTION_HOLE_SPECS_MM)}, " - f"m2_mount_holes={len(BASE_SERVO_M2_HOLE_SPECS_MM)}, " - f"return_mount_holes={len(layout.return_mount_holes)}, " - f"servo_bus_holes={len(layout.servo_bus_mount_holes_yzr)}, " - f"flush_y={layout.face_y:.4f} mm" - ) - return bracket - - -def _build_flat_pattern(layout: BracketLayout) -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = _sendcutsend_outside_setback_90_mm() - bend_allowance = _sendcutsend_bend_allowance_90_mm() - top_flange_outside_length = layout.side_x_max - layout.x_min - return_flange_outside_length = layout.return_width - top_flange_flat_length = top_flange_outside_length - outside_setback - return_flange_flat_length = return_flange_outside_length - outside_setback - min_flange_outside_length = min(top_flange_outside_length, return_flange_outside_length) - min_flange_flat_length = min(top_flange_flat_length, return_flange_flat_length) - if min_flange_outside_length < SENDCUTSEND_MIN_FLANGE_FORMED_MM: - raise RuntimeError( - "Flange length is below the configured bend-rule minimum: " - f"{min_flange_outside_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm" - ) - if min_flange_flat_length <= 0.0: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"{min_flange_flat_length:.3f} mm" - ) - - web_outside_length = layout.y_max - layout.return_y_min - web_flat_length = web_outside_length - (2.0 * SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM) - if web_flat_length <= 0.0: - raise RuntimeError( - f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm" - ) - - web_flat_start = top_flange_flat_length + bend_allowance - web_flat_end = web_flat_start + web_flat_length - total_length = web_flat_end + bend_allowance + return_flange_flat_length - width = layout.z_max - layout.z_min - bend_line_positions = ( - top_flange_flat_length + (0.5 * bend_allowance), - web_flat_end + (0.5 * bend_allowance), - ) - - top_tangent_y = layout.y_max - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - bottom_tangent_y = layout.return_y_min + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - - feature_extents: list[tuple[float, float]] = [] - cut_circles: list[tuple[float, float, float]] = [] - for x, z, radius in layout.attachment_holes: - circle = (x - layout.x_min, z - layout.z_min, radius) - cut_circles.append(circle) - feature_extents.append((circle[0], radius)) - for y, z, radius in layout.servo_bus_mount_holes_yzr: - circle = (web_flat_start + (top_tangent_y - y), z - layout.z_min, radius) - cut_circles.append(circle) - feature_extents.append((circle[0], radius)) - for x, z, radius in layout.return_mount_holes: - circle = (total_length - (x - layout.return_x_min), z - layout.z_min, radius) - cut_circles.append(circle) - feature_extents.append((circle[0], radius)) - - min_feature_to_bend = min( - min(abs(center_u - bend_u) - feature_radius for bend_u in bend_line_positions) - for center_u, feature_radius in feature_extents - ) - - return FlatPattern( - total_length_mm=total_length, - width_mm=width, - top_flange_flat_length_mm=top_flange_flat_length, - return_flange_flat_length_mm=return_flange_flat_length, - web_flat_start_mm=web_flat_start, - web_flat_end_mm=web_flat_end, - bend_line_positions_mm=bend_line_positions, - cut_circles=tuple(cut_circles), - rounded_corner_radius_mm=CORNER_RADIUS_MM, - min_feature_to_bend_mm=min_feature_to_bend, - ) - - -def _add_rounded_rect_outline( - msp: ezdxf.layouts.Modelspace, - *, - length: float, - width: float, - radius: float, -) -> None: - if radius <= 0.0: - raise RuntimeError(f"Corner radius must be positive, got {radius:.3f} mm") - if width <= (2.0 * radius): - raise RuntimeError( - "Corner radius is too large for the base clamp flat-pattern width: " - f"{radius:.3f} mm" - ) - if length <= (2.0 * radius): - raise RuntimeError( - "Corner radius is too large for the base clamp flat-pattern length: " - f"{radius:.3f} mm" - ) - - msp.add_line((radius, 0.0), (length - radius, 0.0), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(length - radius, radius), - radius=radius, - start_angle=-90.0, - end_angle=0.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((length, radius), (length, width - radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(length - radius, width - radius), - radius=radius, - start_angle=0.0, - end_angle=90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((length - radius, width), (radius, width), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(radius, width - radius), - radius=radius, - start_angle=90.0, - end_angle=180.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((0.0, width - radius), (0.0, radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(radius, radius), - radius=radius, - start_angle=180.0, - end_angle=270.0, - dxfattribs={"layer": "CUT"}, - ) - - -def build_dxf() -> ezdxf.document.Drawing: - layout = _build_bracket_layout() - flat_pattern = _build_flat_pattern(layout) - - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - _add_rounded_rect_outline( - msp, - length=flat_pattern.total_length_mm, - width=flat_pattern.width_mm, - radius=flat_pattern.rounded_corner_radius_mm, - ) - for center_u, center_v, radius in flat_pattern.cut_circles: - msp.add_circle((center_u, center_v), radius, dxfattribs={"layer": "CUT"}) - for bend_line_u in flat_pattern.bend_line_positions_mm: - msp.add_line( - (bend_line_u, 0.0), - (bend_line_u, flat_pattern.width_mm), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - - print( - f"{DISPLAY_NAME} DXF: " - f"stock_thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"flat_length={flat_pattern.total_length_mm:.3f} mm, " - f"flat_width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"cut_circles={len(flat_pattern.cut_circles)}" - ) - print( - "DXF bend checks " - f"top_flange_flat={flat_pattern.top_flange_flat_length_mm:.3f} mm, " - f"return_flange_flat={flat_pattern.return_flange_flat_length_mm:.3f} mm, " - f"feature_to_bend_min={flat_pattern.min_feature_to_bend_mm:.3f} mm, " - f"feature_to_bend_required={SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - if flat_pattern.min_feature_to_bend_mm < SENDCUTSEND_HALF_DIE_WIDTH_MM: - print( - "Warning: flat-pattern features enter the bend die region by " - f"{SENDCUTSEND_HALF_DIE_WIDTH_MM - flat_pattern.min_feature_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_bracket(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v1/parts/base_clamp.step b/models/robots/tom/v1/parts/base_clamp.step deleted file mode 100644 index cbdf7298..00000000 --- a/models/robots/tom/v1/parts/base_clamp.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fa2966ceb525ece9fc041ec5cb86bbb7d413d972b59b678e9e3cda2c0dd8f511 -size 102864 diff --git a/models/robots/tom/v1/parts/base_plate.py b/models/robots/tom/v1/parts/base_plate.py deleted file mode 100644 index 7d0d9300..00000000 --- a/models/robots/tom/v1/parts/base_plate.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate a flat base plate derived from the top face of base_clamp. - -This part keeps the exact top-plate footprint and base-servo mounting pattern -from the bent clamp, but strips away the side wall and lower return so it -remains a single flat plate in the same local frame. -""" - -from __future__ import annotations - -from dataclasses import dataclass -import os -import sys -from pathlib import Path - -V1_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V1_DIR.parent -PARTS_DIR = Path(__file__).resolve().parent -_CACHE_HOME = V1_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) - -import build123d - -for path in (TOM_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -from base_clamp import ( - BASE_HORN_CONNECTION_HOLE_SPECS_MM, - BASE_SERVO_M2_HOLE_SPECS_MM, - CORNER_RADIUS_MM, - PLATE_THICKNESS_MM, - _as_single_solid, - _build_bracket_layout, - _cut_base_plate_mount_features, - _fillet_plan_corners, - _make_box, -) - - -DISPLAY_NAME = "Base Plate" - - -@dataclass(frozen=True) -class PlateLayout: - x_min: float - x_max: float - y_min: float - y_max: float - z_min: float - z_max: float - width: float - height: float - face_y: float - attachment_holes: tuple[tuple[float, float, float], ...] - - -def _build_plate_layout() -> PlateLayout: - bracket_layout = _build_bracket_layout() - x_min = bracket_layout.x_min - x_max = bracket_layout.side_x_max - y_min = bracket_layout.y_min - y_max = bracket_layout.y_max - z_min = bracket_layout.z_min - z_max = bracket_layout.z_max - - return PlateLayout( - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - width=x_max - x_min, - height=z_max - z_min, - face_y=y_max, - attachment_holes=bracket_layout.attachment_holes, - ) - - -def build_plate() -> build123d.Solid: - layout = _build_plate_layout() - - plate = _make_box( - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _fillet_plan_corners( - plate, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - include_min_x=True, - include_max_x=False, - ) - plate = _cut_base_plate_mount_features( - plate, - holes_xzr=layout.attachment_holes, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _as_single_solid(plate) - - print( - f"{DISPLAY_NAME}: " - f"width={layout.width:.3f} mm, " - f"height={layout.height:.3f} mm, " - f"thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"corner_radius={CORNER_RADIUS_MM:.3f} mm, " - f"horn_m3_holes={len(BASE_HORN_CONNECTION_HOLE_SPECS_MM)}, " - f"m2_holes={len(BASE_SERVO_M2_HOLE_SPECS_MM)}, " - f"flush_y={layout.face_y:.4f} mm" - ) - return plate - - -def gen_step() -> dict[str, object]: - return { - "shape": build_plate(), - } - - -def gen() -> None: - gen_step() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v1/parts/base_plate.step b/models/robots/tom/v1/parts/base_plate.step deleted file mode 100644 index a7b422e4..00000000 --- a/models/robots/tom/v1/parts/base_plate.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bb7fc37ba0f58d2b769c4f5d5d801597f67bbda0a4864696cd1174cfa00a4b41 -size 58973 diff --git a/models/robots/tom/v1/parts/extrusion_bracket_2020.py b/models/robots/tom/v1/parts/extrusion_bracket_2020.py deleted file mode 100644 index 3db4c8da..00000000 --- a/models/robots/tom/v1/parts/extrusion_bracket_2020.py +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env python3 -""" -Bent-sheet end bracket for wrapping a 2020 extrusion end and two side faces. -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -V1_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V1_DIR.parent -_CACHE_HOME = V1_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import build123d -import ezdxf -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.keyed_connection import ( - KEYED_CONNECTION_HALF_SPAN_MM, - KEYED_CONNECTION_REF, - KEYED_CONNECTION_ROTATION_DEG, - make_keyed_connection_face, - sample_keyed_connection_outline, -) - - -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Extrusion Bracket 2020" - -EXTRUSION_X_SPAN_MM = 20.0 -EXTRUSION_X_HALF_SPAN_MM = 0.5 * EXTRUSION_X_SPAN_MM -EXTRUSION_Y_SPAN_MM = 20.0 -EXTRUSION_Y_HALF_SPAN_MM = 0.5 * EXTRUSION_Y_SPAN_MM -# Keep the bracket seated tightly on the 2020 extrusion; any additional offset -# should come from a future face redesign rather than opening the full wrap gap. -EXTRUSION_CLEARANCE_MM = 0.5 -SHORT_SIDE_MM = EXTRUSION_X_SPAN_MM -SHORT_SIDE_HALF_MM = 0.5 * SHORT_SIDE_MM -SHEET_THICKNESS_MM = 1.6 -BRACKET_DEPTH_MM = 30.0 -FREE_END_CORNER_RADIUS_MM = 1.0 -M5_CLEARANCE_RADIUS_MM = 2.75 -# Keep the inner row a bit farther off the bend die region for SendCutSend. -SIDE_FASTENER_Z_POSITIONS_MM = (10.0, 22.0) -CUT_EXTENSION_MM = 1.0 -EDGE_MATCH_TOLERANCE_MM = 1e-3 - -SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.038 -SENDCUTSEND_K_FACTOR = 0.48 -SENDCUTSEND_BEND_DEDUCTION_90_MM = ( - 2.0 * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SHEET_THICKNESS_MM) -) - ( - (math.pi * 0.5) - * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SHEET_THICKNESS_MM)) -) -SENDCUTSEND_MIN_FLANGE_FORMED_MM = 25.4 * 0.313 -SENDCUTSEND_MIN_FLANGE_FLAT_MM = 25.4 * 0.255 -SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * (25.4 * 0.472) - -INNER_X_MIN = -(EXTRUSION_X_HALF_SPAN_MM + EXTRUSION_CLEARANCE_MM) -INNER_X_MAX = -INNER_X_MIN -INNER_Y_MIN = -(EXTRUSION_Y_HALF_SPAN_MM + EXTRUSION_CLEARANCE_MM) -INNER_Y_MAX = -INNER_Y_MIN -OUTER_X_MIN = -SHORT_SIDE_HALF_MM -OUTER_X_MAX = SHORT_SIDE_HALF_MM - -BASE_Z_MIN = -SHEET_THICKNESS_MM -BASE_Z_MAX = 0.0 -SIDE_Z_MIN = BASE_Z_MIN -SIDE_Z_MAX = BRACKET_DEPTH_MM - -LOWER_Y_MIN = INNER_Y_MIN - SHEET_THICKNESS_MM -LOWER_Y_MAX = INNER_Y_MIN -UPPER_Y_MIN = INNER_Y_MAX -UPPER_Y_MAX = INNER_Y_MAX + SHEET_THICKNESS_MM - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - flange_flat_length_mm: float - web_flat_start_mm: float - web_flat_end_mm: float - bend_line_positions_mm: tuple[float, float] - cut_profile_centers: tuple[tuple[float, float], ...] - cut_circles: tuple[tuple[float, float, float], ...] - rounded_corner_radius_mm: float - min_feature_to_bend_mm: float - - -def _make_box( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _make_keyed_connection_cutter_along_z( - *, - center_x: float, - center_y: float, - start_z: float, - height: float, -) -> build123d.Solid: - face = make_keyed_connection_face( - origin=build123d.Vector(center_x, center_y, start_z), - u_dir=build123d.Vector(1.0, 0.0, 0.0), - v_dir=build123d.Vector(0.0, 1.0, 0.0), - ) - return build123d.Solid.extrude(face, build123d.Vector(0.0, 0.0, height)) - - -def _sendcutsend_outside_setback_90_mm() -> float: - return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SHEET_THICKNESS_MM - - -def _sendcutsend_bend_allowance_90_mm() -> float: - return (math.pi * 0.5) * ( - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SHEET_THICKNESS_MM) - ) - - -def _is_close(value: float, expected: float, tolerance: float = EDGE_MATCH_TOLERANCE_MM) -> bool: - return abs(value - expected) <= tolerance - - -def _validate_sendcutsend_bend_rule() -> None: - bend_deduction_error = abs( - SENDCUTSEND_BEND_DEDUCTION_90_MM - - ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm()) - ) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def build_bracket() -> build123d.Shape: - end_face = _make_box( - x_min=OUTER_X_MIN, - x_max=OUTER_X_MAX, - y_min=INNER_Y_MIN, - y_max=INNER_Y_MAX, - z_min=BASE_Z_MIN, - z_max=BASE_Z_MAX, - ) - - lower_flange = _make_box( - x_min=OUTER_X_MIN, - x_max=OUTER_X_MAX, - y_min=LOWER_Y_MIN, - y_max=LOWER_Y_MAX, - z_min=SIDE_Z_MIN, - z_max=SIDE_Z_MAX, - ) - - upper_flange = _make_box( - x_min=OUTER_X_MIN, - x_max=OUTER_X_MAX, - y_min=UPPER_Y_MIN, - y_max=UPPER_Y_MAX, - z_min=SIDE_Z_MIN, - z_max=SIDE_Z_MAX, - ) - - bracket = end_face.fuse(lower_flange).fuse(upper_flange) - - hole_length = SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - bracket = bracket.cut( - _make_keyed_connection_cutter_along_z( - center_x=0.0, - center_y=0.0, - start_z=BASE_Z_MIN - CUT_EXTENSION_MM, - height=hole_length, - ) - ) - - for z_position in SIDE_FASTENER_Z_POSITIONS_MM: - bracket = bracket.cut( - _make_cylinder_along( - radius=M5_CLEARANCE_RADIUS_MM, - height=hole_length, - origin=build123d.Vector(0.0, LOWER_Y_MIN - CUT_EXTENSION_MM, z_position), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - bracket = bracket.cut( - _make_cylinder_along( - radius=M5_CLEARANCE_RADIUS_MM, - height=hole_length, - origin=build123d.Vector(0.0, UPPER_Y_MAX + CUT_EXTENSION_MM, z_position), - direction=build123d.Vector(0.0, -1.0, 0.0), - ) - ) - - target_edge_centers = ( - build123d.Vector( - -SHORT_SIDE_HALF_MM, - LOWER_Y_MIN + (0.5 * SHEET_THICKNESS_MM), - BRACKET_DEPTH_MM, - ), - build123d.Vector( - SHORT_SIDE_HALF_MM, - LOWER_Y_MIN + (0.5 * SHEET_THICKNESS_MM), - BRACKET_DEPTH_MM, - ), - build123d.Vector( - -SHORT_SIDE_HALF_MM, - UPPER_Y_MAX - (0.5 * SHEET_THICKNESS_MM), - BRACKET_DEPTH_MM, - ), - build123d.Vector( - SHORT_SIDE_HALF_MM, - UPPER_Y_MAX - (0.5 * SHEET_THICKNESS_MM), - BRACKET_DEPTH_MM, - ), - ) - target_edges: list[build123d.Edge] = [] - for edge in bracket.edges(): - if edge.geom_type != build123d.GeomType.LINE: - continue - center = edge.center() - if any( - _is_close(center.X, target_center.X) - and _is_close(center.Y, target_center.Y) - and _is_close(center.Z, target_center.Z) - for target_center in target_edge_centers - ): - target_edges.append(edge) - - if len(target_edges) != len(target_edge_centers): - raise RuntimeError( - "Expected to find the four flange free-end corners for rounding, " - f"found {len(target_edges)}" - ) - - bracket = build123d.fillet(target_edges, FREE_END_CORNER_RADIUS_MM) - - solids = list(bracket.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected one wrap bracket solid, found {len(solids)}") - - bracket = solids[0] - bracket.label = PART_NAME - - bbox = bracket.bounding_box() - print( - "Bent-sheet 2020 end bracket: " - f"clearance={EXTRUSION_CLEARANCE_MM:.3f} mm, " - f"thickness={SHEET_THICKNESS_MM:.3f} mm, " - f"depth={BRACKET_DEPTH_MM:.3f} mm, " - f"free_corner_radius={FREE_END_CORNER_RADIUS_MM:.3f} mm, " - f"size=({bbox.size.X:.3f}, {bbox.size.Y:.3f}, {bbox.size.Z:.3f}) mm, " - f"plate_fasteners={2 * len(SIDE_FASTENER_Z_POSITIONS_MM)}, " - f"connection_ref={KEYED_CONNECTION_REF}, " - f"connection_rotation={KEYED_CONNECTION_ROTATION_DEG:.1f} deg, " - f"hole_diameter={2.0 * M5_CLEARANCE_RADIUS_MM:.3f} mm" - ) - return bracket - - -def _build_flat_pattern() -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = _sendcutsend_outside_setback_90_mm() - bend_allowance = _sendcutsend_bend_allowance_90_mm() - flange_outside_length = BRACKET_DEPTH_MM + SHEET_THICKNESS_MM - flange_flat_length = flange_outside_length - outside_setback - if flange_outside_length < SENDCUTSEND_MIN_FLANGE_FORMED_MM: - raise RuntimeError( - "Formed flange length is below the configured bend-rule minimum: " - f"{flange_outside_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm" - ) - if flange_flat_length < SENDCUTSEND_MIN_FLANGE_FLAT_MM: - raise RuntimeError( - "Flat flange length is below the configured bend-rule minimum: " - f"{flange_flat_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm" - ) - - web_outside_length = INNER_Y_MAX - INNER_Y_MIN - web_flat_length = web_outside_length - (2.0 * SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM) - if web_flat_length <= 0.0: - raise RuntimeError(f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm") - - web_flat_start = flange_flat_length + bend_allowance - web_flat_end = web_flat_start + web_flat_length - total_length = web_flat_end + bend_allowance + flange_flat_length - width = OUTER_X_MAX - OUTER_X_MIN - bend_line_positions = ( - flange_flat_length + (0.5 * bend_allowance), - web_flat_end + (0.5 * bend_allowance), - ) - - def _v_from_x(x_coord: float) -> float: - return x_coord - OUTER_X_MIN - - keyed_connection_center_u = web_flat_start + (-INNER_Y_MIN - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM) - cut_profile_centers = ((keyed_connection_center_u, _v_from_x(0.0)),) - cut_circles: list[tuple[float, float, float]] = [] - for z_position in SIDE_FASTENER_Z_POSITIONS_MM: - lower_u = BRACKET_DEPTH_MM - z_position - upper_u = total_length - (BRACKET_DEPTH_MM - z_position) - cut_circles.append((lower_u, _v_from_x(0.0), M5_CLEARANCE_RADIUS_MM)) - cut_circles.append((upper_u, _v_from_x(0.0), M5_CLEARANCE_RADIUS_MM)) - - min_feature_to_bend = min( - abs(keyed_connection_center_u - bend_u) - KEYED_CONNECTION_HALF_SPAN_MM - for bend_u in bend_line_positions - ) - min_feature_to_bend = min( - min_feature_to_bend, - min( - abs(center_u - bend_u) - radius - for center_u, _center_v, radius in cut_circles - for bend_u in bend_line_positions - ), - ) - return FlatPattern( - total_length_mm=total_length, - width_mm=width, - flange_flat_length_mm=flange_flat_length, - web_flat_start_mm=web_flat_start, - web_flat_end_mm=web_flat_end, - bend_line_positions_mm=bend_line_positions, - cut_profile_centers=cut_profile_centers, - cut_circles=tuple(cut_circles), - rounded_corner_radius_mm=FREE_END_CORNER_RADIUS_MM, - min_feature_to_bend_mm=min_feature_to_bend, - ) - - -def build_dxf() -> ezdxf.document.Drawing: - flat_pattern = _build_flat_pattern() - - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - corner_radius = flat_pattern.rounded_corner_radius_mm - if corner_radius <= 0.0: - outline = ( - (0.0, 0.0), - (flat_pattern.total_length_mm, 0.0), - (flat_pattern.total_length_mm, flat_pattern.width_mm), - (0.0, flat_pattern.width_mm), - ) - msp.add_lwpolyline(outline, format="xy", close=True, dxfattribs={"layer": "CUT"}) - else: - if (2.0 * corner_radius) >= flat_pattern.width_mm: - raise RuntimeError( - "Rounded flange corners exceed the flat-pattern width: " - f"diameter {2.0 * corner_radius:.3f} mm >= width {flat_pattern.width_mm:.3f} mm" - ) - msp.add_line( - (corner_radius, 0.0), - (flat_pattern.total_length_mm - corner_radius, 0.0), - dxfattribs={"layer": "CUT"}, - ) - msp.add_arc( - (corner_radius, corner_radius), - corner_radius, - 180.0, - 270.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_arc( - (flat_pattern.total_length_mm - corner_radius, corner_radius), - corner_radius, - 270.0, - 360.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line( - (flat_pattern.total_length_mm, corner_radius), - (flat_pattern.total_length_mm, flat_pattern.width_mm - corner_radius), - dxfattribs={"layer": "CUT"}, - ) - msp.add_arc( - (flat_pattern.total_length_mm - corner_radius, flat_pattern.width_mm - corner_radius), - corner_radius, - 0.0, - 90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line( - (flat_pattern.total_length_mm - corner_radius, flat_pattern.width_mm), - (corner_radius, flat_pattern.width_mm), - dxfattribs={"layer": "CUT"}, - ) - msp.add_arc( - (corner_radius, flat_pattern.width_mm - corner_radius), - corner_radius, - 90.0, - 180.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line( - (0.0, flat_pattern.width_mm - corner_radius), - (0.0, corner_radius), - dxfattribs={"layer": "CUT"}, - ) - - for center_u, center_v, radius in flat_pattern.cut_circles: - msp.add_circle((center_u, center_v), radius, dxfattribs={"layer": "CUT"}) - for center_u, center_v in flat_pattern.cut_profile_centers: - msp.add_lwpolyline( - sample_keyed_connection_outline(center_u=center_u, center_v=center_v, points_per_arc=16), - format="xy", - close=True, - dxfattribs={"layer": "CUT"}, - ) - - for bend_line_u in flat_pattern.bend_line_positions_mm: - msp.add_line( - (bend_line_u, 0.0), - (bend_line_u, flat_pattern.width_mm), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - - print( - "Flat pattern " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"cut_profiles={len(flat_pattern.cut_profile_centers)}, " - f"cut_circles={len(flat_pattern.cut_circles)}" - ) - print( - "DXF bend checks " - f"flange_flat={flat_pattern.flange_flat_length_mm:.3f} mm, " - f"flange_flat_required={SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm, " - f"feature_to_bend_min={flat_pattern.min_feature_to_bend_mm:.3f} mm, " - f"feature_to_bend_required={SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - if flat_pattern.min_feature_to_bend_mm < SENDCUTSEND_HALF_DIE_WIDTH_MM: - print( - "Warning: flat-pattern keyed feature enters the bend die region by " - f"{SENDCUTSEND_HALF_DIE_WIDTH_MM - flat_pattern.min_feature_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_bracket(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v1/parts/extrusion_bracket_2020.step b/models/robots/tom/v1/parts/extrusion_bracket_2020.step deleted file mode 100644 index 6318edc1..00000000 --- a/models/robots/tom/v1/parts/extrusion_bracket_2020.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe48f16f839a84bfffb00f0786d7aac49a941765ba5c476eda63962e0565ef02 -size 146224 diff --git a/models/robots/tom/v1/parts/gripper/.gripper_left_claw.step.glb b/models/robots/tom/v1/parts/gripper/.gripper_left_claw.step.glb deleted file mode 100644 index b788d515..00000000 --- a/models/robots/tom/v1/parts/gripper/.gripper_left_claw.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b2fa4f22e767aac43ef6e92ae88f2d1c0bc4ca859de3024dee7ba97ba70c7946 -size 2295500 diff --git a/models/robots/tom/v1/parts/gripper/.gripper_right_claw.step.glb b/models/robots/tom/v1/parts/gripper/.gripper_right_claw.step.glb deleted file mode 100644 index a0d860aa..00000000 --- a/models/robots/tom/v1/parts/gripper/.gripper_right_claw.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d3c17d29dbfd88399d956edc2be17b61f91cb15144018bbbf7b06d43f4c694e2 -size 2293600 diff --git a/models/robots/tom/v1/parts/gripper/.gripper_servo_gear.step.glb b/models/robots/tom/v1/parts/gripper/.gripper_servo_gear.step.glb deleted file mode 100644 index f59e12a6..00000000 --- a/models/robots/tom/v1/parts/gripper/.gripper_servo_gear.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:adbb7fb23a179800910e34e0f206cb92c96c65af390796ea1841a96e85656f97 -size 1044524 diff --git a/models/robots/tom/v1/parts/gripper/.gripper_static.step.glb b/models/robots/tom/v1/parts/gripper/.gripper_static.step.glb deleted file mode 100644 index d05ec9bb..00000000 --- a/models/robots/tom/v1/parts/gripper/.gripper_static.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e9bde8782d7dfdd5c7d4393b01480c3e146b61bb4595077da86e7477c065263 -size 8858760 diff --git a/models/robots/tom/v1/parts/gripper/follower_gripper_165x165_bed.stl b/models/robots/tom/v1/parts/gripper/follower_gripper_165x165_bed.stl deleted file mode 100644 index ef5ba3b2..00000000 --- a/models/robots/tom/v1/parts/gripper/follower_gripper_165x165_bed.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3592e09ea49c05a9f6b86ceb6796cb2d98f3d175c513ceac58976070511469ba -size 534184 diff --git a/models/robots/tom/v1/parts/gripper/gripper_component_extract.py b/models/robots/tom/v1/parts/gripper/gripper_component_extract.py deleted file mode 100644 index ab7efdb3..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_component_extract.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import os -import re -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -_CACHE_HOME = REPO_ROOT / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import build123d -from build123d.topology import downcast -from build123d.topology.shape_core import shapetype - -CAD_SCRIPT_PATH = Path(__file__).resolve().parents[6] / "skills" / "cad" / "scripts" -if str(CAD_SCRIPT_PATH) not in sys.path: - sys.path.insert(0, str(CAD_SCRIPT_PATH)) - -from cadpy_common.package_path import ensure_cadpy_package_path - -ensure_cadpy_package_path() - -from cadpy.step_scene import _located_shape, load_step_scene, occurrence_selector_id - - -SOURCE_STEP = Path(__file__).resolve().with_name("rb9_01_061_000_gripper.step") - - -def _cast_shape(obj: object) -> build123d.Shape: - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _safe_label(text: str | None, fallback: str) -> str: - value = re.sub(r"[^A-Za-z0-9_.-]+", "_", text or "").strip("_") - return value or fallback - - -def _leaf_color(scene: object, leaf: object) -> tuple[float, float, float, float] | None: - leaf_color = getattr(leaf, "color", None) - if leaf_color is not None: - return leaf_color - prototype_key = getattr(leaf, "prototype_key", None) - prototype_color = scene.prototype_colors.get(prototype_key) - if prototype_color is not None: - return prototype_color - return None - - -def _find_node(nodes: list[object], selector: str) -> object: - stack = list(reversed(nodes)) - while stack: - node = stack.pop() - if occurrence_selector_id(node) == selector: - return node - stack.extend(reversed(node.children)) - raise RuntimeError(f"Selector {selector} not found in {SOURCE_STEP}") - - -def _leaf_nodes(node: object) -> list[object]: - leaves: list[object] = [] - stack = [node] - while stack: - current = stack.pop() - if current.prototype_key is not None: - leaves.append(current) - stack.extend(reversed(current.children)) - return leaves - - -def _selector_matches(selector: str, parent_selector: str) -> bool: - return selector == parent_selector or selector.startswith(f"{parent_selector}.") - - -def _compound_from_leaves( - scene: object, - leaves: list[object], - *, - label: str, -) -> build123d.Compound: - children: list[build123d.Shape] = [] - for leaf in leaves: - if leaf.prototype_key not in scene.prototype_shapes: - raise RuntimeError(f"Leaf {occurrence_selector_id(leaf)} has no prototype shape") - child = _cast_shape(_located_shape(scene.prototype_shapes[leaf.prototype_key], leaf.location)) - child.label = _safe_label( - leaf.source_name or leaf.name, - occurrence_selector_id(leaf).replace(".", "_"), - ) - color = _leaf_color(scene, leaf) - if color is not None: - child.color = build123d.Color(*color) - children.append(child) - - if not children: - raise RuntimeError(f"No gripper shapes extracted for {label}") - return build123d.Compound(obj=children, children=children, label=label) - - -def extract_occurrences(selectors: tuple[str, ...], *, label: str) -> build123d.Compound: - if not SOURCE_STEP.exists(): - raise FileNotFoundError(f"Missing gripper source STEP: {SOURCE_STEP}") - - scene = load_step_scene(SOURCE_STEP) - leaves: list[object] = [] - for selector in selectors: - leaves.extend(_leaf_nodes(_find_node(scene.roots, selector))) - return _compound_from_leaves(scene, leaves, label=label) - - -def extract_all_except(excluded_selectors: tuple[str, ...], *, label: str) -> build123d.Compound: - if not SOURCE_STEP.exists(): - raise FileNotFoundError(f"Missing gripper source STEP: {SOURCE_STEP}") - - scene = load_step_scene(SOURCE_STEP) - leaves: list[object] = [] - for root in scene.roots: - for leaf in _leaf_nodes(root): - selector = occurrence_selector_id(leaf) - if any(_selector_matches(selector, excluded) for excluded in excluded_selectors): - continue - leaves.append(leaf) - return _compound_from_leaves(scene, leaves, label=label) diff --git a/models/robots/tom/v1/parts/gripper/gripper_left_claw.py b/models/robots/tom/v1/parts/gripper/gripper_left_claw.py deleted file mode 100644 index c57f8a12..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_left_claw.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from gripper_component_extract import extract_occurrences - - -LEFT_CLAW_SELECTORS = ( - "o1.13", # RB9.01.061.020 Clamp - "o1.29", # Gear rack - "o1.14", # Upper linear bearing - "o1.15", # Lower linear bearing -) - - -def build_left_claw(): - return extract_occurrences(LEFT_CLAW_SELECTORS, label="gripper_left_claw") - - -def gen_step() -> dict[str, object]: - return { - "shape": build_left_claw(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v1/parts/gripper/gripper_left_claw.step b/models/robots/tom/v1/parts/gripper/gripper_left_claw.step deleted file mode 100644 index 2931fb8a..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_left_claw.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe4604ea1e3925a76ecc99eaaff5d70ccaa5f9bedfc7da586fe90a6d2f15c4d3 -size 1918886 diff --git a/models/robots/tom/v1/parts/gripper/gripper_right_claw.py b/models/robots/tom/v1/parts/gripper/gripper_right_claw.py deleted file mode 100644 index 86d91fc1..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_right_claw.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from gripper_component_extract import extract_occurrences - - -RIGHT_CLAW_SELECTORS = ( - "o1.2", # RB9.01.061.020 Clamp - "o1.3", # Gear rack - "o1.7", # Upper linear bearing - "o1.12", # Lower linear bearing -) - - -def build_right_claw(): - return extract_occurrences(RIGHT_CLAW_SELECTORS, label="gripper_right_claw") - - -def gen_step() -> dict[str, object]: - return { - "shape": build_right_claw(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v1/parts/gripper/gripper_right_claw.step b/models/robots/tom/v1/parts/gripper/gripper_right_claw.step deleted file mode 100644 index 8350c3bc..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_right_claw.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6187798f70606b80d4333321dbbef1fd481d77a8ff411833dd31f6dfebf1978 -size 1918889 diff --git a/models/robots/tom/v1/parts/gripper/gripper_servo_gear.py b/models/robots/tom/v1/parts/gripper/gripper_servo_gear.py deleted file mode 100644 index e280732d..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_servo_gear.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from gripper_component_extract import extract_occurrences - - -SERVO_GEAR_SELECTORS = ("o1.4",) - - -def build_servo_gear(): - return extract_occurrences(SERVO_GEAR_SELECTORS, label="gripper_servo_gear") - - -def gen_step() -> dict[str, object]: - return { - "shape": build_servo_gear(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v1/parts/gripper/gripper_servo_gear.step b/models/robots/tom/v1/parts/gripper/gripper_servo_gear.step deleted file mode 100644 index 5e47cb95..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_servo_gear.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:410c1c15d516c0fdb3b819fe3be26f17637ae7f209855ab2469c6c35c39d7cb4 -size 849460 diff --git a/models/robots/tom/v1/parts/gripper/gripper_static.py b/models/robots/tom/v1/parts/gripper/gripper_static.py deleted file mode 100644 index 55020c81..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_static.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from gripper_component_extract import extract_all_except -from gripper_left_claw import LEFT_CLAW_SELECTORS -from gripper_right_claw import RIGHT_CLAW_SELECTORS -from gripper_servo_gear import SERVO_GEAR_SELECTORS - - -STATIC_EXCLUDED_SELECTORS = RIGHT_CLAW_SELECTORS + LEFT_CLAW_SELECTORS + SERVO_GEAR_SELECTORS - - -def build_static_gripper(): - return extract_all_except(STATIC_EXCLUDED_SELECTORS, label="gripper_static") - - -def gen_step() -> dict[str, object]: - return { - "shape": build_static_gripper(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v1/parts/gripper/gripper_static.step b/models/robots/tom/v1/parts/gripper/gripper_static.step deleted file mode 100644 index 97fe0466..00000000 --- a/models/robots/tom/v1/parts/gripper/gripper_static.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:99f5718cae21bfc97334b741865e5002a910a7b5a63d15aac48759781ac27c37 -size 8165381 diff --git a/models/robots/tom/v1/parts/gripper/rb9_01_061_000_gripper.step b/models/robots/tom/v1/parts/gripper/rb9_01_061_000_gripper.step deleted file mode 100644 index 3c537f91..00000000 --- a/models/robots/tom/v1/parts/gripper/rb9_01_061_000_gripper.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:24e36ec30a7b517bf75f0dd021ee2ebfb611c21656b9d96f9b0421bbcf67c848 -size 11058995 diff --git a/models/robots/tom/v1/parts/imports/.hfs5_2020_100.step.glb b/models/robots/tom/v1/parts/imports/.hfs5_2020_100.step.glb deleted file mode 100644 index 0da85aa4..00000000 --- a/models/robots/tom/v1/parts/imports/.hfs5_2020_100.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:150235915ee34fc6b79a9132f0ad61cae26693693b6e394192a61d3009a193e1 -size 188056 diff --git a/models/robots/tom/v1/parts/imports/.hfs5_2020_150.step.glb b/models/robots/tom/v1/parts/imports/.hfs5_2020_150.step.glb deleted file mode 100644 index 9e860581..00000000 --- a/models/robots/tom/v1/parts/imports/.hfs5_2020_150.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c401fdb05d9c4fc7d2f957f6ef6da2c924c883e7d56d4ea4bccdd1643f48e4e3 -size 187980 diff --git a/models/robots/tom/v1/parts/imports/.sts3215.step.glb b/models/robots/tom/v1/parts/imports/.sts3215.step.glb deleted file mode 100644 index 935c3d5c..00000000 --- a/models/robots/tom/v1/parts/imports/.sts3215.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0a70ab3f328a922409a622a52e449f8083505522424ebc6780064b2d0fe71bfb -size 9407056 diff --git a/models/robots/tom/v1/parts/imports/.sts3250.step.glb b/models/robots/tom/v1/parts/imports/.sts3250.step.glb deleted file mode 100644 index 88389c60..00000000 --- a/models/robots/tom/v1/parts/imports/.sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ddc6b41fbda2ae4fee080d3f2b82d68ac803c68b0b80e2a3c9b6c722380bf9cc -size 3245664 diff --git a/models/robots/tom/v1/parts/imports/hfs5_2020_100.step b/models/robots/tom/v1/parts/imports/hfs5_2020_100.step deleted file mode 100644 index 51f5fc1d..00000000 --- a/models/robots/tom/v1/parts/imports/hfs5_2020_100.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c8a231f14e3aad8edf84edc82add9196d82dc998d184fba365f1bcda80ad525 -size 238428 diff --git a/models/robots/tom/v1/parts/imports/hfs5_2020_150.step b/models/robots/tom/v1/parts/imports/hfs5_2020_150.step deleted file mode 100644 index 4ad3bb28..00000000 --- a/models/robots/tom/v1/parts/imports/hfs5_2020_150.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6869e2743ae88f4035e847fc01a5220ef939cbee7aee9d5ded709f2467169ee4 -size 238693 diff --git a/models/robots/tom/v1/parts/imports/sts3215.step b/models/robots/tom/v1/parts/imports/sts3215.step deleted file mode 100644 index 6e59375d..00000000 --- a/models/robots/tom/v1/parts/imports/sts3215.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c116e2088d9b1c013c89c640d89093749e8edef93ddaf0f4c13371669da65b0 -size 21941724 diff --git a/models/robots/tom/v1/parts/imports/sts3250.step b/models/robots/tom/v1/parts/imports/sts3250.step deleted file mode 100644 index 746b3e65..00000000 --- a/models/robots/tom/v1/parts/imports/sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5358f001dbdb3a059d625b79f3e0272a4f93dfb7f1e2a50d07803b709a337d29 -size 2299283 diff --git a/models/robots/tom/v1/parts/servo_end_mount.py b/models/robots/tom/v1/parts/servo_end_mount.py deleted file mode 100644 index 0072a7df..00000000 --- a/models/robots/tom/v1/parts/servo_end_mount.py +++ /dev/null @@ -1,1225 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate the localized end mount. - -The part is authored in the imported STS3250 local frame so assemblies can place -it with the same transform as the reference servo geometry. - -Usage: - python STEP/servo_end_mount.py -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -# Keep CAD library caches inside the workspace to avoid permission issues. -V1_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V1_DIR.parent -_CACHE_HOME = V1_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import ezdxf -import build123d -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.keyed_connection import ( - KEYED_CONNECTION_REF, - KEYED_CONNECTION_ROTATION_DEG, - cut_keyed_connection_x_aligned, - sample_keyed_connection_outline, -) -from robot_common.step_import import import_as_shape - - -CAD_DIR = Path(__file__).resolve().parent -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Servo End Mount" - -SERVO_STEP = V1_DIR / "parts" / "imports" / "sts3250.step" - -SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM = 25.4 * 0.255 -SENDCUTSEND_SHEET_THICKNESS_MM = 25.4 * 0.063 -CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM = 25.4 * 0.010 -CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM = ( - SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM - + CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM -) -BRACKET_FACE_CLEARANCE_MM = CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - SENDCUTSEND_SHEET_THICKNESS_MM -SENDCUTSEND_REFERENCE_SHEET_THICKNESS_MM = 25.4 * 0.080 -SENDCUTSEND_DIE_REGION_RULE_SCALE = SENDCUTSEND_SHEET_THICKNESS_MM / SENDCUTSEND_REFERENCE_SHEET_THICKNESS_MM -SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.035 -SENDCUTSEND_K_FACTOR = 0.48 -SENDCUTSEND_BEND_DEDUCTION_90_MM = (2.0 * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM)) - ( - (math.pi * 0.5) - * (SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SENDCUTSEND_SHEET_THICKNESS_MM)) -) -SENDCUTSEND_DIE_WIDTH_MM = 25.4 * 0.472 -SENDCUTSEND_HALF_DIE_WIDTH_MM = (0.5 * SENDCUTSEND_DIE_WIDTH_MM) * SENDCUTSEND_DIE_REGION_RULE_SCALE -SENDCUTSEND_MIN_FLANGE_FLAT_MM = SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM -SENDCUTSEND_MIN_FLANGE_FORMED_MM = ( - SENDCUTSEND_MIN_FLANGE_FLAT_MM - + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - + SENDCUTSEND_SHEET_THICKNESS_MM -) -SENDCUTSEND_BEND_RELIEF_DEPTH_MM = 25.4 * 0.118 -SENDCUTSEND_MIN_CORNER_RELIEF_DISTANCE_MM = 25.4 * 0.037 -SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM = 25.4 -BEND_DIE_REGION_TOLERANCE_MM = 0.1 -# The theoretical outer radius is inner radius + thickness, but the split tab -# geometry cannot accept the full value as a direct solid fillet. Clamp the -# modeled exterior bend to a stable radius that still reads as a formed bend. -BEND_OUTER_RADIUS_MM = min(2.0, SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM) -FLANGE_FREE_CORNER_RADIUS_MM = 1.0 -FLANGE_FREE_CORNER_SAMPLE_COUNT = 8 -MOUNT_PLANE_CLEARANCE_MM = 0.25 -MOUNT_HOLE_EDGE_X_WEB_MM = 1.55 -MOUNT_HOLE_OUTBOARD_Z_WEB_MM = 2.15 -MOUNT_HOLE_INBOARD_Z_WEB_MM = 2.15 -MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM = 1.0 -CUT_EXTENSION_MM = 2.0 -MAX_FACE_TARGET_ERROR_MM = 1.0 -FACE_TARGET_CENTERS_MM: dict[str, tuple[float, float, float]] = { - "end_face": (9.61, -7.65, 0.0), - "end_wrap_neg": (8.843, -7.65, -11.593), - "end_wrap_pos": (8.843, -7.65, 11.593), - "upper_top_neg": (-6.561, 6.4, -9.484), - "upper_top_pos": (-6.561, 6.4, 9.484), - "main_bottom": (-0.881, -25.6, 0.0), -} -@dataclass(frozen=True) -class BracketLayout: - z_min: float - z_max: float - top_holes: tuple[tuple[build123d.Vector, float], ...] - bottom_holes: tuple[tuple[build123d.Vector, float], ...] - top_tab_x_min: float - bottom_tab_x_min: float - bracket_face_inner_x: float - bracket_face_outer_x: float - top_tab_y_min: float - top_tab_y_max: float - bottom_tab_y_min: float - bottom_tab_y_max: float - tab_bands: tuple[tuple[float, float], ...] - bend_bands: tuple[tuple[float, float], ...] - center_bridge_x_min: float | None - front_face_center_y: float - outer_flush_gap: float - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - top_flat_length_mm: float - web_flat_start_mm: float - web_flat_end_mm: float - bottom_flat_start_mm: float - bend_line_positions_mm: tuple[float, float] - bend_line_segments_mm: tuple[tuple[float, float, float], ...] - cut_circles: tuple[tuple[float, float, float], ...] - cut_profile_centers: tuple[tuple[float, float], ...] - cut_segments: tuple[tuple[str, tuple[tuple[float, float], ...]], ...] - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _face_from_wire( - exterior: build123d.Wire, - interior_wires: list[build123d.Wire] | None = None, -) -> build123d.Face: - holes = list(interior_wires or []) - return build123d.Face(exterior, holes) if holes else build123d.Face(exterior) - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _vector_to_tuple(vector: build123d.Vector) -> tuple[float, float, float]: - return (vector.X, vector.Y, vector.Z) - - -def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire: - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}") - return wires[0] - - -def _arc_angles_xz( - *, - center_x: float, - center_z: float, - start_point: tuple[float, float], - end_point: tuple[float, float], -) -> tuple[float, float, build123d.AngularDirection]: - start_angle = math.degrees(math.atan2(start_point[1] - center_z, start_point[0] - center_x)) - end_angle = math.degrees(math.atan2(end_point[1] - center_z, end_point[0] - center_x)) - ccw_span = (end_angle - start_angle) % 360.0 - if ccw_span <= 180.0: - return start_angle, end_angle, build123d.AngularDirection.COUNTER_CLOCKWISE - return start_angle, end_angle, build123d.AngularDirection.CLOCKWISE - - -def _make_arc_edge_xz( - *, - center_x: float, - center_z: float, - radius: float, - start_point: tuple[float, float], - end_point: tuple[float, float], -) -> build123d.Edge: - start_angle, end_angle, angular_direction = _arc_angles_xz( - center_x=center_x, - center_z=center_z, - start_point=start_point, - end_point=end_point, - ) - return build123d.Edge.make_circle( - radius, - plane=build123d.Plane(origin=(center_x, 0.0, center_z), x_dir=(1.0, 0.0, 0.0), z_dir=(0.0, -1.0, 0.0)), - start_angle=start_angle, - end_angle=end_angle, - angular_direction=angular_direction, - ) - - -def _make_wire_prism_y( - *, - edges_xz: list[build123d.Edge], - y_min: float, - y_max: float, -) -> build123d.Solid: - face = _face_from_wire(_assemble_wire(edges_xz)) - solid = build123d.Solid.extrude(face, build123d.Vector(0.0, y_max - y_min, 0.0)) - return solid.translate((0.0, y_min, 0.0)) - - -def _make_rounded_flange_tab( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, - radius: float, -) -> build123d.Shape: - radius = min(radius, 0.5 * (z_max - z_min), x_max - x_min) - if radius <= 1e-6: - return _make_box(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max) - - lower_tangent = (x_min + radius, z_min) - right_lower = (x_max, z_min) - right_upper = (x_max, z_max) - upper_tangent = (x_min + radius, z_max) - left_upper = (x_min, z_max - radius) - left_lower = (x_min, z_min + radius) - - return _make_wire_prism_y( - edges_xz=[ - build123d.Edge.make_line((lower_tangent[0], 0.0, lower_tangent[1]), (right_lower[0], 0.0, right_lower[1])), - build123d.Edge.make_line((right_lower[0], 0.0, right_lower[1]), (right_upper[0], 0.0, right_upper[1])), - build123d.Edge.make_line((right_upper[0], 0.0, right_upper[1]), (upper_tangent[0], 0.0, upper_tangent[1])), - _make_arc_edge_xz( - center_x=x_min + radius, - center_z=z_max - radius, - radius=radius, - start_point=upper_tangent, - end_point=left_upper, - ), - build123d.Edge.make_line((left_upper[0], 0.0, left_upper[1]), (left_lower[0], 0.0, left_lower[1])), - _make_arc_edge_xz( - center_x=x_min + radius, - center_z=z_min + radius, - radius=radius, - start_point=left_lower, - end_point=lower_tangent, - ), - ], - y_min=y_min, - y_max=y_max, - ) - - -def _find_face_near(shape: build123d.Shape, target: tuple[float, float, float], *, geom_type: str | None = None) -> build123d.Face: - target_vec = build123d.Vector(*target) - best: tuple[float, build123d.Face] | None = None - for face in shape.faces(): - if geom_type is not None and _geom_type_name(face) != geom_type: - continue - dist = face.center().sub(target_vec).length - if best is None or dist < best[0]: - best = (dist, face) - - if best is None: - raise RuntimeError(f"No planar face found near {target}") - if best[0] > MAX_FACE_TARGET_ERROR_MM: - raise RuntimeError( - f"Resolved face near {target} was {best[0]:.3f} mm away, which exceeds the tolerance" - ) - return best[1] - - -def _wire_area(wire: build123d.Wire) -> float: - return abs(_face_from_wire(wire).area) - - -def _outer_wire_and_inner_wires(face: build123d.Face) -> tuple[build123d.Wire, list[build123d.Wire]]: - wires = list(face.wires()) - if not wires: - raise RuntimeError("Face has no wires") - - outer_wire = max(wires, key=_wire_area) - inner_wires = [wire for wire in wires if not wire.is_same(outer_wire)] - return outer_wire, inner_wires - - -def _extract_inner_hole_profiles(face: build123d.Face) -> list[tuple[build123d.Vector, float]]: - _outer_wire, inner_wires = _outer_wire_and_inner_wires(face) - profiles: list[tuple[build123d.Vector, float]] = [] - for wire in inner_wires: - circle_edges = [edge for edge in wire.edges() if _geom_type_name(edge) == "CIRCLE"] - if not circle_edges: - continue - center = _face_from_wire(wire).center() - radius = max(edge.radius for edge in circle_edges) - profiles.append((center, radius)) - return profiles - - -def _select_near_end_holes( - profiles: list[tuple[build123d.Vector, float]], - *, - count: int, -) -> list[tuple[build123d.Vector, float]]: - if len(profiles) < count: - raise RuntimeError(f"Expected at least {count} mount holes, found {len(profiles)}") - - selected = sorted(profiles, key=lambda item: (item[0].X, abs(item[0].Z)), reverse=True)[:count] - selected.sort(key=lambda item: item[0].Z) - return selected - - -def _cut_mount_holes( - body: build123d.Shape, - *, - profiles: list[tuple[build123d.Vector, float]], - start_y: float, - direction_y: float, -) -> build123d.Shape: - hole_depth = SENDCUTSEND_SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - for center, radius in profiles: - cutter = _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(center.X, start_y, center.Z), - direction=build123d.Vector(0.0, direction_y, 0.0), - ) - body = body.cut(cutter) - return body - - -def _cut_front_face_connection_frame( - body: build123d.Shape, - *, - face_center_y: float, - x_max: float, -) -> build123d.Shape: - body_bb = body.bounding_box() - return cut_keyed_connection_x_aligned( - body, - center=build123d.Vector(x_max, face_center_y, 0.0), - start_x=x_max + CUT_EXTENSION_MM, - direction_x=-1.0, - thickness_mm=body_bb.size.X, - cut_extension_mm=CUT_EXTENSION_MM, - ) - - -def _apply_bends_and_end_rounds( - bracket: build123d.Shape, - *, - bracket_face_inner_x: float, - bracket_face_outer_x: float, - top_tab_x_min: float, - bottom_tab_x_min: float, - top_tab_y_min: float, - top_tab_y_max: float, - bottom_tab_y_min: float, - bottom_tab_y_max: float, - bend_bands: tuple[tuple[float, float], ...], -) -> build123d.Shape: - return bracket - - -def _min_mount_hole_distance_to_bend_line( - profiles: list[tuple[build123d.Vector, float]], - *, - bend_line_x: float, -) -> float: - return min(abs(center.X - bend_line_x) for center, _radius in profiles) - - -def _merge_z_bands( - bands: list[tuple[float, float]] | tuple[tuple[float, float], ...], - *, - tolerance: float = 1e-6, -) -> tuple[tuple[float, float], ...]: - merged: list[tuple[float, float]] = [] - for start, end in sorted(bands, key=lambda band: band[0]): - if end - start <= tolerance: - continue - if not merged or start > merged[-1][1] + tolerance: - merged.append((start, end)) - continue - merged[-1] = (merged[-1][0], max(merged[-1][1], end)) - return tuple(merged) - - -def _build_layout( - servo_shape: build123d.Shape, - *, - include_top_center_bridge: bool = True, -) -> BracketLayout: - faces = { - "end_face": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE"), - "end_wrap_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_neg"]), - "end_wrap_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_pos"]), - "upper_top_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_neg"], geom_type="PLANE"), - "upper_top_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_pos"], geom_type="PLANE"), - "main_bottom": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["main_bottom"], geom_type="PLANE"), - } - - end_face_bb = faces["end_face"].bounding_box() - end_wrap_neg_bb = faces["end_wrap_neg"].bounding_box() - end_wrap_pos_bb = faces["end_wrap_pos"].bounding_box() - z_min = min( - end_face_bb.min.Z, - end_wrap_neg_bb.min.Z, - end_wrap_pos_bb.min.Z, - ) - z_max = max( - end_face_bb.max.Z, - end_wrap_neg_bb.max.Z, - end_wrap_pos_bb.max.Z, - ) - - top_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_neg"]), count=1) - top_holes += _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_pos"]), count=1) - top_holes.sort(key=lambda item: item[0].Z) - - bottom_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["main_bottom"]), count=2) - - top_mount_y = sum(center.Y for center, _radius in top_holes) / len(top_holes) - bottom_mount_y = sum(center.Y for center, _radius in bottom_holes) / len(bottom_holes) - top_tab_y_min = top_mount_y + MOUNT_PLANE_CLEARANCE_MM - top_tab_y_max = top_tab_y_min + SENDCUTSEND_SHEET_THICKNESS_MM - bottom_tab_y_max = bottom_mount_y - MOUNT_PLANE_CLEARANCE_MM - bottom_tab_y_min = bottom_tab_y_max - SENDCUTSEND_SHEET_THICKNESS_MM - - side_profiles = { - "neg": [top_holes[0], bottom_holes[0]], - "pos": [top_holes[1], bottom_holes[1]], - } - tab_bands: list[tuple[float, float]] = [] - for top_profile, bottom_profile in side_profiles.values(): - profiles = (top_profile, bottom_profile) - if sum(center.Z for center, _radius in profiles) < 0.0: - z_band_min = min(center.Z - radius - MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles) - z_band_max = max(center.Z + radius + MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles) - else: - z_band_min = min(center.Z - radius - MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles) - z_band_max = max(center.Z + radius + MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles) - z_band_min = max(z_band_min, z_min) - z_band_max = min(z_band_max, z_max) - tab_bands.append((z_band_min, z_band_max)) - - sorted_tab_bands = sorted(tab_bands, key=lambda band: band[0]) - bend_bands = _merge_z_bands(sorted_tab_bands) - - top_tab_x_min = min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in top_holes) - bottom_tab_x_min = min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in bottom_holes) - bracket_face_clearance = BRACKET_FACE_CLEARANCE_MM - bracket_face_inner_x = end_face_bb.max.X + bracket_face_clearance - bracket_face_outer_x = bracket_face_inner_x + SENDCUTSEND_SHEET_THICKNESS_MM - center_bridge_x_min: float | None = None - if include_top_center_bridge and len(sorted_tab_bands) >= 2: - center_gap_z_min = sorted_tab_bands[0][1] - center_gap_z_max = sorted_tab_bands[-1][0] - if center_gap_z_max - center_gap_z_min > 1e-3: - candidate_center_bridge_x_min = max( - bracket_face_outer_x - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM, - top_tab_x_min, - ) - if candidate_center_bridge_x_min < bracket_face_outer_x - 1e-3: - center_bridge_x_min = candidate_center_bridge_x_min - bend_bands = _merge_z_bands((*sorted_tab_bands, (center_gap_z_min, center_gap_z_max))) - - min_hole_to_bend_line = min( - _min_mount_hole_distance_to_bend_line(top_holes, bend_line_x=bracket_face_inner_x), - _min_mount_hole_distance_to_bend_line(bottom_holes, bend_line_x=bracket_face_inner_x), - ) - if min_hole_to_bend_line + BEND_DIE_REGION_TOLERANCE_MM < SENDCUTSEND_HALF_DIE_WIDTH_MM: - raise RuntimeError( - "Mount holes are too close to the bend line for the configured sheet-metal bend rule: " - f"{min_hole_to_bend_line:.3f} mm < {SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - - front_face_center_y = 0.5 * (bottom_tab_y_min + top_tab_y_max) - outer_flush_gap = min( - min( - z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min - for center, radius in top_holes - ), - min( - z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min - for center, radius in bottom_holes - ), - ) - if outer_flush_gap <= 0.0: - raise RuntimeError(f"Trimmed mount width cuts into a mounting hole: outboard web {outer_flush_gap:.3f} mm") - - return BracketLayout( - z_min=z_min, - z_max=z_max, - top_holes=tuple(top_holes), - bottom_holes=tuple(bottom_holes), - top_tab_x_min=top_tab_x_min, - bottom_tab_x_min=bottom_tab_x_min, - bracket_face_inner_x=bracket_face_inner_x, - bracket_face_outer_x=bracket_face_outer_x, - top_tab_y_min=top_tab_y_min, - top_tab_y_max=top_tab_y_max, - bottom_tab_y_min=bottom_tab_y_min, - bottom_tab_y_max=bottom_tab_y_max, - tab_bands=tuple(sorted_tab_bands), - bend_bands=bend_bands, - center_bridge_x_min=center_bridge_x_min, - front_face_center_y=front_face_center_y, - outer_flush_gap=outer_flush_gap, - ) - - -def build_bracket( - servo_shape: build123d.Shape, - *, - include_top_center_bridge: bool = True, -) -> tuple[build123d.Shape, BracketLayout]: - layout = _build_layout(servo_shape, include_top_center_bridge=include_top_center_bridge) - - bracket: build123d.Shape = _make_box( - x_min=layout.bracket_face_inner_x, - x_max=layout.bracket_face_outer_x, - y_min=layout.bottom_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - - for z_band_min, z_band_max in layout.tab_bands: - bracket = bracket.fuse( - _make_rounded_flange_tab( - x_min=layout.top_tab_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.top_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=z_band_min, - z_max=z_band_max, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - ) - - if layout.center_bridge_x_min is not None and len(layout.tab_bands) >= 2: - center_gap_z_min = layout.tab_bands[0][1] - center_gap_z_max = layout.tab_bands[-1][0] - bracket = bracket.fuse( - _make_box( - x_min=layout.center_bridge_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.top_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=center_gap_z_min, - z_max=center_gap_z_max, - ) - ) - - bracket = bracket.fuse( - _make_rounded_flange_tab( - x_min=layout.bottom_tab_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.bottom_tab_y_min, - y_max=layout.bottom_tab_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - ) - - bracket = _apply_bends_and_end_rounds( - bracket, - bracket_face_inner_x=layout.bracket_face_inner_x, - bracket_face_outer_x=layout.bracket_face_outer_x, - top_tab_x_min=layout.top_tab_x_min, - bottom_tab_x_min=layout.bottom_tab_x_min, - top_tab_y_min=layout.top_tab_y_min, - top_tab_y_max=layout.top_tab_y_max, - bottom_tab_y_min=layout.bottom_tab_y_min, - bottom_tab_y_max=layout.bottom_tab_y_max, - bend_bands=layout.bend_bands, - ) - - bracket = _cut_front_face_connection_frame( - bracket, - face_center_y=layout.front_face_center_y, - x_max=layout.bracket_face_outer_x, - ) - bracket = _cut_mount_holes( - bracket, - profiles=list(layout.top_holes), - start_y=layout.top_tab_y_max + CUT_EXTENSION_MM, - direction_y=-1.0, - ) - bracket = _cut_mount_holes( - bracket, - profiles=list(layout.bottom_holes), - start_y=layout.bottom_tab_y_min - CUT_EXTENSION_MM, - direction_y=1.0, - ) - - return bracket, layout - - -def _sendcutsend_outside_setback_90_mm() -> float: - return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM - - -def _sendcutsend_bend_allowance_90_mm() -> float: - return (math.pi * 0.5) * ( - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + (SENDCUTSEND_K_FACTOR * SENDCUTSEND_SHEET_THICKNESS_MM) - ) - - -def _validate_sendcutsend_bend_rule() -> None: - bend_deduction_error = abs( - SENDCUTSEND_BEND_DEDUCTION_90_MM - - ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm()) - ) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _min_flange_outside_length(layout: BracketLayout) -> float: - return min( - layout.bracket_face_outer_x - layout.top_tab_x_min, - layout.bracket_face_outer_x - layout.bottom_tab_x_min, - ) - - -def _supported_bend_fraction(layout: BracketLayout) -> float: - bend_width = layout.z_max - layout.z_min - if bend_width <= 0.0: - return 0.0 - supported_width = sum(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands) - return supported_width / bend_width - - -def _min_bend_segment_length(layout: BracketLayout) -> float: - return min(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands) - - -def _center_bridge_z_range(layout: BracketLayout) -> tuple[float, float] | None: - if layout.center_bridge_x_min is None or len(layout.tab_bands) < 2: - return None - - z_min = layout.tab_bands[0][1] - z_max = layout.tab_bands[-1][0] - if z_max - z_min <= 1e-6: - return None - return z_min, z_max - - -def _center_bridge_formed_length(layout: BracketLayout) -> float | None: - if layout.center_bridge_x_min is None: - return None - return layout.bracket_face_inner_x - layout.center_bridge_x_min - - -def _center_bridge_bend_edge_distance(layout: BracketLayout) -> float | None: - if layout.center_bridge_x_min is None: - return None - return layout.bracket_face_outer_x - layout.center_bridge_x_min - - -def _center_bridge_flat_length(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None: - formed_length = _center_bridge_formed_length(layout) - if formed_length is None: - return None - - # Keep the flat-pattern edge at least the required distance from the drawn - # bend line while preserving the formed center-bridge length in the 3D part. - bend_line_clearance_flat_length = ( - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - - (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm) - ) - return max(formed_length, bend_line_clearance_flat_length) - - -def _center_bridge_flat_bend_line_to_edge_distance(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None: - flat_length = _center_bridge_flat_length(layout, flat_pattern) - if flat_length is None: - return None - return flat_length + (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm) - - -def _build_flat_pattern( - layout: BracketLayout, -) -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = _sendcutsend_outside_setback_90_mm() - bend_allowance = _sendcutsend_bend_allowance_90_mm() - top_outside_length = layout.bracket_face_outer_x - layout.top_tab_x_min - bottom_outside_length = layout.bracket_face_outer_x - layout.bottom_tab_x_min - top_flat_length = top_outside_length - outside_setback - bottom_flat_length = bottom_outside_length - outside_setback - if top_flat_length <= 0.0 or bottom_flat_length <= 0.0: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"top={top_flat_length:.3f} mm, bottom={bottom_flat_length:.3f} mm" - ) - flange_outside_min = _min_flange_outside_length(layout) - flange_flat_min = flange_outside_min - outside_setback - center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout) - if ( - center_bridge_bend_edge_distance is not None - and center_bridge_bend_edge_distance < CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - ): - raise RuntimeError( - "Top center bridge bend-edge distance is below the configured clearance target: " - f"{center_bridge_bend_edge_distance:.3f} mm < {CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM:.3f} mm" - ) - if flange_outside_min < SENDCUTSEND_MIN_FLANGE_FORMED_MM: - raise RuntimeError( - "Formed flange length is below the configured bend-rule minimum: " - f"{flange_outside_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm" - ) - if flange_flat_min < SENDCUTSEND_MIN_FLANGE_FLAT_MM: - raise RuntimeError( - "Flat flange length is below the configured bend-rule minimum: " - f"{flange_flat_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm" - ) - - top_web_tangent_y = layout.top_tab_y_min - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - bottom_web_tangent_y = layout.bottom_tab_y_max + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - web_flat_length = top_web_tangent_y - bottom_web_tangent_y - if web_flat_length <= 0.0: - raise RuntimeError(f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm") - - web_flat_start = top_flat_length + bend_allowance - web_flat_end = web_flat_start + web_flat_length - bottom_flat_start = web_flat_end + bend_allowance - total_length = bottom_flat_start + bottom_flat_length - width = layout.z_max - layout.z_min - top_bend_line_x = top_flat_length + (0.5 * bend_allowance) - bottom_bend_line_x = web_flat_end + (0.5 * bend_allowance) - bend_line_segments = ( - (top_bend_line_x, 0.0, width), - (bottom_bend_line_x, 0.0, width), - ) - - cut_circles: list[tuple[float, float, float]] = [] - for center, radius in layout.top_holes: - cut_circles.append( - ( - center.X - layout.top_tab_x_min, - center.Z - layout.z_min, - radius, - ) - ) - for center, radius in layout.bottom_holes: - cut_circles.append( - ( - total_length - (center.X - layout.bottom_tab_x_min), - center.Z - layout.z_min, - radius, - ) - ) - cut_profile_centers = ( - ( - web_flat_start + (top_web_tangent_y - layout.front_face_center_y), - -layout.z_min, - ), - ) - return FlatPattern( - total_length_mm=total_length, - width_mm=width, - top_flat_length_mm=top_flat_length, - web_flat_start_mm=web_flat_start, - web_flat_end_mm=web_flat_end, - bottom_flat_start_mm=bottom_flat_start, - bend_line_positions_mm=(top_bend_line_x, bottom_bend_line_x), - bend_line_segments_mm=bend_line_segments, - cut_circles=tuple(cut_circles), - cut_profile_centers=cut_profile_centers, - cut_segments=(), - ) - - -def _flat_rectangles(layout: BracketLayout, flat_pattern: FlatPattern) -> tuple[tuple[float, float, float, float], ...]: - rectangles: list[tuple[float, float, float, float]] = [ - ( - flat_pattern.top_flat_length_mm, - 0.0, - flat_pattern.bottom_flat_start_mm, - flat_pattern.width_mm, - ) - ] - - for z_band_min, z_band_max in layout.tab_bands: - y_min = z_band_min - layout.z_min - y_max = z_band_max - layout.z_min - rectangles.append((0.0, y_min, flat_pattern.top_flat_length_mm, y_max)) - - center_bridge_z_range = _center_bridge_z_range(layout) - center_bridge_flat_length = _center_bridge_flat_length(layout, flat_pattern) - if center_bridge_z_range is not None and center_bridge_flat_length is not None: - y_min = center_bridge_z_range[0] - layout.z_min - y_max = center_bridge_z_range[1] - layout.z_min - x_min = flat_pattern.top_flat_length_mm - center_bridge_flat_length - rectangles.append((x_min, y_min, flat_pattern.top_flat_length_mm, y_max)) - - rectangles.append((flat_pattern.bottom_flat_start_mm, 0.0, flat_pattern.total_length_mm, flat_pattern.width_mm)) - - return tuple(rectangle for rectangle in rectangles if rectangle[2] - rectangle[0] > 1e-6 and rectangle[3] - rectangle[1] > 1e-6) - - -def _flange_free_corner_points( - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> set[tuple[float, float]]: - points: set[tuple[float, float]] = set() - for z_band_min, z_band_max in layout.tab_bands: - flat_z_min = z_band_min - layout.z_min - flat_z_max = z_band_max - layout.z_min - points.add((0.0, flat_z_min)) - points.add((0.0, flat_z_max)) - points.add((flat_pattern.total_length_mm, 0.0)) - points.add((flat_pattern.total_length_mm, flat_pattern.width_mm)) - return points - - -def _sample_short_arc_2d( - *, - center: tuple[float, float], - start: tuple[float, float], - end: tuple[float, float], - count: int, -) -> list[tuple[float, float]]: - start_angle = math.atan2(start[1] - center[1], start[0] - center[0]) - end_angle = math.atan2(end[1] - center[1], end[0] - center[0]) - ccw_span = (end_angle - start_angle) % (2.0 * math.pi) - if ccw_span <= math.pi: - span = ccw_span - else: - span = -((start_angle - end_angle) % (2.0 * math.pi)) - - samples = max(count, 2) - radius = math.hypot(start[0] - center[0], start[1] - center[1]) - return [ - ( - center[0] + (radius * math.cos(start_angle + (span * index / (samples - 1)))), - center[1] + (radius * math.sin(start_angle + (span * index / (samples - 1)))), - ) - for index in range(samples) - ] - - -def _rounded_outline_loop_points( - loop: list[tuple[float, float]], - *, - rounded_corners: set[tuple[float, float]], - radius: float, -) -> list[tuple[float, float]]: - if radius <= 1e-6: - return loop - - rounded_loop: list[tuple[float, float]] = [] - for index, point in enumerate(loop): - if point not in rounded_corners: - rounded_loop.append(point) - continue - - previous_point = loop[index - 1] - next_point = loop[(index + 1) % len(loop)] - prev_vec = (previous_point[0] - point[0], previous_point[1] - point[1]) - next_vec = (next_point[0] - point[0], next_point[1] - point[1]) - prev_len = math.hypot(*prev_vec) - next_len = math.hypot(*next_vec) - corner_radius = min(radius, 0.5 * prev_len, 0.5 * next_len) - if corner_radius <= 1e-6: - rounded_loop.append(point) - continue - - prev_unit = (prev_vec[0] / prev_len, prev_vec[1] / prev_len) - next_unit = (next_vec[0] / next_len, next_vec[1] / next_len) - tangent_in = ( - point[0] + (prev_unit[0] * corner_radius), - point[1] + (prev_unit[1] * corner_radius), - ) - tangent_out = ( - point[0] + (next_unit[0] * corner_radius), - point[1] + (next_unit[1] * corner_radius), - ) - center = ( - point[0] + ((prev_unit[0] + next_unit[0]) * corner_radius), - point[1] + ((prev_unit[1] + next_unit[1]) * corner_radius), - ) - arc_points = _sample_short_arc_2d( - center=center, - start=tangent_in, - end=tangent_out, - count=FLANGE_FREE_CORNER_SAMPLE_COUNT, - ) - rounded_loop.extend(arc_points) - - return rounded_loop - - -def _outline_loops_from_rectangles( - rectangles: tuple[tuple[float, float, float, float], ...], -) -> list[list[tuple[float, float]]]: - x_coords = sorted({coord for x0, _y0, x1, _y1 in rectangles for coord in (x0, x1)}) - y_coords = sorted({coord for _x0, y0, _x1, y1 in rectangles for coord in (y0, y1)}) - filled_cells: set[tuple[int, int]] = set() - - for x_index in range(len(x_coords) - 1): - for y_index in range(len(y_coords) - 1): - x_mid = 0.5 * (x_coords[x_index] + x_coords[x_index + 1]) - y_mid = 0.5 * (y_coords[y_index] + y_coords[y_index + 1]) - if any(x0 < x_mid < x1 and y0 < y_mid < y1 for x0, y0, x1, y1 in rectangles): - filled_cells.add((x_index, y_index)) - - edges: dict[tuple[tuple[float, float], tuple[float, float]], None] = {} - - def add_edge(start: tuple[float, float], end: tuple[float, float]) -> None: - reverse = (end, start) - if reverse in edges: - del edges[reverse] - return - edges[(start, end)] = None - - for x_index, y_index in filled_cells: - x0 = x_coords[x_index] - x1 = x_coords[x_index + 1] - y0 = y_coords[y_index] - y1 = y_coords[y_index + 1] - add_edge((x0, y0), (x1, y0)) - add_edge((x1, y0), (x1, y1)) - add_edge((x1, y1), (x0, y1)) - add_edge((x0, y1), (x0, y0)) - - outgoing: dict[tuple[float, float], list[tuple[float, float]]] = {} - for start, end in edges: - outgoing.setdefault(start, []).append(end) - - loops: list[list[tuple[float, float]]] = [] - while outgoing: - start = min(outgoing) - loop = [start] - current = start - while True: - next_points = outgoing.get(current) - if not next_points: - raise RuntimeError("Flat-pattern outline could not be assembled from panel rectangles") - next_point = next_points.pop() - if not next_points: - del outgoing[current] - if next_point == start: - break - loop.append(next_point) - current = next_point - - area = _polygon_area(loop) - if area < 0.0: - loop.reverse() - loops.append(loop) - - return loops - - -def _polygon_area(points: list[tuple[float, float]]) -> float: - return 0.5 * sum( - (points[index][0] * points[(index + 1) % len(points)][1]) - - (points[(index + 1) % len(points)][0] * points[index][1]) - for index in range(len(points)) - ) - - -def _add_reference_arc( - msp: ezdxf.layouts.Modelspace, - *, - start: tuple[float, float], - midpoint: tuple[float, float], - end: tuple[float, float], -) -> None: - from ezdxf.math import ConstructionArc - - arc = ConstructionArc.from_3p(start, end, def_point=midpoint, ccw=True) - center = (arc.center.x, arc.center.y) - start_angle = math.atan2(start[1] - center[1], start[0] - center[0]) - mid_angle = math.atan2(midpoint[1] - center[1], midpoint[0] - center[0]) - end_angle = math.atan2(end[1] - center[1], end[0] - center[0]) - ccw_sweep = (end_angle - start_angle) % (2.0 * math.pi) - mid_sweep = (mid_angle - start_angle) % (2.0 * math.pi) - if mid_sweep <= ccw_sweep + 1e-6: - msp.add_arc( - center=center, - radius=arc.radius, - start_angle=math.degrees(start_angle), - end_angle=math.degrees(end_angle), - dxfattribs={"layer": "CUT"}, - ) - return - - msp.add_arc( - center=center, - radius=arc.radius, - start_angle=math.degrees(end_angle), - end_angle=math.degrees(start_angle), - dxfattribs={"layer": "CUT"}, - ) - - -def _build_flat_pattern_dxf(flat_pattern: FlatPattern, layout: BracketLayout) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - rounded_corners = _flange_free_corner_points(layout, flat_pattern) - for loop in _outline_loops_from_rectangles(_flat_rectangles(layout, flat_pattern)): - rounded_loop = _rounded_outline_loop_points( - loop, - rounded_corners=rounded_corners, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - for index, point in enumerate(rounded_loop): - msp.add_line(point, rounded_loop[(index + 1) % len(rounded_loop)], dxfattribs={"layer": "CUT"}) - - for x, y, cut_radius in flat_pattern.cut_circles: - msp.add_circle((x, y), cut_radius, dxfattribs={"layer": "CUT"}) - - for center_x, center_y in flat_pattern.cut_profile_centers: - msp.add_lwpolyline( - sample_keyed_connection_outline(center_u=center_x, center_v=center_y, points_per_arc=16), - format="xy", - close=True, - dxfattribs={"layer": "CUT"}, - ) - - for segment_type, points in flat_pattern.cut_segments: - if segment_type == "LINE": - msp.add_line(points[0], points[-1], dxfattribs={"layer": "CUT"}) - elif segment_type == "ARC": - _add_reference_arc(msp, start=points[0], midpoint=points[1], end=points[-1]) - else: - raise RuntimeError(f"Unsupported flat-pattern cut segment type: {segment_type}") - - for bend_line_x, bend_y_min, bend_y_max in flat_pattern.bend_line_segments_mm: - msp.add_line( - (bend_line_x, bend_y_min), - (bend_line_x, bend_y_max), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def build_step( - *, - include_top_center_bridge: bool = True, -) -> build123d.Shape: - CAD_DIR.mkdir(parents=True, exist_ok=True) - - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - bracket, layout = build_bracket(servo_shape, include_top_center_bridge=include_top_center_bridge) - flat_pattern = _build_flat_pattern(layout) - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected bracket solid, found {len(solids)}") - - intersection_volume = servo_shape.intersect(bracket).volume - if intersection_volume > 1e-3: - raise RuntimeError(f"Bracket intersects the servo by {intersection_volume:.6f} mm^3") - - bracket.label = PART_NAME - - bracket_bb = bracket.bounding_box() - print( - "Bracket envelope " - f"X={bracket_bb.size.X:.3f} mm, " - f"Y={bracket_bb.size.Y:.3f} mm, " - f"Z={bracket_bb.size.Z:.3f} mm" - ) - print( - "SendCutSend setup " - f"material=5052-H32, thickness={SENDCUTSEND_SHEET_THICKNESS_MM:.3f} mm, " - f"bend_radius={SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM:.3f} mm, " - f"k_factor={SENDCUTSEND_K_FACTOR:.2f}, " - f"bend_deduction_90={SENDCUTSEND_BEND_DEDUCTION_90_MM:.3f} mm, " - f"anodize_min_short_dim={SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM:.3f} mm" - ) - sendcutsend_checks = ( - "SendCutSend checks " - f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, " - f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, " - f"bend_support={_supported_bend_fraction(layout):.1%}, " - f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm, " - f"flange_free_corner_radius={FLANGE_FREE_CORNER_RADIUS_MM:.3f} mm" - ) - print(sendcutsend_checks) - if layout.outer_flush_gap < MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM: - print( - "Warning: trimmed outer side leaves a small hole edge web: " - f"{layout.outer_flush_gap:.3f} mm < {MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM:.3f} mm" - ) - end_face_x = _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE").bounding_box().max.X - print(f"Bracket face clearance: {layout.bracket_face_inner_x - end_face_x:.3f} mm") - center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout) - center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern) - if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None: - print( - "Top center bridge " - f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, " - f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm" - ) - print(f"Bracket face inner X: {layout.bracket_face_inner_x:.3f} mm") - print( - "Front-face keyed connection " - f"ref={KEYED_CONNECTION_REF}, " - f"center=(x=max, y={layout.front_face_center_y:.6f}, z=0.000000) mm, " - f"rotation={KEYED_CONNECTION_ROTATION_DEG:.1f} deg, " - "profiles=1" - ) - print( - "Top mount holes used (x, y, z) mm: " - + ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.top_holes) - ) - print( - "Bottom mount holes used (x, y, z) mm: " - + ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.bottom_holes) - ) - print(f"Servo/bracket interference volume (mm^3): {intersection_volume:.6f}") - return bracket - - -def build_dxf( - *, - include_top_center_bridge: bool = True, -) -> ezdxf.document.Drawing: - CAD_DIR.mkdir(parents=True, exist_ok=True) - - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - layout = _build_layout(servo_shape, include_top_center_bridge=include_top_center_bridge) - flat_pattern = _build_flat_pattern(layout) - doc = _build_flat_pattern_dxf(flat_pattern, layout) - - outside_setback = _sendcutsend_outside_setback_90_mm() - flange_outside_min = _min_flange_outside_length(layout) - print( - "Flat pattern " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"bend_segments={len(flat_pattern.bend_line_segments_mm)}, " - f"cut_circles={len(flat_pattern.cut_circles)}, " - f"front_cut_profiles={len(flat_pattern.cut_profile_centers)}, " - f"front_cut_segments={len(flat_pattern.cut_segments)}" - ) - dxf_bend_checks = ( - "DXF bend checks " - f"flange_outside_min={flange_outside_min:.3f} mm, " - f"flange_flat_min={flange_outside_min - outside_setback:.3f} mm, " - f"flange_flat_required={SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm, " - f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, " - f"bend_support={_supported_bend_fraction(layout):.1%}, " - f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, " - f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm" - ) - print(dxf_bend_checks) - center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout) - center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern) - if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None: - print( - "DXF center bridge " - f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, " - f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v1/parts/servo_end_mount.step b/models/robots/tom/v1/parts/servo_end_mount.step deleted file mode 100644 index 01cf3999..00000000 --- a/models/robots/tom/v1/parts/servo_end_mount.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cbf40e6ac92ae150e1c13d4d9591a6fa5404844099b15a2718c7cbe566e49914 -size 156879 diff --git a/models/robots/tom/v1/parts/servo_horn_yoke.py b/models/robots/tom/v1/parts/servo_horn_yoke.py deleted file mode 100644 index 1f905e06..00000000 --- a/models/robots/tom/v1/parts/servo_horn_yoke.py +++ /dev/null @@ -1,1116 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate the localized horn-mounted yoke. - -This variant preserves the existing horn interface geometry, uses keyed cuts -at the horn flanges and closing web, and adds servo-aligned M2 holes on the -closing web. - -Usage: - python STEP/servo_horn_yoke.py -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -# Keep CAD library caches inside the workspace to avoid permission issues. -V1_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V1_DIR.parent -_CACHE_HOME = V1_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import ezdxf -import build123d -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.keyed_connection import ( - KEYED_CONNECTION_HALF_SPAN_MM, - KEYED_CONNECTION_REF, - KEYED_CONNECTION_ROTATION_DEG, - cut_keyed_connection_x_aligned, - cut_keyed_connection_y_aligned, - sample_keyed_connection_outline, -) -from robot_common.step_import import import_as_shape - - -CAD_DIR = Path(__file__).resolve().parent -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Servo Horn Yoke" - -SERVO_STEP = V1_DIR / "parts" / "imports" / "sts3250.step" - -UPPER_HORN_SELECTOR = "xcaf:1.4" -LOWER_HORN_SELECTOR = "xcaf:1.5" -UPPER_HORN_NAME = "4" -LOWER_HORN_NAME = "5" - -# Keep the rotating web outside the primary servo's left-side corner sweep. -# This is measured from the flange servo horn axis to the yoke web's outer face. -YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM = 32.0 -SERVO_HORN_AXIS_X_MM = -25.5 -SERVO_REFERENCE_FACE_TARGET_CENTER_MM = (-35.61, -7.65, 0.0) -SERVO_REFERENCE_FACE_MAX_ERROR_MM = 0.5 -HORN_FACE_GAP_MM = 0.25 -MOUNT_HOLE_MIN_OFFSET_MM = 3.0 -CUT_EXTENSION_MM = 2.0 -WEB_M2_CLEARANCE_RADIUS_MM = 1.1 -WEB_SERVO_M2_HOLE_CENTERS_YZ_MM = ( - (-17.4, -10.25), - (-17.4, 10.25), - (-0.8, -10.25), - (-0.8, 10.25), -) -REFERENCE_SHEET_THICKNESS_MM = 25.4 * 0.080 -SHEET_THICKNESS_MM = 25.4 * 0.063 -SHEET_RULE_SCALE = SHEET_THICKNESS_MM / REFERENCE_SHEET_THICKNESS_MM - - -@dataclass(frozen=True) -class BendRule: - material_name: str - thickness_mm: float - effective_inside_radius_mm: float - k_factor: float - bend_deduction_90_mm: float - half_die_width_mm: float - min_flange_flat_mm: float - min_flange_formed_mm: float - min_acute_centerline_length_mm: float - channel_one_to_one_max_flange_mm: float - channel_narrow_min_base_mm: float - channel_base_spacing_min_mm: float - max_bend_angle_deg: float - - @property - def outside_setback_90_mm(self) -> float: - return self.effective_inside_radius_mm + self.thickness_mm - - @property - def bend_allowance_90_mm(self) -> float: - return (math.pi * 0.5) * (self.effective_inside_radius_mm + (self.k_factor * self.thickness_mm)) - - @property - def outer_radius_mm(self) -> float: - return self.effective_inside_radius_mm + self.thickness_mm - - def flat_length_from_outside_length_90(self, outside_length_mm: float) -> float: - return outside_length_mm - self.outside_setback_90_mm - - -SHEET_RULE = BendRule( - material_name="5052-H32 1.6 mm", - thickness_mm=SHEET_THICKNESS_MM, - effective_inside_radius_mm=25.4 * 0.038, - k_factor=0.48, - bend_deduction_90_mm=(2.0 * ((25.4 * 0.038) + SHEET_THICKNESS_MM)) - - ((math.pi * 0.5) * ((25.4 * 0.038) + (0.48 * SHEET_THICKNESS_MM))), - half_die_width_mm=(0.5 * (25.4 * 0.472)) * SHEET_RULE_SCALE, - min_flange_flat_mm=(25.4 * 0.255) * SHEET_RULE_SCALE, - min_flange_formed_mm=(25.4 * 0.313) * SHEET_RULE_SCALE, - min_acute_centerline_length_mm=(25.4 * 0.332) * SHEET_RULE_SCALE, - channel_one_to_one_max_flange_mm=25.4 * 3.000, - channel_narrow_min_base_mm=25.4 * 1.000, - channel_base_spacing_min_mm=(2.0 * (25.4 * 0.255)) * SHEET_RULE_SCALE, - max_bend_angle_deg=130.0, -) - - -@dataclass(frozen=True) -class HornMountSpec: - name: str - mount_face_y: float - mount_face_center: build123d.Vector - mount_face_bbox: build123d.BoundBox - half_width_z: float - center_clearance_radius: float - mount_holes: tuple[tuple[build123d.Vector, float], ...] - - -@dataclass(frozen=True) -class BracketLayout: - web_inner_x: float - web_outer_x: float - top_y_min: float - top_y_max: float - bottom_y_min: float - bottom_y_max: float - top_z_min: float - top_z_max: float - bottom_z_min: float - bottom_z_max: float - global_z_min: float - global_z_max: float - - -@dataclass(frozen=True) -class BracketMetrics: - min_flange_outside_length_mm: float - min_flange_flat_length_mm: float - bend_line_spacing_mm: float - min_hole_edge_to_bend_mm: float - min_web_feature_edge_to_bend_mm: float - base_to_flange_ratio: float - bend_deduction_formula_mm: float - - -@dataclass(frozen=True) -class GeneratorInputs: - servo_shape: build123d.Shape - reference_face_x: float - sheet_half_width_z: float - upper_horn: HornMountSpec - lower_horn: HornMountSpec - - -@dataclass(frozen=True) -class CutProfileSpec: - center_x: float - center_y: float - rotation_degrees: float - - -@dataclass(frozen=True) -class WebHoleSpec: - center_y: float - center_z: float - radius: float - - -@dataclass(frozen=True) -class CircleProfileSpec: - center_x: float - center_y: float - radius: float - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - cap_radius_mm: float - bend_line_positions_mm: tuple[float, float] - cut_profiles: tuple[CutProfileSpec, ...] - round_hole_profiles: tuple[CircleProfileSpec, ...] - - -def _cast_shape(obj: object) -> build123d.Shape: - from build123d.topology import downcast - from build123d.topology.shape_core import shapetype - - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _face_from_wire( - exterior: build123d.Wire, - interior_wires: list[build123d.Wire] | None = None, -) -> build123d.Face: - holes = list(interior_wires or []) - return build123d.Face(exterior, holes) if holes else build123d.Face(exterior) - - -def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire: - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}") - return wires[0] - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _vector_to_tuple(vector: build123d.Vector) -> tuple[float, float, float]: - return (vector.X, vector.Y, vector.Z) - - -def _validate_bend_rule(rule: BendRule) -> None: - bend_deduction_error = abs(rule.bend_deduction_90_mm - ((2.0 * rule.outside_setback_90_mm) - rule.bend_allowance_90_mm)) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _extract_selector_shape(step_path: Path, selector: str) -> tuple[str, build123d.Shape]: - from OCP.STEPCAFControl import STEPCAFControl_Reader - from OCP.TCollection import TCollection_ExtendedString - from OCP.TDataStd import TDataStd_Name - from OCP.TDF import TDF_Label, TDF_LabelSequence - from OCP.TDocStd import TDocStd_Document - from OCP.XCAFApp import XCAFApp_Application - from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool - - def _label_name(label: object) -> str: - name = TDataStd_Name() - if not label.FindAttribute(TDataStd_Name.GetID_s(), name): - return "" - return name.Get().ToExtString() - - def _resolve_label(shape_tool: object, label: object) -> object: - if not shape_tool.IsReference_s(label): - return label - referred = TDF_Label() - if shape_tool.GetReferredShape_s(label, referred): - return referred - return label - - def _child_components(shape_tool: object, label: object) -> list[object]: - children = TDF_LabelSequence() - ok = XCAFDoc_ShapeTool.GetComponents_s(label, children, False) - resolved = _resolve_label(shape_tool, label) - if (not ok or children.Length() <= 0) and resolved != label: - children = TDF_LabelSequence() - ok = XCAFDoc_ShapeTool.GetComponents_s(resolved, children, False) - if not ok or children.Length() <= 0: - return [] - return [children.Value(index) for index in range(1, children.Length() + 1)] - - def _shape_location(shape: object) -> build123d.Location: - try: - return build123d.Location(shape.Location().Transformation()) - except Exception: - return build123d.Location() - - def _located_shape(shape: object, location: build123d.Location | None) -> object: - if location is None: - return shape - return shape.Located(location.wrapped) - - app = XCAFApp_Application.GetApplication_s() - doc = TDocStd_Document(TCollection_ExtendedString("servo_horn_yoke_doc")) - app.NewDocument(TCollection_ExtendedString("MDTV-XCAF"), doc) - - reader = STEPCAFControl_Reader() - read_status = reader.ReadFile(str(step_path)) - if int(read_status) != 1: - raise RuntimeError(f"Could not read {step_path} (status={int(read_status)})") - if not reader.Transfer(doc): - raise RuntimeError(f"Could not transfer STEP document from {step_path}") - - shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) - free_labels = TDF_LabelSequence() - shape_tool.GetFreeShapes(free_labels) - if free_labels.Length() <= 0: - raise RuntimeError(f"No free shapes found in {step_path}") - - if not selector.startswith("xcaf:"): - raise ValueError(f"Unsupported selector format: {selector}") - segments = [int(value) for value in selector.split(":", 1)[1].split(".") if value] - if not segments: - raise ValueError(f"Selector does not contain XCAF path segments: {selector}") - if segments[0] < 1 or segments[0] > free_labels.Length(): - raise RuntimeError(f"Selector root index is out of range for {step_path}: {selector}") - - label = free_labels.Value(segments[0]) - current_location: build123d.Location | None = None - for index in segments[1:]: - parent_location = _shape_location(shape_tool.GetShape_s(label)) - current_location = parent_location if current_location is None else current_location * parent_location - children = _child_components(shape_tool, label) - if index < 1 or index > len(children): - raise RuntimeError(f"Selector segment {index} is out of range for {selector}") - label = children[index - 1] - - resolved = _resolve_label(shape_tool, label) - instance_shape = shape_tool.GetShape_s(label) - resolved_shape = shape_tool.GetShape_s(resolved) - base_shape = instance_shape if not instance_shape.IsNull() else resolved_shape - if base_shape.IsNull(): - raise RuntimeError(f"Selector {selector} did not resolve to a shape") - - base_location = _shape_location(base_shape) - current_location = base_location if current_location is None else current_location * base_location - extracted = _cast_shape(_located_shape(base_shape, current_location)) - return _label_name(label), extracted - - -def _wire_area(wire: build123d.Wire) -> float: - return abs(_face_from_wire(wire).area) - - -def _outer_wire_and_inner_wires(face: build123d.Face) -> tuple[build123d.Wire, list[build123d.Wire]]: - wires = list(face.wires()) - if not wires: - raise RuntimeError("Face has no wires") - - outer_wire = max(wires, key=_wire_area) - inner_wires = [wire for wire in wires if not wire.is_same(outer_wire)] - return outer_wire, inner_wires - - -def _select_mount_face(shape: build123d.Shape, *, outward_sign: float) -> build123d.Face: - shape_bb = shape.bounding_box() - target_y = shape_bb.max.Y if outward_sign > 0.0 else shape_bb.min.Y - candidates: list[tuple[float, build123d.Face]] = [] - for face in shape.faces(): - if face.geom_type.name != "PLANE": - continue - normal = face.normal_at() - if outward_sign > 0.0 and normal.Y < 0.99: - continue - if outward_sign < 0.0 and normal.Y > -0.99: - continue - if abs(face.center().Y - target_y) > 0.25: - continue - candidates.append((face.area, face)) - - if not candidates: - raise RuntimeError(f"Could not find a horn mount face on y sign {outward_sign}") - return max(candidates, key=lambda item: item[0])[1] - - -def _extract_circular_wire_profiles(face: build123d.Face) -> list[tuple[build123d.Vector, float]]: - _outer_wire, inner_wires = _outer_wire_and_inner_wires(face) - profiles: list[tuple[build123d.Vector, float]] = [] - for wire in inner_wires: - circle_edges = [edge for edge in wire.edges() if edge.geom_type.name == "CIRCLE"] - if not circle_edges: - continue - center = _face_from_wire(wire).center() - radius = max(edge.radius for edge in circle_edges) - profiles.append((center, radius)) - return profiles - - -def _extract_horn_mount_spec(shape: build123d.Shape, *, name: str, outward_sign: float) -> HornMountSpec: - mount_face = _select_mount_face(shape, outward_sign=outward_sign) - face_center = mount_face.center() - shape_bbox = shape.bounding_box() - profiles = _extract_circular_wire_profiles(mount_face) - if not profiles: - raise RuntimeError(f"No circular profiles found on horn mount face for {name}") - - center_profiles = [ - (center, radius) - for center, radius in profiles - if center.sub(face_center).length <= MOUNT_HOLE_MIN_OFFSET_MM - ] - if not center_profiles: - raise RuntimeError(f"No center clearance profile found on horn mount face for {name}") - center_clearance_radius = max(radius for _center, radius in center_profiles) - - mount_holes = [ - (center, radius) - for center, radius in profiles - if center.sub(face_center).length > MOUNT_HOLE_MIN_OFFSET_MM - ] - if len(mount_holes) != 4: - raise RuntimeError(f"Expected 4 horn mount holes on {name}, found {len(mount_holes)}") - - mount_holes.sort(key=lambda item: (item[0].X, item[0].Z)) - return HornMountSpec( - name=name, - mount_face_y=face_center.Y, - mount_face_center=face_center, - mount_face_bbox=mount_face.bounding_box(), - half_width_z=max(face_center.Z - shape_bbox.min.Z, shape_bbox.max.Z - face_center.Z), - center_clearance_radius=center_clearance_radius, - mount_holes=tuple(mount_holes), - ) - - -def _find_reference_face_x(shape: build123d.Shape) -> float: - target = build123d.Vector(*SERVO_REFERENCE_FACE_TARGET_CENTER_MM) - candidates: list[tuple[float, float, build123d.Face]] = [] - for face in shape.faces(): - if face.geom_type.name != "PLANE": - continue - normal = face.normal_at() - if normal.X > -0.99: - continue - center = face.center() - dist = center.sub(target).length - candidates.append((dist, -face.area, face)) - - if not candidates: - raise RuntimeError("Could not find the requested negative-X reference face on the servo") - - dist, _neg_area, face = min(candidates, key=lambda item: (item[0], item[1])) - if dist > SERVO_REFERENCE_FACE_MAX_ERROR_MM: - raise RuntimeError( - "Resolved servo reference face is too far from the requested target center: " - f"{dist:.3f} mm > {SERVO_REFERENCE_FACE_MAX_ERROR_MM:.3f} mm" - ) - return face.bounding_box().min.X - - -def _shape_half_width_z(shape: build123d.Shape) -> float: - bbox = shape.bounding_box() - return max(abs(bbox.min.Z), abs(bbox.max.Z)) - - -def _load_generator_inputs() -> GeneratorInputs: - _validate_bend_rule(SHEET_RULE) - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - reference_face_x = _find_reference_face_x(servo_shape) - - upper_label, upper_horn_shape = _extract_selector_shape(SERVO_STEP, UPPER_HORN_SELECTOR) - lower_label, lower_horn_shape = _extract_selector_shape(SERVO_STEP, LOWER_HORN_SELECTOR) - if upper_label != UPPER_HORN_NAME: - raise RuntimeError( - f"Expected upper horn selector {UPPER_HORN_SELECTOR} to resolve to {UPPER_HORN_NAME}, got {upper_label}" - ) - if lower_label != LOWER_HORN_NAME: - raise RuntimeError( - f"Expected lower horn selector {LOWER_HORN_SELECTOR} to resolve to {LOWER_HORN_NAME}, got {lower_label}" - ) - - upper_horn = _extract_horn_mount_spec(upper_horn_shape, name=upper_label, outward_sign=1.0) - lower_horn = _extract_horn_mount_spec(lower_horn_shape, name=lower_label, outward_sign=-1.0) - return GeneratorInputs( - servo_shape=servo_shape, - reference_face_x=reference_face_x, - sheet_half_width_z=_shape_half_width_z(servo_shape), - upper_horn=upper_horn, - lower_horn=lower_horn, - ) - - -def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _make_capped_flange( - *, - face_center: build123d.Vector, - web_inner_x: float, - y_min: float, - y_max: float, - cap_radius: float, -) -> build123d.Shape: - strip = _make_box( - x_min=web_inner_x, - x_max=face_center.X, - y_min=y_min, - y_max=y_max, - z_min=face_center.Z - cap_radius, - z_max=face_center.Z + cap_radius, - ) - cap = _make_cylinder_along( - radius=cap_radius, - height=y_max - y_min, - origin=build123d.Vector(face_center.X, y_min, face_center.Z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - return strip.fuse(cap) - - -def _web_pattern_center_from_values( - *, - web_outer_x: float, - top_y_max: float, - bottom_y_min: float, - z_center: float, -) -> build123d.Vector: - return build123d.Vector( - web_outer_x, - 0.5 * (bottom_y_min + top_y_max), - z_center, - ) - - -def _web_pattern_center(layout: BracketLayout) -> build123d.Vector: - return _web_pattern_center_from_values( - web_outer_x=layout.web_outer_x, - top_y_max=layout.top_y_max, - bottom_y_min=layout.bottom_y_min, - z_center=0.5 * (layout.global_z_min + layout.global_z_max), - ) - - -def _web_m2_hole_specs() -> tuple[WebHoleSpec, ...]: - return tuple( - WebHoleSpec(center_y=center_y, center_z=center_z, radius=WEB_M2_CLEARANCE_RADIUS_MM) - for center_y, center_z in WEB_SERVO_M2_HOLE_CENTERS_YZ_MM - ) - - -def _web_round_hole_specs(layout: BracketLayout) -> tuple[WebHoleSpec, ...]: - return _web_m2_hole_specs() - - -def _cut_round_web_holes( - shape: build123d.Shape, - *, - layout: BracketLayout, - rule: BendRule, -) -> build123d.Shape: - start_x = layout.web_outer_x - CUT_EXTENSION_MM - for hole in _web_round_hole_specs(layout): - cutter = _make_cylinder_along( - radius=hole.radius, - height=rule.thickness_mm + (2.0 * CUT_EXTENSION_MM), - origin=build123d.Vector(start_x, hole.center_y, hole.center_z), - direction=build123d.Vector(1.0, 0.0, 0.0), - ) - shape = shape.cut(cutter) - return shape - - -def _make_bent_channel_body( - *, - top_end_x: float, - bottom_end_x: float, - layout: BracketLayout, -) -> build123d.Shape: - # Build the channel body with sharp corners instead of formed bend radii. - x_inner = layout.web_inner_x - x_outer = layout.web_outer_x - - profile_points = [ - build123d.Vector(top_end_x, layout.top_y_max, 0.0), - build123d.Vector(x_outer, layout.top_y_max, 0.0), - build123d.Vector(x_outer, layout.bottom_y_min, 0.0), - build123d.Vector(bottom_end_x, layout.bottom_y_min, 0.0), - build123d.Vector(bottom_end_x, layout.bottom_y_max, 0.0), - build123d.Vector(x_inner, layout.bottom_y_max, 0.0), - build123d.Vector(x_inner, layout.top_y_min, 0.0), - build123d.Vector(top_end_x, layout.top_y_min, 0.0), - ] - profile_edges = [ - build123d.Edge.make_line(profile_points[0], profile_points[1]), - build123d.Edge.make_line(profile_points[1], profile_points[2]), - build123d.Edge.make_line(profile_points[2], profile_points[3]), - build123d.Edge.make_line(profile_points[3], profile_points[4]), - build123d.Edge.make_line(profile_points[4], profile_points[5]), - build123d.Edge.make_line(profile_points[5], profile_points[6]), - build123d.Edge.make_line(profile_points[6], profile_points[7]), - build123d.Edge.make_line(profile_points[7], profile_points[0]), - ] - face = _face_from_wire(_assemble_wire(profile_edges)) - return build123d.Solid.extrude( - face, - build123d.Vector(0.0, 0.0, layout.global_z_max - layout.global_z_min), - ).translate((0.0, 0.0, layout.global_z_min)) - - -def _min_keyed_cut_edge_to_bend_line(center_u: float, *, bend_line_u: float) -> float: - return (center_u - KEYED_CONNECTION_HALF_SPAN_MM) - bend_line_u - - -def _min_web_feature_edge_to_bend_lines( - *, - layout: BracketLayout, -) -> float: - center = _web_pattern_center(layout) - distances: list[float] = [] - distances.append((center.Y - KEYED_CONNECTION_HALF_SPAN_MM) - layout.bottom_y_max) - distances.append(layout.top_y_min - (center.Y + KEYED_CONNECTION_HALF_SPAN_MM)) - for hole in _web_round_hole_specs(layout): - distances.append((hole.center_y - hole.radius) - layout.bottom_y_max) - distances.append(layout.top_y_min - (hole.center_y + hole.radius)) - return min(distances) - - -def _build_metrics( - *, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - layout: BracketLayout, - rule: BendRule, -) -> BracketMetrics: - upper_outside_length = upper_horn.mount_face_center.X - layout.web_outer_x - lower_outside_length = lower_horn.mount_face_center.X - layout.web_outer_x - min_flange_outside_length = min(upper_outside_length, lower_outside_length) - min_flange_flat_length = rule.flat_length_from_outside_length_90(min_flange_outside_length) - bend_line_spacing = layout.top_y_min - layout.bottom_y_max - - min_hole_edge_to_bend = min( - _min_keyed_cut_edge_to_bend_line(upper_horn.mount_face_center.X, bend_line_u=layout.web_inner_x), - _min_keyed_cut_edge_to_bend_line(lower_horn.mount_face_center.X, bend_line_u=layout.web_inner_x), - ) - min_web_feature_edge_to_bend = _min_web_feature_edge_to_bend_lines(layout=layout) - - max_flange_outside = max(upper_outside_length, lower_outside_length) - base_to_flange_ratio = bend_line_spacing / max_flange_outside - - return BracketMetrics( - min_flange_outside_length_mm=min_flange_outside_length, - min_flange_flat_length_mm=min_flange_flat_length, - bend_line_spacing_mm=bend_line_spacing, - min_hole_edge_to_bend_mm=min_hole_edge_to_bend, - min_web_feature_edge_to_bend_mm=min_web_feature_edge_to_bend, - base_to_flange_ratio=base_to_flange_ratio, - bend_deduction_formula_mm=(2.0 * rule.outside_setback_90_mm) - rule.bend_allowance_90_mm, - ) - - -def _validate_metrics(metrics: BracketMetrics, *, rule: BendRule) -> None: - if metrics.min_flange_outside_length_mm < rule.min_flange_formed_mm: - raise RuntimeError( - "Flange is shorter than the configured bend-rule formed minimum: " - f"{metrics.min_flange_outside_length_mm:.3f} mm < {rule.min_flange_formed_mm:.3f} mm" - ) - if metrics.min_flange_flat_length_mm < rule.min_flange_flat_mm: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"{metrics.min_flange_flat_length_mm:.3f} mm < {rule.min_flange_flat_mm:.3f} mm" - ) - if metrics.bend_line_spacing_mm < rule.channel_base_spacing_min_mm: - raise RuntimeError( - "Channel base spacing is below the SendCutSend consecutive-bend minimum: " - f"{metrics.bend_line_spacing_mm:.3f} mm < {rule.channel_base_spacing_min_mm:.3f} mm" - ) - if metrics.min_hole_edge_to_bend_mm < rule.half_die_width_mm: - raise RuntimeError( - "Horn flange features are too close to the bend die region: " - f"{metrics.min_hole_edge_to_bend_mm:.3f} mm < {rule.half_die_width_mm:.3f} mm" - ) - if metrics.min_web_feature_edge_to_bend_mm < rule.half_die_width_mm: - raise RuntimeError( - "Closing-face horn pattern is too close to a bend die region: " - f"{metrics.min_web_feature_edge_to_bend_mm:.3f} mm < {rule.half_die_width_mm:.3f} mm" - ) - if metrics.min_flange_outside_length_mm <= rule.channel_one_to_one_max_flange_mm and metrics.base_to_flange_ratio < 1.0: - raise RuntimeError( - "U-channel base-to-flange ratio is below the configured 1:1 manufacturing check for this stock: " - f"{metrics.base_to_flange_ratio:.3f} < 1.000" - ) - - -def _build_layout( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> tuple[BracketLayout, float]: - web_outer_x = SERVO_HORN_AXIS_X_MM - YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM - web_inner_x = web_outer_x + rule.thickness_mm - - top_y_min = upper_horn.mount_face_y + HORN_FACE_GAP_MM - top_y_max = top_y_min + rule.thickness_mm - bottom_y_max = lower_horn.mount_face_y - HORN_FACE_GAP_MM - bottom_y_min = bottom_y_max - rule.thickness_mm - - cap_radius = max(upper_horn.half_width_z, lower_horn.half_width_z, sheet_half_width_z) - - top_z_min = upper_horn.mount_face_center.Z - cap_radius - top_z_max = upper_horn.mount_face_center.Z + cap_radius - bottom_z_min = lower_horn.mount_face_center.Z - cap_radius - bottom_z_max = lower_horn.mount_face_center.Z + cap_radius - global_z_min = min(top_z_min, bottom_z_min) - global_z_max = max(top_z_max, bottom_z_max) - - layout = BracketLayout( - web_inner_x=web_inner_x, - web_outer_x=web_outer_x, - top_y_min=top_y_min, - top_y_max=top_y_max, - bottom_y_min=bottom_y_min, - bottom_y_max=bottom_y_max, - top_z_min=top_z_min, - top_z_max=top_z_max, - bottom_z_min=bottom_z_min, - bottom_z_max=bottom_z_max, - global_z_min=global_z_min, - global_z_max=global_z_max, - ) - return layout, cap_radius - - -def build_bracket( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> tuple[build123d.Shape, BracketLayout, BracketMetrics]: - layout, cap_radius = _build_layout( - reference_face_x, - sheet_half_width_z=sheet_half_width_z, - upper_horn=upper_horn, - lower_horn=lower_horn, - rule=rule, - ) - web_inner_x = layout.web_inner_x - web_outer_x = layout.web_outer_x - top_y_min = layout.top_y_min - top_y_max = layout.top_y_max - bottom_y_min = layout.bottom_y_min - bottom_y_max = layout.bottom_y_max - global_z_min = layout.global_z_min - global_z_max = layout.global_z_max - - bracket = _make_bent_channel_body( - top_end_x=upper_horn.mount_face_center.X + cap_radius, - bottom_end_x=lower_horn.mount_face_center.X + cap_radius, - layout=layout, - ) - cap_mask = ( - _make_box( - x_min=web_outer_x, - x_max=web_inner_x, - y_min=bottom_y_min, - y_max=top_y_max, - z_min=global_z_min, - z_max=global_z_max, - ) - .fuse( - _make_capped_flange( - face_center=upper_horn.mount_face_center, - web_inner_x=web_inner_x, - y_min=top_y_min, - y_max=top_y_max, - cap_radius=cap_radius, - ) - ) - .fuse( - _make_capped_flange( - face_center=lower_horn.mount_face_center, - web_inner_x=web_inner_x, - y_min=bottom_y_min, - y_max=bottom_y_max, - cap_radius=cap_radius, - ) - ) - ) - bracket = bracket.intersect(cap_mask) - - top_start_y = top_y_max + CUT_EXTENSION_MM - bracket = cut_keyed_connection_y_aligned( - bracket, - center=upper_horn.mount_face_center, - start_y=top_start_y, - direction_y=-1.0, - thickness_mm=rule.thickness_mm, - cut_extension_mm=CUT_EXTENSION_MM, - ) - - bottom_start_y = bottom_y_min - CUT_EXTENSION_MM - bracket = cut_keyed_connection_y_aligned( - bracket, - center=lower_horn.mount_face_center, - start_y=bottom_start_y, - direction_y=1.0, - thickness_mm=rule.thickness_mm, - cut_extension_mm=CUT_EXTENSION_MM, - ) - - web_hole_start_x = web_outer_x - CUT_EXTENSION_MM - bracket = cut_keyed_connection_x_aligned( - bracket, - center=_web_pattern_center(layout), - start_x=web_hole_start_x, - direction_x=1.0, - thickness_mm=rule.thickness_mm, - cut_extension_mm=CUT_EXTENSION_MM, - ) - - bracket = _cut_round_web_holes( - bracket, - layout=layout, - rule=rule, - ) - metrics = _build_metrics( - upper_horn=upper_horn, - lower_horn=lower_horn, - layout=layout, - rule=rule, - ) - _validate_metrics(metrics, rule=rule) - return bracket, layout, metrics - - -def build_flat_pattern( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> tuple[FlatPattern, BracketMetrics]: - if abs(upper_horn.mount_face_center.Z - lower_horn.mount_face_center.Z) > 1e-6: - raise RuntimeError("Flat-pattern export currently requires the horn faces to share the same Z centerline") - - layout, cap_radius = _build_layout( - reference_face_x, - sheet_half_width_z=sheet_half_width_z, - upper_horn=upper_horn, - lower_horn=lower_horn, - rule=rule, - ) - metrics = _build_metrics( - upper_horn=upper_horn, - lower_horn=lower_horn, - layout=layout, - rule=rule, - ) - _validate_metrics(metrics, rule=rule) - - top_end_x = upper_horn.mount_face_center.X + cap_radius - bottom_end_x = lower_horn.mount_face_center.X + cap_radius - top_outside_length = top_end_x - layout.web_outer_x - bottom_outside_length = bottom_end_x - layout.web_outer_x - top_flat_length = top_outside_length - rule.outside_setback_90_mm - bottom_flat_length = bottom_outside_length - rule.outside_setback_90_mm - - bend_allowance = rule.bend_allowance_90_mm - top_web_tangent_y = layout.top_y_min - rule.effective_inside_radius_mm - bottom_web_tangent_y = layout.bottom_y_max + rule.effective_inside_radius_mm - web_flat_length = top_web_tangent_y - bottom_web_tangent_y - total_flat_length = top_flat_length + bend_allowance + web_flat_length + bend_allowance + bottom_flat_length - flat_width = layout.global_z_max - layout.global_z_min - top_bend_line_x = top_flat_length + (0.5 * bend_allowance) - bottom_bend_line_x = top_flat_length + bend_allowance + web_flat_length + (0.5 * bend_allowance) - - web_center = _web_pattern_center(layout) - cut_profiles = ( - CutProfileSpec( - center_x=top_end_x - upper_horn.mount_face_center.X, - center_y=upper_horn.mount_face_center.Z - layout.global_z_min, - rotation_degrees=KEYED_CONNECTION_ROTATION_DEG, - ), - CutProfileSpec( - center_x=top_flat_length + bend_allowance + (top_web_tangent_y - web_center.Y), - center_y=web_center.Z - layout.global_z_min, - rotation_degrees=KEYED_CONNECTION_ROTATION_DEG, - ), - CutProfileSpec( - center_x=total_flat_length - (bottom_end_x - lower_horn.mount_face_center.X), - center_y=lower_horn.mount_face_center.Z - layout.global_z_min, - rotation_degrees=KEYED_CONNECTION_ROTATION_DEG, - ), - ) - round_hole_profiles = tuple( - CircleProfileSpec( - center_x=top_flat_length + bend_allowance + (top_web_tangent_y - hole.center_y), - center_y=hole.center_z - layout.global_z_min, - radius=hole.radius, - ) - for hole in _web_round_hole_specs(layout) - ) - - return ( - FlatPattern( - total_length_mm=total_flat_length, - width_mm=flat_width, - cap_radius_mm=cap_radius, - bend_line_positions_mm=(top_bend_line_x, bottom_bend_line_x), - cut_profiles=cut_profiles, - round_hole_profiles=round_hole_profiles, - ), - metrics, - ) - - -def _build_flat_pattern_dxf(flat_pattern: FlatPattern) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - radius = flat_pattern.cap_radius_mm - width = flat_pattern.width_mm - total_length = flat_pattern.total_length_mm - - msp.add_line((radius, 0.0), (total_length - radius, 0.0), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(total_length - radius, radius), - radius=radius, - start_angle=-90.0, - end_angle=90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((total_length - radius, width), (radius, width), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(radius, radius), - radius=radius, - start_angle=90.0, - end_angle=270.0, - dxfattribs={"layer": "CUT"}, - ) - - for cut_profile in flat_pattern.cut_profiles: - msp.add_lwpolyline( - sample_keyed_connection_outline( - center_u=cut_profile.center_x, - center_v=cut_profile.center_y, - points_per_arc=16, - rotation_degrees=cut_profile.rotation_degrees, - ), - format="xy", - close=True, - dxfattribs={"layer": "CUT"}, - ) - - for round_hole in flat_pattern.round_hole_profiles: - msp.add_circle( - (round_hole.center_x, round_hole.center_y), - radius=round_hole.radius, - dxfattribs={"layer": "CUT"}, - ) - - for bend_line_x in flat_pattern.bend_line_positions_mm: - msp.add_line( - (bend_line_x, 0.0), - (bend_line_x, width), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def build_step() -> build123d.Shape: - CAD_DIR.mkdir(parents=True, exist_ok=True) - inputs = _load_generator_inputs() - bracket, layout, metrics = build_bracket( - inputs.reference_face_x, - sheet_half_width_z=inputs.sheet_half_width_z, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected bracket solid, found {len(solids)}") - - interference_volume = inputs.servo_shape.intersect(bracket).volume - if interference_volume > 1e-3: - raise RuntimeError(f"Bracket intersects the servo by {interference_volume:.6f} mm^3") - - bracket_bb = bracket.bounding_box() - print( - "Bracket envelope " - f"X={bracket_bb.size.X:.3f} mm, " - f"Y={bracket_bb.size.Y:.3f} mm, " - f"Z={bracket_bb.size.Z:.3f} mm" - ) - print( - "Connection features " - f"ref={KEYED_CONNECTION_REF}, " - f"horn_keyed_half_span={KEYED_CONNECTION_HALF_SPAN_MM:.3f} mm, " - f"web_keyed_half_span={KEYED_CONNECTION_HALF_SPAN_MM:.3f} mm, " - f"web_m2_holes={len(WEB_SERVO_M2_HOLE_CENTERS_YZ_MM)}x{2.0 * WEB_M2_CLEARANCE_RADIUS_MM:.3f} mm" - ) - print( - "SendCutSend bend rule " - f"material={SHEET_RULE.material_name}, " - f"thickness={SHEET_RULE.thickness_mm:.3f} mm, " - f"inside_radius={SHEET_RULE.effective_inside_radius_mm:.3f} mm, " - f"outer_radius={SHEET_RULE.outer_radius_mm:.3f} mm, " - f"k_factor={SHEET_RULE.k_factor:.2f}, " - f"bend_deduction_90={SHEET_RULE.bend_deduction_90_mm:.3f} mm" - ) - print( - "SendCutSend checks " - f"flange_outside_min={metrics.min_flange_outside_length_mm:.3f} mm, " - f"flange_flat_min={metrics.min_flange_flat_length_mm:.3f} mm, " - f"bend_spacing={metrics.bend_line_spacing_mm:.3f} mm, " - f"hole_edge_to_bend_min={metrics.min_hole_edge_to_bend_mm:.3f} mm, " - f"web_feature_edge_to_bend_min={metrics.min_web_feature_edge_to_bend_mm:.3f} mm, " - f"base_to_flange_ratio={metrics.base_to_flange_ratio:.3f}" - ) - print( - "Bracket placement " - f"inner_web_x={layout.web_inner_x:.3f} mm, " - f"outer_web_x={layout.web_outer_x:.3f} mm, " - f"yoke_length={YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM:.3f} mm, " - f"reference_face_x={inputs.reference_face_x:.3f} mm, " - f"reference_clearance={inputs.reference_face_x - layout.web_inner_x:.3f} mm" - ) - print( - "Upper horn " - f"selector={UPPER_HORN_SELECTOR}, face_y={inputs.upper_horn.mount_face_y:.3f} mm, " - "connection_profiles=1" - ) - print( - "Lower horn " - f"selector={LOWER_HORN_SELECTOR}, face_y={inputs.lower_horn.mount_face_y:.3f} mm, " - "connection_profiles=1" - ) - print(f"Servo/bracket interference volume (mm^3): {interference_volume:.6f}") - return bracket - - -def build_dxf() -> ezdxf.document.Drawing: - CAD_DIR.mkdir(parents=True, exist_ok=True) - inputs = _load_generator_inputs() - flat_pattern, metrics = build_flat_pattern( - inputs.reference_face_x, - sheet_half_width_z=inputs.sheet_half_width_z, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - doc = _build_flat_pattern_dxf(flat_pattern) - print( - "Flat pattern " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"keyed_profiles={len(flat_pattern.cut_profiles)}, " - f"round_holes={len(flat_pattern.round_hole_profiles)}" - ) - print( - "DXF checks " - f"flange_flat_min={metrics.min_flange_flat_length_mm:.3f} mm, " - f"bend_spacing={metrics.bend_line_spacing_mm:.3f} mm, " - f"hole_edge_to_bend_min={metrics.min_hole_edge_to_bend_mm:.3f} mm, " - f"web_feature_edge_to_bend_min={metrics.min_web_feature_edge_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v1/parts/servo_horn_yoke.step b/models/robots/tom/v1/parts/servo_horn_yoke.step deleted file mode 100644 index 29f8747b..00000000 --- a/models/robots/tom/v1/parts/servo_horn_yoke.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce221d084347d1e7c59df7f4bdb2023ab9b3d3fb6928d865df200b202eaed216 -size 277031 diff --git a/models/robots/tom/v1/tom.py b/models/robots/tom/v1/tom.py deleted file mode 100644 index 5253efb0..00000000 --- a/models/robots/tom/v1/tom.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import PurePosixPath, Path - -TOM_DIR = Path(__file__).resolve().parent.parent -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.robot_arm import ( - robot_arm_assembly_children, - robot_arm_motion_envelope, - robot_arm_srdf_envelope, - robot_arm_urdf_envelope, -) - - -def _v1_step_path(path: str) -> str: - step_path = PurePosixPath(path) - if step_path.parts and step_path.parts[0] == "assemblies": - return step_path.as_posix() - if step_path.parts and step_path.parts[0] in {"imports", "gripper"}: - return (PurePosixPath("parts") / step_path).as_posix() - return (PurePosixPath("parts") / step_path.name).as_posix() - - -def gen_step() -> dict[str, object]: - children = [ - { - **child, - "path": _v1_step_path(str(child["path"])), - } - for child in robot_arm_assembly_children() - ] - return { - "children": children, - } - - -def gen_urdf() -> dict[str, object]: - payload = robot_arm_urdf_envelope(urdf_output="tom.urdf") - return {"xml": payload["xml"]} - - -def gen_srdf() -> dict[str, object]: - return robot_arm_srdf_envelope(urdf="tom.urdf") - - -def gen_motion() -> dict[str, object]: - return robot_arm_motion_envelope(urdf="tom.urdf") diff --git a/models/robots/tom/v1/tom.srdf b/models/robots/tom/v1/tom.srdf deleted file mode 100644 index b74b2d12..00000000 --- a/models/robots/tom/v1/tom.srdf +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v1/tom.step b/models/robots/tom/v1/tom.step deleted file mode 100644 index 1d85a89a..00000000 --- a/models/robots/tom/v1/tom.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2abb187418ab0b0e47cb65217d86274b726512cf5aed0b502b53a90897258e14 -size 37774259 diff --git a/models/robots/tom/v1/tom.urdf b/models/robots/tom/v1/tom.urdf deleted file mode 100644 index c874b9e3..00000000 --- a/models/robots/tom/v1/tom.urdf +++ /dev/null @@ -1,457 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/.tom.step.glb b/models/robots/tom/v2/.tom.step.glb deleted file mode 100644 index 2cfbb123..00000000 --- a/models/robots/tom/v2/.tom.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70cda622f7308f20cf20df906ed26e4c546d5e5e32ac342340d23ba4fb55b9b2 -size 23780860 diff --git a/models/robots/tom/v2/.tom_double.step.glb b/models/robots/tom/v2/.tom_double.step.glb deleted file mode 100644 index 22822222..00000000 --- a/models/robots/tom/v2/.tom_double.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78e8d17ccda0c4d22972813636c6feb3bb600e1a3600759f1c14b7b95465415e -size 24951808 diff --git a/models/robots/tom/v2/.tom_with_gripper.step.glb b/models/robots/tom/v2/.tom_with_gripper.step.glb deleted file mode 100644 index 5a0414ed..00000000 --- a/models/robots/tom/v2/.tom_with_gripper.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a4e1eea5fc21aaa4eb1550025b5c9ab472087bb7dd5077003d85d261a35a8395 -size 34239356 diff --git a/models/robots/tom/v2/3MF/base_plate.3mf b/models/robots/tom/v2/3MF/base_plate.3mf deleted file mode 100644 index 09584efc..00000000 --- a/models/robots/tom/v2/3MF/base_plate.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:03d1add7636c444a44d83d6e28e183e9bcaf320f26564d46b84199ac51bb80eb -size 33435 diff --git a/models/robots/tom/v2/3MF/gripper_left_claw.3mf b/models/robots/tom/v2/3MF/gripper_left_claw.3mf deleted file mode 100644 index e7a32695..00000000 --- a/models/robots/tom/v2/3MF/gripper_left_claw.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7cbffd50496caa60895ffad0087c1a612d3a7bad8de86c77aff9fe609b38de6d -size 65609 diff --git a/models/robots/tom/v2/3MF/gripper_right_claw.3mf b/models/robots/tom/v2/3MF/gripper_right_claw.3mf deleted file mode 100644 index 0c2049b9..00000000 --- a/models/robots/tom/v2/3MF/gripper_right_claw.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8736b551c1f5ac3a484bcf48c6ce89fd6a878f2c134c8da5ef76d758b26bbbc -size 65591 diff --git a/models/robots/tom/v2/3MF/gripper_servo_gear.3mf b/models/robots/tom/v2/3MF/gripper_servo_gear.3mf deleted file mode 100644 index 504fb818..00000000 --- a/models/robots/tom/v2/3MF/gripper_servo_gear.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bfbcdbde32ff26d41f99ad0513afe3d9ea4baa10e5ef8012a9efc8f4adb34bee -size 30673 diff --git a/models/robots/tom/v2/3MF/gripper_static.3mf b/models/robots/tom/v2/3MF/gripper_static.3mf deleted file mode 100644 index bbd4b0f9..00000000 --- a/models/robots/tom/v2/3MF/gripper_static.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c961e3d07e07cb6bfe5a5544edcbc9238c2a4681e955e08c9ea172558052295f -size 353321 diff --git a/models/robots/tom/v2/3MF/link_bracket_left.3mf b/models/robots/tom/v2/3MF/link_bracket_left.3mf deleted file mode 100644 index be468839..00000000 --- a/models/robots/tom/v2/3MF/link_bracket_left.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:21942228378d59571598619647a17adbab77ca025ccb1860c0848b24a5f6b6c9 -size 58566 diff --git a/models/robots/tom/v2/3MF/link_bracket_right.3mf b/models/robots/tom/v2/3MF/link_bracket_right.3mf deleted file mode 100644 index 42f4a8d6..00000000 --- a/models/robots/tom/v2/3MF/link_bracket_right.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3124907c542c6e3dfc9fa421ffbfb6208105c1d6562e8d9c1c1b5ff4ab46c51c -size 59239 diff --git a/models/robots/tom/v2/3MF/link_standoff_m3_35.3mf b/models/robots/tom/v2/3MF/link_standoff_m3_35.3mf deleted file mode 100644 index 121900b2..00000000 --- a/models/robots/tom/v2/3MF/link_standoff_m3_35.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7c002c70eeadd453e9f52ac2cc0378332819f499c11072d038159855cec4448a -size 11730 diff --git a/models/robots/tom/v2/3MF/servo_end_mount.3mf b/models/robots/tom/v2/3MF/servo_end_mount.3mf deleted file mode 100644 index f2f43707..00000000 --- a/models/robots/tom/v2/3MF/servo_end_mount.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da2d6c5f1a2f834becf805744fc9afb3bbce74bc58cbda0abe3a7ebf48d29b97 -size 45079 diff --git a/models/robots/tom/v2/3MF/servo_end_mount_double.3mf b/models/robots/tom/v2/3MF/servo_end_mount_double.3mf deleted file mode 100644 index e48adde7..00000000 --- a/models/robots/tom/v2/3MF/servo_end_mount_double.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d75552cbedb4291d11ae9e93669878819dc3d1631aa8aab269d01811f33b14c -size 55931 diff --git a/models/robots/tom/v2/3MF/servo_horn_yoke.3mf b/models/robots/tom/v2/3MF/servo_horn_yoke.3mf deleted file mode 100644 index 9ebf11d2..00000000 --- a/models/robots/tom/v2/3MF/servo_horn_yoke.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4c3aa7586b599184bf12077c971ddcf58f4eeae1fabdad9888a60876f029c227 -size 52463 diff --git a/models/robots/tom/v2/3MF/servo_horn_yoke_double_horn.3mf b/models/robots/tom/v2/3MF/servo_horn_yoke_double_horn.3mf deleted file mode 100644 index 4ee2d525..00000000 --- a/models/robots/tom/v2/3MF/servo_horn_yoke_double_horn.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4409718168a2a20e97667374a6e850dc88608cb92f942e041cf0386ad5d3f4c0 -size 50147 diff --git a/models/robots/tom/v2/3MF/sts3215.3mf b/models/robots/tom/v2/3MF/sts3215.3mf deleted file mode 100644 index 6f0fe173..00000000 --- a/models/robots/tom/v2/3MF/sts3215.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65a10d52c1d5f61706436b335014c6f7f6f9b869e07d2fff8ccc30b95f8b5ba4 -size 647698 diff --git a/models/robots/tom/v2/3MF/sts3250.3mf b/models/robots/tom/v2/3MF/sts3250.3mf deleted file mode 100644 index 182b3656..00000000 --- a/models/robots/tom/v2/3MF/sts3250.3mf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0453772f80a60de27c057b562180585f5160f1cee61e987fc651c46be4290a41 -size 115734 diff --git a/models/robots/tom/v2/DXF/base_clamp.dxf b/models/robots/tom/v2/DXF/base_clamp.dxf deleted file mode 100644 index e37b803a..00000000 --- a/models/robots/tom/v2/DXF/base_clamp.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ed86e592dbcdb87f257bc17d3de8fbc8854c6c0c62cd7ccf9c7feab54a00fab2 -size 163477 diff --git a/models/robots/tom/v2/DXF/link_bracket_left.dxf b/models/robots/tom/v2/DXF/link_bracket_left.dxf deleted file mode 100644 index bf12634d..00000000 --- a/models/robots/tom/v2/DXF/link_bracket_left.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2a9a6a6b571e5bf02546bff9df4407acaa708ef5bc57c0787b16869f2a7cb3b -size 83890 diff --git a/models/robots/tom/v2/DXF/link_bracket_right.dxf b/models/robots/tom/v2/DXF/link_bracket_right.dxf deleted file mode 100644 index e44cf2e7..00000000 --- a/models/robots/tom/v2/DXF/link_bracket_right.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e9e256272a2ef019a4a6a35a1d9780719facc8fef95f2038353e4e9a2095b2a6 -size 83891 diff --git a/models/robots/tom/v2/DXF/servo_end_mount.dxf b/models/robots/tom/v2/DXF/servo_end_mount.dxf deleted file mode 100644 index 990e957e..00000000 --- a/models/robots/tom/v2/DXF/servo_end_mount.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4aa173997fd595de91dfb7310873673336b47eee8fa2cab45da73341a3971843 -size 68372 diff --git a/models/robots/tom/v2/DXF/servo_end_mount_double.dxf b/models/robots/tom/v2/DXF/servo_end_mount_double.dxf deleted file mode 100644 index 0ab9b7a4..00000000 --- a/models/robots/tom/v2/DXF/servo_end_mount_double.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0442b504e28a3abcf91be56ae6acd91a0e01deea53ac477ee4f85bf78380c47 -size 81814 diff --git a/models/robots/tom/v2/DXF/servo_horn_yoke.dxf b/models/robots/tom/v2/DXF/servo_horn_yoke.dxf deleted file mode 100644 index 10237f53..00000000 --- a/models/robots/tom/v2/DXF/servo_horn_yoke.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:487c36889152fada32333a31fd4617ed36c08ad7c2f2f2e41f225bededfb35a7 -size 111426 diff --git a/models/robots/tom/v2/DXF/servo_horn_yoke_double_horn.dxf b/models/robots/tom/v2/DXF/servo_horn_yoke_double_horn.dxf deleted file mode 100644 index 9464b031..00000000 --- a/models/robots/tom/v2/DXF/servo_horn_yoke_double_horn.dxf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78784f0169386490699a843a5f4e9213abf15d4629d0977c56f79f411c023beb -size 121660 diff --git a/models/robots/tom/v2/assemblies/.base_link.step.glb b/models/robots/tom/v2/assemblies/.base_link.step.glb deleted file mode 100644 index 7bb23a63..00000000 --- a/models/robots/tom/v2/assemblies/.base_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1154693e2e0363df4c153c3cc88515931e788d51f1f3914bdd9df02d9cf10381 -size 3574896 diff --git a/models/robots/tom/v2/assemblies/.link_assembly.step.glb b/models/robots/tom/v2/assemblies/.link_assembly.step.glb deleted file mode 100644 index bdfc9206..00000000 --- a/models/robots/tom/v2/assemblies/.link_assembly.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b6238af9b8b649a69f7f626133ee691c2a6866af7e71dd85628c2585b415c09e -size 15105072 diff --git a/models/robots/tom/v2/assemblies/.pitch_link_sts3215.step.glb b/models/robots/tom/v2/assemblies/.pitch_link_sts3215.step.glb deleted file mode 100644 index 35920171..00000000 --- a/models/robots/tom/v2/assemblies/.pitch_link_sts3215.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:313712fbb92a8374c660fc50cce935a1a23c7ddb00fa5700599672259962ee64 -size 11794548 diff --git a/models/robots/tom/v2/assemblies/.pitch_link_sts3250.step.glb b/models/robots/tom/v2/assemblies/.pitch_link_sts3250.step.glb deleted file mode 100644 index 5d0aacff..00000000 --- a/models/robots/tom/v2/assemblies/.pitch_link_sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:929389ac54d54f2dbd743135963b379eb1ee1230eaaf141db299e4e830777497 -size 3782228 diff --git a/models/robots/tom/v2/assemblies/.servo_end_mount_sts3250.step.glb b/models/robots/tom/v2/assemblies/.servo_end_mount_sts3250.step.glb deleted file mode 100644 index 14c5572a..00000000 --- a/models/robots/tom/v2/assemblies/.servo_end_mount_sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b03a9491cf7c4ce3f6210568422c3ac5f314c7d0568575b92d5755cf7c80384b -size 3712392 diff --git a/models/robots/tom/v2/assemblies/.servo_horn_yoke_double.step.glb b/models/robots/tom/v2/assemblies/.servo_horn_yoke_double.step.glb deleted file mode 100644 index 0fd6f6c6..00000000 --- a/models/robots/tom/v2/assemblies/.servo_horn_yoke_double.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:05f90e3389eba3588a076385e11a60828349451ce93383168e512b939cc48e1d -size 4169192 diff --git a/models/robots/tom/v2/assemblies/.servo_horn_yoke_mount.step.glb b/models/robots/tom/v2/assemblies/.servo_horn_yoke_mount.step.glb deleted file mode 100644 index bf22aab5..00000000 --- a/models/robots/tom/v2/assemblies/.servo_horn_yoke_mount.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:af07f8d14d019d4ea48e7a85ee359f13c545f362310db41cbabc3be20099025f -size 3778020 diff --git a/models/robots/tom/v2/assemblies/.shoulder_yaw_link.step.glb b/models/robots/tom/v2/assemblies/.shoulder_yaw_link.step.glb deleted file mode 100644 index e52cdb40..00000000 --- a/models/robots/tom/v2/assemblies/.shoulder_yaw_link.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b1201013e70bca995b0682eaecc59b7c9737a10c60197bfc1923fcc72b3ba43 -size 3712332 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp.step.glb deleted file mode 100644 index 5a1e9b86..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:49cec5877ab512904ab069025a4ddab3d4cb7acf03dc302156d77ee7ffd6415d -size 1074856 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp_assembly.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp_assembly.step.glb deleted file mode 100644 index 902baa83..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_clamp_assembly.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7eb3f203f915dd68264114d0dff983abc08b9a0cbb6b0c24e08e5a04cca6020 -size 25158688 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_hub_shell.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_hub_shell.step.glb deleted file mode 100644 index 9fec10f7..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.base_hub_shell.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:666bbf0565aabbe67453f2894b21dc4d6dbcbd76c0bba5d5f96b1588c5996f4c -size 91184 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_hex_spacer_6mm.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_hex_spacer_6mm.step.glb deleted file mode 100644 index 1f430fff..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_hex_spacer_6mm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff28cdaaa8cdaf56bd6df9119c67a6e382690b015af6443673c45358c79dd643 -size 152944 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_spacer_12mm.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_spacer_12mm.step.glb deleted file mode 100644 index 0f11d645..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_5_spacer_12mm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:39d77cfc01a2c994777e90d0679fe75e007fa34be6c5c415b24466974eb7e717 -size 186112 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_3mm.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_3mm.step.glb deleted file mode 100644 index 5513b3db..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_3mm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6568b1dae59f2be186c52eeef0eb8d76f340f134b03982fcffe14eb1ccf62225 -size 185988 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_5mm.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_5mm.step.glb deleted file mode 100644 index af7ccd42..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.m2_spacer_5mm.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:258cf47f99dd40479b96c2c791723d1d51e61636f7aa9ef17d76e290f4a47dac -size 185996 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.sts3250.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.sts3250.step.glb deleted file mode 100644 index 6269f9de..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae5ac94f12fbe3d208054021347997498763afab72505a1d1408b028360345ee -size 3245664 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_servo_adapter_a.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_servo_adapter_a.step.glb deleted file mode 100644 index 6a12661f..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_servo_adapter_a.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a19810ee1ddfe79e26844ed982a9b63b841eeb52f155fd3b4c3e8a8ee7f466c2 -size 21127972 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_stripped.step.glb b/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_stripped.step.glb deleted file mode 100644 index dbb88a64..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/.waveshare_bus_stripped.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc144b98214d6c3821d35d740441ee4d608827d03871bf6b5eeac9713ea8d968 -size 195144 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.py deleted file mode 100644 index 4e12c266..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.py +++ /dev/null @@ -1,1733 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate a bent base clamp that wraps under the base servo footprint. - -The upper plate stays in the servo base frame, a side wall rises from the -positive-x side, and a lower return closes the U so the part can clamp to a -desk or fixture from below. -""" - -from __future__ import annotations - -from dataclasses import dataclass -import math -import os -import sys -from pathlib import Path - -from OCP.BRepFilletAPI import BRepFilletAPI_MakeFillet - -TOM_DIR = Path(__file__).resolve().parents[3] -V2_DIR = Path(__file__).resolve().parents[2] -PARTS_DIR = V2_DIR / "parts" -REPO_ROOT = TOM_DIR -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import build123d -import ezdxf -from ezdxf.units import MM as DXF_UNIT_MM - -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) -if str(PARTS_DIR) not in sys.path: - sys.path.insert(0, str(PARTS_DIR)) - -from robot_common.materials import GRAY_ALUMINUM_COLOR -import dxf_topology - - -DISPLAY_NAME = "Base Clamp" -INCLUDE_BOTTOM_SURFACE_MOUNT_FEATURES = False -INCLUDE_RETURN_SURFACE_MOUNT_FEATURES = True -PLATE_THICKNESS_MM = 25.4 * 0.125 -STANDARD_DESK_CLAMP_CLEARANCE_MM = 65.0 -STANDARD_DESK_CLAMP_CONTACT_WIDTH_MM = 33.5 -SADDLE_FOOTPRINT_X_BUFFER_MM = 2.0 -SIDE_WALL_THICKNESS_MM = PLATE_THICKNESS_MM -BOOLEAN_OVERLAP_MM = 0.05 -CUT_EXTENSION_MM = 0.5 -CORNER_RADIUS_MM = 3.0 -RETURN_FACE_LENGTH_RATIO = 2.0 / 3.0 -M3_CLEARANCE_RADIUS_MM = 1.6 -BASE_CENTER_HORN_FACE_RADIUS_MM = 2.8 -BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM = 0.25 -BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM = ( - BASE_CENTER_HORN_FACE_RADIUS_MM + BASE_CENTER_HORN_FACE_CLEARANCE_BUFFER_MM -) -M8_CLEARANCE_DIAMETER_MM = 8.5 -M8_CLEARANCE_RADIUS_MM = 0.5 * M8_CLEARANCE_DIAMETER_MM -SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.125 -SENDCUTSEND_K_FACTOR = 0.44 -SENDCUTSEND_BEND_DEDUCTION_90_MM = 25.4 * 0.216 -SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * (25.4 * 0.630) -SENDCUTSEND_MIN_FLANGE_FORMED_MM = 25.4 * 0.476 -SENDCUTSEND_BEND_RELIEF_DEPTH_MM = 25.4 * 0.270 -SENDCUTSEND_MIN_CORNER_RELIEF_FROM_BEND_MM = 25.4 * 0.032 -SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_MIN_MM = 25.4 * 0.246 -SENDCUTSEND_COUNTERSINK_CENTER_TO_BEND_MIN_MM = 25.4 * 0.517 -SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_BUFFER_MM = 0.5 -SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM = 0.5 -BEND_INNER_RADIUS_MM = 0.0 -BEND_OUTER_RADIUS_MM = 0.0 - -# Base servo footprint bounds in robot-arm local coordinates. -SADDLE_BASE_FACE_Y_MM = -29.8002 -SADDLE_BASE_X_MIN_MM = -35.61 -SADDLE_BASE_X_MAX_MM = 6.338516 -SADDLE_BASE_Z_MIN_MM = -15.960226 -SADDLE_BASE_Z_MAX_MM = 15.960224 - -BASE_SERVO_FOOTPRINT_X_MIN_MM = SADDLE_BASE_X_MIN_MM - SADDLE_FOOTPRINT_X_BUFFER_MM -BASE_SERVO_FOOTPRINT_X_MAX_MM = SADDLE_BASE_X_MAX_MM + SADDLE_FOOTPRINT_X_BUFFER_MM -BASE_SERVO_SIDE_X_MIN_MM = BASE_SERVO_FOOTPRINT_X_MAX_MM - BOOLEAN_OVERLAP_MM -BASE_SERVO_SIDE_X_MAX_MM = BASE_SERVO_SIDE_X_MIN_MM + SIDE_WALL_THICKNESS_MM -BASE_SERVO_END_FOR_END_PIVOT_LOCAL_X_MM = 0.5 * ( - BASE_SERVO_FOOTPRINT_X_MIN_MM + BASE_SERVO_SIDE_X_MAX_MM -) -BASE_SERVO_END_FOR_END_PIVOT_LOCAL_Z_MM = 0.5 * (SADDLE_BASE_Z_MIN_MM + SADDLE_BASE_Z_MAX_MM) - -SERVO_STANDOFF_HEIGHT_MM = 5.0 -SERVO_STANDOFF_BODY_DIAMETER_MM = 3.2 -SERVO_MOUNT_X_SHIFT_FOR_UNDER_ACCESS_MM = BASE_SERVO_FOOTPRINT_X_MIN_MM -BASE_PLATE_SERVO_MOUNT_HOLE_EDGE_BUFFER_MM = 3.0 -BASE_PLATE_UNDER_ACCESS_CLEARANCE_MM = 5.0 - -# 5x M3 lower-horn clearance pattern on the base plate. -BASE_HORN_CONNECTION_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = ( - (-32.5, 0.0, M3_CLEARANCE_RADIUS_MM), - (-25.5, -7.0, M3_CLEARANCE_RADIUS_MM), - (-25.5, 0.0, BASE_CENTER_HORN_FACE_CLEARANCE_RADIUS_MM), - (-25.5, 7.0, M3_CLEARANCE_RADIUS_MM), - (-18.5, 0.0, M3_CLEARANCE_RADIUS_MM), -) -BASE_SERVO_HORN_LOCAL_X_MM = -25.5 - -# Resolved from: -# @cad[STEP/imports/sts3250#o1.2.e920,o1.2.e922] -# @cad[STEP/imports/sts3250#o1.2.e921,o1.2.e923] -M2_CLEARANCE_RADIUS_MM = 1.1 -M2_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM = 2.15 -M2_UNDERSIDE_COUNTERSINK_DEPTH_MM = 1.05 -M2_5_CLEARANCE_RADIUS_MM = 0.5 * 2.7 -M2_5_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM = 2.5 -M2_5_UNDERSIDE_COUNTERSINK_DEPTH_MM = 1.15 -SERVO_BUS_STANDOFF_HEIGHT_MM = 6.0 -SERVO_BUS_STANDOFF_HEX_ACROSS_FLATS_MM = 5.0 -BASE_SERVO_M2_HOLE_SPECS_MM: tuple[tuple[float, float, float], ...] = ( - (-17.2, -10.25, M2_CLEARANCE_RADIUS_MM), - (7.25, -10.25, M2_CLEARANCE_RADIUS_MM), - (-17.2, 10.25, M2_CLEARANCE_RADIUS_MM), - (7.25, 10.25, M2_CLEARANCE_RADIUS_MM), -) -BASE_SERVO_STANDOFF_MOUNT_HOLES_MM: tuple[tuple[float, float, float], ...] = tuple( - ( - (2.0 * BASE_SERVO_END_FOR_END_PIVOT_LOCAL_X_MM) - - x - + SERVO_MOUNT_X_SHIFT_FOR_UNDER_ACCESS_MM, - (2.0 * BASE_SERVO_END_FOR_END_PIVOT_LOCAL_Z_MM) - z, - M2_CLEARANCE_RADIUS_MM, - ) - for x, z, _radius in BASE_SERVO_M2_HOLE_SPECS_MM -) -BASE_SERVO_MOUNTED_FOOTPRINT_X_MIN_MM = ( - (2.0 * BASE_SERVO_END_FOR_END_PIVOT_LOCAL_X_MM) - + SERVO_MOUNT_X_SHIFT_FOR_UNDER_ACCESS_MM - - SADDLE_BASE_X_MAX_MM - - SADDLE_FOOTPRINT_X_BUFFER_MM -) -BASE_SERVO_MOUNTED_FOOTPRINT_X_MAX_MM = BASE_SERVO_MOUNTED_FOOTPRINT_X_MIN_MM + ( - BASE_SERVO_FOOTPRINT_X_MAX_MM - BASE_SERVO_FOOTPRINT_X_MIN_MM -) -BASE_SERVO_MOUNTED_HORN_CENTER_X_MM = ( - (2.0 * BASE_SERVO_END_FOR_END_PIVOT_LOCAL_X_MM) - + SERVO_MOUNT_X_SHIFT_FOR_UNDER_ACCESS_MM - - BASE_SERVO_HORN_LOCAL_X_MM -) -BASE_ATTACHMENT_HOLES_MM: tuple[tuple[float, float, float], ...] = ( - BASE_SERVO_STANDOFF_MOUNT_HOLES_MM - + ( - BASE_HORN_CONNECTION_HOLE_SPECS_MM + BASE_SERVO_M2_HOLE_SPECS_MM - if INCLUDE_BOTTOM_SURFACE_MOUNT_FEATURES - else () - ) -) - -# Resolved from the four PCB spacer hole centers in the original SO-101 bus board. -SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM: tuple[tuple[float, float], ...] = ( - (2.4930065, 2.4999875), - (2.4930065, 30.4999925), - (39.493009, 2.4999875), - (39.505008000000004, 30.4999925), -) -SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM = M2_5_CLEARANCE_RADIUS_MM -SERVO_BUS_MOUNT_LOCAL_X_MIN_MM = min(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_X_MAX_MM = max(x for x, _y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_X_MIN_MM + SERVO_BUS_MOUNT_LOCAL_X_MAX_MM) -SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM = min(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM = max(y for _x, y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM) -SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM = 0.5 * (SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM + SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM) -SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM = 0.97 -BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM = 0.53 -BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM = ( - SENDCUTSEND_MIN_HOLE_TO_EDGE_QUOTE_MM + BASE_CLAMP_HOLE_TO_EDGE_BUFFER_MM -) -BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM = ( - (SERVO_BUS_MOUNT_LOCAL_Y_MAX_MM - SERVO_BUS_MOUNT_LOCAL_Y_MIN_MM) - + (2.0 * (SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM + BASE_CLAMP_HOLE_TO_EDGE_TARGET_MM)) -) - -# SendCutSend's current .125" 5052 guideline uses a 0.630" die width for -# bending; cut features should stay outside half of that die width from the -# bend line. We size the wall from the mount-hole span plus that offset. -SENDCUTSEND_5052_125_DIE_WIDTH_MM = 0.630 * 25.4 -SENDCUTSEND_FEATURE_TO_BEND_MIN_MM = (0.5 * SENDCUTSEND_5052_125_DIE_WIDTH_MM) + SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM -U_BRACKET_CLEARANCE_MM = ( - max( - STANDARD_DESK_CLAMP_CLEARANCE_MM, - (SERVO_BUS_MOUNT_LOCAL_X_MAX_MM - SERVO_BUS_MOUNT_LOCAL_X_MIN_MM) - + (2.0 * SENDCUTSEND_FEATURE_TO_BEND_MIN_MM) - - (2.0 * PLATE_THICKNESS_MM), - ) -) - -# Resolved from @cad[STEP/imports/servo_bus]. -SERVO_BUS_LOCAL_X_MIN_MM = -0.390013 -SERVO_BUS_LOCAL_X_MAX_MM = 42.401989 -SERVO_BUS_LOCAL_Y_MIN_MM = -0.012095 -SERVO_BUS_LOCAL_Y_MAX_MM = 33.012075 -SERVO_BUS_LOCAL_Z_MIN_MM = -6.0 -SERVO_BUS_LOCAL_Z_MAX_MM = 1.599999 -SERVO_BUS_HOLE_CENTER_TO_BOARD_X_EDGE_MIN_MM = min( - min(bus_x - SERVO_BUS_LOCAL_X_MIN_MM, SERVO_BUS_LOCAL_X_MAX_MM - bus_x) - for bus_x, _bus_y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM -) -SERVO_BUS_TOP_EDGE_BUFFER_MM = max( - 2.0, - SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_MIN_MM - + SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_BUFFER_MM - - SERVO_BUS_HOLE_CENTER_TO_BOARD_X_EDGE_MIN_MM, -) -SERVO_BUS_TO_SERVO_CLEARANCE_MM = 2.0 -SERVO_BUS_TOP_BOARD_X_MIN_MM = ( - BASE_SERVO_MOUNTED_FOOTPRINT_X_MIN_MM - + (BASE_SERVO_FOOTPRINT_X_MAX_MM - BASE_SERVO_FOOTPRINT_X_MIN_MM) - + SERVO_BUS_TO_SERVO_CLEARANCE_MM -) -SERVO_BUS_TOP_BOARD_X_MAX_MM = SERVO_BUS_TOP_BOARD_X_MIN_MM + ( - SERVO_BUS_LOCAL_Y_MAX_MM - SERVO_BUS_LOCAL_Y_MIN_MM -) -TOP_PLATE_NECK_CLEARANCE_AFTER_BUS_MM = 2.0 -TOP_PLATE_NECK_START_X_MM = SERVO_BUS_TOP_BOARD_X_MAX_MM + TOP_PLATE_NECK_CLEARANCE_AFTER_BUS_MM -TOP_PLATE_NECK_TO_BEND_MIN_MM = SENDCUTSEND_BEND_RELIEF_DEPTH_MM + 2.0 -TOP_PLATE_SHOULDER_WIDTH_MM = TOP_PLATE_NECK_TO_BEND_MIN_MM + SIDE_WALL_THICKNESS_MM -SERVO_BUS_TOP_TO_BASE_CLAMP_X_OFFSET_MM = SERVO_BUS_TOP_BOARD_X_MIN_MM - SERVO_BUS_LOCAL_Y_MIN_MM -SERVO_BUS_TOP_TO_BASE_CLAMP_Z_OFFSET_MM = -SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM -SERVO_BUS_TOP_MOUNT_HOLES_MM: tuple[tuple[float, float, float], ...] = tuple( - ( - bus_y + SERVO_BUS_TOP_TO_BASE_CLAMP_X_OFFSET_MM, - bus_x + SERVO_BUS_TOP_TO_BASE_CLAMP_Z_OFFSET_MM, - SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM, - ) - for bus_x, bus_y in SERVO_BUS_BOARD_SPACER_CENTERS_XY_MM -) -BASE_ATTACHMENT_HOLES_MM = BASE_ATTACHMENT_HOLES_MM + SERVO_BUS_TOP_MOUNT_HOLES_MM -BASE_PLATE_CASE_MIN_WIDTH_MM = 68.0 -BASE_PLATE_Z_WIDTH_MM = max( - SERVO_BUS_LOCAL_Y_MAX_MM - SERVO_BUS_LOCAL_Y_MIN_MM, - BASE_CLAMP_SERVO_BUS_SPACER_WIDTH_MM, - (SERVO_BUS_LOCAL_X_MAX_MM - SERVO_BUS_LOCAL_X_MIN_MM) - + (2.0 * SERVO_BUS_TOP_EDGE_BUFFER_MM), - STANDARD_DESK_CLAMP_CONTACT_WIDTH_MM + (2.0 * TOP_PLATE_SHOULDER_WIDTH_MM), - STANDARD_DESK_CLAMP_CONTACT_WIDTH_MM, - BASE_PLATE_CASE_MIN_WIDTH_MM, -) -BASE_PLATE_HUB_SLOT_LENGTH_MM = 26.0 -BASE_PLATE_HUB_SLOT_WIDTH_MM = 4.0 -BASE_PLATE_HUB_SLOT_EDGE_INSET_MM = 5.5 -BASE_PLATE_HUB_END_SLOT_X_INSET_MM = 6.0 -BASE_PLATE_HUB_SIDE_SLOT_COUNT = 2 -BASE_PLATE_OCTAGON_LEFT_CHAMFER_X_MM = 10.0 -BASE_PLATE_OCTAGON_LEFT_CHAMFER_Z_MM = 10.0 -BASE_PLATE_MAIN_SURFACE_CENTER_X_MM = BASE_SERVO_MOUNTED_HORN_CENTER_X_MM -BASE_PLATE_LEFT_EDGE_EXTRA_INSET_MM = 0.0 -BASE_PLATE_SHARP_NECK_CORNER_INDICES = (2, 5) - - -@dataclass(frozen=True) -class BracketLayout: - x_min: float - x_max: float - y_min: float - y_max: float - z_min: float - z_max: float - clamp_z_min: float - clamp_z_max: float - neck_start_x: float - width: float - height: float - clamp_height: float - return_width: float - return_x_min: float - return_y_min: float - return_y_max: float - side_x_min: float - side_x_max: float - overall_y_min: float - face_y: float - attachment_holes: tuple[tuple[float, float, float], ...] - return_mount_holes: tuple[tuple[float, float, float], ...] - servo_bus_mount_holes_xzr: tuple[tuple[float, float, float], ...] - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - top_flange_flat_length_mm: float - return_flange_flat_length_mm: float - web_flat_start_mm: float - web_flat_end_mm: float - bend_line_positions_mm: tuple[float, float] - cut_circles: tuple[tuple[float, float, float], ...] - cut_slots: tuple[tuple[float, float, float, float, float], ...] - rounded_corner_radius_mm: float - min_feature_to_bend_mm: float - - -def _make_box( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _make_xz_outline_solid( - points_xz: tuple[tuple[float, float], ...], - *, - y_min: float, - y_max: float, -) -> build123d.Shape: - points_xyz = tuple((x, y_min, z) for x, z in points_xz) - face = build123d.make_face(build123d.Polyline(*points_xyz, close=True)) - return build123d.extrude(face, amount=y_max - y_min, dir=(0.0, 1.0, 0.0)) - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _make_cone_along( - *, - base_radius: float, - top_radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cone( - base_radius, - top_radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _sendcutsend_outside_setback_90_mm() -> float: - return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + PLATE_THICKNESS_MM - - -def _sendcutsend_bend_allowance_90_mm() -> float: - return (2.0 * _sendcutsend_outside_setback_90_mm()) - SENDCUTSEND_BEND_DEDUCTION_90_MM - - -def _validate_sendcutsend_bend_rule() -> None: - bend_deduction_error = abs( - SENDCUTSEND_BEND_DEDUCTION_90_MM - - ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm()) - ) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _cast_shape(obj: object) -> build123d.Shape: - from build123d.topology import downcast - from build123d.topology.shape_core import shapetype - - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _build_bracket_layout() -> BracketLayout: - x_min = min( - BASE_SERVO_FOOTPRINT_X_MIN_MM, - BASE_SERVO_MOUNTED_FOOTPRINT_X_MIN_MM, - min( - x - radius - BASE_PLATE_SERVO_MOUNT_HOLE_EDGE_BUFFER_MM - for x, _z, radius in BASE_SERVO_STANDOFF_MOUNT_HOLES_MM - ), - ) - x_max = BASE_SERVO_FOOTPRINT_X_MAX_MM - if BASE_ATTACHMENT_HOLES_MM: - required_x_max_for_mount_holes = max( - x - + SENDCUTSEND_COUNTERSINK_CENTER_TO_BEND_MIN_MM - + SENDCUTSEND_MOUNT_HOLE_TO_BEND_BUFFER_MM - for x, _z, _radius in BASE_ATTACHMENT_HOLES_MM - ) - x_max = max(x_max, required_x_max_for_mount_holes) - x_max = max(x_max, TOP_PLATE_NECK_START_X_MM + TOP_PLATE_NECK_TO_BEND_MIN_MM) - required_x_min = x_min - x_min = ( - (2.0 * BASE_PLATE_MAIN_SURFACE_CENTER_X_MM) - - TOP_PLATE_NECK_START_X_MM - + BASE_PLATE_LEFT_EDGE_EXTRA_INSET_MM - ) - if x_min > required_x_min: - raise RuntimeError( - "Base plate main-surface centering would clip required geometry: " - f"x_min={x_min:.3f} mm > required_x_min={required_x_min:.3f} mm" - ) - z_center = 0.5 * (SADDLE_BASE_Z_MIN_MM + SADDLE_BASE_Z_MAX_MM) - z_min = z_center - (0.5 * BASE_PLATE_Z_WIDTH_MM) - z_max = z_center + (0.5 * BASE_PLATE_Z_WIDTH_MM) - clamp_z_min = z_center - (0.5 * STANDARD_DESK_CLAMP_CONTACT_WIDTH_MM) - clamp_z_max = z_center + (0.5 * STANDARD_DESK_CLAMP_CONTACT_WIDTH_MM) - y_min = SADDLE_BASE_FACE_Y_MM - PLATE_THICKNESS_MM - y_max = SADDLE_BASE_FACE_Y_MM - return_y_max = y_min - U_BRACKET_CLEARANCE_MM - return_y_min = return_y_max - PLATE_THICKNESS_MM - side_x_max = x_max - side_x_min = side_x_max - SIDE_WALL_THICKNESS_MM - return_x_min = side_x_min - ( - RETURN_FACE_LENGTH_RATIO * (side_x_max - BASE_SERVO_FOOTPRINT_X_MIN_MM) - ) - standoff_under_access_clearance = return_x_min - max( - x for x, _z, _radius in BASE_SERVO_STANDOFF_MOUNT_HOLES_MM - ) - if standoff_under_access_clearance < BASE_PLATE_UNDER_ACCESS_CLEARANCE_MM: - raise RuntimeError( - "Base servo standoff holes are too close to the lower return face for " - "driver access: " - f"{standoff_under_access_clearance:.3f} mm < " - f"{BASE_PLATE_UNDER_ACCESS_CLEARANCE_MM:.3f} mm" - ) - return_mount_holes = ( - ( - 0.5 * (return_x_min + side_x_min), - 0.5 * (clamp_z_min + clamp_z_max), - M8_CLEARANCE_RADIUS_MM, - ), - ) if INCLUDE_RETURN_SURFACE_MOUNT_FEATURES else () - - return BracketLayout( - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - clamp_z_min=clamp_z_min, - clamp_z_max=clamp_z_max, - neck_start_x=TOP_PLATE_NECK_START_X_MM, - width=x_max - x_min, - height=z_max - z_min, - clamp_height=clamp_z_max - clamp_z_min, - return_width=side_x_max - return_x_min, - return_x_min=return_x_min, - return_y_min=return_y_min, - return_y_max=return_y_max, - side_x_min=side_x_min, - side_x_max=side_x_max, - overall_y_min=return_y_min, - face_y=y_max, - attachment_holes=BASE_ATTACHMENT_HOLES_MM, - return_mount_holes=return_mount_holes, - servo_bus_mount_holes_xzr=SERVO_BUS_TOP_MOUNT_HOLES_MM, - ) - - -def _min_servo_bus_hole_center_to_plate_edge(layout: BracketLayout) -> float: - if not SERVO_BUS_TOP_MOUNT_HOLES_MM: - return math.inf - return min( - min(z - layout.z_min, layout.z_max - z) - for _x, z, _radius in SERVO_BUS_TOP_MOUNT_HOLES_MM - ) - - -def _fillet_plan_corners( - plate: build123d.Solid, - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, - include_min_x: bool = True, - include_max_x: bool = True, -) -> build123d.Shape: - matches: list[build123d.Edge] = [] - for edge in plate.edges(): - if _geom_type_name(edge) != "LINE": - continue - bb = edge.bounding_box() - if abs((bb.max.Y - bb.min.Y) - (y_max - y_min)) > 1e-3: - continue - if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Z - bb.min.Z) > 1e-3: - continue - center = edge.center() - if abs(abs(center.X - ((x_min + x_max) * 0.5)) - ((x_max - x_min) * 0.5)) > 1e-3: - continue - if abs(abs(center.Z - ((z_min + z_max) * 0.5)) - ((z_max - z_min) * 0.5)) > 1e-3: - continue - at_min_x = abs(center.X - x_min) <= 1e-3 - at_max_x = abs(center.X - x_max) <= 1e-3 - if at_min_x and not include_min_x: - continue - if at_max_x and not include_max_x: - continue - matches.append(edge) - - expected_count = (2 if include_min_x else 0) + (2 if include_max_x else 0) - if len(matches) != expected_count: - raise RuntimeError(f"Expected {expected_count} plan-view corner edges to fillet, found {len(matches)}") - - mk = BRepFilletAPI_MakeFillet(plate.wrapped) - for edge in matches: - mk.Add(CORNER_RADIUS_MM, edge.wrapped) - mk.Build() - if not mk.IsDone(): - raise RuntimeError("Failed to round U-bracket plate corners") - return _cast_shape(mk.Shape()) - - -def _fillet_vertical_plan_edges( - plate: build123d.Shape, - *, - x: float, - z_values: tuple[float, ...], - y_min: float, - y_max: float, - radius: float, -) -> build123d.Shape: - matches: list[build123d.Edge] = [] - for edge in plate.edges(): - if _geom_type_name(edge) != "LINE": - continue - bb = edge.bounding_box() - if abs((bb.max.Y - bb.min.Y) - (y_max - y_min)) > 1e-3: - continue - if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Z - bb.min.Z) > 1e-3: - continue - center = edge.center() - if abs(center.X - x) > 1e-3: - continue - if not any(abs(center.Z - z) <= 1e-3 for z in z_values): - continue - matches.append(edge) - - if len(matches) != len(z_values): - raise RuntimeError( - f"Expected {len(z_values)} vertical plan edges to fillet, found {len(matches)}" - ) - - mk = BRepFilletAPI_MakeFillet(plate.wrapped) - for edge in matches: - mk.Add(radius, edge.wrapped) - mk.Build() - if not mk.IsDone(): - raise RuntimeError("Failed to round selected vertical plan edges") - return _cast_shape(mk.Shape()) - - -def _fillet_plan_outline_vertices( - plate: build123d.Shape, - *, - points_xz: tuple[tuple[float, float], ...], - y_min: float, - y_max: float, - radius: float, -) -> build123d.Shape: - matches: list[build123d.Edge] = [] - for edge in plate.edges(): - if _geom_type_name(edge) != "LINE": - continue - bb = edge.bounding_box() - if abs((bb.max.Y - bb.min.Y) - (y_max - y_min)) > 1e-3: - continue - if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Z - bb.min.Z) > 1e-3: - continue - center = edge.center() - if not any( - abs(center.X - point_x) <= 1e-3 and abs(center.Z - point_z) <= 1e-3 - for point_x, point_z in points_xz - ): - continue - matches.append(edge) - - if len(matches) != len(points_xz): - raise RuntimeError( - f"Expected {len(points_xz)} outline vertex edges to fillet, found {len(matches)}" - ) - - mk = BRepFilletAPI_MakeFillet(plate.wrapped) - for edge in matches: - mk.Add(radius, edge.wrapped) - mk.Build() - if not mk.IsDone(): - raise RuntimeError("Failed to round top-plate outline") - return _cast_shape(mk.Shape()) - - -def _cut_y_holes( - plate: build123d.Shape, - *, - holes_xzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - hole_depth = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - for x, z, radius in holes_xzr: - if not (x_min + radius <= x <= x_max - radius): - raise RuntimeError(f"Hole at x={x:.3f} mm does not fit within the bracket x span") - if not (z_min + radius <= z <= z_max - radius): - raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span") - plate = plate.cut( - _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(x, y_min - CUT_EXTENSION_MM, z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - return plate - - -def _cut_y_slots( - plate: build123d.Shape, - *, - slots_xzlw: tuple[tuple[float, float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - slot_depth = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - for x, z, length, width in slots_xzlw: - if length <= width: - raise RuntimeError( - f"Slot length must exceed slot width, got {length:.3f} x {width:.3f} mm" - ) - if not (x_min + (0.5 * length) <= x <= x_max - (0.5 * length)): - raise RuntimeError(f"Slot at x={x:.3f} mm does not fit within the bracket x span") - if not (z_min + (0.5 * width) <= z <= z_max - (0.5 * width)): - raise RuntimeError(f"Slot at z={z:.3f} mm does not fit within the bracket z span") - - radius = 0.5 * width - half_straight = 0.5 * (length - width) - plate = plate.cut( - _make_box( - x_min=x - half_straight, - x_max=x + half_straight, - y_min=y_min - CUT_EXTENSION_MM, - y_max=y_min - CUT_EXTENSION_MM + slot_depth, - z_min=z - radius, - z_max=z + radius, - ) - ) - for end_x in (x - half_straight, x + half_straight): - plate = plate.cut( - _make_cylinder_along( - radius=radius, - height=slot_depth, - origin=build123d.Vector(end_x, y_min - CUT_EXTENSION_MM, z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - return plate - - -def _cut_y_oriented_slots( - plate: build123d.Shape, - *, - slots_xzlwr: tuple[tuple[float, float, float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - slot_depth = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - for x, z, length, width, rotation_deg in slots_xzlwr: - if length <= width: - raise RuntimeError( - f"Slot length must exceed slot width, got {length:.3f} x {width:.3f} mm" - ) - angle = math.radians(rotation_deg) - unit_x = math.cos(angle) - unit_z = math.sin(angle) - perp_x = -unit_z - perp_z = unit_x - radius = 0.5 * width - half_straight = 0.5 * (length - width) - half_length = 0.5 * length - projected_half_x = abs(unit_x) * half_length + abs(perp_x) * radius - projected_half_z = abs(unit_z) * half_length + abs(perp_z) * radius - if not (x_min + projected_half_x <= x <= x_max - projected_half_x): - raise RuntimeError(f"Oriented slot at x={x:.3f} mm does not fit within the bracket x span") - if not (z_min + projected_half_z <= z <= z_max - projected_half_z): - raise RuntimeError(f"Oriented slot at z={z:.3f} mm does not fit within the bracket z span") - - rect_points = ( - ( - x - (half_straight * unit_x) - (radius * perp_x), - z - (half_straight * unit_z) - (radius * perp_z), - ), - ( - x + (half_straight * unit_x) - (radius * perp_x), - z + (half_straight * unit_z) - (radius * perp_z), - ), - ( - x + (half_straight * unit_x) + (radius * perp_x), - z + (half_straight * unit_z) + (radius * perp_z), - ), - ( - x - (half_straight * unit_x) + (radius * perp_x), - z - (half_straight * unit_z) + (radius * perp_z), - ), - ) - plate = plate.cut( - _make_xz_outline_solid( - rect_points, - y_min=y_min - CUT_EXTENSION_MM, - y_max=y_min - CUT_EXTENSION_MM + slot_depth, - ) - ) - for sign in (-1.0, 1.0): - plate = plate.cut( - _make_cylinder_along( - radius=radius, - height=slot_depth, - origin=build123d.Vector( - x + (sign * half_straight * unit_x), - y_min - CUT_EXTENSION_MM, - z + (sign * half_straight * unit_z), - ), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - return plate - - -def _cut_x_holes( - bracket: build123d.Shape, - *, - holes_yzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - hole_depth = (x_max - x_min) + (2.0 * CUT_EXTENSION_MM) - for y, z, radius in holes_yzr: - if not (y_min + radius <= y <= y_max - radius): - raise RuntimeError(f"Hole at y={y:.3f} mm does not fit within the bracket y span") - if not (z_min + radius <= z <= z_max - radius): - raise RuntimeError(f"Hole at z={z:.3f} mm does not fit within the bracket z span") - bracket = bracket.cut( - _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(x_min - CUT_EXTENSION_MM, y, z), - direction=build123d.Vector(1.0, 0.0, 0.0), - ) - ) - return bracket - - -def _cut_base_plate_mount_features( - plate: build123d.Shape, - *, - holes_xzr: tuple[tuple[float, float, float], ...], - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Shape: - return _cut_y_holes( - plate, - holes_xzr=holes_xzr, - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - ) - - -def _cut_underside_countersinks( - plate: build123d.Shape, - *, - holes_xzr: tuple[tuple[float, float, float], ...], - y_min: float, - clearance_radius: float, - countersink_major_radius: float, - countersink_depth: float, -) -> build123d.Shape: - for x, z, radius in holes_xzr: - if abs(radius - clearance_radius) > 1e-6: - continue - plate = plate.cut( - _make_cone_along( - base_radius=countersink_major_radius, - top_radius=radius, - height=countersink_depth, - origin=build123d.Vector(x, y_min, z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - ) - return plate - - -def _as_single_solid(shape: build123d.Shape) -> build123d.Solid: - solids = list(shape.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected a single solid, found {len(solids)}") - return solids[0] - - -def _top_plate_outline_points(layout: BracketLayout) -> tuple[tuple[float, float], ...]: - left_flat_x = min( - layout.x_min + BASE_PLATE_OCTAGON_LEFT_CHAMFER_X_MM, - layout.neck_start_x - BASE_PLATE_HUB_SLOT_LENGTH_MM, - ) - left_lower_z = min( - layout.z_min + BASE_PLATE_OCTAGON_LEFT_CHAMFER_Z_MM, - layout.clamp_z_min - BASE_PLATE_HUB_SLOT_WIDTH_MM, - ) - left_upper_z = max( - layout.z_max - BASE_PLATE_OCTAGON_LEFT_CHAMFER_Z_MM, - layout.clamp_z_max + BASE_PLATE_HUB_SLOT_WIDTH_MM, - ) - return ( - (left_flat_x, layout.z_min), - (layout.neck_start_x, layout.z_min), - (layout.neck_start_x, layout.clamp_z_min), - (layout.x_max, layout.clamp_z_min), - (layout.x_max, layout.clamp_z_max), - (layout.neck_start_x, layout.clamp_z_max), - (layout.neck_start_x, layout.z_max), - (left_flat_x, layout.z_max), - (layout.x_min, left_upper_z), - (layout.x_min, left_lower_z), - ) - - -def _top_plate_hub_slot_specs(layout: BracketLayout) -> tuple[tuple[float, float, float, float, float], ...]: - outline_points = _top_plate_outline_points(layout) - side_start_x = outline_points[0][0] - side_end_x = layout.neck_start_x - side_span = side_end_x - side_start_x - side_slot_gap = ( - side_span - (BASE_PLATE_HUB_SIDE_SLOT_COUNT * BASE_PLATE_HUB_SLOT_LENGTH_MM) - ) / (BASE_PLATE_HUB_SIDE_SLOT_COUNT + 1) - if side_slot_gap <= BASE_PLATE_HUB_SLOT_WIDTH_MM: - raise RuntimeError( - "Side case-retention slots cannot be evenly spaced with the current " - f"edge span: {side_span:.3f} mm" - ) - slot_z_abs = (0.5 * layout.height) - BASE_PLATE_HUB_SLOT_EDGE_INSET_MM - side_slot_xs = tuple( - side_start_x - + side_slot_gap - + (0.5 * BASE_PLATE_HUB_SLOT_LENGTH_MM) - + (index * (BASE_PLATE_HUB_SLOT_LENGTH_MM + side_slot_gap)) - for index in range(BASE_PLATE_HUB_SIDE_SLOT_COUNT) - ) - slots: list[tuple[float, float, float, float, float]] = [] - for slot_x in side_slot_xs: - for slot_z in (-slot_z_abs, slot_z_abs): - slots.append( - ( - slot_x, - slot_z, - BASE_PLATE_HUB_SLOT_LENGTH_MM, - BASE_PLATE_HUB_SLOT_WIDTH_MM, - 0.0, - ) - ) - slots.append( - ( - layout.x_min + BASE_PLATE_HUB_END_SLOT_X_INSET_MM, - 0.5 * (layout.z_min + layout.z_max), - BASE_PLATE_HUB_SLOT_LENGTH_MM, - BASE_PLATE_HUB_SLOT_WIDTH_MM, - 90.0, - ) - ) - return tuple(slots) - - -def _make_lower_plate(layout: BracketLayout) -> build123d.Shape: - outline_points = _top_plate_outline_points(layout) - plate = _make_xz_outline_solid( - outline_points, - y_min=layout.y_min, - y_max=layout.y_max, - ) - rounded_outline_points = tuple( - point - for index, point in enumerate(outline_points) - if index not in BASE_PLATE_SHARP_NECK_CORNER_INDICES - ) - plate = _fillet_plan_outline_vertices( - plate, - points_xz=rounded_outline_points, - y_min=layout.y_min, - y_max=layout.y_max, - radius=CORNER_RADIUS_MM, - ) - plate = _cut_base_plate_mount_features( - plate, - holes_xzr=layout.attachment_holes, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _cut_y_oriented_slots( - plate, - slots_xzlwr=_top_plate_hub_slot_specs(layout), - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _cut_underside_countersinks( - plate, - holes_xzr=BASE_SERVO_STANDOFF_MOUNT_HOLES_MM, - y_min=layout.y_min, - clearance_radius=M2_CLEARANCE_RADIUS_MM, - countersink_major_radius=M2_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM, - countersink_depth=M2_UNDERSIDE_COUNTERSINK_DEPTH_MM, - ) - plate = _cut_underside_countersinks( - plate, - holes_xzr=layout.servo_bus_mount_holes_xzr, - y_min=layout.y_min, - clearance_radius=SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM, - countersink_major_radius=M2_5_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM, - countersink_depth=M2_5_UNDERSIDE_COUNTERSINK_DEPTH_MM, - ) - return _as_single_solid(plate) - - -def _make_return_plate(layout: BracketLayout) -> build123d.Shape: - plate = _make_box( - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - ) - plate = _fillet_plan_corners( - plate, - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - include_min_x=True, - include_max_x=False, - ) - plate = _cut_y_holes( - plate, - holes_xzr=layout.return_mount_holes, - x_min=layout.return_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.return_y_max, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - ) - return _as_single_solid(plate) - - -def _make_side_wall(layout: BracketLayout) -> build123d.Shape: - return _make_box( - x_min=layout.side_x_min, - x_max=layout.side_x_max, - y_min=layout.return_y_min, - y_max=layout.y_max, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - ) - - -def _long_z_edges_near( - shape: build123d.Shape, - *, - x: float, - y: float, - z_min: float, - z_max: float, - tolerance: float = 0.08, -) -> build123d.ShapeList: - matches = build123d.ShapeList() - min_length = 0.8 * (z_max - z_min) - for edge in shape.edges(): - if _geom_type_name(edge) != "LINE": - continue - bb = edge.bounding_box() - if (bb.max.X - bb.min.X) > 1e-3 or (bb.max.Y - bb.min.Y) > 1e-3: - continue - if (bb.max.Z - bb.min.Z) < min_length: - continue - center = edge.center() - if abs(center.X - x) > tolerance or abs(center.Y - y) > tolerance: - continue - matches.append(edge) - return matches - - -def _apply_sendcutsend_bend_radii( - bracket: build123d.Shape, - *, - layout: BracketLayout, -) -> build123d.Shape: - if BEND_INNER_RADIUS_MM < 0.0 or BEND_OUTER_RADIUS_MM < 0.0: - raise ValueError("Bend radii must be >= 0") - if BEND_INNER_RADIUS_MM == 0.0 and BEND_OUTER_RADIUS_MM == 0.0: - return bracket - - inner_bend_edges = build123d.ShapeList() - for bend_y in (layout.y_min, layout.return_y_max): - inner_bend_edges.extend( - _long_z_edges_near( - bracket, - x=layout.side_x_min, - y=bend_y, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - ) - ) - if len(inner_bend_edges) != 2: - raise RuntimeError(f"Expected two inside bend edges to fillet, found {len(inner_bend_edges)}") - - if BEND_INNER_RADIUS_MM > 0.0: - bracket = bracket.fillet(BEND_INNER_RADIUS_MM, inner_bend_edges) - - outer_bend_edges = build123d.ShapeList() - for bend_y in (layout.y_max, layout.return_y_min): - outer_bend_edges.extend( - _long_z_edges_near( - bracket, - x=layout.side_x_max, - y=bend_y, - z_min=layout.clamp_z_min, - z_max=layout.clamp_z_max, - tolerance=0.25, - ) - ) - if len(outer_bend_edges) != 2: - raise RuntimeError(f"Expected two outside bend edges to fillet, found {len(outer_bend_edges)}") - - if BEND_OUTER_RADIUS_MM > 0.0: - bracket = bracket.fillet(BEND_OUTER_RADIUS_MM, outer_bend_edges) - - return bracket - - -def build_bracket() -> build123d.Solid: - layout = _build_bracket_layout() - hub_slot_specs = _top_plate_hub_slot_specs(layout) - standoff_under_access_clearance = layout.return_x_min - max( - x for x, _z, _radius in BASE_SERVO_STANDOFF_MOUNT_HOLES_MM - ) - servo_bus_countersink_edge_clearance = _min_servo_bus_hole_center_to_plate_edge(layout) - if servo_bus_countersink_edge_clearance < SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_MIN_MM: - raise RuntimeError( - "Servo bus countersunk holes are too close to the plate edge: " - f"{servo_bus_countersink_edge_clearance:.3f} mm < " - f"{SENDCUTSEND_COUNTERSINK_CENTER_TO_EDGE_MIN_MM:.3f} mm" - ) - - bracket = _make_lower_plate(layout) - bracket = _as_single_solid(bracket.fuse(_make_side_wall(layout))) - bracket = _as_single_solid(bracket.fuse(_make_return_plate(layout))) - bracket = _as_single_solid(_apply_sendcutsend_bend_radii(bracket, layout=layout)) - - print( - f"{DISPLAY_NAME}: " - f"lower_width={layout.width:.3f} mm, " - f"return_width={layout.return_width:.3f} mm, " - f"lower_height={layout.height:.3f} mm, " - f"plate_thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"clearance={U_BRACKET_CLEARANCE_MM:.3f} mm, " - f"overall_y_min={layout.overall_y_min:.4f} mm, " - f"top_surface_holes={len(layout.attachment_holes)}, " - f"bottom_surface_holes={len(layout.return_mount_holes)}, " - f"horn_m3_holes={len(BASE_HORN_CONNECTION_HOLE_SPECS_MM) if INCLUDE_BOTTOM_SURFACE_MOUNT_FEATURES else 0}, " - f"m2_mount_holes={len(BASE_SERVO_M2_HOLE_SPECS_MM) if INCLUDE_BOTTOM_SURFACE_MOUNT_FEATURES else 0}, " - f"m2_standoff_mount_holes={len(BASE_SERVO_STANDOFF_MOUNT_HOLES_MM)}, " - f"m2_standoff_height={SERVO_STANDOFF_HEIGHT_MM:.3f} mm, " - f"m2_plate_clearance_diameter={2.0 * M2_CLEARANCE_RADIUS_MM:.3f} mm, " - f"m2_countersink_major_diameter={2.0 * M2_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM:.3f} mm, " - f"m2_under_access_clearance={standoff_under_access_clearance:.3f} mm, " - f"return_mount_holes={len(layout.return_mount_holes)}, " - f"servo_bus_top_holes={len(layout.servo_bus_mount_holes_xzr)}, " - f"case_retention_slots={len(hub_slot_specs)}x" - f"{BASE_PLATE_HUB_SLOT_LENGTH_MM:.3f}x{BASE_PLATE_HUB_SLOT_WIDTH_MM:.3f} mm, " - f"case_retention_slot_centers={[(round(slot[0], 3), round(slot[1], 3)) for slot in hub_slot_specs]}, " - f"servo_bus_standoff_height={SERVO_BUS_STANDOFF_HEIGHT_MM:.3f} mm, " - f"servo_bus_plate_clearance_diameter={2.0 * SERVO_BUS_PLATE_CLEARANCE_RADIUS_MM:.3f} mm, " - f"servo_bus_countersink_major_diameter={2.0 * M2_5_FLAT_HEAD_COUNTERSINK_MAJOR_RADIUS_MM:.3f} mm, " - f"servo_bus_countersink_center_to_edge={servo_bus_countersink_edge_clearance:.3f} mm, " - f"servo_bus_countersink_center_to_bend_min={SENDCUTSEND_COUNTERSINK_CENTER_TO_BEND_MIN_MM:.3f} mm, " - f"neck_to_bend_min={TOP_PLATE_NECK_TO_BEND_MIN_MM:.3f} mm, " - f"bend_radii_inner_outer=({BEND_INNER_RADIUS_MM:.3f}, {BEND_OUTER_RADIUS_MM:.3f}) mm, " - f"horn_center_x={BASE_SERVO_MOUNTED_HORN_CENTER_X_MM:.3f} mm, " - f"flush_y={layout.face_y:.4f} mm" - ) - bracket.color = GRAY_ALUMINUM_COLOR - return bracket - - -def _build_flat_pattern(layout: BracketLayout) -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = _sendcutsend_outside_setback_90_mm() - bend_allowance = _sendcutsend_bend_allowance_90_mm() - top_flange_outside_length = layout.side_x_max - layout.x_min - return_flange_outside_length = layout.return_width - top_flange_flat_length = top_flange_outside_length - outside_setback - return_flange_flat_length = return_flange_outside_length - outside_setback - min_flange_outside_length = min(top_flange_outside_length, return_flange_outside_length) - min_flange_flat_length = min(top_flange_flat_length, return_flange_flat_length) - if min_flange_outside_length < SENDCUTSEND_MIN_FLANGE_FORMED_MM: - raise RuntimeError( - "Flange length is below the configured bend-rule minimum: " - f"{min_flange_outside_length:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm" - ) - if min_flange_flat_length <= 0.0: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"{min_flange_flat_length:.3f} mm" - ) - - web_outside_length = layout.y_max - layout.return_y_min - web_flat_length = web_outside_length - (2.0 * SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM) - if web_flat_length <= 0.0: - raise RuntimeError( - f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm" - ) - - web_flat_start = top_flange_flat_length + bend_allowance - web_flat_end = web_flat_start + web_flat_length - total_length = web_flat_end + bend_allowance + return_flange_flat_length - flat_width = layout.z_max - layout.z_min - bend_line_positions = ( - top_flange_flat_length + (0.5 * bend_allowance), - web_flat_end + (0.5 * bend_allowance), - ) - - feature_extents: list[tuple[float, float]] = [] - cut_circles: list[tuple[float, float, float]] = [] - cut_slots: list[tuple[float, float, float, float, float]] = [] - for x, z, radius in layout.attachment_holes: - circle = (x - layout.x_min, z - layout.z_min, radius) - cut_circles.append(circle) - feature_extents.append((circle[0], radius)) - for x, z, length, slot_width, rotation_deg in _top_plate_hub_slot_specs(layout): - slot = (x - layout.x_min, z - layout.z_min, length, slot_width, rotation_deg) - cut_slots.append(slot) - feature_extents.append((slot[0], 0.5 * (length if abs(rotation_deg) < 45.0 else slot_width))) - for x, z, radius in layout.return_mount_holes: - circle = (total_length - (x - layout.return_x_min), z - layout.z_min, radius) - cut_circles.append(circle) - feature_extents.append((circle[0], radius)) - - min_feature_to_bend = min( - min(abs(center_u - bend_u) - feature_radius for bend_u in bend_line_positions) - for center_u, feature_radius in feature_extents - ) - - return FlatPattern( - total_length_mm=total_length, - width_mm=flat_width, - top_flange_flat_length_mm=top_flange_flat_length, - return_flange_flat_length_mm=return_flange_flat_length, - web_flat_start_mm=web_flat_start, - web_flat_end_mm=web_flat_end, - bend_line_positions_mm=bend_line_positions, - cut_circles=tuple(cut_circles), - cut_slots=tuple(cut_slots), - rounded_corner_radius_mm=CORNER_RADIUS_MM, - min_feature_to_bend_mm=min_feature_to_bend, - ) - - -def _add_rounded_rect_outline( - msp: ezdxf.layouts.Modelspace, - *, - length: float, - width: float, - radius: float, -) -> None: - if radius <= 0.0: - raise RuntimeError(f"Corner radius must be positive, got {radius:.3f} mm") - if width <= (2.0 * radius): - raise RuntimeError( - "Corner radius is too large for the base clamp flat-pattern width: " - f"{radius:.3f} mm" - ) - if length <= (2.0 * radius): - raise RuntimeError( - "Corner radius is too large for the base clamp flat-pattern length: " - f"{radius:.3f} mm" - ) - - msp.add_line((radius, 0.0), (length - radius, 0.0), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(length - radius, radius), - radius=radius, - start_angle=-90.0, - end_angle=0.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((length, radius), (length, width - radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(length - radius, width - radius), - radius=radius, - start_angle=0.0, - end_angle=90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((length - radius, width), (radius, width), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(radius, width - radius), - radius=radius, - start_angle=90.0, - end_angle=180.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((0.0, width - radius), (0.0, radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(radius, radius), - radius=radius, - start_angle=180.0, - end_angle=270.0, - dxfattribs={"layer": "CUT"}, - ) - - -def _add_horizontal_slot_outline( - msp: ezdxf.layouts.Modelspace, - *, - center_u: float, - center_v: float, - length: float, - width: float, -) -> None: - if length <= width: - raise RuntimeError(f"Slot length must exceed slot width, got {length:.3f} x {width:.3f} mm") - radius = 0.5 * width - half_straight = 0.5 * (length - width) - left_u = center_u - half_straight - right_u = center_u + half_straight - - msp.add_line((left_u, center_v - radius), (right_u, center_v - radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(right_u, center_v), - radius=radius, - start_angle=-90.0, - end_angle=90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((right_u, center_v + radius), (left_u, center_v + radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(left_u, center_v), - radius=radius, - start_angle=90.0, - end_angle=270.0, - dxfattribs={"layer": "CUT"}, - ) - - -def _add_oriented_slot_outline( - msp: ezdxf.layouts.Modelspace, - *, - center_u: float, - center_v: float, - length: float, - width: float, - rotation_deg: float, -) -> None: - if abs(rotation_deg) <= 1e-6: - _add_horizontal_slot_outline( - msp, - center_u=center_u, - center_v=center_v, - length=length, - width=width, - ) - return - - if length <= width: - raise RuntimeError(f"Slot length must exceed slot width, got {length:.3f} x {width:.3f} mm") - angle = math.radians(rotation_deg) - unit_u = math.cos(angle) - unit_v = math.sin(angle) - perp_u = -unit_v - perp_v = unit_u - radius = 0.5 * width - half_straight = 0.5 * (length - width) - start_u = center_u - (half_straight * unit_u) - start_v = center_v - (half_straight * unit_v) - end_u = center_u + (half_straight * unit_u) - end_v = center_v + (half_straight * unit_v) - - line_a_start = (start_u + (radius * perp_u), start_v + (radius * perp_v)) - line_a_end = (end_u + (radius * perp_u), end_v + (radius * perp_v)) - line_b_start = (start_u - (radius * perp_u), start_v - (radius * perp_v)) - line_b_end = (end_u - (radius * perp_u), end_v - (radius * perp_v)) - msp.add_line(line_a_start, line_a_end, dxfattribs={"layer": "CUT"}) - msp.add_line(line_b_end, line_b_start, dxfattribs={"layer": "CUT"}) - - perp_angle = math.degrees(math.atan2(perp_v, perp_u)) - msp.add_arc( - center=(end_u, end_v), - radius=radius, - start_angle=perp_angle, - end_angle=perp_angle + 180.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_arc( - center=(start_u, start_v), - radius=radius, - start_angle=perp_angle + 180.0, - end_angle=perp_angle + 360.0, - dxfattribs={"layer": "CUT"}, - ) - - -def _slot_profile_points( - *, - center_u: float, - center_v: float, - length: float, - width: float, - rotation_deg: float, -) -> list[tuple[float, float]]: - if length <= width: - raise RuntimeError(f"Slot length must exceed slot width, got {length:.3f} x {width:.3f} mm") - from shapely.geometry import LineString - - angle = math.radians(rotation_deg) - half_straight = 0.5 * (length - width) - start = ( - center_u - (half_straight * math.cos(angle)), - center_v - (half_straight * math.sin(angle)), - ) - end = ( - center_u + (half_straight * math.cos(angle)), - center_v + (half_straight * math.sin(angle)), - ) - profile = LineString([start, end]).buffer(0.5 * width, cap_style=1, resolution=16) - points = [(float(x), float(y)) for x, y in profile.exterior.coords] - if points and points[0] == points[-1]: - points.pop() - return points - - -def _strip_cutout_interiors_from_geometry(geometry): - from shapely.geometry import Polygon - from shapely.ops import unary_union - - polygons = [geometry] if geometry.geom_type == "Polygon" else list(geometry.geoms) - stripped = [Polygon(polygon.exterior) for polygon in polygons] - result = unary_union(stripped).buffer(0) - if not result.is_valid: - result = result.buffer(0) - if result.is_empty: - raise RuntimeError("Base clamp topology DXF produced empty exterior geometry") - return result - - -def _add_flat_pattern_service_cutouts( - msp: ezdxf.layouts.Modelspace, - flat_pattern: FlatPattern, -) -> None: - # SendCutSend's secondary-service detector is much happier when round - # service holes are exact circular contours rather than sampled polylines. - # Use arc-bulge LWPOLYLINEs to avoid extra entity types in the upload. - for center_u, center_v, radius in flat_pattern.cut_circles: - dxf_topology.add_circle_polyline( - msp, - (center_u, center_v), - radius, - layer="CUT", - ) - for center_u, center_v, length, width, rotation_deg in flat_pattern.cut_slots: - msp.add_lwpolyline( - _slot_profile_points( - center_u=center_u, - center_v=center_v, - length=length, - width=width, - rotation_deg=rotation_deg, - ), - close=True, - dxfattribs={"layer": "CUT"}, - ) - - -def _flat_u_from_step_x( - x: float, - *, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> float: - if abs(x - layout.side_x_max) <= 1e-3: - return flat_pattern.top_flange_flat_length_mm - return x - layout.x_min - - -def _flat_v_from_step_z(z: float, *, layout: BracketLayout) -> float: - return z - layout.z_min - - -def _flat_point_from_step_vector( - point: build123d.Vector, - *, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> tuple[float, float]: - return ( - _flat_u_from_step_x(point.X, layout=layout, flat_pattern=flat_pattern), - _flat_v_from_step_z(point.Z, layout=layout), - ) - - -def _is_top_bend_edge(edge: build123d.Edge, *, layout: BracketLayout) -> bool: - if _geom_type_name(edge) != "LINE": - return False - bb = edge.bounding_box() - return ( - abs(bb.min.X - layout.side_x_max) <= 1e-3 - and abs(bb.max.X - layout.side_x_max) <= 1e-3 - and abs(bb.min.Z - layout.clamp_z_min) <= 1e-3 - and abs(bb.max.Z - layout.clamp_z_max) <= 1e-3 - ) - - -def _angle_deg(point: tuple[float, float], center: tuple[float, float]) -> float: - return math.degrees(math.atan2(point[1] - center[1], point[0] - center[0])) - - -def _ccw_delta_deg(start: float, end: float) -> float: - return (end - start) % 360.0 - - -def _add_step_projected_edge( - msp: ezdxf.layouts.Modelspace, - edge: build123d.Edge, - *, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> None: - edge_type = _geom_type_name(edge) - vertices = list(edge.vertices()) - if edge_type == "LINE": - if len(vertices) < 2: - raise RuntimeError("STEP line edge is missing vertices for DXF projection") - msp.add_line( - _flat_point_from_step_vector(vertices[0].center(), layout=layout, flat_pattern=flat_pattern), - _flat_point_from_step_vector(vertices[1].center(), layout=layout, flat_pattern=flat_pattern), - dxfattribs={"layer": "CUT"}, - ) - return - - if edge_type != "CIRCLE": - raise RuntimeError(f"Unsupported STEP top-face edge type for DXF projection: {edge_type}") - - center_point = edge.arc_center - center = _flat_point_from_step_vector(center_point, layout=layout, flat_pattern=flat_pattern) - radius = float(edge.radius) - if len(vertices) < 2 or abs(edge.length - (2.0 * math.pi * radius)) <= 1e-3: - msp.add_circle(center, radius, dxfattribs={"layer": "CUT"}) - return - - start = _flat_point_from_step_vector(vertices[0].center(), layout=layout, flat_pattern=flat_pattern) - end = _flat_point_from_step_vector(vertices[1].center(), layout=layout, flat_pattern=flat_pattern) - mid = _flat_point_from_step_vector(edge.center(), layout=layout, flat_pattern=flat_pattern) - start_angle = _angle_deg(start, center) - end_angle = _angle_deg(end, center) - mid_angle = _angle_deg(mid, center) - ccw_span = _ccw_delta_deg(start_angle, end_angle) - mid_span = _ccw_delta_deg(start_angle, mid_angle) - if mid_span > ccw_span + 1e-3: - start_angle, end_angle = end_angle, start_angle - msp.add_arc( - center=center, - radius=radius, - start_angle=start_angle, - end_angle=end_angle, - dxfattribs={"layer": "CUT"}, - ) - - -def _step_top_face(shape: build123d.Shape, *, layout: BracketLayout) -> build123d.Face: - matches = [] - for face in shape.faces(): - bb = face.bounding_box() - if abs(bb.min.Y - layout.y_max) <= 1e-3 and abs(bb.max.Y - layout.y_max) <= 1e-3: - matches.append(face) - if not matches: - raise RuntimeError("Could not find the base clamp top face in the generated STEP file") - return max(matches, key=lambda face: face.area) - - -def _add_step_projected_top_flange( - msp: ezdxf.layouts.Modelspace, - *, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> Path: - step_path = Path(__file__).with_suffix(".step") - if not step_path.exists(): - raise RuntimeError(f"STEP-derived DXF requires the generated STEP file: {step_path}") - shape = build123d.import_step(step_path) - top_face = _step_top_face(shape, layout=layout) - projected_edges = 0 - for edge in top_face.edges(): - if _is_top_bend_edge(edge, layout=layout): - continue - _add_step_projected_edge( - msp, - edge, - layout=layout, - flat_pattern=flat_pattern, - ) - projected_edges += 1 - if projected_edges <= 0: - raise RuntimeError("No STEP top-face edges were projected into the DXF") - return step_path - - -def _add_web_and_return_flat_outline( - msp: ezdxf.layouts.Modelspace, - *, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> None: - start_u = flat_pattern.top_flange_flat_length_mm - end_u = flat_pattern.total_length_mm - min_v = layout.clamp_z_min - layout.z_min - max_v = layout.clamp_z_max - layout.z_min - radius = min(CORNER_RADIUS_MM, 0.5 * (max_v - min_v), flat_pattern.return_flange_flat_length_mm) - - if radius <= 0.0: - msp.add_line((start_u, min_v), (end_u, min_v), dxfattribs={"layer": "CUT"}) - msp.add_line((end_u, min_v), (end_u, max_v), dxfattribs={"layer": "CUT"}) - msp.add_line((end_u, max_v), (start_u, max_v), dxfattribs={"layer": "CUT"}) - return - - msp.add_line((start_u, min_v), (end_u - radius, min_v), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(end_u - radius, min_v + radius), - radius=radius, - start_angle=-90.0, - end_angle=0.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((end_u, min_v + radius), (end_u, max_v - radius), dxfattribs={"layer": "CUT"}) - msp.add_arc( - center=(end_u - radius, max_v - radius), - radius=radius, - start_angle=0.0, - end_angle=90.0, - dxfattribs={"layer": "CUT"}, - ) - msp.add_line((end_u - radius, max_v), (start_u, max_v), dxfattribs={"layer": "CUT"}) - - -def _topology_flat_pattern_dxf( - bracket: build123d.Shape, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - return_start_u = flat_pattern.total_length_mm - flat_pattern.return_flange_flat_length_mm - web_source_span = layout.y_max - layout.return_y_min - web_flat_span = return_start_u - flat_pattern.top_flange_flat_length_mm - if web_source_span <= 0.0 or web_flat_span <= 0.0: - raise RuntimeError("Base clamp topology DXF has invalid web span") - - def project_top(point: build123d.Vector) -> tuple[float, float]: - return ( - _flat_u_from_step_x(point.X, layout=layout, flat_pattern=flat_pattern), - _flat_v_from_step_z(point.Z, layout=layout), - ) - - def project_web(point: build123d.Vector) -> tuple[float, float]: - t = (layout.y_max - point.Y) / web_source_span - return ( - flat_pattern.top_flange_flat_length_mm + (t * web_flat_span), - _flat_v_from_step_z(point.Z, layout=layout), - ) - - def project_return(point: build123d.Vector) -> tuple[float, float]: - if abs(point.X - layout.side_x_max) <= 1e-3: - u = return_start_u - else: - u = flat_pattern.total_length_mm - (point.X - layout.return_x_min) - return (u, _flat_v_from_step_z(point.Z, layout=layout)) - - top_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=1.0, - coordinate_axis="y", - coordinate=layout.y_max, - ) - web_faces = dxf_topology.planar_faces( - bracket, - normal_axis="x", - normal_sign=1.0, - coordinate_axis="x", - coordinate=layout.side_x_max, - ) - return_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=-1.0, - coordinate_axis="y", - coordinate=layout.return_y_min, - ) - geometry = dxf_topology.union_projected_faces( - ( - (top_faces, project_top), - (web_faces, project_web), - (return_faces, project_return), - ) - ) - geometry = _strip_cutout_interiors_from_geometry(geometry) - dxf_topology.add_shapely_geometry(doc.modelspace(), geometry, layer="CUT") - _add_flat_pattern_service_cutouts(doc.modelspace(), flat_pattern) - - for bend_line_u in flat_pattern.bend_line_positions_mm: - doc.modelspace().add_line( - (bend_line_u, layout.clamp_z_min - layout.z_min), - (bend_line_u, layout.clamp_z_max - layout.z_min), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def build_dxf() -> ezdxf.document.Drawing: - layout = _build_bracket_layout() - flat_pattern = _build_flat_pattern(layout) - doc = _topology_flat_pattern_dxf(build_bracket(), layout, flat_pattern) - - print( - f"{DISPLAY_NAME} DXF: " - f"stock_thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"flat_length={flat_pattern.total_length_mm:.3f} mm, " - f"flat_width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - "derived_from_topology=build_bracket()" - ) - print( - "DXF bend checks " - f"top_flange_flat={flat_pattern.top_flange_flat_length_mm:.3f} mm, " - f"return_flange_flat={flat_pattern.return_flange_flat_length_mm:.3f} mm, " - f"feature_to_bend_min={flat_pattern.min_feature_to_bend_mm:.3f} mm, " - f"feature_to_bend_required={SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - if flat_pattern.min_feature_to_bend_mm < SENDCUTSEND_HALF_DIE_WIDTH_MM: - print( - "Warning: flat-pattern features enter the bend die region by " - f"{SENDCUTSEND_HALF_DIE_WIDTH_MM - flat_pattern.min_feature_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_bracket(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.step deleted file mode 100644 index af35cbbd..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:874d91d43ae4d86773930300153926504b186d3707e70c24bbd14f52a62fc26c -size 244162 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.py deleted file mode 100644 index d9094a28..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.py +++ /dev/null @@ -1,180 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -ASSEMBLY_DIR = Path(__file__).resolve().parent -if str(ASSEMBLY_DIR) not in sys.path: - sys.path.insert(0, str(ASSEMBLY_DIR)) - -import base_clamp - - -def multiply_transforms( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> list[float]: - return [ - sum(float(left[row * 4 + inner]) * float(right[inner * 4 + col]) for inner in range(4)) - for row in range(4) - for col in range(4) - ] - - -BASE_CLAMP_LAYOUT = base_clamp._build_bracket_layout() -STS3250_BASE_PLATE_FACE_LOCAL_Y_MM = -27.4 -STS3250_UNDERSIDE_M2_MOUNT_FACE_LOCAL_Y_MM = -25.6 -STS3250_BASE_PLATE_CLEARANCE_MM = base_clamp.SERVO_STANDOFF_HEIGHT_MM - ( - STS3250_UNDERSIDE_M2_MOUNT_FACE_LOCAL_Y_MM - STS3250_BASE_PLATE_FACE_LOCAL_Y_MM -) -STS3250_END_FOR_END_PIVOT_LOCAL_X_MM = base_clamp.BASE_SERVO_END_FOR_END_PIVOT_LOCAL_X_MM -STS3250_END_FOR_END_PIVOT_LOCAL_Z_MM = base_clamp.BASE_SERVO_END_FOR_END_PIVOT_LOCAL_Z_MM -STS3250_END_FOR_END_ROTATION_TRANSFORM = [ - -1.0, - 0.0, - 0.0, - (2.0 * STS3250_END_FOR_END_PIVOT_LOCAL_X_MM) - + base_clamp.SERVO_MOUNT_X_SHIFT_FOR_UNDER_ACCESS_MM, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, -1.0, 2.0 * STS3250_END_FOR_END_PIVOT_LOCAL_Z_MM, - 0.0, 0.0, 0.0, 1.0, -] -STS3250_BASE_PLATE_TO_CLAMP_FACE_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, - 1.0, - 0.0, - base_clamp.SADDLE_BASE_FACE_Y_MM - + STS3250_BASE_PLATE_CLEARANCE_MM - - STS3250_BASE_PLATE_FACE_LOCAL_Y_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] -STS3250_TO_BASE_CLAMP_TRANSFORM = multiply_transforms( - STS3250_BASE_PLATE_TO_CLAMP_FACE_TRANSFORM, - STS3250_END_FOR_END_ROTATION_TRANSFORM, -) -BASE_CLAMP_F12_TO_FLOOR_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, -1.0, 0.0, - 0.0, 1.0, 0.0, -BASE_CLAMP_LAYOUT.overall_y_min, - 0.0, 0.0, 0.0, 1.0, -] -STS3250_FLOOR_TRANSFORM = multiply_transforms( - BASE_CLAMP_F12_TO_FLOOR_TRANSFORM, - STS3250_TO_BASE_CLAMP_TRANSFORM, -) -BASE_CLAMP_TOP_FACE_WORLD_Z_MM = base_clamp.SADDLE_BASE_FACE_Y_MM - BASE_CLAMP_LAYOUT.overall_y_min -M2_STANDOFF_FLOOR_TRANSFORMS = tuple( - [ - 1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, -z, - 0.0, 0.0, 1.0, BASE_CLAMP_TOP_FACE_WORLD_Z_MM, - 0.0, 0.0, 0.0, 1.0, - ] - for x, z, _radius in base_clamp.BASE_SERVO_STANDOFF_MOUNT_HOLES_MM -) -M2_5_BUS_STANDOFF_FLOOR_TRANSFORMS = tuple( - [ - 1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, -z, - 0.0, 0.0, 1.0, BASE_CLAMP_TOP_FACE_WORLD_Z_MM, - 0.0, 0.0, 0.0, 1.0, - ] - for x, z, _radius in base_clamp.SERVO_BUS_TOP_MOUNT_HOLES_MM -) - -WAVESHARE_BUS_PCB_BOTTOM_LOCAL_Z_MM = -1.599999 -WAVESHARE_BUS_LOWEST_LOCAL_Z_MM = -11.499994 -WAVESHARE_BUS_UNDERSIDE_CLEARANCE_MM = base_clamp.SERVO_BUS_STANDOFF_HEIGHT_MM - ( - WAVESHARE_BUS_PCB_BOTTOM_LOCAL_Z_MM - WAVESHARE_BUS_LOWEST_LOCAL_Z_MM -) -WAVESHARE_BUS_MOUNT_CENTER_LOCAL_X_MM = base_clamp.SERVO_BUS_MOUNT_LOCAL_X_CENTER_MM -WAVESHARE_BUS_MOUNT_CENTER_LOCAL_Y_MM = base_clamp.SERVO_BUS_MOUNT_LOCAL_Y_CENTER_MM -WAVESHARE_BUS_UNFLIPPED_WORLD_X_MM = base_clamp.SERVO_BUS_TOP_TO_BASE_CLAMP_X_OFFSET_MM -WAVESHARE_BUS_UNFLIPPED_WORLD_Y_MM = -base_clamp.SERVO_BUS_TOP_TO_BASE_CLAMP_Z_OFFSET_MM - -WAVESHARE_BUS_FLOOR_TRANSFORM = [ - 0.0, - -1.0, - 0.0, - WAVESHARE_BUS_UNFLIPPED_WORLD_X_MM - + (2.0 * WAVESHARE_BUS_MOUNT_CENTER_LOCAL_Y_MM), - 1.0, - 0.0, - 0.0, - WAVESHARE_BUS_UNFLIPPED_WORLD_Y_MM - - (2.0 * WAVESHARE_BUS_MOUNT_CENTER_LOCAL_X_MM), - 0.0, - 0.0, - 1.0, - BASE_CLAMP_TOP_FACE_WORLD_Z_MM - + base_clamp.SERVO_BUS_STANDOFF_HEIGHT_MM - - WAVESHARE_BUS_PCB_BOTTOM_LOCAL_Z_MM, - 0.0, - 0.0, - 0.0, - 1.0, -] - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "sts3250.step", - "name": "sts3250", - "transform": STS3250_FLOOR_TRANSFORM, - }, - *( - { - "path": "m2_spacer_5mm.step", - "name": f"m2_spacer_{index}", - "transform": transform, - "use_source_colors": True, - } - for index, transform in enumerate(M2_STANDOFF_FLOOR_TRANSFORMS, start=1) - ), - { - "path": "base_clamp.step", - "name": "base_clamp", - "transform": BASE_CLAMP_F12_TO_FLOOR_TRANSFORM, - }, - *( - { - "path": "m2_5_hex_spacer_6mm.step", - "name": f"m2_5_bus_spacer_{index}", - "transform": transform, - "use_source_colors": True, - } - for index, transform in enumerate(M2_5_BUS_STANDOFF_FLOOR_TRANSFORMS, start=1) - ), - { - "path": "waveshare_bus_servo_adapter_a.step", - "name": "waveshare_bus_servo_adapter_a", - "transform": WAVESHARE_BUS_FLOOR_TRANSFORM, - "use_source_colors": True, - }, - ], - "assembly_mates": [ - { - "sourceLabel": "base_servo_to_base_clamp", - "type": "rigid", - "relation": "rigid", - "fixed": "base_clamp:top_servo_face", - "moving": "sts3250:base_plate_face", - "parameters": { - "base_plate_clearance_mm": STS3250_BASE_PLATE_CLEARANCE_MM, - "underside_m2_mount_face_clearance_mm": base_clamp.SERVO_STANDOFF_HEIGHT_MM, - }, - "fixedEndpoint": { - "part": "base_clamp", - "frame": "top_servo_face", - }, - "movingEndpoint": { - "part": "sts3250", - "frame": "base_plate_face", - }, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.step deleted file mode 100644 index 95b7c7aa..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_clamp_assembly.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eaa05fe5afaac7ed8c41dfbe72822f9e05720bf7c4c3ef358ae4576caff2787b -size 29209846 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.py deleted file mode 100644 index 2f38dcfd..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -"""Generate a lightweight printed shell that keys into the base clamp slots.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import build123d - -ASSEMBLY_DIR = Path(__file__).resolve().parent -if str(ASSEMBLY_DIR) not in sys.path: - sys.path.insert(0, str(ASSEMBLY_DIR)) - -import base_clamp - - -DISPLAY_NAME = "Base Hub Shell" -PLASTIC_SHELL_COLOR = build123d.Color(0.18, 0.42, 0.78, 0.34) -PLASTIC_SHELL_EDGE_INSET_MM = 3.0 -PLASTIC_SHELL_WALL_MM = 2.4 -PLASTIC_SHELL_HEIGHT_MM = 18.0 -PLASTIC_SHELL_BOTTOM_LIFT_MM = 0.15 -PLASTIC_SHELL_LEFT_CHAMFER_X_MM = 13.0 -PLASTIC_SHELL_LEFT_CHAMFER_Z_MM = 10.0 -PLASTIC_SHELL_TAB_CLEARANCE_MM = 0.65 -PLASTIC_SHELL_TAB_LENGTH_CLEARANCE_MM = 4.0 -PLASTIC_SHELL_TAB_DROP_BELOW_PLATE_MM = 0.4 -PLASTIC_SHELL_TAB_RISE_ABOVE_PLATE_MM = 1.4 -PLASTIC_SHELL_RIGHT_SERVICE_OPENING_EXTRA_MM = 0.5 - - -def _shell_outline_points( - layout: base_clamp.BracketLayout, - *, - inset: float, -) -> tuple[tuple[float, float], ...]: - x_min = layout.x_min + PLASTIC_SHELL_EDGE_INSET_MM + inset - x_max = layout.neck_start_x - PLASTIC_SHELL_EDGE_INSET_MM - inset - z_min = layout.z_min + PLASTIC_SHELL_EDGE_INSET_MM + inset - z_max = layout.z_max - PLASTIC_SHELL_EDGE_INSET_MM - inset - chamfer_x = x_min + max(PLASTIC_SHELL_LEFT_CHAMFER_X_MM - inset, PLASTIC_SHELL_WALL_MM) - lower_left_z = z_min + max(PLASTIC_SHELL_LEFT_CHAMFER_Z_MM - inset, PLASTIC_SHELL_WALL_MM) - upper_left_z = z_max - max(PLASTIC_SHELL_LEFT_CHAMFER_Z_MM - inset, PLASTIC_SHELL_WALL_MM) - if x_max <= chamfer_x or upper_left_z <= lower_left_z: - raise RuntimeError("Plastic shell outline collapsed; reduce shell inset or wall thickness") - return ( - (chamfer_x, z_min), - (x_max, z_min), - (x_max, z_max), - (chamfer_x, z_max), - (x_min, upper_left_z), - (x_min, lower_left_z), - ) - - -def _make_oriented_tab( - *, - x: float, - z: float, - length: float, - width: float, - rotation_deg: float, - y_min: float, - y_max: float, -) -> build123d.Shape: - import math - - angle = math.radians(rotation_deg) - unit_x = math.cos(angle) - unit_z = math.sin(angle) - perp_x = -unit_z - perp_z = unit_x - half_length = 0.5 * length - half_width = 0.5 * width - points = ( - ( - x - (half_length * unit_x) - (half_width * perp_x), - z - (half_length * unit_z) - (half_width * perp_z), - ), - ( - x + (half_length * unit_x) - (half_width * perp_x), - z + (half_length * unit_z) - (half_width * perp_z), - ), - ( - x + (half_length * unit_x) + (half_width * perp_x), - z + (half_length * unit_z) + (half_width * perp_z), - ), - ( - x - (half_length * unit_x) + (half_width * perp_x), - z - (half_length * unit_z) + (half_width * perp_z), - ), - ) - return base_clamp._make_xz_outline_solid(points, y_min=y_min, y_max=y_max) - - -def build_shell() -> build123d.Solid: - layout = base_clamp._build_bracket_layout() - y_min = layout.y_max + PLASTIC_SHELL_BOTTOM_LIFT_MM - y_max = y_min + PLASTIC_SHELL_HEIGHT_MM - outer = base_clamp._make_xz_outline_solid( - _shell_outline_points(layout, inset=0.0), - y_min=y_min, - y_max=y_max, - ) - inner = base_clamp._make_xz_outline_solid( - _shell_outline_points(layout, inset=PLASTIC_SHELL_WALL_MM), - y_min=y_min - base_clamp.CUT_EXTENSION_MM, - y_max=y_max + base_clamp.CUT_EXTENSION_MM, - ) - shell = outer.cut(inner) - shell_x_max = layout.neck_start_x - PLASTIC_SHELL_EDGE_INSET_MM - shell = shell.cut( - base_clamp._make_box( - x_min=shell_x_max - - PLASTIC_SHELL_WALL_MM - - PLASTIC_SHELL_RIGHT_SERVICE_OPENING_EXTRA_MM, - x_max=shell_x_max + PLASTIC_SHELL_RIGHT_SERVICE_OPENING_EXTRA_MM, - y_min=y_min - base_clamp.CUT_EXTENSION_MM, - y_max=y_max + base_clamp.CUT_EXTENSION_MM, - z_min=layout.z_min, - z_max=layout.z_max, - ) - ) - - tab_length = base_clamp.BASE_PLATE_HUB_SLOT_LENGTH_MM - PLASTIC_SHELL_TAB_LENGTH_CLEARANCE_MM - tab_width = base_clamp.BASE_PLATE_HUB_SLOT_WIDTH_MM - PLASTIC_SHELL_TAB_CLEARANCE_MM - if tab_length <= tab_width: - raise RuntimeError( - f"Plastic shell tab is too short: {tab_length:.3f} x {tab_width:.3f} mm" - ) - for x, z, _slot_length, _slot_width, rotation_deg in base_clamp._top_plate_hub_slot_specs(layout): - shell = shell.fuse( - _make_oriented_tab( - x=x, - z=z, - length=tab_length, - width=tab_width, - rotation_deg=rotation_deg, - y_min=layout.y_min - PLASTIC_SHELL_TAB_DROP_BELOW_PLATE_MM, - y_max=layout.y_max + PLASTIC_SHELL_TAB_RISE_ABOVE_PLATE_MM, - ) - ) - - shell = base_clamp._as_single_solid(shell) - shell.color = PLASTIC_SHELL_COLOR - print( - f"{DISPLAY_NAME}: " - f"wall={PLASTIC_SHELL_WALL_MM:.3f} mm, " - f"height={PLASTIC_SHELL_HEIGHT_MM:.3f} mm, " - f"right_service_opening_start_x={shell_x_max - PLASTIC_SHELL_WALL_MM - PLASTIC_SHELL_RIGHT_SERVICE_OPENING_EXTRA_MM:.3f} mm, " - f"tabs={len(base_clamp._top_plate_hub_slot_specs(layout))}, " - f"tab_size={tab_length:.3f}x{tab_width:.3f} mm" - ) - return shell - - -def gen_step() -> dict[str, object]: - return { - "shape": build_shell(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.step deleted file mode 100644 index 1cffbc9d..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/base_hub_shell.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5506dc08316ac6b1b949b5ea1bfae79586069b56c076f6adfd87ec6a6b47796e -size 165630 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.py deleted file mode 100644 index e1dfddbe..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -""" -6 mm M2.5 hex spacer for the Waveshare bus board on the tom v2 base clamp. - -The simplified body represents an off-the-shelf female-female hex spacer. It is -authored upright on +Z so the assembly can place it between the clamp plate and -the Waveshare board mounting holes. -""" - -from __future__ import annotations - -import math -import sys -from pathlib import Path - -ASSEMBLY_DIR = Path(__file__).resolve().parent -if str(ASSEMBLY_DIR) not in sys.path: - sys.path.insert(0, str(ASSEMBLY_DIR)) - -import build123d - -import base_clamp - - -PART_NAME = Path(__file__).stem -M2_5_THREAD_MINOR_RADIUS_MM = 0.5 * 2.05 -THREAD_ENTRY_RADIUS_MM = 1.35 -THREAD_ENTRY_DEPTH_MM = 0.7 -CUT_EXTENSION_MM = 0.5 -BRASS_COLOR = build123d.Color(0.82, 0.63, 0.31, 1.0) - - -def _make_z_cylinder(*, radius: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - z_max - z_min, - plane=build123d.Plane( - origin=build123d.Vector(0.0, 0.0, z_min), - z_dir=build123d.Vector(0.0, 0.0, 1.0), - ), - ) - - -def _make_hex_prism(*, across_flats: float, height: float) -> build123d.Shape: - circumradius = across_flats / math.sqrt(3.0) - with build123d.BuildPart() as spacer: - with build123d.BuildSketch(): - build123d.RegularPolygon(circumradius, 6, rotation=30.0) - build123d.extrude(amount=height) - return spacer.part - - -def build_step() -> build123d.Shape: - height = base_clamp.SERVO_BUS_STANDOFF_HEIGHT_MM - across_flats = base_clamp.SERVO_BUS_STANDOFF_HEX_ACROSS_FLATS_MM - if height <= 0.0: - raise ValueError("SERVO_BUS_STANDOFF_HEIGHT_MM must be positive") - if across_flats <= (2.0 * THREAD_ENTRY_RADIUS_MM): - raise ValueError("M2.5 spacer hex body is too small for the thread entry") - - body = _make_hex_prism(across_flats=across_flats, height=height) - body = body.cut( - _make_z_cylinder( - radius=M2_5_THREAD_MINOR_RADIUS_MM, - z_min=-CUT_EXTENSION_MM, - z_max=height + CUT_EXTENSION_MM, - ) - ) - for z_min in (0.0, height - THREAD_ENTRY_DEPTH_MM): - body = body.cut( - _make_z_cylinder( - radius=THREAD_ENTRY_RADIUS_MM, - z_min=z_min - CUT_EXTENSION_MM, - z_max=z_min + THREAD_ENTRY_DEPTH_MM + CUT_EXTENSION_MM, - ) - ) - - solids = list(body.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected one M2.5 hex spacer solid, found {len(solids)}") - - spacer = solids[0] - spacer.label = PART_NAME - spacer.color = BRASS_COLOR - print( - "M2.5 bus hex spacer " - f"height={height:.3f} mm, " - f"across_flats={across_flats:.3f} mm, " - f"thread_minor_diameter={2.0 * M2_5_THREAD_MINOR_RADIUS_MM:.3f} mm" - ) - return spacer - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.step deleted file mode 100644 index 2f29bf9e..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_hex_spacer_6mm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:295a78211fd019033166f85dd5649d46e646ad7e1460534e9eba5539292d740f -size 36506 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_spacer_12mm.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_spacer_12mm.step deleted file mode 100644 index 06365a7b..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_5_spacer_12mm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cf0328d98df31401bfc222ee3719117b96b48efc87e7754f2548d8fad1a18337 -size 18508 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_3mm.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_3mm.step deleted file mode 100644 index df28c048..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_3mm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0c3323e3cc58ede93dfcd4d7110abef5c0eb8bcee6c71fc70d2e34dab904b72 -size 18466 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.py deleted file mode 100644 index 043af6a2..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env python3 -""" -M2 spacer for the tom v2 base clamp servo mount. - -The simplified body represents an off-the-shelf M2 PCB/servo spacer. It is -authored upright on +Z so the assembly can drop it directly between the base -plate and the selected underside mount face on the servo. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -ASSEMBLY_DIR = Path(__file__).resolve().parent -if str(ASSEMBLY_DIR) not in sys.path: - sys.path.insert(0, str(ASSEMBLY_DIR)) - -import build123d - -import base_clamp - - -PART_NAME = Path(__file__).stem -M2_THREAD_MINOR_RADIUS_MM = 0.8 -THREAD_ENTRY_RADIUS_MM = 1.05 -THREAD_ENTRY_DEPTH_MM = 0.6 -CUT_EXTENSION_MM = 0.5 -BRASS_COLOR = build123d.Color(0.82, 0.63, 0.31, 1.0) - - -def _make_z_cylinder(*, radius: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - z_max - z_min, - plane=build123d.Plane( - origin=build123d.Vector(0.0, 0.0, z_min), - z_dir=build123d.Vector(0.0, 0.0, 1.0), - ), - ) - - -def build_step() -> build123d.Shape: - height = base_clamp.SERVO_STANDOFF_HEIGHT_MM - outer_radius = 0.5 * base_clamp.SERVO_STANDOFF_BODY_DIAMETER_MM - if height <= 0.0: - raise ValueError("SERVO_STANDOFF_HEIGHT_MM must be positive") - if outer_radius <= THREAD_ENTRY_RADIUS_MM: - raise ValueError("M2 spacer outer diameter is too small for the thread entry") - - body: build123d.Shape = _make_z_cylinder(radius=outer_radius, z_min=0.0, z_max=height) - body = body.cut( - _make_z_cylinder( - radius=M2_THREAD_MINOR_RADIUS_MM, - z_min=-CUT_EXTENSION_MM, - z_max=height + CUT_EXTENSION_MM, - ) - ) - for z_min in (0.0, height - THREAD_ENTRY_DEPTH_MM): - body = body.cut( - _make_z_cylinder( - radius=THREAD_ENTRY_RADIUS_MM, - z_min=z_min - CUT_EXTENSION_MM, - z_max=z_min + THREAD_ENTRY_DEPTH_MM + CUT_EXTENSION_MM, - ) - ) - - solids = list(body.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected one M2 spacer solid, found {len(solids)}") - - spacer = solids[0] - spacer.label = PART_NAME - spacer.color = BRASS_COLOR - print( - "M2 spacer " - f"height={height:.3f} mm, " - f"outer_diameter={base_clamp.SERVO_STANDOFF_BODY_DIAMETER_MM:.3f} mm, " - f"thread_minor_diameter={2.0 * M2_THREAD_MINOR_RADIUS_MM:.3f} mm" - ) - return spacer - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.step deleted file mode 100644 index 9fc7e35f..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/m2_spacer_5mm.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d8d0f83d14a8d863c85fea892f46b8799db5d3cc8e4fd9f0385711792b74fbfd -size 18466 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/sts3250.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/sts3250.step deleted file mode 100644 index 746b3e65..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5358f001dbdb3a059d625b79f3e0272a4f93dfb7f1e2a50d07803b709a337d29 -size 2299283 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_servo_adapter_a.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_servo_adapter_a.step deleted file mode 100644 index 593f0b94..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_servo_adapter_a.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b04c6802fe661c3f3f2ed02c4decdb2f557bc0d8a85376b6ad6c38db2bb667f -size 26606280 diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.py b/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.py deleted file mode 100644 index 81b903da..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -""" -Stripped Waveshare bus board envelope for base-clamp fit checks. - -This intentionally models only the PCB outline and the four M2.5 mounting -holes, leaving connectors and electronics out so the clamp assembly stays easy -to inspect. -""" - -from __future__ import annotations - -from pathlib import Path - -import build123d - - -PART_NAME = Path(__file__).stem -BOARD_X_MIN_MM = -0.390013 -BOARD_X_MAX_MM = 42.401989 -BOARD_Y_MIN_MM = -0.012095 -BOARD_Y_MAX_MM = 33.012075 -BOARD_THICKNESS_MM = 1.6 -MOUNT_HOLE_RADIUS_MM = 1.25 -CUT_EXTENSION_MM = 0.5 -BOARD_COLOR = build123d.Color(0.08, 0.24, 0.2, 1.0) - -MOUNT_HOLE_CENTERS_XY_MM: tuple[tuple[float, float], ...] = ( - (2.4930065, 2.4999875), - (2.4930065, 30.4999925), - (39.493009, 2.4999875), - (39.505008000000004, 30.4999925), -) - - -def _make_z_cylinder(*, radius: float, x: float, y: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - z_max - z_min, - plane=build123d.Plane( - origin=build123d.Vector(x, y, z_min), - z_dir=build123d.Vector(0.0, 0.0, 1.0), - ), - ) - - -def build_step() -> build123d.Solid: - board: build123d.Shape = build123d.Solid.make_box( - BOARD_X_MAX_MM - BOARD_X_MIN_MM, - BOARD_Y_MAX_MM - BOARD_Y_MIN_MM, - BOARD_THICKNESS_MM, - plane=build123d.Plane( - origin=build123d.Vector(BOARD_X_MIN_MM, BOARD_Y_MIN_MM, 0.0), - z_dir=build123d.Vector(0.0, 0.0, 1.0), - ), - ) - - for x, y in MOUNT_HOLE_CENTERS_XY_MM: - board = board.cut( - _make_z_cylinder( - radius=MOUNT_HOLE_RADIUS_MM, - x=x, - y=y, - z_min=-CUT_EXTENSION_MM, - z_max=BOARD_THICKNESS_MM + CUT_EXTENSION_MM, - ) - ) - - solids = list(board.solids()) - if len(solids) != 1: - raise RuntimeError(f"Expected one stripped bus solid, found {len(solids)}") - - stripped_bus = solids[0] - stripped_bus.label = PART_NAME - stripped_bus.color = BOARD_COLOR - print( - "Waveshare bus stripped " - f"size={BOARD_X_MAX_MM - BOARD_X_MIN_MM:.3f}x" - f"{BOARD_Y_MAX_MM - BOARD_Y_MIN_MM:.3f}x{BOARD_THICKNESS_MM:.3f} mm, " - f"mount_holes={len(MOUNT_HOLE_CENTERS_XY_MM)}, " - f"hole_diameter={2.0 * MOUNT_HOLE_RADIUS_MM:.3f} mm" - ) - return stripped_bus - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.step b/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.step deleted file mode 100644 index 55b92123..00000000 --- a/models/robots/tom/v2/assemblies/base_clamp_assembly/waveshare_bus_stripped.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:badec954c6e7abb91432ca5e4244a7b5fb91a703436c0e0d295221c27f69e833 -size 32161 diff --git a/models/robots/tom/v2/assemblies/base_link.py b/models/robots/tom/v2/assemblies/base_link.py deleted file mode 100644 index ec2ff551..00000000 --- a/models/robots/tom/v2/assemblies/base_link.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import annotations - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.19]" - -BASE_PLATE_TOP_Y_MM = -29.8002 -BASE_PLATE_THICKNESS_MM = 25.4 * 0.080 -BASE_PLATE_BOTTOM_Y_MM = BASE_PLATE_TOP_Y_MM - BASE_PLATE_THICKNESS_MM -BASE_LINK_FLOOR_Y_OFFSET_MM = -BASE_PLATE_BOTTOM_Y_MM -STS3250_BASE_PLATE_FACE_LOCAL_Y_MM = -27.4 -STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM = ( - BASE_PLATE_TOP_Y_MM - - STS3250_BASE_PLATE_FACE_LOCAL_Y_MM -) - -STS3250_BASE_PLATE_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, BASE_LINK_FLOOR_Y_OFFSET_MM + STS3250_BASE_PLATE_FLUSH_Y_OFFSET_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - -BASE_PLATE_TRANSFORM = [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, BASE_LINK_FLOOR_Y_OFFSET_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_3", - "transform": STS3250_BASE_PLATE_TRANSFORM, - }, - { - "path": "../parts/base_plate.step", - "name": "base_plate", - "transform": BASE_PLATE_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/base_link.step b/models/robots/tom/v2/assemblies/base_link.step deleted file mode 100644 index c6acaffe..00000000 --- a/models/robots/tom/v2/assemblies/base_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f487fab31bf8e01de92d69365935b406d01c4ffc243d0919bdcb773e6da0001a -size 2672431 diff --git a/models/robots/tom/v2/assemblies/link_assembly.py b/models/robots/tom/v2/assemblies/link_assembly.py deleted file mode 100644 index 0652f2b0..00000000 --- a/models/robots/tom/v2/assemblies/link_assembly.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -""" -Verification assembly for the tom v2 link bracket pair. - -Both servos sit exactly as in the case-mount variation. One two-bend wrap -plate connects them along the +Y side; a mirrored instance of the same -plate connects the -Y side. - -Usage: - python v2/assemblies/link_assembly.py -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -ASSEMBLIES_DIR = Path(__file__).resolve().parent -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR, ASSEMBLIES_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc -import link_bracket -import pitch_link_sts3250 -import servo_horn_yoke - - -YOKE_HORN_SPAN_CENTER_LOCAL_Y_MM = -9.1 -IDENTITY_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - -MIRROR_Y_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - - -YOKE_ON_SERVO_HORNS_TRANSFORM = tuple( - servo_horn_yoke.STANDALONE_YOKE_ON_SERVO_HORNS_TRANSFORM -) - - -def _matrix(transform: list[float] | tuple[float, ...]) -> list[list[float]]: - return [list(transform[index : index + 4]) for index in range(0, 16, 4)] - - -def _flatten(matrix: list[list[float]]) -> list[float]: - return [value for row in matrix for value in row] - - -def _matmul( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> list[float]: - left_matrix = _matrix(left) - right_matrix = _matrix(right) - return _flatten( - [ - [ - sum(left_matrix[row][inner] * right_matrix[inner][col] for inner in range(4)) - for col in range(4) - ] - for row in range(4) - ] - ) - - -def _invert_rigid_transform(transform: list[float] | tuple[float, ...]) -> list[float]: - matrix = _matrix(transform) - rotation = [row[:3] for row in matrix[:3]] - translation = [matrix[row][3] for row in range(3)] - inverse_rotation = [[rotation[col][row] for col in range(3)] for row in range(3)] - inverse_translation = [ - -sum(inverse_rotation[row][col] * translation[col] for col in range(3)) - for row in range(3) - ] - return _flatten( - [ - [*inverse_rotation[0], inverse_translation[0]], - [*inverse_rotation[1], inverse_translation[1]], - [*inverse_rotation[2], inverse_translation[2]], - [0.0, 0.0, 0.0, 1.0], - ] - ) - - -YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM = _matmul( - lc.BOTTOM_SERVO_TRANSFORM, - _invert_rigid_transform(pitch_link_sts3250.STS3250_TRANSFORM), -) - -THIRD_SERVO_HORNS_IN_YOKE_TRANSFORM = _matmul( - YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM, - _invert_rigid_transform(YOKE_ON_SERVO_HORNS_TRANSFORM), -) - -TOP_SERVO_FACE_TO_FLANGE_TRANSFORM = link_bracket.top_servo_case_transform( - case_span_centering_offset_mm=link_bracket.TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM, -) - - -def _translation_transform(x: float, y: float, z: float) -> list[float]: - return [ - 1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, y, - 0.0, 0.0, 1.0, z, - 0.0, 0.0, 0.0, 1.0, - ] - - -def _standoff_children() -> list[dict[str, object]]: - return [ - { - "path": "../parts/link_standoff_m3_35.step", - "name": f"link_standoff_{label}", - "transform": _translation_transform(x, 0.0, z), - "use_source_colors": True, - } - for label, x, z in link_bracket.STANDOFF_CENTER_XZ_MM - ] - - -def _mate( - source_label: str, - *, - fixed: str, - moving: str, - relation: str = "rigid", -) -> dict[str, object]: - fixed_part, fixed_frame = fixed.split(":", 1) - moving_part, moving_frame = moving.split(":", 1) - return { - "sourceLabel": source_label, - "type": relation, - "relation": relation, - "fixed": fixed, - "moving": moving, - "parameters": {}, - "fixedEndpoint": { - "part": fixed_part, - "frame": fixed_frame, - }, - "movingEndpoint": { - "part": moving_part, - "frame": moving_frame, - }, - } - - -def _standoff_mates() -> list[dict[str, object]]: - mates: list[dict[str, object]] = [] - for label, _x, _z in link_bracket.STANDOFF_CENTER_XZ_MM: - mates.append( - _mate( - f"{label}_standoff_to_right_bracket", - fixed=f"link_bracket_right:{label}_standoff_hole", - moving=f"link_standoff_{label}:positive_y_thread", - ) - ) - mates.append( - _mate( - f"{label}_standoff_to_left_bracket", - fixed=f"link_bracket_left:{label}_standoff_hole", - moving=f"link_standoff_{label}:negative_y_thread", - ) - ) - return mates - - -def gen_step() -> dict[str, object]: - return { - "children": [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_top", - "transform": list(TOP_SERVO_FACE_TO_FLANGE_TRANSFORM), - }, - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_bottom", - "transform": list(lc.BOTTOM_SERVO_TRANSFORM), - "use_source_colors": True, - }, - { - "path": "../parts/link_bracket_right.step", - "name": "link_bracket_right", - "transform": list(IDENTITY_TRANSFORM), - }, - { - "path": "../parts/link_bracket_left.step", - "name": "link_bracket_left", - "transform": list(IDENTITY_TRANSFORM), - }, - *_standoff_children(), - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM, - }, - { - "path": "../parts/imports/sts3215.step", - "name": "sts3215_yoke_servo", - "transform": THIRD_SERVO_HORNS_IN_YOKE_TRANSFORM, - "use_source_colors": True, - }, - ], - "assembly_mates": [ - _mate( - "bottom_servo_to_right_bracket", - fixed="sts3250_bottom:upstream_case", - moving="link_bracket_right:bottom_servo_mount", - ), - _mate( - "bottom_servo_to_left_bracket", - fixed="sts3250_bottom:upstream_case", - moving="link_bracket_left:bottom_servo_mount", - ), - _mate( - "right_bracket_to_top_servo", - fixed="link_bracket_right:top_servo_mount", - moving="sts3250_top:case_mount", - ), - _mate( - "left_bracket_to_top_servo", - fixed="link_bracket_left:top_servo_mount", - moving="sts3250_top:case_mount", - ), - _mate( - "bottom_servo_output_horn_to_yoke_web", - fixed="sts3250_bottom:horn_axis", - moving="servo_horn_yoke:web_horn_axis", - ), - _mate( - "third_servo_horns_to_yoke", - fixed="servo_horn_yoke:horn_axis", - moving="sts3215_yoke_servo:horn_axis", - ), - *_standoff_mates(), - ], - } diff --git a/models/robots/tom/v2/assemblies/link_assembly.step b/models/robots/tom/v2/assemblies/link_assembly.step deleted file mode 100644 index 4b4ec7b4..00000000 --- a/models/robots/tom/v2/assemblies/link_assembly.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3a612816d2c39f042cec27378d1981f4074bc391c99db55d2b1ab0aa90dcbcd -size 25967744 diff --git a/models/robots/tom/v2/assemblies/pitch_link_sts3215.py b/models/robots/tom/v2/assemblies/pitch_link_sts3215.py deleted file mode 100644 index 0eb84f33..00000000 --- a/models/robots/tom/v2/assemblies/pitch_link_sts3215.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import servo_horn_yoke - - -YOKE_PLATE_FACE_X_MM = ( - servo_horn_yoke.SERVO_HORN_AXIS_X_MM - - servo_horn_yoke.YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM -) -YOKE_HORN_AXIS_Y_MM = -9.1 -YOKE_HORN_AXIS_Z_MM = 0.0 -# The horn's outer contact face seats directly on the outside yoke plate face. -YOKE_HORN_MATING_FACE_X_MM = YOKE_PLATE_FACE_X_MM -# Use the output horn face against the yoke plate so the servo's case-bottom -# cable face points toward the next roll-link bracket instead of away from it. -STS3215_OUTPUT_HORN_AXIS_LOCAL_X_MM = -25.5 -STS3215_OUTPUT_HORN_FACE_LOCAL_Y_MM = 9.2 -MATE_TOLERANCE_MM = 1e-6 - -STS3215_DESIGN_TRANSFORM = [ - 0.0, - 1.0, - 0.0, - ( - YOKE_HORN_MATING_FACE_X_MM - - STS3215_OUTPUT_HORN_FACE_LOCAL_Y_MM - ), - -1.0, - 0.0, - 0.0, - YOKE_HORN_AXIS_Y_MM + STS3215_OUTPUT_HORN_AXIS_LOCAL_X_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] -STS3215_TRANSFORM = list( - servo_horn_yoke.multiply_transforms( - servo_horn_yoke.DESIGN_TO_STANDALONE_TRANSFORM, - STS3215_DESIGN_TRANSFORM, - ) -) - - -def _validate_sts3215_transform() -> None: - design_transform = servo_horn_yoke.multiply_transforms( - servo_horn_yoke.STANDALONE_TO_DESIGN_TRANSFORM, - STS3215_TRANSFORM, - ) - horn_face_x = ( - design_transform[1] * STS3215_OUTPUT_HORN_FACE_LOCAL_Y_MM - + design_transform[3] - ) - if abs(horn_face_x - YOKE_HORN_MATING_FACE_X_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3215 output horn face is not seated on the yoke horn mating face: " - f"{horn_face_x:.6f} != {YOKE_HORN_MATING_FACE_X_MM:.6f}" - ) - horn_axis_y = ( - design_transform[4] * STS3215_OUTPUT_HORN_AXIS_LOCAL_X_MM - + design_transform[7] - ) - if abs(horn_axis_y - YOKE_HORN_AXIS_Y_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3215 output horn axis is not centered on the yoke horn face: " - f"{horn_axis_y:.6f} != {YOKE_HORN_AXIS_Y_MM:.6f}" - ) - horn_axis_z = design_transform[11] - if abs(horn_axis_z - YOKE_HORN_AXIS_Z_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3215 output horn axis Z is not centered on the yoke horn face: " - f"{horn_axis_z:.6f} != {YOKE_HORN_AXIS_Z_MM:.6f}" - ) - - -def gen_step() -> dict[str, object]: - _validate_sts3215_transform() - return { - "instances": [ - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - ], - }, - { - "path": "../parts/imports/sts3215.step", - "name": "sts3215", - "transform": STS3215_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/pitch_link_sts3215.step b/models/robots/tom/v2/assemblies/pitch_link_sts3215.step deleted file mode 100644 index a791ffee..00000000 --- a/models/robots/tom/v2/assemblies/pitch_link_sts3215.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bf019acb6a8d15439ab206c760018e43c0bda12cff3d051952eb32642b9d18bf -size 22664831 diff --git a/models/robots/tom/v2/assemblies/pitch_link_sts3250.py b/models/robots/tom/v2/assemblies/pitch_link_sts3250.py deleted file mode 100644 index d6448489..00000000 --- a/models/robots/tom/v2/assemblies/pitch_link_sts3250.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import servo_horn_yoke - - -YOKE_PLATE_FACE_X_MM = ( - servo_horn_yoke.SERVO_HORN_AXIS_X_MM - - servo_horn_yoke.YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM -) -YOKE_HORN_AXIS_Y_MM = -9.1 -YOKE_HORN_AXIS_Z_MM = 0.0 -# The horn's outer contact face seats directly on the outside yoke plate face. -YOKE_HORN_MATING_FACE_X_MM = YOKE_PLATE_FACE_X_MM -# Use the output horn face against the yoke plate so the servo's case-bottom -# cable face points toward the next roll-link bracket instead of away from it. -STS3250_OUTPUT_HORN_AXIS_LOCAL_X_MM = -25.5 -STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM = 9.2 -MATE_TOLERANCE_MM = 1e-6 - -STS3250_DESIGN_TRANSFORM = [ - 0.0, - 1.0, - 0.0, - ( - YOKE_HORN_MATING_FACE_X_MM - - STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM - ), - -1.0, - 0.0, - 0.0, - YOKE_HORN_AXIS_Y_MM + STS3250_OUTPUT_HORN_AXIS_LOCAL_X_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -] -STS3250_TRANSFORM = list( - servo_horn_yoke.multiply_transforms( - servo_horn_yoke.DESIGN_TO_STANDALONE_TRANSFORM, - STS3250_DESIGN_TRANSFORM, - ) -) - - -def _validate_sts3250_transform() -> None: - design_transform = servo_horn_yoke.multiply_transforms( - servo_horn_yoke.STANDALONE_TO_DESIGN_TRANSFORM, - STS3250_TRANSFORM, - ) - horn_face_x = ( - design_transform[1] * STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM - + design_transform[3] - ) - if abs(horn_face_x - YOKE_HORN_MATING_FACE_X_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3250 output horn face is not seated on the yoke horn mating face: " - f"{horn_face_x:.6f} != {YOKE_HORN_MATING_FACE_X_MM:.6f}" - ) - horn_axis_y = ( - design_transform[4] * STS3250_OUTPUT_HORN_AXIS_LOCAL_X_MM - + design_transform[7] - ) - if abs(horn_axis_y - YOKE_HORN_AXIS_Y_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3250 output horn axis is not centered on the yoke horn face: " - f"{horn_axis_y:.6f} != {YOKE_HORN_AXIS_Y_MM:.6f}" - ) - horn_axis_z = design_transform[11] - if abs(horn_axis_z - YOKE_HORN_AXIS_Z_MM) > MATE_TOLERANCE_MM: - raise RuntimeError( - "STS3250 output horn axis Z is not centered on the yoke horn face: " - f"{horn_axis_z:.6f} != {YOKE_HORN_AXIS_Z_MM:.6f}" - ) - - -def gen_step() -> dict[str, object]: - _validate_sts3250_transform() - return { - "instances": [ - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": [ - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - ], - }, - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250", - "transform": STS3250_TRANSFORM, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/pitch_link_sts3250.step b/models/robots/tom/v2/assemblies/pitch_link_sts3250.step deleted file mode 100644 index 8ddf6912..00000000 --- a/models/robots/tom/v2/assemblies/pitch_link_sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0804e9bc42c6c53e98cc289f2532edb5c3d4ed03be2ebb36b0f88736b5d910f2 -size 2749017 diff --git a/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.py b/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.py deleted file mode 100644 index 53f5d026..00000000 --- a/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -""" -Verification assembly for the tom v2 servo end mount on an STS3250. - -The mount is authored directly in the imported STS3250 local frame, so both -parts use identity transforms here. This keeps the assembly useful as a direct -fit check for the flange wrap clearances and screw-hole placement. -""" - -from __future__ import annotations - - -IDENTITY_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250", - "transform": list(IDENTITY_TRANSFORM), - "use_source_colors": True, - }, - { - "path": "../parts/servo_end_mount.step", - "name": "servo_end_mount", - "transform": list(IDENTITY_TRANSFORM), - "use_source_colors": True, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.step b/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.step deleted file mode 100644 index c337492a..00000000 --- a/models/robots/tom/v2/assemblies/servo_end_mount_sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd0501f4e4a884e8c821ff37ca550ad6754bb4f433ba4f67c8bab22e070555fb -size 2790735 diff --git a/models/robots/tom/v2/assemblies/servo_horn_yoke_double.py b/models/robots/tom/v2/assemblies/servo_horn_yoke_double.py deleted file mode 100644 index 2f351121..00000000 --- a/models/robots/tom/v2/assemblies/servo_horn_yoke_double.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc - - -SIDE_BY_SIDE_CLEARANCE_MM = 2.0 -SERVO_CENTER_SPACING_Y_MM = (2.0 * abs(lc.SERVO_REAR_EXTREME_LOCAL_Y_MM)) + SIDE_BY_SIDE_CLEARANCE_MM -SERVO_CENTER_OFFSET_Y_MM = 0.5 * SERVO_CENTER_SPACING_Y_MM -YOKE_HORN_FACE_CLEARANCE_MM = 0.0 - -IDENTITY_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - - -def _translate_y(y_mm: float) -> list[float]: - transform = list(IDENTITY_TRANSFORM) - transform[7] = y_mm - return transform - - -def _flip_about_x_then_translate_y(y_mm: float) -> list[float]: - return [ - 1.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, y_mm, - 0.0, 0.0, -1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, - ] - - -def _mate( - source_label: str, - *, - fixed: str, - moving: str, - clearance_mm: float = YOKE_HORN_FACE_CLEARANCE_MM, -) -> dict[str, object]: - fixed_part, fixed_frame = fixed.split(":", 1) - moving_part, moving_frame = moving.split(":", 1) - return { - "sourceLabel": source_label, - "type": "rigid", - "relation": "rigid", - "fixed": fixed, - "moving": moving, - "parameters": {"clearance_mm": clearance_mm}, - "fixedEndpoint": { - "part": fixed_part, - "frame": fixed_frame, - }, - "movingEndpoint": { - "part": moving_part, - "frame": moving_frame, - }, - } - - -def _servo_instance(label: str, transform: list[float]) -> dict[str, object]: - return { - "path": "../parts/imports/sts3250.step", - "name": f"sts3250_{label}", - "transform": transform, - "use_source_colors": True, - } - - -def _servo_yoke_mates() -> list[dict[str, object]]: - yoke = "servo_horn_yoke_double_horn" - mount = "servo_end_mount_double" - return [ - _mate( - "rear_servo_outer_horn_to_double_horn", - fixed="sts3250_rear:output_horn_face", - moving=f"{yoke}:rear_outer_horn_plate", - ), - _mate( - "front_servo_outer_horn_to_double_horn", - fixed="sts3250_front:output_horn_face", - moving=f"{yoke}:front_outer_horn_plate", - ), - _mate( - "rear_servo_case_top_to_double_mount", - fixed="sts3250_rear:case_top_face", - moving=f"{mount}:rear_case_wrap_flange", - ), - _mate( - "front_servo_case_top_to_double_mount", - fixed="sts3250_front:case_top_face", - moving=f"{mount}:front_case_wrap_flange", - ), - ] - - -def gen_step() -> dict[str, object]: - instances = [ - _servo_instance("rear", _flip_about_x_then_translate_y(-SERVO_CENTER_OFFSET_Y_MM)), - _servo_instance("front", _translate_y(SERVO_CENTER_OFFSET_Y_MM)), - { - "path": "../parts/servo_horn_yoke_double_horn.step", - "name": "servo_horn_yoke_double_horn", - "transform": IDENTITY_TRANSFORM, - }, - { - "path": "../parts/servo_end_mount_double.step", - "name": "servo_end_mount_double", - "transform": IDENTITY_TRANSFORM, - }, - ] - return { - "instances": instances, - "assembly_mates": _servo_yoke_mates(), - } diff --git a/models/robots/tom/v2/assemblies/servo_horn_yoke_double.step b/models/robots/tom/v2/assemblies/servo_horn_yoke_double.step deleted file mode 100644 index bb9a47eb..00000000 --- a/models/robots/tom/v2/assemblies/servo_horn_yoke_double.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c5a66ec9d7a6ce64c9dd4fd8ad57742caa3db797cd596f4307f364d7d9800d8 -size 2936234 diff --git a/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.py b/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.py deleted file mode 100644 index c438f978..00000000 --- a/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import servo_horn_yoke - -# Demonstration assembly for the servo horn yoke's intended mate: the yoke -# straddles both the output horn and rear horn, with the yoke flipped 180 degrees -# about its web axis so the web remains outside the servo case. -YOKE_HORN_SPAN_CENTER_LOCAL_Y_MM = -9.1 -YOKE_HORN_FACE_CLEARANCE_MM = 0.0 -YOKE_180_ABOUT_WEB_AXIS_TRANSFORM = list( - servo_horn_yoke.STANDALONE_YOKE_ON_SERVO_HORNS_TRANSFORM -) - - -def gen_step() -> dict[str, object]: - return { - "instances": [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250", - "transform": [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - ], - "use_source_colors": True, - }, - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": YOKE_180_ABOUT_WEB_AXIS_TRANSFORM, - }, - ], - "assembly_mates": [ - { - "sourceLabel": "yoke_rear_plate_to_output_horn", - "type": "rigid", - "relation": "rigid", - "fixed": "sts3250:output_horn_face", - "moving": "servo_horn_yoke:rear_horn_plate", - "parameters": {"clearance_mm": YOKE_HORN_FACE_CLEARANCE_MM}, - "fixedEndpoint": { - "part": "sts3250", - "frame": "output_horn_face", - }, - "movingEndpoint": { - "part": "servo_horn_yoke", - "frame": "rear_horn_plate", - }, - }, - { - "sourceLabel": "yoke_output_plate_to_rear_horn", - "type": "rigid", - "relation": "rigid", - "fixed": "sts3250:rear_horn_face", - "moving": "servo_horn_yoke:output_horn_plate", - "parameters": {"clearance_mm": YOKE_HORN_FACE_CLEARANCE_MM}, - "fixedEndpoint": { - "part": "sts3250", - "frame": "rear_horn_face", - }, - "movingEndpoint": { - "part": "servo_horn_yoke", - "frame": "output_horn_plate", - }, - }, - ], - } diff --git a/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.step b/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.step deleted file mode 100644 index 48cc81ef..00000000 --- a/models/robots/tom/v2/assemblies/servo_horn_yoke_mount.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f8d899f1f08fc365af92982772779c219b10ab4c87c8ce67ee849068093133aa -size 2748942 diff --git a/models/robots/tom/v2/assemblies/shoulder_yaw_link.py b/models/robots/tom/v2/assemblies/shoulder_yaw_link.py deleted file mode 100644 index a0e081eb..00000000 --- a/models/robots/tom/v2/assemblies/shoulder_yaw_link.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from v1.assemblies.robot_arm_link_common import rebased_robot_arm_instances - - -SOURCE_REF = "@cad[STEP/robot_arm#o1.2]" -ANCHOR_INSTANCE_NAME = "sts3250_1" -EXTRACTED_INSTANCE_NAMES = ( - "sts3250_1", - "servo_end_mount", -) - - -def gen_step() -> dict[str, object]: - instances = rebased_robot_arm_instances( - source_ref=SOURCE_REF, - anchor_instance_name=ANCHOR_INSTANCE_NAME, - extracted_instance_names=EXTRACTED_INSTANCE_NAMES, - ) - for instance in instances: - if instance.get("name") == "servo_end_mount": - instance["path"] = "../parts/servo_end_mount.step" - instance["use_source_colors"] = True - return { - "instances": instances, - } diff --git a/models/robots/tom/v2/assemblies/shoulder_yaw_link.step b/models/robots/tom/v2/assemblies/shoulder_yaw_link.step deleted file mode 100644 index 4cbdb6c4..00000000 --- a/models/robots/tom/v2/assemblies/shoulder_yaw_link.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a82d900ea84046d5a56708f44370780a1407cfcbc520ef0bba19b118777857a4 -size 2790717 diff --git a/models/robots/tom/v2/assemblies/v2_roll_link_common.py b/models/robots/tom/v2/assemblies/v2_roll_link_common.py deleted file mode 100644 index 4a4ede80..00000000 --- a/models/robots/tom/v2/assemblies/v2_roll_link_common.py +++ /dev/null @@ -1,290 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[1] -TOM_DIR = V2_DIR.parent -ASSEMBLIES_DIR = V2_DIR / "assemblies" -PARTS_DIR = V2_DIR / "parts" -for path in (TOM_DIR, V2_DIR, PARTS_DIR, ASSEMBLIES_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc -import link_bracket -import pitch_link_sts3215 -import pitch_link_sts3250 -from robot_common import robot_arm - - -MATE_TRANSFORM_TOLERANCE = 1e-6 -RIGHT_BRACKET_PATH = "../parts/link_bracket_right.step" -LEFT_BRACKET_PATH = "../parts/link_bracket_left.step" -LINK_STANDOFF_PATH = "../parts/link_standoff_m3_35.step" -TOP_SERVO_FACE_TO_FLANGE_TRANSFORM = tuple( - link_bracket.top_servo_case_transform( - case_span_centering_offset_mm=link_bracket.TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM, - ) -) - - -@dataclass(frozen=True) -class RollLinkMateSpec: - anchor: str - upstream_anchor: str - upstream_servo: str - upstream_servo_transform: tuple[float, ...] - downstream_servo: str - downstream_servo_path: str - - -@dataclass(frozen=True) -class RollLinkMates: - spec: RollLinkMateSpec - bracket_local: list[float] - downstream_shift_local: tuple[float, float, float] - downstream_servo_local: list[float] - - -ROLL_LINK_SPECS = { - "shoulder": RollLinkMateSpec( - anchor="secondary_servo_2020_connector", - upstream_anchor="servo_horn_yoke", - upstream_servo="sts3250_2", - upstream_servo_transform=tuple(pitch_link_sts3250.STS3250_DESIGN_TRANSFORM), - downstream_servo="sts3250_4", - downstream_servo_path="../parts/imports/sts3250.step", - ), - "elbow": RollLinkMateSpec( - anchor="quinary_servo_2020_connector", - upstream_anchor="quinary_horn_yoke", - upstream_servo="sts3215_5", - upstream_servo_transform=tuple(pitch_link_sts3215.STS3215_DESIGN_TRANSFORM), - downstream_servo="sts3215_6", - downstream_servo_path="../parts/imports/sts3215.step", - ), -} - - -def _matrix(transform: list[float] | tuple[float, ...]) -> list[list[float]]: - return [list(transform[index : index + 4]) for index in range(0, 16, 4)] - - -def _flatten(matrix: list[list[float]]) -> list[float]: - return [value for row in matrix for value in row] - - -def _matmul(left: list[list[float]], right: list[list[float]]) -> list[list[float]]: - return [ - [ - sum(left[row][inner] * right[inner][col] for inner in range(4)) - for col in range(4) - ] - for row in range(4) - ] - - -def invert_rigid_transform(transform: list[float] | tuple[float, ...]) -> list[float]: - matrix = _matrix(transform) - rotation = [row[:3] for row in matrix[:3]] - translation = [matrix[row][3] for row in range(3)] - inverse_rotation = [[rotation[col][row] for col in range(3)] for row in range(3)] - inverse_translation = [ - -sum(inverse_rotation[row][col] * translation[col] for col in range(3)) - for row in range(3) - ] - return _flatten( - [ - [*inverse_rotation[0], inverse_translation[0]], - [*inverse_rotation[1], inverse_translation[1]], - [*inverse_rotation[2], inverse_translation[2]], - [0.0, 0.0, 0.0, 1.0], - ] - ) - - -def relative_transform( - *, - anchor_transform: list[float] | tuple[float, ...], - transform: list[float] | tuple[float, ...], -) -> list[float]: - return multiply_transforms(invert_rigid_transform(anchor_transform), transform) - - -def multiply_transforms( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> list[float]: - return _flatten(_matmul(_matrix(left), _matrix(right))) - - -def translate_transform( - transform: list[float] | tuple[float, ...], - delta_xyz: tuple[float, float, float], -) -> list[float]: - translated = [float(value) for value in transform] - translated[3] += delta_xyz[0] - translated[7] += delta_xyz[1] - translated[11] += delta_xyz[2] - return translated - - -def translation_transform(x: float, y: float, z: float) -> list[float]: - return [ - 1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, y, - 0.0, 0.0, 1.0, z, - 0.0, 0.0, 0.0, 1.0, - ] - - -def transform_local_vector_world( - transform: list[float] | tuple[float, ...], - vector_xyz_mm: tuple[float, float, float], -) -> tuple[float, float, float]: - return ( - transform[0] * vector_xyz_mm[0] - + transform[1] * vector_xyz_mm[1] - + transform[2] * vector_xyz_mm[2], - transform[4] * vector_xyz_mm[0] - + transform[5] * vector_xyz_mm[1] - + transform[6] * vector_xyz_mm[2], - transform[8] * vector_xyz_mm[0] - + transform[9] * vector_xyz_mm[1] - + transform[10] * vector_xyz_mm[2], - ) - - -def _max_transform_delta( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> float: - return max(abs(float(left[index]) - float(right[index])) for index in range(16)) - - -def _validate_roll_link_mates(kind: str, mates: RollLinkMates, upstream_servo_local: list[float]) -> None: - bottom_servo_relative = multiply_transforms( - invert_rigid_transform(mates.bracket_local), - upstream_servo_local, - ) - bottom_delta = _max_transform_delta(bottom_servo_relative, lc.BOTTOM_SERVO_TRANSFORM) - if bottom_delta > MATE_TRANSFORM_TOLERANCE: - raise RuntimeError( - f"{kind} roll-link bottom servo mate drifted by {bottom_delta:.9f}; " - "the bracket is no longer seated on the authored bottom-servo frame" - ) - - cable_face_normal = transform_local_vector_world(bottom_servo_relative, (0.0, -1.0, 0.0)) - cable_delta = max( - abs(cable_face_normal[index] - expected) - for index, expected in enumerate((0.0, 0.0, 1.0)) - ) - if cable_delta > MATE_TRANSFORM_TOLERANCE: - raise RuntimeError( - f"{kind} roll-link bottom servo cable face is not pointing up: " - f"{tuple(round(value, 6) for value in cable_face_normal)}" - ) - - top_servo_relative = multiply_transforms( - invert_rigid_transform(mates.bracket_local), - mates.downstream_servo_local, - ) - top_delta = _max_transform_delta(top_servo_relative, TOP_SERVO_FACE_TO_FLANGE_TRANSFORM) - if top_delta > MATE_TRANSFORM_TOLERANCE: - raise RuntimeError( - f"{kind} roll-link top servo mate drifted by {top_delta:.9f}; " - "the downstream servo is no longer seated on the authored top-servo frame" - ) - - -def _source_instances_by_name() -> dict[str, dict[str, object]]: - return { - str(instance["name"]): instance - for instance in robot_arm._assembly_instances() - } - - -def roll_link_mates(kind: str) -> RollLinkMates: - spec = ROLL_LINK_SPECS[kind] - source_instances = _source_instances_by_name() - anchor = [float(value) for value in source_instances[spec.anchor]["transform"]] - upstream_anchor_transform = [ - float(value) for value in source_instances[spec.upstream_anchor]["transform"] - ] - upstream_servo_transform = multiply_transforms( - upstream_anchor_transform, - spec.upstream_servo_transform, - ) - upstream_servo_local = relative_transform( - anchor_transform=anchor, - transform=upstream_servo_transform, - ) - old_downstream_servo_local = relative_transform( - anchor_transform=anchor, - transform=[ - float(value) for value in source_instances[spec.downstream_servo]["transform"] - ], - ) - bracket_local = multiply_transforms( - upstream_servo_local, - invert_rigid_transform(lc.BOTTOM_SERVO_TRANSFORM), - ) - downstream_servo_local = multiply_transforms( - bracket_local, - TOP_SERVO_FACE_TO_FLANGE_TRANSFORM, - ) - downstream_shift_local = tuple( - downstream_servo_local[index] - old_downstream_servo_local[index] - for index in (3, 7, 11) - ) - - mates = RollLinkMates( - spec=spec, - bracket_local=bracket_local, - downstream_shift_local=downstream_shift_local, - downstream_servo_local=downstream_servo_local, - ) - _validate_roll_link_mates(kind, mates, upstream_servo_local) - return mates - - -def downstream_mate_shift_local(kind: str) -> tuple[float, float, float]: - return roll_link_mates(kind).downstream_shift_local - - -def roll_link_instances(kind: str) -> list[dict[str, object]]: - mates = roll_link_mates(kind) - spec = mates.spec - - return [ - { - "path": RIGHT_BRACKET_PATH, - "name": f"{kind}_link_bracket_right", - "transform": mates.bracket_local, - }, - { - "path": LEFT_BRACKET_PATH, - "name": f"{kind}_link_bracket_left", - "transform": mates.bracket_local, - }, - *[ - { - "path": LINK_STANDOFF_PATH, - "name": f"{kind}_link_standoff_{label}", - "transform": multiply_transforms( - mates.bracket_local, - translation_transform(x, 0.0, z), - ), - "use_source_colors": True, - } - for label, x, z in link_bracket.STANDOFF_CENTER_XZ_MM - ], - { - "path": spec.downstream_servo_path, - "name": spec.downstream_servo, - "transform": mates.downstream_servo_local, - "use_source_colors": True, - }, - ] diff --git a/models/robots/tom/v2/parts/.base_plate.step.glb b/models/robots/tom/v2/parts/.base_plate.step.glb deleted file mode 100644 index 014c95fc..00000000 --- a/models/robots/tom/v2/parts/.base_plate.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3bb48717ded98ae53ad2b2e2d94ed536211a23230bbdd6a4cecb765979a447bb -size 440800 diff --git a/models/robots/tom/v2/parts/.link_bracket_left.step.glb b/models/robots/tom/v2/parts/.link_bracket_left.step.glb deleted file mode 100644 index 6c7bdb77..00000000 --- a/models/robots/tom/v2/parts/.link_bracket_left.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:542f8f1f3b7e5bc6426e9f53f4d39b17498943edf0e8ae4d1a756e8a3c10fadc -size 791984 diff --git a/models/robots/tom/v2/parts/.link_bracket_right.step.glb b/models/robots/tom/v2/parts/.link_bracket_right.step.glb deleted file mode 100644 index c2167919..00000000 --- a/models/robots/tom/v2/parts/.link_bracket_right.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:872518114f1aaeefa9a01ef6f88a9e120f0389821061c65b61db2025d14135f4 -size 790916 diff --git a/models/robots/tom/v2/parts/.link_standoff_m3_35.step.glb b/models/robots/tom/v2/parts/.link_standoff_m3_35.step.glb deleted file mode 100644 index dd5d898b..00000000 --- a/models/robots/tom/v2/parts/.link_standoff_m3_35.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:558e4e741ac952c9f612ca45af747e23fdafff92ba11790dce08123c9ab0e299 -size 186252 diff --git a/models/robots/tom/v2/parts/.servo_end_mount.step.glb b/models/robots/tom/v2/parts/.servo_end_mount.step.glb deleted file mode 100644 index 250ed0b6..00000000 --- a/models/robots/tom/v2/parts/.servo_end_mount.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4e8a724ab9137046324052a028917613e6db5228671edbb3bbb955ffbe2a3b56 -size 634064 diff --git a/models/robots/tom/v2/parts/.servo_end_mount_double.step.glb b/models/robots/tom/v2/parts/.servo_end_mount_double.step.glb deleted file mode 100644 index 7a66767c..00000000 --- a/models/robots/tom/v2/parts/.servo_end_mount_double.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9b3032520bb1582d1911222052827a3bdabd65d75f4cbfc4eb4bb4d4cf699548 -size 794692 diff --git a/models/robots/tom/v2/parts/.servo_horn_yoke.step.glb b/models/robots/tom/v2/parts/.servo_horn_yoke.step.glb deleted file mode 100644 index a1c29dee..00000000 --- a/models/robots/tom/v2/parts/.servo_horn_yoke.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:75fdbc4e3728c52d2c410084f9b545baa70a094623f4445dd6f215849e11da89 -size 743444 diff --git a/models/robots/tom/v2/parts/.servo_horn_yoke_double_horn.step.glb b/models/robots/tom/v2/parts/.servo_horn_yoke_double_horn.step.glb deleted file mode 100644 index 9adfcf28..00000000 --- a/models/robots/tom/v2/parts/.servo_horn_yoke_double_horn.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d64d15d0b8ad4ed8912dc30a19e94e7fd869bf77c1451413936b706807e66b8 -size 744108 diff --git a/models/robots/tom/v2/parts/base_plate.py b/models/robots/tom/v2/parts/base_plate.py deleted file mode 100644 index c710c30b..00000000 --- a/models/robots/tom/v2/parts/base_plate.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate a flat base plate derived from the top face of base_clamp. - -This part keeps the exact top-plate footprint and base-servo mounting pattern -from the bent clamp, but strips away the side wall and lower return so it -remains a single flat plate in the same local frame. -""" - -from __future__ import annotations - -from dataclasses import dataclass -import os -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -V2_DIR = Path(__file__).resolve().parents[1] -BASE_CLAMP_ASSEMBLY_DIR = V2_DIR / "assemblies" / "base_clamp_assembly" -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) - -import build123d - -for path in (TOM_DIR, BASE_CLAMP_ASSEMBLY_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -from base_clamp import ( - BASE_HORN_CONNECTION_HOLE_SPECS_MM, - BASE_SERVO_M2_HOLE_SPECS_MM, - CORNER_RADIUS_MM, - _as_single_solid, - _build_bracket_layout, - _cut_base_plate_mount_features, - _fillet_plan_corners, - _make_box, -) -from robot_common.materials import GRAY_ALUMINUM_COLOR - - -DISPLAY_NAME = "Base Plate" -PLATE_THICKNESS_MM = 25.4 * 0.080 - - -@dataclass(frozen=True) -class PlateLayout: - x_min: float - x_max: float - y_min: float - y_max: float - z_min: float - z_max: float - width: float - height: float - face_y: float - attachment_holes: tuple[tuple[float, float, float], ...] - - -def _build_plate_layout() -> PlateLayout: - bracket_layout = _build_bracket_layout() - x_min = bracket_layout.x_min - x_max = bracket_layout.side_x_max - y_max = bracket_layout.y_max - y_min = y_max - PLATE_THICKNESS_MM - z_min = bracket_layout.z_min - z_max = bracket_layout.z_max - - return PlateLayout( - x_min=x_min, - x_max=x_max, - y_min=y_min, - y_max=y_max, - z_min=z_min, - z_max=z_max, - width=x_max - x_min, - height=z_max - z_min, - face_y=y_max, - attachment_holes=bracket_layout.attachment_holes, - ) - - -def build_plate() -> build123d.Solid: - layout = _build_plate_layout() - - plate = _make_box( - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _fillet_plan_corners( - plate, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - include_min_x=True, - include_max_x=False, - ) - plate = _cut_base_plate_mount_features( - plate, - holes_xzr=layout.attachment_holes, - x_min=layout.x_min, - x_max=layout.x_max, - y_min=layout.y_min, - y_max=layout.y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - plate = _as_single_solid(plate) - plate.color = GRAY_ALUMINUM_COLOR - - print( - f"{DISPLAY_NAME}: " - f"width={layout.width:.3f} mm, " - f"height={layout.height:.3f} mm, " - f"thickness={PLATE_THICKNESS_MM:.3f} mm, " - f"corner_radius={CORNER_RADIUS_MM:.3f} mm, " - f"horn_m3_holes={len(BASE_HORN_CONNECTION_HOLE_SPECS_MM)}, " - f"m2_holes={len(BASE_SERVO_M2_HOLE_SPECS_MM)}, " - f"flush_y={layout.face_y:.4f} mm" - ) - return plate - - -def gen_step() -> dict[str, object]: - return { - "shape": build_plate(), - } - - -def gen() -> None: - gen_step() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/parts/base_plate.step b/models/robots/tom/v2/parts/base_plate.step deleted file mode 100644 index a1692c37..00000000 --- a/models/robots/tom/v2/parts/base_plate.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a95aa7c8a0085f23536f117ae5e0302175d18470c0c4f10445db8df599d5437 -size 59422 diff --git a/models/robots/tom/v2/parts/dxf_topology.py b/models/robots/tom/v2/parts/dxf_topology.py deleted file mode 100644 index a894f775..00000000 --- a/models/robots/tom/v2/parts/dxf_topology.py +++ /dev/null @@ -1,301 +0,0 @@ -from __future__ import annotations - -import math -from typing import Callable, Iterable - -import build123d - - -Point2D = tuple[float, float] -ProjectFn = Callable[[build123d.Vector], Point2D] -MIN_CUT_CONTOUR_AREA_MM2 = 0.05 -# Drop micron-scale duplicate edges from topology projection before writing DXF. -# This is far below fabrication tolerance but keeps upload parsers from seeing -# accidental stray/duplicate cut fragments. -POINT_MERGE_TOLERANCE_MM = 0.01 - - -def axis_value(vector: build123d.Vector, axis: str) -> float: - return {"x": vector.X, "y": vector.Y, "z": vector.Z}[axis] - - -def snap(value: float, source: float, target: float, *, tolerance: float = 1e-3) -> float: - return target if abs(value - source) <= tolerance else value - - -def planar_faces( - shape: build123d.Shape, - *, - normal_axis: str, - normal_sign: float, - coordinate_axis: str, - coordinate: float, - tolerance: float = 0.02, - min_area: float = 1e-5, -) -> tuple[build123d.Face, ...]: - matches: list[build123d.Face] = [] - for face in shape.faces(): - if face.geom_type != build123d.GeomType.PLANE: - continue - normal = face.normal_at() - center = face.center() - if abs(axis_value(normal, normal_axis) - normal_sign) > 0.02: - continue - if abs(axis_value(center, coordinate_axis) - coordinate) > tolerance: - continue - if face.area <= min_area: - continue - matches.append(face) - if not matches: - raise RuntimeError( - "Could not find planar topology faces for DXF projection: " - f"normal {normal_sign:+.0f}{normal_axis}, {coordinate_axis}={coordinate:.3f}" - ) - return tuple(matches) - - -def project_wire_points( - wire: build123d.Wire, - project: ProjectFn, - *, - max_segment_mm: float = 0.25, -) -> list[Point2D]: - sample_count = max(16, int(math.ceil(wire.length / max_segment_mm))) - points: list[Point2D] = [] - for index in range(sample_count): - point = project(wire.position_at(index / sample_count)) - if not points or math.dist(points[-1], point) > 1e-6: - points.append(point) - if len(points) >= 2 and math.dist(points[0], points[-1]) < 1e-6: - points.pop() - return points - - -def project_face_polygon(face: build123d.Face, project: ProjectFn): - from shapely.geometry import Polygon - - outer = project_wire_points(face.outer_wire(), project) - holes = [ - project_wire_points(wire, project) - for wire in face.inner_wires() - if wire.length > 1e-6 - ] - polygon = Polygon(outer, holes) - if not polygon.is_valid: - polygon = polygon.buffer(0) - if polygon.is_empty: - raise RuntimeError("Projected topology face produced an empty DXF polygon") - return polygon - - -def union_projected_faces( - face_groups: Iterable[tuple[Iterable[build123d.Face], ProjectFn]], - *, - merge_touching_tolerance_mm: float = 0.0, -): - from shapely.ops import unary_union - - polygons = [ - project_face_polygon(face, project) - for faces, project in face_groups - for face in faces - ] - if not polygons: - raise RuntimeError("No topology faces were projected into the DXF") - if merge_touching_tolerance_mm > 0.0: - expanded = [ - polygon.buffer(merge_touching_tolerance_mm, join_style=2) - for polygon in polygons - ] - geometry = unary_union(expanded).buffer( - -merge_touching_tolerance_mm, - join_style=2, - ) - else: - geometry = unary_union(polygons) - geometry = geometry.buffer(0) - if not geometry.is_valid: - geometry = geometry.buffer(0) - if geometry.is_empty: - raise RuntimeError("Projected topology union produced an empty DXF geometry") - return geometry - - -def polyline_points_from_ring(ring) -> list[Point2D]: - points = [(float(x), float(y)) for x, y in ring.coords] - if ( - len(points) >= 2 - and math.dist(points[0], points[-1]) <= POINT_MERGE_TOLERANCE_MM - ): - points.pop() - return clean_closed_polyline_points(points) - - -def clean_closed_polyline_points( - points: list[Point2D], - *, - tolerance_mm: float = POINT_MERGE_TOLERANCE_MM, -) -> list[Point2D]: - cleaned: list[Point2D] = [] - for point in points: - if not cleaned or math.dist(cleaned[-1], point) > tolerance_mm: - cleaned.append(point) - - while ( - len(cleaned) >= 2 - and math.dist(cleaned[0], cleaned[-1]) <= tolerance_mm - ): - cleaned.pop() - - index = 0 - while len(cleaned) >= 2 and index < len(cleaned): - next_index = (index + 1) % len(cleaned) - if math.dist(cleaned[index], cleaned[next_index]) <= tolerance_mm: - del cleaned[next_index] - if next_index < index: - index = 0 - continue - index += 1 - - return cleaned - - -def polygon_area(points: list[Point2D]) -> float: - if len(points) < 3: - return 0.0 - area = 0.0 - for index, point in enumerate(points): - next_point = points[(index + 1) % len(points)] - area += (point[0] * next_point[1]) - (next_point[0] * point[1]) - return abs(0.5 * area) - - -def circle_from_ring( - ring, - *, - tolerance_mm: float = 0.03, - min_points: int = 12, -) -> tuple[Point2D, float] | None: - points = polyline_points_from_ring(ring) - if len(points) < min_points: - return None - matrix = [[0.0, 0.0, 0.0] for _ in range(3)] - vector = [0.0, 0.0, 0.0] - for x, y in points: - row = (x, y, 1.0) - rhs = -(x * x + y * y) - for i in range(3): - vector[i] += row[i] * rhs - for j in range(3): - matrix[i][j] += row[i] * row[j] - try: - coeff_a, coeff_b, coeff_c = _solve_3x3(matrix, vector) - except ZeroDivisionError: - return None - center = (-0.5 * coeff_a, -0.5 * coeff_b) - radius_squared = (center[0] * center[0]) + (center[1] * center[1]) - coeff_c - if radius_squared <= 0.0: - return None - radius = math.sqrt(radius_squared) - xs = [point[0] for point in points] - ys = [point[1] for point in points] - min_x = min(xs) - max_x = max(xs) - min_y = min(ys) - max_y = max(ys) - width = max_x - min_x - height = max_y - min_y - if abs(width - height) > max(tolerance_mm, 0.04 * radius): - return None - radial_errors = [ - abs(math.dist(point, center) - radius) - for point in points - ] - if max(radial_errors) > max(tolerance_mm, 0.02 * radius): - return None - return center, radius - - -def _solve_3x3(matrix: list[list[float]], vector: list[float]) -> tuple[float, float, float]: - augmented = [row[:] + [value] for row, value in zip(matrix, vector)] - for column in range(3): - pivot_row = max(range(column, 3), key=lambda row: abs(augmented[row][column])) - if abs(augmented[pivot_row][column]) < 1e-12: - raise ZeroDivisionError("singular circle fit") - if pivot_row != column: - augmented[column], augmented[pivot_row] = augmented[pivot_row], augmented[column] - pivot = augmented[column][column] - for item in range(column, 4): - augmented[column][item] /= pivot - for row in range(3): - if row == column: - continue - factor = augmented[row][column] - for item in range(column, 4): - augmented[row][item] -= factor * augmented[column][item] - return (augmented[0][3], augmented[1][3], augmented[2][3]) - - -def add_closed_polyline(msp, points: list[Point2D], *, layer: str = "CUT") -> None: - points = clean_closed_polyline_points(points) - if len(points) < 3: - return - msp.add_lwpolyline(points, close=True, dxfattribs={"layer": layer}) - - -def add_circle_polyline( - msp, - center: Point2D, - radius: float, - *, - layer: str = "CUT", - segments: int = 72, -) -> None: - # Keep circles as plain closed polylines. Bulge arcs are valid DXF, but - # some upload parsers classify them as unexpected entity data. - if radius <= 0.0: - return - center_x, center_y = center - point_count = max(24, int(segments)) - points = [ - ( - center_x + (radius * math.cos((2.0 * math.pi * index) / point_count)), - center_y + (radius * math.sin((2.0 * math.pi * index) / point_count)), - ) - for index in range(point_count) - ] - msp.add_lwpolyline( - points, - close=True, - dxfattribs={"layer": layer}, - ) - - -def add_ring( - msp, - ring, - *, - layer: str = "CUT", - prefer_circle: bool = False, - min_area_mm2: float = MIN_CUT_CONTOUR_AREA_MM2, -) -> None: - points = polyline_points_from_ring(ring) - if polygon_area(points) < min_area_mm2: - return - if prefer_circle: - circle = circle_from_ring(ring) - if circle is not None: - center, radius = circle - add_circle_polyline(msp, center, radius, layer=layer) - return - add_closed_polyline(msp, points, layer=layer) - - -def add_shapely_geometry(msp, geometry, *, layer: str = "CUT") -> None: - polygons = [geometry] if geometry.geom_type == "Polygon" else list(geometry.geoms) - for polygon in polygons: - if polygon.area < MIN_CUT_CONTOUR_AREA_MM2: - continue - add_ring(msp, polygon.exterior, layer=layer) - for interior in polygon.interiors: - add_ring(msp, interior, layer=layer, prefer_circle=True) diff --git a/models/robots/tom/v2/parts/gripper/.rb9_01_061_000_gripper.step.glb b/models/robots/tom/v2/parts/gripper/.rb9_01_061_000_gripper.step.glb deleted file mode 100644 index 7f37340d..00000000 --- a/models/robots/tom/v2/parts/gripper/.rb9_01_061_000_gripper.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f53b779f1f429274f09df018d56f89d076cabc140fdc989d9bbfa6fd5756b102 -size 11204868 diff --git a/models/robots/tom/v2/parts/gripper/gripper_component_extract.py b/models/robots/tom/v2/parts/gripper/gripper_component_extract.py deleted file mode 100644 index 84fd8462..00000000 --- a/models/robots/tom/v2/parts/gripper/gripper_component_extract.py +++ /dev/null @@ -1,128 +0,0 @@ -from __future__ import annotations - -import os -import re -import sys -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[2] -_CACHE_HOME = REPO_ROOT / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import build123d -from build123d.topology import downcast -from build123d.topology.shape_core import shapetype - -CAD_SCRIPT_PATH = Path(__file__).resolve().parents[5] / "skills" / "cad" / "scripts" -if str(CAD_SCRIPT_PATH) not in sys.path: - sys.path.insert(0, str(CAD_SCRIPT_PATH)) - -from cadpy_common.package_path import ensure_cadpy_package_path - -ensure_cadpy_package_path() - -from cadpy.step_scene import _located_shape, load_step_scene, occurrence_selector_id - - -SOURCE_STEP = Path(__file__).resolve().with_name("rb9_01_061_000_gripper.step") - - -def _cast_shape(obj: object) -> build123d.Shape: - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _safe_label(text: str | None, fallback: str) -> str: - value = re.sub(r"[^A-Za-z0-9_.-]+", "_", text or "").strip("_") - return value or fallback - - -def _leaf_color(scene: object, leaf: object) -> tuple[float, float, float, float] | None: - leaf_color = getattr(leaf, "color", None) - if leaf_color is not None: - return leaf_color - prototype_key = getattr(leaf, "prototype_key", None) - prototype_color = scene.prototype_colors.get(prototype_key) - if prototype_color is not None: - return prototype_color - return None - - -def _find_node(nodes: list[object], selector: str) -> object: - stack = list(reversed(nodes)) - while stack: - node = stack.pop() - if occurrence_selector_id(node) == selector: - return node - stack.extend(reversed(node.children)) - raise RuntimeError(f"Selector {selector} not found in {SOURCE_STEP}") - - -def _leaf_nodes(node: object) -> list[object]: - leaves: list[object] = [] - stack = [node] - while stack: - current = stack.pop() - if current.prototype_key is not None: - leaves.append(current) - stack.extend(reversed(current.children)) - return leaves - - -def _selector_matches(selector: str, parent_selector: str) -> bool: - return selector == parent_selector or selector.startswith(f"{parent_selector}.") - - -def _compound_from_leaves( - scene: object, - leaves: list[object], - *, - label: str, -) -> build123d.Compound: - children: list[build123d.Shape] = [] - for leaf in leaves: - if leaf.prototype_key not in scene.prototype_shapes: - raise RuntimeError(f"Leaf {occurrence_selector_id(leaf)} has no prototype shape") - child = _cast_shape(_located_shape(scene.prototype_shapes[leaf.prototype_key], leaf.location)) - child.label = _safe_label( - leaf.source_name or leaf.name, - occurrence_selector_id(leaf).replace(".", "_"), - ) - color = _leaf_color(scene, leaf) - if color is not None: - child.color = build123d.Color(*color) - children.append(child) - - if not children: - raise RuntimeError(f"No gripper shapes extracted for {label}") - return build123d.Compound(obj=children, children=children, label=label) - - -def extract_occurrences(selectors: tuple[str, ...], *, label: str) -> build123d.Compound: - if not SOURCE_STEP.exists(): - raise FileNotFoundError(f"Missing gripper source STEP: {SOURCE_STEP}") - - scene = load_step_scene(SOURCE_STEP) - leaves: list[object] = [] - for selector in selectors: - leaves.extend(_leaf_nodes(_find_node(scene.roots, selector))) - return _compound_from_leaves(scene, leaves, label=label) - - -def extract_all_except(excluded_selectors: tuple[str, ...], *, label: str) -> build123d.Compound: - if not SOURCE_STEP.exists(): - raise FileNotFoundError(f"Missing gripper source STEP: {SOURCE_STEP}") - - scene = load_step_scene(SOURCE_STEP) - leaves: list[object] = [] - for root in scene.roots: - for leaf in _leaf_nodes(root): - selector = occurrence_selector_id(leaf) - if any(_selector_matches(selector, excluded) for excluded in excluded_selectors): - continue - leaves.append(leaf) - return _compound_from_leaves(scene, leaves, label=label) diff --git a/models/robots/tom/v2/parts/gripper/rb9_01_061_000_gripper.step b/models/robots/tom/v2/parts/gripper/rb9_01_061_000_gripper.step deleted file mode 100644 index 3c537f91..00000000 --- a/models/robots/tom/v2/parts/gripper/rb9_01_061_000_gripper.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:24e36ec30a7b517bf75f0dd021ee2ebfb611c21656b9d96f9b0421bbcf67c848 -size 11058995 diff --git a/models/robots/tom/v2/parts/imports/.sts3215.step.glb b/models/robots/tom/v2/parts/imports/.sts3215.step.glb deleted file mode 100644 index 1d87a2c3..00000000 --- a/models/robots/tom/v2/parts/imports/.sts3215.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:caceb32bdc491c5b04cea16b9e12e01b9aa3664e8fafa132eb41e4f646f4fa50 -size 11326536 diff --git a/models/robots/tom/v2/parts/imports/.sts3250.step.glb b/models/robots/tom/v2/parts/imports/.sts3250.step.glb deleted file mode 100644 index 6269f9de..00000000 --- a/models/robots/tom/v2/parts/imports/.sts3250.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ae5ac94f12fbe3d208054021347997498763afab72505a1d1408b028360345ee -size 3245664 diff --git a/models/robots/tom/v2/parts/imports/.sts3250_no_rear_horn.step.glb b/models/robots/tom/v2/parts/imports/.sts3250_no_rear_horn.step.glb deleted file mode 100644 index 4df5e72e..00000000 --- a/models/robots/tom/v2/parts/imports/.sts3250_no_rear_horn.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe4452db1b44e238e8b6ee6522bc854e225f8d65d3efdfece5e923ebe054d731 -size 2973160 diff --git a/models/robots/tom/v2/parts/imports/sts3215.step b/models/robots/tom/v2/parts/imports/sts3215.step deleted file mode 100644 index 6e59375d..00000000 --- a/models/robots/tom/v2/parts/imports/sts3215.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c116e2088d9b1c013c89c640d89093749e8edef93ddaf0f4c13371669da65b0 -size 21941724 diff --git a/models/robots/tom/v2/parts/imports/sts3250.step b/models/robots/tom/v2/parts/imports/sts3250.step deleted file mode 100644 index 746b3e65..00000000 --- a/models/robots/tom/v2/parts/imports/sts3250.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5358f001dbdb3a059d625b79f3e0272a4f93dfb7f1e2a50d07803b709a337d29 -size 2299283 diff --git a/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.py b/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.py deleted file mode 100644 index f7c969dd..00000000 --- a/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate an STS3250 variant with the rear (bottom) servo horn removed. - -The stock import at STEP/imports/sts3250.step contains five solids; the -fifth is the detachable rear-journal horn disk (diameter 19.2 mm, 3.1 mm -thick, on the case-bottom side). The tom v2 link seats bracket feet -directly on the bottom servo's case-bottom face, so this variant drops -that horn solid. The case's rear journal/screw stub (about 6 mm across, -protruding 2.6 mm past the case-bottom plane) belongs to the case solid -and remains. - -The remaining solids are tinted black aluminum to match the uniform -source color of the stock import, which would otherwise be lost when -re-exporting through the Python generator. - -Usage: - python v2/imports/sts3250_no_rear_horn.py -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parents[2] -TOM_DIR = V2_DIR.parent -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) - -import build123d - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.materials import BLACK_ALUMINUM_COLOR -from robot_common.step_import import import_as_shape - - -PART_NAME = Path(__file__).stem - -SERVO_STEP = V2_DIR / "parts" / "imports" / "sts3250.step" - -REAR_HORN_DIAMETER_MM = 19.2 -REAR_HORN_THICKNESS_MM = 3.1 -REAR_HORN_CENTER_Y_MM = -25.85 -SOLID_MATCH_TOLERANCE_MM = 0.2 -EXPECTED_SOLID_COUNT = 5 - - -def _is_rear_horn(solid: build123d.Solid) -> bool: - bb = solid.bounding_box() - return ( - abs(bb.size.X - REAR_HORN_DIAMETER_MM) <= SOLID_MATCH_TOLERANCE_MM - and abs(bb.size.Z - REAR_HORN_DIAMETER_MM) <= SOLID_MATCH_TOLERANCE_MM - and abs(bb.size.Y - REAR_HORN_THICKNESS_MM) <= SOLID_MATCH_TOLERANCE_MM - and abs(bb.center().Y - REAR_HORN_CENTER_Y_MM) <= SOLID_MATCH_TOLERANCE_MM - ) - - -def build_step() -> build123d.Compound: - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - - servo = import_as_shape(SERVO_STEP) - solids = servo.solids() - if len(solids) != EXPECTED_SOLID_COUNT: - raise RuntimeError( - f"Expected {EXPECTED_SOLID_COUNT} solids in the STS3250 import, found {len(solids)}" - ) - - rear_horns = [solid for solid in solids if _is_rear_horn(solid)] - if len(rear_horns) != 1: - raise RuntimeError( - f"Expected exactly one rear horn solid in the STS3250 import, found {len(rear_horns)}" - ) - - kept = [solid for solid in solids if not solid.is_same(rear_horns[0])] - for solid in kept: - solid.color = BLACK_ALUMINUM_COLOR - - variant = build123d.Compound(children=kept, label=PART_NAME) - - bb = variant.bounding_box() - source_bb = servo.bounding_box() - for measured, expected, axis in ( - (bb.size.X, source_bb.size.X, "X"), - (bb.size.Y, source_bb.size.Y, "Y"), - (bb.size.Z, source_bb.size.Z, "Z"), - ): - if abs(measured - expected) > 0.01: - raise RuntimeError( - f"Variant {axis} extent {measured:.4f} mm deviates from the stock servo {expected:.4f} mm; " - "removing the rear horn must not change the body envelope" - ) - - horn_bb = rear_horns[0].bounding_box() - print( - "Removed rear horn solid: " - f"bb min=({horn_bb.min.X:.2f}, {horn_bb.min.Y:.2f}, {horn_bb.min.Z:.2f}) mm, " - f"max=({horn_bb.max.X:.2f}, {horn_bb.max.Y:.2f}, {horn_bb.max.Z:.2f}) mm" - ) - print(f"Variant solids: {len(kept)}; envelope X={bb.size.X:.3f} Y={bb.size.Y:.3f} Z={bb.size.Z:.3f} mm") - return variant - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.step b/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.step deleted file mode 100644 index 94a06f75..00000000 --- a/models/robots/tom/v2/parts/imports/sts3250_no_rear_horn.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8be5f78673ae505dc70d114f13af7d34a048b9d7b83d8e1c690c4f5064f7b134 -size 2383014 diff --git a/models/robots/tom/v2/parts/link_bracket.py b/models/robots/tom/v2/parts/link_bracket.py deleted file mode 100644 index cb7eed2e..00000000 --- a/models/robots/tom/v2/parts/link_bracket.py +++ /dev/null @@ -1,1149 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate the tom v2 wrap link plate: one sheet part with two bend lines -connecting the two servos along the +Y side of the link. - -Geometry per the side-view draft and the referenced features: -- foot: lies on the bottom servo's case-bottom (up-facing) plane, - connecting the two +Y mount holes at (X = 8.3 | 32.75, Y = 10.25), - its inner edge wrapping around the cable bay with a 0.4 mm overhang - and clearing the raised center zone, -- bend 1 (axis X): split into two segments over the hole zones, each - with a 7.7 mm formed flange, -- riser leg: lies flat against the top servo's +Y side face (the - referenced face spans X 7.9..15.2, Z 102.1..129.3), full foot width - at the bottom and cut partway up to the face's width ("finishing - width"), with a corner extension past the servo's case-top corner, -- bend 2 (axis Z, full length): wraps onto the case-top face plane and - connects to its two +Y screw holes at (Y = 10.25, Z = 106 | 126.7). - The flange stands 1.5 mm off the face on spacers (5x M2 washers or a - 1.5 mm spacer per screw) so it clears the case-top center ridge - (1.1 mm proud) and the output horn boss (1.3 mm proud), allowing a - full-depth rectangular flange that meets the published minimum - flange length. - -Advisory (not in SendCutSend's published ruleset): the servo's holes -sit 2.1 mm from its case corners, so both bends run 2.7-3.2 mm from -screw holes - inside the press-brake die span. Expect minor hole -distortion; ream after forming if screws bind. - -Usage: - python v2/link_bracket.py -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parent -if str(V2_DIR) not in sys.path: - sys.path.insert(0, str(V2_DIR)) - -import build123d - -import dxf_topology -import link_common as lc - - -PART_NAME = Path(__file__).stem - -CUT_EXTENSION_MM = 2.0 - - -@dataclass(frozen=True) -class FlatPattern: - outside_setback_mm: float - bend_allowance_mm: float - bend_deduction_mm: float - panel_join_shift_mm: float - foot_bend_line_y_mm: float - wrap_bend_line_x_mm: float - - -def _env_float(name: str, default: float) -> float: - raw = os.environ.get(name) - if raw is None: - return default - try: - return float(raw) - except ValueError as exc: - raise ValueError(f"{name} must be a floating point number, got {raw!r}") from exc - - -def _env_float_any(names: tuple[str, ...], default: float) -> float: - for name in names: - raw = os.environ.get(name) - if raw is None: - continue - try: - return float(raw) - except ValueError as exc: - raise ValueError(f"{name} must be a floating point number, got {raw!r}") from exc - return default - -# Plate planes. The paired brackets are spaced to accept off-the-shelf -# female-female M3 cross standoffs. The flange seats close to the case-top -# face (0.25 mm) so it bolts down flush rather than floating; its inner edge -# stays outboard of the case-top center ridge (proud only over Y 5-7). The -# bend position follows FLANGE_INNER_X parametrically. -CASE_SPAN_CENTERING_OFFSET_MM = _env_float( - "TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM", - lc.CASE_SPAN_CENTERING_OFFSET_MM, -) -BASELINE_LINK_BRACKET_OVERALL_HEIGHT_MM = lc.TOP_PIVOT_Z_MM - lc.FOOT_BOTTOM_Z_MM -DEFAULT_LINK_BRACKET_OVERALL_HEIGHT_MM = 150.0 -LINK_BRACKET_OVERALL_HEIGHT_MM = _env_float_any( - ( - "TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM", - "TOM_V2_LINK_BRACKET_HEIGHT_MM", - ), - DEFAULT_LINK_BRACKET_OVERALL_HEIGHT_MM, -) -if LINK_BRACKET_OVERALL_HEIGHT_MM < 100.0: - raise ValueError( - "TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM must be at least 100 mm " - "to keep the top servo mount above the sloped web" - ) -HEIGHT_SCALE = LINK_BRACKET_OVERALL_HEIGHT_MM / BASELINE_LINK_BRACKET_OVERALL_HEIGHT_MM -TOP_PIVOT_Z_MM = lc.FOOT_BOTTOM_Z_MM + LINK_BRACKET_OVERALL_HEIGHT_MM -TOP_BODY_DOWN_Z_OFFSET_MM = TOP_PIVOT_Z_MM + lc.SERVO_SHAFT_LOCAL_X_MM -CASE_FLUSH_HOLE_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM - lc.SERVO_MOUNT_HOLE_LOCAL_X_MM[1] -CASE_OFFSET_HOLE_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM - lc.SERVO_TOP_MOUNT_HOLE_LOCAL_X_MM[1] -LINK_STANDOFF_LENGTH_MM = _env_float("TOM_V2_LINK_STANDOFF_LENGTH_MM", 35.0) -LEG_INNER_Y_MM = 0.5 * LINK_STANDOFF_LENGTH_MM -LEG_FACE_CLEARANCE_MM = LEG_INNER_Y_MM - lc.SERVO_HALF_WIDTH_MM -if LEG_FACE_CLEARANCE_MM < 1.5: - raise ValueError( - "TOM_V2_LINK_STANDOFF_LENGTH_MM leaves too little side clearance around the servo body" - ) -FLANGE_FACE_CLEARANCE_MM = 0.25 -LEG_OUTER_Y_MM = LEG_INNER_Y_MM + lc.SHEET_THICKNESS_MM -CASE_TOP_FACE_X_MM = lc.SERVO_CASE_TOP_LOCAL_Y_MM + CASE_SPAN_CENTERING_OFFSET_MM -FLANGE_INNER_X_MM = CASE_TOP_FACE_X_MM + FLANGE_FACE_CLEARANCE_MM -FLANGE_OUTER_X_MM = FLANGE_INNER_X_MM + lc.SHEET_THICKNESS_MM -# Output-horn relief: concentric with the servo's output horn boss on the pitch -# axis, so the flange's curved cut is inline with the horn and symmetric. -# Keep this tunable because this gap is a tight tradeoff: reducing the relief -# radius gives the top flange screw more web, while increasing it gives the horn -# more radial clearance. -OUTPUT_HORN_RADIUS_MM = lc.SERVO_OUTPUT_HORN_BOSS_RADIUS_MM -DEFAULT_OUTPUT_HORN_RELIEF_RADIUS_MM = 11.0 -OUTPUT_HORN_RELIEF_RADIUS_MM = _env_float( - "TOM_V2_OUTPUT_HORN_RELIEF_RADIUS_MM", - DEFAULT_OUTPUT_HORN_RELIEF_RADIUS_MM, -) - -# Foot outline. The foot keeps a short Y=7.5 tab around each servo screw -# (left X 6.0-10.6, right X 30.4-35.0 - matching widths) and is set back to -# Y=9.6 everywhere between them, clearing both the cable hole and the -# raised center panel. The left tab is sized to match the right tab. -FOOT_X_MIN_MM = 6.0 -FOOT_X_MAX_MM = 35.0 -FOOT_EDGE_DEFAULT_Y_MM = 7.5 -FOOT_EDGE_RAISED_Y_MM = 9.6 -LEFT_TAB_RIGHT_X_MM = 10.6 # 4.6 mm tab (X 6.0-10.6) around the left screw at 8.3 -RAISED_ZONE_X_MM = (LEFT_TAB_RIGHT_X_MM, 30.4) - -# Leg profile (in the X-Z plane). The side-view layout is three constant-width -# bands from the bottom-servo mounting plane to the 180 mm top pitch axis: -# lower vertical section, centered sloped transition, and upper vertical -# section that carries the top-servo face features. A local 2t rectangular web -# ear gives the interrupted wrap bend room before the one-thickness flange turns -# onto the servo case face. -SECTION_WIDTH_MM = FOOT_X_MAX_MM - FOOT_X_MIN_MM -TOP_FLANGE_BEND_LEEWAY_MM = 2.0 * lc.SHEET_THICKNESS_MM -# Keep the top servo body vertically in line with the yoke-mounted servo body: -# both servos use the same local orientation, so matching the case transform's -# X translation makes their body centers share the same world X coordinate. -TOP_SERVO_ALIGNED_CASE_SPAN_CENTERING_OFFSET_MM = lc.HORN_SPAN_CENTERING_OFFSET_MM -FLANGE_WRAP_INNER_X_MM = ( - TOP_SERVO_ALIGNED_CASE_SPAN_CENTERING_OFFSET_MM + lc.SERVO_CASE_TOP_LOCAL_Y_MM -) -FLANGE_WRAP_OUTER_X_MM = FLANGE_WRAP_INNER_X_MM + lc.SHEET_THICKNESS_MM -TOP_FLANGE_EAR_X_MAX_MM = FLANGE_WRAP_INNER_X_MM -TOP_FLANGE_EAR_X_MIN_MM = TOP_FLANGE_EAR_X_MAX_MM - TOP_FLANGE_BEND_LEEWAY_MM -UPPER_X_MAX_MM = TOP_FLANGE_EAR_X_MIN_MM -UPPER_X_MIN_MM = UPPER_X_MAX_MM - SECTION_WIDTH_MM -TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM = ( - FLANGE_WRAP_INNER_X_MM - lc.SERVO_CASE_TOP_LOCAL_Y_MM -) -TOP_SERVO_FACE_TO_FLANGE_X_OFFSET_MM = ( - TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM - CASE_SPAN_CENTERING_OFFSET_MM -) -RIGHT_SLOPE_BOTTOM_Z_MM = lc.FOOT_BOTTOM_Z_MM + (60.0 - lc.FOOT_BOTTOM_Z_MM) * HEIGHT_SCALE -RIGHT_SLOPE_TOP_Z_MM = lc.FOOT_BOTTOM_Z_MM + (120.0 - lc.FOOT_BOTTOM_Z_MM) * HEIGHT_SCALE -SLOPE_SECTION_HEIGHT_MM = RIGHT_SLOPE_TOP_Z_MM - RIGHT_SLOPE_BOTTOM_Z_MM -SLOPE_RUN_X_MM = abs(FOOT_X_MAX_MM - UPPER_X_MAX_MM) -LOW_X_SLOPE_Z_SHIFT_DOWN_MM = SECTION_WIDTH_MM * ( - math.hypot(SLOPE_RUN_X_MM, SLOPE_SECTION_HEIGHT_MM) - SLOPE_SECTION_HEIGHT_MM -) / SLOPE_RUN_X_MM -LEFT_SLOPE_BOTTOM_Z_MM = RIGHT_SLOPE_BOTTOM_Z_MM - LOW_X_SLOPE_Z_SHIFT_DOWN_MM -LEFT_SLOPE_TOP_Z_MM = RIGHT_SLOPE_TOP_Z_MM - LOW_X_SLOPE_Z_SHIFT_DOWN_MM -SLOPE_DX_PER_Z_MM = (UPPER_X_MAX_MM - FOOT_X_MAX_MM) / SLOPE_SECTION_HEIGHT_MM -SLOPE_HIGH_X_INTERCEPT_MM = FOOT_X_MAX_MM - SLOPE_DX_PER_Z_MM * RIGHT_SLOPE_BOTTOM_Z_MM -SLOPE_LOW_X_INTERCEPT_MM = FOOT_X_MIN_MM - SLOPE_DX_PER_Z_MM * LEFT_SLOPE_BOTTOM_Z_MM -SLOPED_BAND_NORMAL_WIDTH_MM = abs( - SLOPE_HIGH_X_INTERCEPT_MM - SLOPE_LOW_X_INTERCEPT_MM -) / math.hypot(1.0, SLOPE_DX_PER_Z_MM) -SLOPE_SECTION_CENTER_Z_MM = 0.25 * ( - LEFT_SLOPE_BOTTOM_Z_MM - + RIGHT_SLOPE_BOTTOM_Z_MM - + LEFT_SLOPE_TOP_Z_MM - + RIGHT_SLOPE_TOP_Z_MM -) -# The local bend ear carries the wrap lead-in; the flange box itself remains -# one sheet thickness. -LEG_RUN_X_MM = ( - min(FOOT_X_MIN_MM, UPPER_X_MIN_MM), - max(FOOT_X_MAX_MM, FLANGE_WRAP_OUTER_X_MM), -) -# Servo face #o1.1.1.1.f19 in link_assembly, expressed in STS3250 local X. -# Keeping this in local coordinates makes the bracket top track height changes. -TOP_SERVO_INLINE_FACE_LOCAL_X_MM = -20.038516 -LEG_TOP_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM - TOP_SERVO_INLINE_FACE_LOCAL_X_MM -OUTPUT_HORN_TOP_EDGE_GAP_MM = TOP_PIVOT_Z_MM - LEG_TOP_Z_MM -FLANGE_BOTTOM_Z_MM = CASE_OFFSET_HOLE_Z_MM - 4.0 -BASELINE_TRANSITION_CORNER_FILLET_RADIUS_MM = 24.0 -DEFAULT_TRANSITION_CORNER_FILLET_RADIUS_MM = 0.5 * BASELINE_TRANSITION_CORNER_FILLET_RADIUS_MM -TRANSITION_CORNER_FILLET_RADIUS_MM = _env_float( - "TOM_V2_TRANSITION_CORNER_FILLET_RADIUS_MM", - DEFAULT_TRANSITION_CORNER_FILLET_RADIUS_MM, -) -FORMED_FLANGE_CORNER_FILLET_RADIUS_MM = 0.8 -# Uniform flange inner edge. Place it at the case-top ridge clearance datum -# (ridge half-width 7.0 mm plus 0.5 mm tolerance clearance) so the wrap flange -# reaches closer to the servo face while leaving the horn relief, not the free -# edge, as the limiting boundary near the top screw hole. -FLANGE_EDGE_Y_MM = lc.CASE_OFFSET_RELIEF_HALF_WIDTH_MM -TOP_FLANGE_UPPER_HOLE_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM + 17.2 -TOP_FLANGE_HOLE_TO_RELIEF_EDGE_GAP_MM = ( - math.hypot(lc.HOLE_SIDE_OFFSET_MM, TOP_PIVOT_Z_MM - TOP_FLANGE_UPPER_HOLE_Z_MM) - - lc.SERVO_MOUNT_HOLE_RADIUS_MM - - OUTPUT_HORN_RELIEF_RADIUS_MM -) - -# Side-web lightening slots, paired per sketch in the lower, sloped, and -# upper bands. Slots cut through the sheet along Y and are kept away from -# the bend reliefs and servo screw holes. -SLOT_CUT_Y_OFFSET_MM = LEG_OUTER_Y_MM + CUT_EXTENSION_MM -SLOT_CUT_Y_SPAN_MM = lc.SHEET_THICKNESS_MM + 2.0 * CUT_EXTENSION_MM -SLOT_WIDTH_MM = 4.0 -TOP_SLOT_WIDTH_MM = 4.0 -BASELINE_LOWER_SLOT_LENGTH_MM = 38.0 -BASELINE_MIDDLE_SLOT_LENGTH_MM = 38.0 -BASELINE_TOP_SLOT_LENGTH_MM = 34.0 -LOWER_SLOT_LENGTH_MM = BASELINE_LOWER_SLOT_LENGTH_MM * HEIGHT_SCALE -MIDDLE_SLOT_LENGTH_MM = BASELINE_MIDDLE_SLOT_LENGTH_MM * HEIGHT_SCALE -TOP_SLOT_LENGTH_MM = BASELINE_TOP_SLOT_LENGTH_MM * HEIGHT_SCALE -LOWER_SLOT_CENTER_Z_MM = 0.5 * (lc.FOOT_BOTTOM_Z_MM + RIGHT_SLOPE_BOTTOM_Z_MM) -MIDDLE_SLOT_CENTER_Z_MM = SLOPE_SECTION_CENTER_Z_MM -TOP_SLOT_CENTER_Z_MM = 0.5 * (RIGHT_SLOPE_TOP_Z_MM + LEG_TOP_Z_MM) -LOWER_SLOT_CENTER_X_MM = (14.0, 27.0) -MIDDLE_SLOT_CENTER_SPACING_X_MM = 13.0 -MIDDLE_SLOT_PAIR_CENTER_X_MM = 0.5 * ( - (SLOPE_DX_PER_Z_MM * MIDDLE_SLOT_CENTER_Z_MM + SLOPE_LOW_X_INTERCEPT_MM) - + (SLOPE_DX_PER_Z_MM * MIDDLE_SLOT_CENTER_Z_MM + SLOPE_HIGH_X_INTERCEPT_MM) -) -MIDDLE_SLOT_CENTER_X_MM = ( - MIDDLE_SLOT_PAIR_CENTER_X_MM - 0.5 * MIDDLE_SLOT_CENTER_SPACING_X_MM, - MIDDLE_SLOT_PAIR_CENTER_X_MM + 0.5 * MIDDLE_SLOT_CENTER_SPACING_X_MM, -) -MIDDLE_SLOT_CENTER_XZ_MM = tuple( - (slot_x, MIDDLE_SLOT_CENTER_Z_MM) for slot_x in MIDDLE_SLOT_CENTER_X_MM -) -MIDDLE_SLOT_CENTER_NORMAL_MM = tuple( - (slot_x - SLOPE_DX_PER_Z_MM * MIDDLE_SLOT_CENTER_Z_MM - SLOPE_LOW_X_INTERCEPT_MM) - / math.hypot(1.0, SLOPE_DX_PER_Z_MM) - for slot_x in MIDDLE_SLOT_CENTER_X_MM -) -MIDDLE_SLOT_LOW_EDGE_MARGIN_MM = MIDDLE_SLOT_CENTER_NORMAL_MM[0] - 0.5 * SLOT_WIDTH_MM -MIDDLE_SLOT_HIGH_EDGE_MARGIN_MM = ( - SLOPED_BAND_NORMAL_WIDTH_MM - MIDDLE_SLOT_CENTER_NORMAL_MM[1] - 0.5 * SLOT_WIDTH_MM -) -MIDDLE_SLOT_ROTATION_DEG = math.degrees( - math.atan2( - 0.5 * (LEFT_SLOPE_TOP_Z_MM + RIGHT_SLOPE_TOP_Z_MM) - - 0.5 * (LEFT_SLOPE_BOTTOM_Z_MM + RIGHT_SLOPE_BOTTOM_Z_MM), - UPPER_X_MAX_MM - FOOT_X_MAX_MM, - ) -) -VERTICAL_SLOT_ROTATION_DEG = 90.0 -TOP_SLOT_PAIR_CENTER_X_MM = 0.5 * (UPPER_X_MIN_MM + UPPER_X_MAX_MM) -TOP_SLOT_CENTER_SPACING_X_MM = 13.0 -TOP_SLOT_CENTER_X_MM = ( - TOP_SLOT_PAIR_CENTER_X_MM - 0.5 * TOP_SLOT_CENTER_SPACING_X_MM, - TOP_SLOT_PAIR_CENTER_X_MM + 0.5 * TOP_SLOT_CENTER_SPACING_X_MM, -) -M3_STANDOFF_CLEARANCE_RADIUS_MM = 1.7 -MIDDLE_SLOT_HALF_DX_MM = ( - math.cos(math.radians(MIDDLE_SLOT_ROTATION_DEG)) * 0.5 * MIDDLE_SLOT_LENGTH_MM -) -MIDDLE_SLOT_HALF_DZ_MM = ( - math.sin(math.radians(MIDDLE_SLOT_ROTATION_DEG)) * 0.5 * MIDDLE_SLOT_LENGTH_MM -) -LOWER_SLOT_TOP_Z_MM = LOWER_SLOT_CENTER_Z_MM + 0.5 * LOWER_SLOT_LENGTH_MM -MIDDLE_SLOT_LOWER_END_X_MM = tuple( - slot_x - MIDDLE_SLOT_HALF_DX_MM for slot_x in MIDDLE_SLOT_CENTER_X_MM -) -MIDDLE_SLOT_LOWER_END_Z_MM = MIDDLE_SLOT_CENTER_Z_MM - MIDDLE_SLOT_HALF_DZ_MM -MIDDLE_SLOT_UPPER_END_X_MM = tuple( - slot_x + MIDDLE_SLOT_HALF_DX_MM for slot_x in MIDDLE_SLOT_CENTER_X_MM -) -MIDDLE_SLOT_UPPER_END_Z_MM = MIDDLE_SLOT_CENTER_Z_MM + MIDDLE_SLOT_HALF_DZ_MM -TOP_SLOT_BOTTOM_Z_MM = TOP_SLOT_CENTER_Z_MM - 0.5 * TOP_SLOT_LENGTH_MM - - -def _side_profile_x_bounds_at_z(z_mm: float) -> tuple[float, float]: - if z_mm < LEFT_SLOPE_BOTTOM_Z_MM: - left_x = FOOT_X_MIN_MM - elif z_mm <= LEFT_SLOPE_TOP_Z_MM: - left_x = SLOPE_DX_PER_Z_MM * z_mm + SLOPE_LOW_X_INTERCEPT_MM - else: - left_x = UPPER_X_MIN_MM - - if z_mm < RIGHT_SLOPE_BOTTOM_Z_MM: - right_x = FOOT_X_MAX_MM - elif z_mm <= RIGHT_SLOPE_TOP_Z_MM: - right_x = SLOPE_DX_PER_Z_MM * z_mm + SLOPE_HIGH_X_INTERCEPT_MM - else: - right_x = UPPER_X_MAX_MM - - return left_x, right_x - - -def _centered_pair_x_at_z(z_mm: float) -> tuple[float, float]: - left_x, right_x = _side_profile_x_bounds_at_z(z_mm) - width = right_x - left_x - return left_x + 0.25 * width, left_x + 0.75 * width - - -LOWER_INTER_SLOT_STANDOFF_CENTER_Z_MM = 0.5 * ( - LOWER_SLOT_TOP_Z_MM + MIDDLE_SLOT_LOWER_END_Z_MM -) -LOWER_INTER_SLOT_STANDOFF_CENTER_X_MM = _centered_pair_x_at_z( - LOWER_INTER_SLOT_STANDOFF_CENTER_Z_MM -) -UPPER_INTER_SLOT_STANDOFF_CENTER_Z_MM = 0.5 * ( - MIDDLE_SLOT_UPPER_END_Z_MM + TOP_SLOT_BOTTOM_Z_MM -) -UPPER_INTER_SLOT_STANDOFF_CENTER_X_MM = _centered_pair_x_at_z( - UPPER_INTER_SLOT_STANDOFF_CENTER_Z_MM -) -STANDOFF_CENTER_XZ_MM = ( - ("lower_left", LOWER_INTER_SLOT_STANDOFF_CENTER_X_MM[0], LOWER_INTER_SLOT_STANDOFF_CENTER_Z_MM), - ("lower_right", LOWER_INTER_SLOT_STANDOFF_CENTER_X_MM[1], LOWER_INTER_SLOT_STANDOFF_CENTER_Z_MM), - ("upper_left", UPPER_INTER_SLOT_STANDOFF_CENTER_X_MM[0], UPPER_INTER_SLOT_STANDOFF_CENTER_Z_MM), - ("upper_right", UPPER_INTER_SLOT_STANDOFF_CENTER_X_MM[1], UPPER_INTER_SLOT_STANDOFF_CENTER_Z_MM), -) - - -def top_servo_case_transform( - case_span_centering_offset_mm: float = CASE_SPAN_CENTERING_OFFSET_MM, -) -> tuple[float, ...]: - return ( - 0.0, 1.0, 0.0, case_span_centering_offset_mm, - 0.0, 0.0, -1.0, 0.0, - -1.0, 0.0, 0.0, TOP_BODY_DOWN_Z_OFFSET_MM, - 0.0, 0.0, 0.0, 1.0, - ) - - -def _verify_top_servo_case_holes( - placed_servo: build123d.Shape, - *, - case_span_centering_offset_mm: float, -) -> None: - lc._verify_holes( - lc._planar_hole_centers( - placed_servo, - axis="x", - plane_coordinate=lc.SERVO_CASE_BOTTOM_LOCAL_Y_MM + case_span_centering_offset_mm, - ), - expected=tuple( - (side * lc.HOLE_SIDE_OFFSET_MM, CASE_FLUSH_HOLE_Z_MM) - for side in (-1.0, 1.0) - ), - radius=lc.SERVO_MOUNT_HOLE_RADIUS_MM, - label="case-bottom-face", - ) - lc._verify_holes( - lc._planar_hole_centers( - placed_servo, - axis="x", - plane_coordinate=lc.SERVO_CASE_TOP_LOCAL_Y_MM + case_span_centering_offset_mm, - ), - expected=tuple( - (side * lc.HOLE_SIDE_OFFSET_MM, CASE_OFFSET_HOLE_Z_MM) - for side in (-1.0, 1.0) - ), - radius=lc.SERVO_MOUNT_HOLE_RADIUS_MM, - label="case-top-face", - ) - - -def _placed_link_servos_case() -> tuple[build123d.Shape, build123d.Shape]: - top = lc.place_servo( - lc._load_servo_shape(lc.SERVO_STEP), - top_servo_case_transform(TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM), - ) - bottom = lc.place_servo( - lc._load_servo_shape(lc.SERVO_NO_REAR_HORN_STEP), - lc.BOTTOM_SERVO_TRANSFORM, - ) - _verify_top_servo_case_holes( - top, - case_span_centering_offset_mm=TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM, - ) - lc.verify_bottom_servo_mount_holes(bottom) - return top, bottom - - -def _xz_unit(vector: tuple[float, float]) -> tuple[float, float]: - length = math.hypot(vector[0], vector[1]) - if length <= 1e-9: - raise ValueError("Cannot normalize a zero-length X-Z vector") - return (vector[0] / length, vector[1] / length) - - -def _xz_add( - left: tuple[float, float], - right: tuple[float, float], -) -> tuple[float, float]: - return (left[0] + right[0], left[1] + right[1]) - - -def _xz_scale(vector: tuple[float, float], scale: float) -> tuple[float, float]: - return (vector[0] * scale, vector[1] * scale) - - -def _profile_point(point: tuple[float, float]) -> tuple[float, float, float]: - return (point[0], LEG_INNER_Y_MM, point[1]) - - -def _rounded_profile_edges( - points: list[tuple[float, float]], - *, - rounded_indices: set[int], - radius: float, -) -> list[build123d.Edge]: - if radius < 0.0: - raise ValueError("TRANSITION_CORNER_FILLET_RADIUS_MM must be >= 0") - - corner_data: list[tuple[tuple[float, float], tuple[float, float], tuple[float, float] | None]] = [] - for index, corner in enumerate(points): - if index not in rounded_indices or radius <= 0.0: - corner_data.append((corner, corner, None)) - continue - - previous = points[index - 1] - next_point = points[(index + 1) % len(points)] - toward_previous = _xz_unit((previous[0] - corner[0], previous[1] - corner[1])) - toward_next = _xz_unit((next_point[0] - corner[0], next_point[1] - corner[1])) - dot = max( - -1.0, - min(1.0, toward_previous[0] * toward_next[0] + toward_previous[1] * toward_next[1]), - ) - angle = math.acos(dot) - tangent = radius / math.tan(0.5 * angle) - previous_length = math.dist(previous, corner) - next_length = math.dist(next_point, corner) - if tangent >= min(previous_length, next_length): - raise ValueError( - "TRANSITION_CORNER_FILLET_RADIUS_MM is too large for the current " - f"profile: radius={radius:.3f} mm at point {corner}" - ) - - start = _xz_add(corner, _xz_scale(toward_previous, tangent)) - end = _xz_add(corner, _xz_scale(toward_next, tangent)) - bisector = _xz_unit(_xz_add(toward_previous, toward_next)) - center_distance = radius / math.sin(0.5 * angle) - center = _xz_add(corner, _xz_scale(bisector, center_distance)) - start_radius = (start[0] - center[0], start[1] - center[1]) - end_radius = (end[0] - center[0], end[1] - center[1]) - mid_radius = _xz_unit(_xz_add(start_radius, end_radius)) - midpoint = _xz_add(center, _xz_scale(mid_radius, radius)) - corner_data.append((start, end, midpoint)) - - edges: list[build123d.Edge] = [] - for index, (_start, end, _midpoint) in enumerate(corner_data): - next_start, next_end, next_midpoint = corner_data[(index + 1) % len(corner_data)] - if math.dist(end, next_start) > 1e-6: - edges.append(build123d.Edge.make_line(_profile_point(end), _profile_point(next_start))) - if next_midpoint is not None: - edges.append( - build123d.Edge.make_three_point_arc( - _profile_point(next_start), - _profile_point(next_midpoint), - _profile_point(next_end), - ) - ) - return edges - - -def _leg_profile() -> build123d.Face: - points = [ - (FOOT_X_MIN_MM, lc.FOOT_BOTTOM_Z_MM), - (FOOT_X_MAX_MM, lc.FOOT_BOTTOM_Z_MM), - (FOOT_X_MAX_MM, RIGHT_SLOPE_BOTTOM_Z_MM), - (UPPER_X_MAX_MM, RIGHT_SLOPE_TOP_Z_MM), - (UPPER_X_MAX_MM, LEG_TOP_Z_MM), - (UPPER_X_MIN_MM, LEG_TOP_Z_MM), - (UPPER_X_MIN_MM, LEFT_SLOPE_TOP_Z_MM), - (FOOT_X_MIN_MM, LEFT_SLOPE_BOTTOM_Z_MM), - ] - profile_edges = _rounded_profile_edges( - points, - rounded_indices={2, 3, 6, 7}, - radius=TRANSITION_CORNER_FILLET_RADIUS_MM, - ) - wires = list(build123d.Wire.combine(profile_edges)) - if len(wires) != 1: - raise RuntimeError(f"Leg profile produced {len(wires)} wires") - return build123d.Face(wires[0]) - - -def _slot_cut( - *, - x: float, - z: float, - length: float, - width: float, - rotation: float, -) -> build123d.Shape: - with build123d.BuildPart() as slot: - with build123d.BuildSketch(build123d.Plane.XZ): - with build123d.Locations((x, z)): - build123d.SlotOverall(width=length, height=width, rotation=rotation) - build123d.extrude(amount=SLOT_CUT_Y_SPAN_MM) - - return slot.part.moved(build123d.Location((0.0, SLOT_CUT_Y_OFFSET_MM, 0.0))) - - -def _slot_cuts() -> list[build123d.Shape]: - slots: list[build123d.Shape] = [] - for slot_x in LOWER_SLOT_CENTER_X_MM: - slots.append( - _slot_cut( - x=slot_x, - z=LOWER_SLOT_CENTER_Z_MM, - length=LOWER_SLOT_LENGTH_MM, - width=SLOT_WIDTH_MM, - rotation=VERTICAL_SLOT_ROTATION_DEG, - ) - ) - for slot_x, slot_z in MIDDLE_SLOT_CENTER_XZ_MM: - slots.append( - _slot_cut( - x=slot_x, - z=slot_z, - length=MIDDLE_SLOT_LENGTH_MM, - width=SLOT_WIDTH_MM, - rotation=MIDDLE_SLOT_ROTATION_DEG, - ) - ) - for slot_x in TOP_SLOT_CENTER_X_MM: - slots.append( - _slot_cut( - x=slot_x, - z=TOP_SLOT_CENTER_Z_MM, - length=TOP_SLOT_LENGTH_MM, - width=TOP_SLOT_WIDTH_MM, - rotation=VERTICAL_SLOT_ROTATION_DEG, - ) - ) - return slots - - -def _transition_corner_edges(bracket: build123d.Shape) -> list[build123d.Edge]: - corner_points = ( - (FOOT_X_MAX_MM, RIGHT_SLOPE_BOTTOM_Z_MM), - (UPPER_X_MAX_MM, RIGHT_SLOPE_TOP_Z_MM), - (UPPER_X_MIN_MM, LEFT_SLOPE_TOP_Z_MM), - (FOOT_X_MIN_MM, LEFT_SLOPE_BOTTOM_Z_MM), - ) - return list( - bracket.edges().filter_by(build123d.Axis.Y).filter_by( - lambda e: any( - abs(e.center().X - x) < 0.05 - and abs(e.center().Z - z) < 0.05 - and abs(e.length - lc.SHEET_THICKNESS_MM) < 0.05 - for x, z in corner_points - ) - ) - ) - - -def build_bracket() -> build123d.Shape: - leg = build123d.Solid.extrude( - _leg_profile(), build123d.Vector(0.0, lc.SHEET_THICKNESS_MM, 0.0) - ) - foot = lc.make_box( - x_min=FOOT_X_MIN_MM, - x_max=FOOT_X_MAX_MM, - y_min=FOOT_EDGE_DEFAULT_Y_MM, - y_max=LEG_OUTER_Y_MM, - z_min=lc.FOOT_BOTTOM_Z_MM, - z_max=lc.FOOT_TOP_Z_MM, - ) - bend_ear = lc.make_box( - x_min=TOP_FLANGE_EAR_X_MIN_MM, - x_max=TOP_FLANGE_EAR_X_MAX_MM, - y_min=LEG_INNER_Y_MM, - y_max=LEG_OUTER_Y_MM, - z_min=FLANGE_BOTTOM_Z_MM, - z_max=LEG_TOP_Z_MM, - ) - flange = lc.make_box( - x_min=FLANGE_WRAP_INNER_X_MM, - x_max=FLANGE_WRAP_OUTER_X_MM, - y_min=FLANGE_EDGE_Y_MM, - y_max=LEG_OUTER_Y_MM, - z_min=FLANGE_BOTTOM_Z_MM, - z_max=LEG_TOP_Z_MM, - ) - bracket: build123d.Shape = leg.fuse(foot).fuse(bend_ear).fuse(flange) - - # Raised-panel clearance: the only setback in the otherwise straight, - # flush foot inner edge. The servo's raised center panel protrudes to - # Z=1.9 over this X-span, so the foot edge steps out to clear it. The - # bay is a recess, so the foot covers it flush with no setback. - bracket = bracket.cut( - lc.make_box( - x_min=RAISED_ZONE_X_MM[0], - x_max=RAISED_ZONE_X_MM[1], - y_min=FOOT_EDGE_DEFAULT_Y_MM - CUT_EXTENSION_MM, - y_max=FOOT_EDGE_RAISED_Y_MM, - z_min=lc.FOOT_BOTTOM_Z_MM - CUT_EXTENSION_MM, - z_max=lc.FOOT_TOP_Z_MM + CUT_EXTENSION_MM, - ) - ) - for slot in _slot_cuts(): - bracket = bracket.cut(slot) - - # M3 cross-standoff holes. These bridge the left/right plates with - # 35 mm female-female standoffs, turning the pair into a ladder frame. - for _label, standoff_x, standoff_z in STANDOFF_CENTER_XZ_MM: - bracket = bracket.cut( - lc.make_y_cylinder( - x=standoff_x, - z=standoff_z, - radius=M3_STANDOFF_CLEARANCE_RADIUS_MM, - y_min=LEG_INNER_Y_MM - CUT_EXTENSION_MM, - y_max=LEG_OUTER_Y_MM + CUT_EXTENSION_MM, - ) - ) - - # Foot servo holes. - for hole_x in (lc.HOLE_NEAR_OFFSET_MM, lc.HOLE_FAR_OFFSET_MM): - bracket = bracket.cut( - lc.make_z_cylinder( - x=hole_x, - y=lc.HOLE_SIDE_OFFSET_MM, - radius=lc.SERVO_MOUNT_HOLE_RADIUS_MM, - z_min=lc.FOOT_BOTTOM_Z_MM - CUT_EXTENSION_MM, - z_max=lc.FOOT_TOP_Z_MM + CUT_EXTENSION_MM, - ) - ) - # Horn clearance: the output horn boss sits on the pitch axis and reaches - # toward the wrap flange. The relief arc stays concentric with the horn so - # the remaining screw web can be measured directly from that axis. - bracket = bracket.cut( - lc.make_x_cylinder( - y=0.0, - z=TOP_PIVOT_Z_MM, - radius=OUTPUT_HORN_RELIEF_RADIUS_MM, - x_min=FLANGE_WRAP_INNER_X_MM - CUT_EXTENSION_MM, - x_max=FLANGE_WRAP_OUTER_X_MM + CUT_EXTENSION_MM, - ) - ) - # Flange servo holes (the case-top +Y pair). - for hole_z in (CASE_OFFSET_HOLE_Z_MM, TOP_FLANGE_UPPER_HOLE_Z_MM): - bracket = bracket.cut( - lc.make_x_cylinder( - y=lc.HOLE_SIDE_OFFSET_MM, - z=hole_z, - radius=lc.SERVO_MOUNT_HOLE_RADIUS_MM, - x_min=FLANGE_WRAP_INNER_X_MM - CUT_EXTENSION_MM, - x_max=FLANGE_WRAP_OUTER_X_MM + CUT_EXTENSION_MM, - ) - ) - - # Round the foot-tab inside corners (Z-direction edges at Y=7.5 where - # the tabs meet the setback) and the leg top corner. - foot_tab_x = (FOOT_X_MIN_MM, LEFT_TAB_RIGHT_X_MM, RAISED_ZONE_X_MM[1], FOOT_X_MAX_MM) - leg_x = LEG_RUN_X_MM[0] - foot_tab_corners = bracket.edges().filter_by(build123d.Axis.Z).filter_by( - lambda e: abs(e.center().Y - FOOT_EDGE_DEFAULT_Y_MM) < 0.05 - and any(abs(e.center().X - x) < 0.05 for x in foot_tab_x) - and abs(e.length - lc.SHEET_THICKNESS_MM) < 0.05 - ) - bracket = bracket.fillet(FORMED_FLANGE_CORNER_FILLET_RADIUS_MM, foot_tab_corners) - - wrap_flange_bottom_free_edges = bracket.edges().filter_by(build123d.Axis.X).filter_by( - lambda e: abs(e.center().Y - FLANGE_EDGE_Y_MM) < 0.05 - and abs(e.center().Z - FLANGE_BOTTOM_Z_MM) < 0.05 - and abs(e.length - lc.SHEET_THICKNESS_MM) < 0.05 - ) - if len(wrap_flange_bottom_free_edges) != 1: - raise RuntimeError( - "Expected one wrap flange bottom free corner to fillet, " - f"found {len(wrap_flange_bottom_free_edges)}" - ) - bracket = bracket.fillet( - FORMED_FLANGE_CORNER_FILLET_RADIUS_MM, - wrap_flange_bottom_free_edges, - ) - - leg_top_corners = bracket.edges().filter_by(build123d.Axis.Y).filter_by( - lambda e: abs(e.center().X - leg_x) < 0.05 - and abs(e.center().Z - LEG_TOP_Z_MM) < 0.05 - and abs(e.length - lc.SHEET_THICKNESS_MM) < 0.05 - ) - bracket = bracket.fillet(1.5, leg_top_corners) - - return bracket - - -def build_step() -> build123d.Shape: - bracket = build_bracket() - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected plate solid, found {len(solids)}") - - top_servo, bottom_servo = _placed_link_servos_case() - top_volume = lc.verify_no_interference(bracket, top_servo, label="the top servo") - bottom_volume = lc.verify_no_interference(bracket, bottom_servo, label="the bottom servo") - - bracket.label = PART_NAME - bracket.color = lc.BRACKET_COLOR - - bb = bracket.bounding_box() - foot_flange = LEG_OUTER_Y_MM - FOOT_EDGE_DEFAULT_Y_MM - foot_flange_raised = LEG_OUTER_Y_MM - FOOT_EDGE_RAISED_Y_MM - wrap_flange = LEG_OUTER_Y_MM - FLANGE_EDGE_Y_MM - # Worst-case wrap-flange depth at the horn-relief intrusion (top of flange). - horn_intrusion_y = ( - OUTPUT_HORN_RELIEF_RADIUS_MM**2 - (TOP_PIVOT_Z_MM - LEG_TOP_Z_MM) ** 2 - ) ** 0.5 - wrap_flange_at_horn = LEG_OUTER_Y_MM - horn_intrusion_y - hole_to_bend = LEG_OUTER_Y_MM - lc.HOLE_SIDE_OFFSET_MM - half_die = lc.SCS_HALF_DIE_WIDTH_MM - min_flange = lc.SCS_MIN_FLANGE_AFTER_BEND_MM - flange_check_status = ( - "PASS" - if min(foot_flange, foot_flange_raised, wrap_flange, wrap_flange_at_horn) - + 1e-6 - >= min_flange - else "CHECK" - ) - hole_to_bend_status = "PASS" if hole_to_bend + 1e-6 >= half_die else "CHECK" - print( - "Wrap plate envelope " - f"X={bb.size.X:.3f} mm, Y={bb.size.Y:.3f} mm, Z={bb.size.Z:.3f} mm" - ) - print( - f"Height parameter: overall={LINK_BRACKET_OVERALL_HEIGHT_MM:.3f} mm " - f"(scale {HEIGHT_SCALE:.4f} from the 180 mm baseline)" - ) - print( - "Top case centering offset " - f"{TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM:.3f} mm " - f"(face-to-flange shift {TOP_SERVO_FACE_TO_FLANGE_X_OFFSET_MM:.3f} mm); " - f"case-top face X={FLANGE_WRAP_INNER_X_MM:.3f} mm" - ) - print( - f"Side profile bands: lower to low/high-X Z=" - f"{LEFT_SLOPE_BOTTOM_Z_MM:.1f}/{RIGHT_SLOPE_BOTTOM_Z_MM:.1f} mm, " - f"steeper slope to low/high-X Z=" - f"{LEFT_SLOPE_TOP_Z_MM:.1f}/{RIGHT_SLOPE_TOP_Z_MM:.1f} mm, " - f"upper to axis Z={TOP_PIVOT_Z_MM:.1f} mm; " - f"top servo pitch axis Z={TOP_PIVOT_Z_MM:.1f} mm" - ) - print( - f"Web width check: lower/upper bands {SECTION_WIDTH_MM:.3f} mm, " - f"local flange bend leeway {TOP_FLANGE_BEND_LEEWAY_MM:.3f} mm, " - f"sloped band normal width {SLOPED_BAND_NORMAL_WIDTH_MM:.3f} mm, " - f"low-X transition shift {LOW_X_SLOPE_Z_SHIFT_DOWN_MM:.3f} mm down" - ) - print( - "Transition corner radius: " - + ( - "disabled" - if TRANSITION_CORNER_FILLET_RADIUS_MM == 0.0 - else f"{TRANSITION_CORNER_FILLET_RADIUS_MM:.1f} mm" - ) - ) - print( - "Slots: 2x lower vertical, 2x sloped transition, 2x upper vertical " - f"(lengths {LOWER_SLOT_LENGTH_MM:.1f}/{MIDDLE_SLOT_LENGTH_MM:.1f}/" - f"{TOP_SLOT_LENGTH_MM:.1f} mm, widths {SLOT_WIDTH_MM:.1f}/{TOP_SLOT_WIDTH_MM:.1f} mm)" - ) - print( - "Cross standoffs: " - f"{len(STANDOFF_CENTER_XZ_MM)}x M3 female-female, length={LINK_STANDOFF_LENGTH_MM:.3f} mm, " - f"inner bracket gap={2.0 * LEG_INNER_Y_MM:.3f} mm, " - f"clearance per servo side={LEG_FACE_CLEARANCE_MM:.3f} mm" - ) - print( - f"Sloped slot margins: low edge {MIDDLE_SLOT_LOW_EDGE_MARGIN_MM:.3f} mm, " - f"high edge {MIDDLE_SLOT_HIGH_EDGE_MARGIN_MM:.3f} mm" - ) - print( - "Sheet setup " - f"material=5052-H32 (ALU-063), thickness={lc.SHEET_THICKNESS_MM:.4f} mm, " - "bends=2 (continuous foot bend, full-length wrap bend)" - ) - print( - "SendCutSend flange checks (min after-bend " - f"{min_flange:.3f} mm): " - f"foot default {foot_flange:.2f} mm, " - f"foot raised-zone setback {foot_flange_raised:.2f} mm, " - f"wrap main {wrap_flange:.2f} mm, " - f"wrap at horn-relief Z={LEG_TOP_Z_MM:.1f} {wrap_flange_at_horn:.2f} mm " - f"-- {flange_check_status}" - ) - print( - f"SendCutSend hole-to-bend checks (half die {half_die:.3f} mm): " - f"foot screws {hole_to_bend:.2f} mm, wrap-flange screws {hole_to_bend:.2f} mm " - f"-- {hole_to_bend_status}" - ) - print( - "Top flange hole-to-horn-relief web: " - f"{TOP_FLANGE_HOLE_TO_RELIEF_EDGE_GAP_MM:.3f} mm " - f"(relief radius {OUTPUT_HORN_RELIEF_RADIUS_MM:.3f} mm)" - ) - print( - f"Clearances: leg gap {LEG_FACE_CLEARANCE_MM:.1f} mm off the servo side face; " - f"bend-ear gives {TOP_FLANGE_BEND_LEEWAY_MM:.3f} mm of flat lead-in; " - "flange inner face seats on the aligned top servo case face; " - f"flange inner edge at Y={FLANGE_EDGE_Y_MM} clears the case-top ridge" - ) - print(f"Top servo interference volume (mm^3): {top_volume:.6f}") - print(f"Bottom servo interference volume (mm^3): {bottom_volume:.6f}") - return bracket - - -def _polyline_points_from_ring(ring) -> list[tuple[float, float]]: - points = [(float(x), float(y)) for x, y in ring.coords] - if points and points[0] == points[-1]: - points.pop() - return points - - -def _add_closed_polyline(msp, points: list[tuple[float, float]], *, layer: str) -> None: - if len(points) < 3: - return - msp.add_lwpolyline(points, close=True, dxfattribs={"layer": layer}) - - -def _axis_value(vector: build123d.Vector, axis: str) -> float: - return {"x": vector.X, "y": vector.Y, "z": vector.Z}[axis] - - -def _select_planar_face( - shape: build123d.Shape, - *, - normal_axis: str, - normal_sign: float, - coordinate_axis: str, - coordinate: float, -) -> build123d.Face: - candidates = [] - for face in shape.faces(): - if face.geom_type != build123d.GeomType.PLANE: - continue - normal = face.normal_at() - center = face.center() - if abs(_axis_value(normal, normal_axis) - normal_sign) > 0.01: - continue - if abs(_axis_value(center, coordinate_axis) - coordinate) > 0.05: - continue - candidates.append(face) - if not candidates: - raise RuntimeError( - "Could not find topology face for DXF unfolding: " - f"normal {normal_sign:+.0f}{normal_axis}, {coordinate_axis}={coordinate:.3f}" - ) - return max(candidates, key=lambda face: face.area) - - -def _project_wire_points( - wire: build123d.Wire, - project, - *, - max_segment_mm: float = 0.25, -) -> list[tuple[float, float]]: - sample_count = max(16, int(math.ceil(wire.length / max_segment_mm))) - points: list[tuple[float, float]] = [] - for index in range(sample_count): - point = project(wire.position_at(index / sample_count)) - if not points or math.dist(points[-1], point) > 1e-6: - points.append(point) - if len(points) >= 2 and math.dist(points[0], points[-1]) < 1e-6: - points.pop() - return points - - -def _project_face_polygon(face: build123d.Face, project): - from shapely.geometry import Polygon - - outer = _project_wire_points(face.outer_wire(), project) - holes = [ - _project_wire_points(wire, project) - for wire in face.inner_wires() - if wire.length > 1e-6 - ] - polygon = Polygon(outer, holes) - if not polygon.is_valid: - polygon = polygon.buffer(0) - if polygon.is_empty: - raise RuntimeError("Projected STEP topology face produced an empty flat-pattern polygon") - return polygon - - -def _validate_sendcutsend_bend_rule() -> None: - expected_bend_allowance = ( - 2.0 * lc.SCS_OUTSIDE_SETBACK_90_MM - ) - lc.SCS_BEND_DEDUCTION_90_MM - if abs(expected_bend_allowance - lc.SCS_BEND_ALLOWANCE_90_MM) > 0.01: - raise RuntimeError( - "Inconsistent SendCutSend bend rule constants: " - f"outside_setback={lc.SCS_OUTSIDE_SETBACK_90_MM:.3f} mm, " - f"bend_deduction={lc.SCS_BEND_DEDUCTION_90_MM:.3f} mm, " - f"bend_allowance={lc.SCS_BEND_ALLOWANCE_90_MM:.3f} mm, " - f"expected={expected_bend_allowance:.3f} mm" - ) - - -def _build_flat_pattern() -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = lc.SCS_OUTSIDE_SETBACK_90_MM - bend_allowance = lc.SCS_BEND_ALLOWANCE_90_MM - panel_join_shift = bend_allowance - outside_setback - return FlatPattern( - outside_setback_mm=outside_setback, - bend_allowance_mm=bend_allowance, - bend_deduction_mm=lc.SCS_BEND_DEDUCTION_90_MM, - panel_join_shift_mm=panel_join_shift, - foot_bend_line_y_mm=lc.FOOT_BOTTOM_Z_MM + (0.5 * bend_allowance), - wrap_bend_line_x_mm=( - FLANGE_WRAP_INNER_X_MM + panel_join_shift + (0.5 * bend_allowance) - ), - ) - - -def _flat_pattern_geometry_from_step_topology(): - from shapely.ops import unary_union - - flat_pattern = _build_flat_pattern() - outside_setback = flat_pattern.outside_setback_mm - panel_join_shift = flat_pattern.panel_join_shift_mm - - bracket = build_bracket() - leg_face = _select_planar_face( - bracket, - normal_axis="y", - normal_sign=1.0, - coordinate_axis="y", - coordinate=LEG_OUTER_Y_MM, - ) - foot_face = _select_planar_face( - bracket, - normal_axis="z", - normal_sign=-1.0, - coordinate_axis="z", - coordinate=lc.FOOT_BOTTOM_Z_MM, - ) - wrap_flange_face = _select_planar_face( - bracket, - normal_axis="x", - normal_sign=1.0, - coordinate_axis="x", - coordinate=FLANGE_WRAP_OUTER_X_MM, - ) - - def project_leg(point: build123d.Vector) -> tuple[float, float]: - flat_x = point.X - flat_z = point.Z - if abs(point.X - FLANGE_WRAP_INNER_X_MM) <= 1e-3: - flat_x = FLANGE_WRAP_INNER_X_MM + panel_join_shift - if abs(point.Z - lc.FOOT_BOTTOM_Z_MM) <= 1e-3: - flat_z = lc.FOOT_BOTTOM_Z_MM - elif point.Z > lc.FOOT_BOTTOM_Z_MM: - flat_z = point.Z + panel_join_shift - return (flat_x, flat_z) - - def project_foot(point: build123d.Vector) -> tuple[float, float]: - if abs(point.Y - LEG_OUTER_Y_MM) <= 1e-3: - flat_y = lc.FOOT_BOTTOM_Z_MM - else: - flat_y = point.Y - LEG_OUTER_Y_MM + outside_setback - return (point.X, flat_y) - - def project_wrap_flange(point: build123d.Vector) -> tuple[float, float]: - if abs(point.Y - LEG_OUTER_Y_MM) <= 1e-3: - flat_x = FLANGE_WRAP_INNER_X_MM + panel_join_shift - else: - flat_x = ( - FLANGE_WRAP_INNER_X_MM - + panel_join_shift - + (LEG_OUTER_Y_MM - point.Y) - - outside_setback - + flat_pattern.bend_allowance_mm - ) - flat_z = point.Z + panel_join_shift if point.Z > lc.FOOT_BOTTOM_Z_MM else point.Z - return (flat_x, flat_z) - - leg = _project_face_polygon(leg_face, project_leg) - foot = _project_face_polygon( - foot_face, - project_foot, - ) - wrap_flange = _project_face_polygon( - wrap_flange_face, - project_wrap_flange, - ) - _validate_projected_flange_depths(foot, wrap_flange, flat_pattern) - - flat_geometry = unary_union([leg, foot, wrap_flange]).buffer(0) - if not flat_geometry.is_valid: - flat_geometry = flat_geometry.buffer(0) - # Keep the linework compact while preserving the sampled STEP curves within - # a few microns. The samples still originate from the formed solid topology. - flat_geometry = flat_geometry.simplify(0.002, preserve_topology=True) - - from shapely.geometry import LineString - - foot_bounds = foot_face.bounding_box() - wrap_bounds = wrap_flange_face.bounding_box() - bend_lines = ( - LineString( - [ - (foot_bounds.min.X, lc.FOOT_BOTTOM_Z_MM), - (foot_bounds.max.X, lc.FOOT_BOTTOM_Z_MM), - ] - ), - LineString( - [ - ( - flat_pattern.wrap_bend_line_x_mm, - wrap_bounds.min.Z + panel_join_shift, - ), - ( - flat_pattern.wrap_bend_line_x_mm, - wrap_bounds.max.Z + panel_join_shift, - ), - ] - ), - ) - bend_lines = ( - LineString( - [ - (foot_bounds.min.X, flat_pattern.foot_bend_line_y_mm), - (foot_bounds.max.X, flat_pattern.foot_bend_line_y_mm), - ] - ), - bend_lines[1], - ) - return flat_geometry, bend_lines, flat_pattern - - -def _validate_projected_flange_depths( - foot, - wrap_flange, - flat_pattern: FlatPattern, -) -> None: - foot_formed_depth = LEG_OUTER_Y_MM - FOOT_EDGE_DEFAULT_Y_MM - wrap_formed_depth = LEG_OUTER_Y_MM - FLANGE_EDGE_Y_MM - foot_expected = ( - foot_formed_depth - - flat_pattern.outside_setback_mm - + (0.5 * flat_pattern.bend_allowance_mm) - ) - wrap_expected = ( - wrap_formed_depth - - flat_pattern.outside_setback_mm - + (0.5 * flat_pattern.bend_allowance_mm) - ) - foot_actual = flat_pattern.foot_bend_line_y_mm - foot.bounds[1] - wrap_actual = wrap_flange.bounds[2] - flat_pattern.wrap_bend_line_x_mm - - checks = ( - ("bottom flange", foot_actual, foot_expected), - ("top wrap flange", wrap_actual, wrap_expected), - ) - for name, actual, expected in checks: - if abs(actual - expected) > 0.01: - raise RuntimeError( - f"Projected {name} DXF depth is inconsistent with bend math: " - f"actual={actual:.3f} mm, expected={expected:.3f} mm" - ) - - if abs(foot_formed_depth - wrap_formed_depth) <= 0.01 and abs(foot_actual - wrap_actual) > 0.01: - raise RuntimeError( - "Projected link bracket flange depths are not equal despite equal formed depths: " - f"bottom={foot_actual:.3f} mm, top={wrap_actual:.3f} mm" - ) - - -def _add_flat_pattern_entities(msp, flat_geometry, bend_lines) -> None: - polygons = [flat_geometry] if flat_geometry.geom_type == "Polygon" else list(flat_geometry.geoms) - for polygon in polygons: - dxf_topology.add_ring(msp, polygon.exterior, layer="CUT") - for interior in polygon.interiors: - dxf_topology.add_ring(msp, interior, layer="CUT", prefer_circle=True) - - for line in bend_lines: - coords = list(line.coords) - msp.add_line( - coords[0], - coords[-1], - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - - -def build_dxf(*, part_name: str = PART_NAME): - import ezdxf - from ezdxf.units import MM as DXF_UNIT_MM - - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - flat_geometry, bend_lines, flat_pattern = _flat_pattern_geometry_from_step_topology() - _add_flat_pattern_entities(doc.modelspace(), flat_geometry, bend_lines) - - min_x, min_y, max_x, max_y = flat_geometry.bounds - print( - f"{part_name} flat DXF " - f"overall_height={LINK_BRACKET_OVERALL_HEIGHT_MM:.3f} mm, " - f"bounds=({max_x - min_x:.3f} x {max_y - min_y:.3f}) mm, " - f"topology_unfold_faces=3, bend_deduction_90={flat_pattern.bend_deduction_mm:.3f} mm, " - f"bend_allowance_90={flat_pattern.bend_allowance_mm:.3f} mm, " - f"bend_lines={len(bend_lines)}, layers=CUT/BEND" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/parts/link_bracket_left.py b/models/robots/tom/v2/parts/link_bracket_left.py deleted file mode 100644 index d1e46b68..00000000 --- a/models/robots/tom/v2/parts/link_bracket_left.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from v2_variant_common import finish_part, loaded_v2_module, mirror_y - - -PART_NAME = Path(__file__).stem - - -def build_step(): - with loaded_v2_module("link_bracket") as module: - return finish_part(mirror_y(module.build_step()), PART_NAME) - - -def build_dxf(): - with loaded_v2_module("link_bracket") as module: - return module.build_dxf(part_name=PART_NAME) - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } diff --git a/models/robots/tom/v2/parts/link_bracket_left.step b/models/robots/tom/v2/parts/link_bracket_left.step deleted file mode 100644 index 6492f4df..00000000 --- a/models/robots/tom/v2/parts/link_bracket_left.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5c6178adc2dc5d6649904f29998d1e2fcd777afcb6a81fae6480f95a329ebeb -size 245709 diff --git a/models/robots/tom/v2/parts/link_bracket_right.py b/models/robots/tom/v2/parts/link_bracket_right.py deleted file mode 100644 index 90e17dfa..00000000 --- a/models/robots/tom/v2/parts/link_bracket_right.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from v2_variant_common import finish_part, loaded_v2_module - - -PART_NAME = Path(__file__).stem - - -def build_step(): - with loaded_v2_module("link_bracket") as module: - return finish_part(module.build_step(), PART_NAME) - - -def build_dxf(): - with loaded_v2_module("link_bracket") as module: - return module.build_dxf(part_name=PART_NAME) - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } diff --git a/models/robots/tom/v2/parts/link_bracket_right.step b/models/robots/tom/v2/parts/link_bracket_right.step deleted file mode 100644 index a6129479..00000000 --- a/models/robots/tom/v2/parts/link_bracket_right.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb4ea32b712ccba38deec849dcb86cd9ea1b387cbb5f469ad2b622d17808fac8 -size 267080 diff --git a/models/robots/tom/v2/parts/link_common.py b/models/robots/tom/v2/parts/link_common.py deleted file mode 100644 index 2b587e4e..00000000 --- a/models/robots/tom/v2/parts/link_common.py +++ /dev/null @@ -1,651 +0,0 @@ -#!/usr/bin/env python3 -""" -Shared layout for the tom v2 servo-to-servo link brackets. - -Link frame convention (millimeters, Z-up): -- +Z is up. The bottom servo's output shaft is coaxial with the link's - vertical centerline (X = Y = 0) and points down (-Z); its case-bottom - face is up at Z = 0 and its body runs along +X (rear end at - X = -10.11, front end at X = +35.11). It uses the rear-horn-less - variant (parts/imports/sts3250_no_rear_horn.step) so bracket feet seat flat - on its case-bottom face; only the case's small rear journal/screw stub - still protrudes there. -- The top servo forms a horizontal pitch joint: it stands on end with - its horn axis along X at (Y = 0, Z = TOP_PIVOT_Z_MM) and the - horn-to-horn span centered on the link centerline (output horn face at - X = +18.3, rear horn face at X = -18.3). Two mounting variations are - supported: - - horn mount: the bracket horn plates bolt to both horn faces - (4x M3-pattern screws per side) and the servo body, which belongs to - the next link, is flipped to stand above the pivot so it rotates - freely. - - case mount: the brackets bolt to the servo case with 2x M2 screws - per side (flush side into the case-bottom face holes at - Y = +/-10.25, Z = 102.25; offset side into the case-top face holes - at Y = +/-10.25, Z = 106.0), the body hangs down into the link, and - the next link's yoke grabs the free horns. - -All brackets are authored directly in this link frame so the bracket -STEP files and the verification assemblies compose with identity -transforms for the brackets. - -Servo source geometry (STS3250 local frame, from -parts/imports/sts3250.step): -- body bounds x [-35.61, 9.61], y [-28.2, 9.2], z [-12.36, +12.36] -- output shaft axis at (x, z) = (-25.5, 0), pointing +y; output horn - face at y = +9.2, rear horn face at y = -27.4, each with four - diameter-3.2 screw holes on a 7 mm circle plus a center hole; the rear - journal/screw stub protrudes to y = -28.2 -- case-bottom plane y = -25.6 with four M2 holes (r 1.1) at - x in {-17.2, +7.25}, z = +/-10.25, facing -y -- case-top plane y = +6.4 with four M2 holes (r 1.1) at - x in {-17.2, +3.5}, z = +/-10.25, facing +y, around the output horn - boss (radius 11.2) -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -TOM_DIR = Path(__file__).resolve().parents[2] -V2_DIR = Path(__file__).resolve().parents[1] -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) - -import build123d - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.materials import GRAY_ALUMINUM_COLOR -from robot_common.step_import import import_as_shape - - -# Plain bare-aluminum tint for the v2 brackets. Without an explicit source -# color the viewer falls back to its dark theme paint, which reads as a -# deliberately colored part. -BRACKET_COLOR = GRAY_ALUMINUM_COLOR - -SERVO_STEP = V2_DIR / "parts" / "imports" / "sts3250.step" -SERVO_NO_REAR_HORN_STEP = V2_DIR / "parts" / "imports" / "sts3250_no_rear_horn.step" - -def _env_float(name: str, default: float) -> float: - value = os.environ.get(name) - if value is None: - return default - try: - parsed = float(value) - except ValueError as exc: - raise ValueError(f"{name} must be a number, got {value!r}") from exc - if parsed <= 0.0: - raise ValueError(f"{name} must be positive, got {parsed!r}") - return parsed - - -# Sheet stock for the v2 aluminum brackets (SendCutSend 0.063 in 5052-H32). -SENDCUTSEND_ALU_063_THICKNESS_MM = 25.4 * 0.063 -SHEET_THICKNESS_MM = _env_float( - "TOM_V2_SHEET_THICKNESS_MM", - SENDCUTSEND_ALU_063_THICKNESS_MM, -) -SCS_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.035 -SCS_K_FACTOR = 0.42 -SCS_BEND_DEDUCTION_90_MM = 25.4 * 0.096 -SCS_OUTSIDE_SETBACK_90_MM = SCS_EFFECTIVE_BEND_RADIUS_MM + SHEET_THICKNESS_MM -SCS_BEND_ALLOWANCE_90_MM = (2.0 * SCS_OUTSIDE_SETBACK_90_MM) - SCS_BEND_DEDUCTION_90_MM -MOUNT_PLANE_CLEARANCE_MM = 0.0 -SIDE_FACE_CLEARANCE_MM = 0.25 -# Gap between a horn mount face and its bracket horn plate, matching the -# v1 servo_horn_yoke convention. Also used for case-face plates. -HORN_FACE_GAP_MM = 0.25 -ROTATING_PART_CLEARANCE_MM = 0.5 - -# STS3250 local-frame facts (verified at build time against the STEP). -SERVO_HALF_WIDTH_MM = 12.360025 -SERVO_BODY_X_MIN_MM = -35.610023 -SERVO_BODY_X_MAX_MM = 9.610026 -SERVO_SHAFT_LOCAL_X_MM = -25.5 -SERVO_CASE_BOTTOM_LOCAL_Y_MM = -25.6 -SERVO_CASE_TOP_LOCAL_Y_MM = 6.4 -SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM = 9.2 -SERVO_REAR_HORN_FACE_LOCAL_Y_MM = -27.4 -# The rear journal/screw stub protrudes 0.8 mm past the rear horn face. -SERVO_REAR_EXTREME_LOCAL_Y_MM = -28.2 -SERVO_MOUNT_HOLE_LOCAL_X_MM = (-17.2, 7.25) -SERVO_TOP_MOUNT_HOLE_LOCAL_X_MM = (-17.2, 3.5) -SERVO_MOUNT_HOLE_LOCAL_Z_MM = 10.25 -SERVO_MOUNT_HOLE_RADIUS_MM = 1.1 -SERVO_HORN_SCREW_CIRCLE_RADIUS_MM = 7.0 -SERVO_HORN_SCREW_HOLE_RADIUS_MM = 1.6 -SERVO_REAR_HORN_DISK_RADIUS_MM = 9.6 -SERVO_OUTPUT_HORN_BOSS_RADIUS_MM = 11.2 -SERVO_REAR_JOURNAL_RADIUS_MM = 3.05 -REAR_JOURNAL_CLEARANCE_RADIUS_MM = 3.75 - -# Link-frame layout derived from the facts above. -BOTTOM_FACE_Z_MM = 0.0 -PLATE_HALF_SPAN_MM = SERVO_HALF_WIDTH_MM -PLATE_INNER_OFFSET_MM = SERVO_HALF_WIDTH_MM + SIDE_FACE_CLEARANCE_MM -PLATE_OUTER_OFFSET_MM = PLATE_INNER_OFFSET_MM + SHEET_THICKNESS_MM -# Servo body extents measured from the shaft axis along the body direction. -BODY_REAR_OVERHANG_MM = -(SERVO_BODY_X_MIN_MM - SERVO_SHAFT_LOCAL_X_MM) # 10.11 -BODY_FRONT_REACH_MM = SERVO_BODY_X_MAX_MM - SERVO_SHAFT_LOCAL_X_MM # 35.11 -# Mount hole columns measured from the shaft axis along the body direction. -HOLE_NEAR_OFFSET_MM = SERVO_MOUNT_HOLE_LOCAL_X_MM[0] - SERVO_SHAFT_LOCAL_X_MM # 8.3 -HOLE_FAR_OFFSET_MM = SERVO_MOUNT_HOLE_LOCAL_X_MM[1] - SERVO_SHAFT_LOCAL_X_MM # 32.75 -HOLE_SIDE_OFFSET_MM = SERVO_MOUNT_HOLE_LOCAL_Z_MM # 10.25 - -# The offset-side web sits outboard of the bottom servo's front end face. -# Its position is set by SendCutSend's U/C-channel rule: the jog channel -# base (web inner face to offset-wall outer face) must be at least 2x the -# screw-tab flange height. 39.5 gives a 22.15 mm base vs a 21.62 mm -# requirement. -OUTER_WEB_INNER_X_MM = 39.5 -OUTER_WEB_OUTER_X_MM = OUTER_WEB_INNER_X_MM + SHEET_THICKNESS_MM -OUTER_WEB_CLEARANCE_MM = OUTER_WEB_INNER_X_MM - BODY_FRONT_REACH_MM - -# Top pitch joint: the horn axis runs along X at this height. For the -# case-mounted link, center the visible case span on the link centerline so -# the top servo body reads centered over the bottom horn axis; the pitch axis -# still intersects that vertical axis at X = Y = 0. -TOP_PIVOT_Z_MM = 180.0 -HORN_HALF_SPAN_MM = 0.5 * ( - SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - SERVO_REAR_HORN_FACE_LOCAL_Y_MM -) # 18.3 -# Offset that centers the horn span: local y -> X = y + this value. -HORN_SPAN_CENTERING_OFFSET_MM = HORN_HALF_SPAN_MM - SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM # 9.1 -# Offset that centers the case side faces for the case-mounted top servo. -CASE_SPAN_CENTERING_OFFSET_MM = -0.5 * ( - SERVO_CASE_BOTTOM_LOCAL_Y_MM + SERVO_CASE_TOP_LOCAL_Y_MM -) # 9.6 -# Z mapping constants for the two top-servo orientations. -TOP_BODY_DOWN_Z_OFFSET_MM = TOP_PIVOT_Z_MM + SERVO_SHAFT_LOCAL_X_MM # Z = 109.5 - x -TOP_BODY_UP_Z_OFFSET_MM = TOP_PIVOT_Z_MM - SERVO_SHAFT_LOCAL_X_MM # Z = x + 160.5 - -# Horn-mount plates (horn variation). -HORN_PLATE_INNER_X_MM = HORN_HALF_SPAN_MM + HORN_FACE_GAP_MM -HORN_PLATE_OUTER_X_MM = HORN_PLATE_INNER_X_MM + SHEET_THICKNESS_MM -HORN_PLATE_TOP_Z_MM = TOP_PIVOT_Z_MM + 13.0 - -# Case-mount plates (case variation). The plates sit against the case -# faces and stop below the rotating horn parts. -CASE_BOTTOM_FACE_X_MM = SERVO_CASE_BOTTOM_LOCAL_Y_MM + CASE_SPAN_CENTERING_OFFSET_MM # -16.0 -CASE_TOP_FACE_X_MM = SERVO_CASE_TOP_LOCAL_Y_MM + CASE_SPAN_CENTERING_OFFSET_MM # +16.0 -CASE_FLUSH_PLATE_INNER_X_MM = -(abs(CASE_BOTTOM_FACE_X_MM) + HORN_FACE_GAP_MM) # -16.75 -CASE_FLUSH_PLATE_OUTER_X_MM = CASE_FLUSH_PLATE_INNER_X_MM - SHEET_THICKNESS_MM -CASE_OFFSET_PLATE_INNER_X_MM = CASE_TOP_FACE_X_MM + HORN_FACE_GAP_MM # +15.75 -CASE_OFFSET_PLATE_OUTER_X_MM = CASE_OFFSET_PLATE_INNER_X_MM + SHEET_THICKNESS_MM -CASE_FLUSH_PLATE_TOP_Z_MM = ( - TOP_PIVOT_Z_MM - SERVO_REAR_HORN_DISK_RADIUS_MM - ROTATING_PART_CLEARANCE_MM -) # 124.9 -CASE_OFFSET_PLATE_TOP_Z_MM = ( - TOP_PIVOT_Z_MM - SERVO_OUTPUT_HORN_BOSS_RADIUS_MM - ROTATING_PART_CLEARANCE_MM -) # 123.3 -# Case-mount screw hole heights (Z = TOP_BODY_DOWN_Z_OFFSET_MM - local x). -CASE_FLUSH_HOLE_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM - SERVO_MOUNT_HOLE_LOCAL_X_MM[1] # 102.25 -CASE_OFFSET_HOLE_Z_MM = TOP_BODY_DOWN_Z_OFFSET_MM - SERVO_TOP_MOUNT_HOLE_LOCAL_X_MM[1] # 106.0 -# Both case faces carry raised center sections that stand proud of the -# screw-boss planes (measured from the STEP): a 1.9 mm panel within -# Y +/-9.242 starting at Z = 105.3 on the case-bottom side, and a 1.1 mm -# ridge within Y +/-7.0 starting at the body's lower end (Z = 99.89) on -# the case-top side. The case plates get open-top relief notches around -# them and seat on the flat outer margins. -CASE_FLUSH_RELIEF_HALF_WIDTH_MM = 9.242 + ROTATING_PART_CLEARANCE_MM -CASE_FLUSH_RELIEF_BOTTOM_Z_MM = 105.297 - ROTATING_PART_CLEARANCE_MM -CASE_OFFSET_RELIEF_HALF_WIDTH_MM = 7.0 + ROTATING_PART_CLEARANCE_MM -CASE_OFFSET_RELIEF_BOTTOM_Z_MM = 99.894 - ROTATING_PART_CLEARANCE_MM - -# Case-mount brackets: two separate formed parts (split per SendCutSend's -# U/C-channel forming rule). The flush wall ends flat below the -# case-bottom raised panel; the offset wall ends just above its M2 screws -# as two short tabs whose bend is split through the middle (the case-top -# holes sit above the center-ridge relief line, so a continuous bend -# there would leave an under-minimum local flange). -CASE_FLUSH_WALL_TOP_Z_MM = CASE_FLUSH_RELIEF_BOTTOM_Z_MM # 104.797 -CASE_OFFSET_TAB_TOP_Z_MM = ( - CASE_OFFSET_HOLE_Z_MM + SERVO_MOUNT_HOLE_RADIUS_MM + 1.5 -) # 108.6 -# SendCutSend ALU-063 bend-service rule values for 5052-H32 0.063 in. -SCS_MIN_FLANGE_BEFORE_BEND_MM = 25.4 * 0.255 -SCS_MIN_FLANGE_AFTER_BEND_MM = 25.4 * 0.303 -SCS_CHANNEL_BASE_TO_FLANGE_RATIO = 2.0 -SCS_DIE_WIDTH_MM = 25.4 * 0.472 -SCS_HALF_DIE_WIDTH_MM = 0.5 * SCS_DIE_WIDTH_MM -SCS_BEND_RELIEF_DEPTH_MM = 25.4 * 0.118 -SCS_MIN_CORNER_RELIEF_DISTANCE_FROM_BEND_LINE_MM = 25.4 * 0.030 - -# Bottom-servo up-face obstacles (extents measured from the STEP, with -# clearance margins): the cable bay and the raised center zone that runs -# from the bay through the raised panel. -CABLE_BAY_X_MIN_MM = 11.65 - ROTATING_PART_CLEARANCE_MM -CABLE_BAY_X_MAX_MM = 16.25 + ROTATING_PART_CLEARANCE_MM -CABLE_BAY_HALF_WIDTH_MM = 9.1 + ROTATING_PART_CLEARANCE_MM -BOTTOM_PANEL_X_MAX_MM = 24.59 + 0.5 * 9.63 + ROTATING_PART_CLEARANCE_MM # 29.905 - -# Bracket bodies are 25.8 mm wide (0.54 mm clear of the servo width per -# side). The flush foot's stiffening lips are protruding tabs folded down -# at the body edge, so each lip bend line terminates at free edges of the -# flat blank and needs no bend relief. -BODY_HALF_WIDTH_MM = 12.9 -LIP_OUTER_HALF_WIDTH_MM = BODY_HALF_WIDTH_MM + SHEET_THICKNESS_MM -LIP_X_MIN_MM = -13.8 -LIP_X_MAX_MM = 2.0 - -FOOT_BOTTOM_Z_MM = BOTTOM_FACE_Z_MM + MOUNT_PLANE_CLEARANCE_MM -FOOT_TOP_Z_MM = FOOT_BOTTOM_Z_MM + SHEET_THICKNESS_MM -# Lip legs hang 9.5 mm (formed outside length) below the foot, keeping -# the flat flange 0.5 mm above the before-bend minimum. -LIP_BOTTOM_Z_MM = FOOT_TOP_Z_MM - 9.5 -# The shelf tucks 0.5 mm under the hanging top servo's lower end face so -# the screw-tab flange above it stays as short as possible (jog channel -# 2:1 rule). -SHELF_TOP_Z_MM = CASE_OFFSET_RELIEF_BOTTOM_Z_MM # 99.394 -SHELF_BOTTOM_Z_MM = SHELF_TOP_Z_MM - SHEET_THICKNESS_MM # 97.362 - -# Feet stop clear of the cable bay (flush side) and the raised panel -# (offset side), so the split feet need no windows. -FLUSH_FOOT_REACH_X_MM = CABLE_BAY_X_MIN_MM # 11.15 -OFFSET_FOOT_REACH_X_MM = BOTTOM_PANEL_X_MAX_MM # 29.905 - -# Single-bend offset bracket: the case-top plate descends to the bottom -# servo and one bend turns it outboard across the servo's up-face. The -# foot keeps two side rails seated on the servo's Z=0 rim strips -# (inner rim edges at Y = +/-12.06) and a window around the raised -# center zone (measured at Y <= +/-9.242, proud up to Z = 1.9). -OFFSET_FOOT_END_X_MM = 35.5 -OFFSET_WINDOW_HALF_WIDTH_MM = 9.7 -# The window also relieves the plate's bottom edge between the rails so -# the interrupted foot bend terminates at free edges, and doubles as the -# bottom servo's cable exit slot over its cable bay. The extra 1.5 mm -# keeps the relief past the published minimum regardless of where the -# flat-pattern bend line lands within the bend allowance. -OFFSET_PLATE_RELIEF_TOP_Z_MM = FOOT_TOP_Z_MM + SCS_BEND_RELIEF_DEPTH_MM + 1.5 # 7.342 -# The bend zone and foot flare outboard so each interrupted-bend rail is -# three sheet thicknesses wide instead of the two-thickness rule-of-thumb -# minimum; the flare wings end at free edges above the bend. -OFFSET_FOOT_FLARE_HALF_WIDTH_MM = 14.5 -OFFSET_FLARE_TOP_Z_MM = 8.0 - -INTERFERENCE_VOLUME_LIMIT_MM3 = 1e-3 -HOLE_MATCH_TOLERANCE_MM = 0.05 - -# --- Carbon-tube link variant (RoArm-style) --------------------------- -# Off-the-shelf clamps: SK10 (base-mount shaft support, holds the tube -# against the vertical top plate) and SHF10 (flange-mount shaft support, -# holds the tube on the horizontal bottom plate). step.parts only hosts -# generic envelopes for these SKUs, so the assembly uses true-dimension -# envelopes built from the standard SK10/SHF10 catalog dimensions below. -# Verify against the sourced vendor's datasheet before cutting plates. -# The carbon tube itself is a step.parts catalog miss (recorded -# 2026-06-13); it is generic 3K roll-wrapped 10 x 8 mm stock cut to -# length. -SK10_CENTER_HEIGHT_MM = 20.0 -SK10_BOLT_PITCH_MM = 32.0 -SK10_BASE_WIDTH_MM = 42.0 -SK10_THICKNESS_MM = 14.0 -SK10_DEPTH_MM = 27.0 -SK10_BASE_SLAB_MM = 8.0 -SHF10_BOLT_PITCH_MM = 35.0 -SHF10_FLANGE_WIDTH_MM = 46.5 -SHF10_FLANGE_THICKNESS_MM = 7.0 -SHF10_HUB_RADIUS_MM = 10.0 -SHF10_HUB_HEIGHT_MM = 13.0 -CLAMP_BOLT_CLEARANCE_RADIUS_MM = 2.9 # M5 clearance -TUBE_OUTER_RADIUS_MM = 5.0 -TUBE_INNER_RADIUS_MM = 4.0 -TUBE_BORE_CLEARANCE_MM = 0.05 - -# Tube-link layout. The flat plates are 0.125 in 5052 so the bottom -# servo's rear journal/screw stub (2.6 mm proud) is buried inside the -# bottom plate's through hole. -TUBE_PLATE_THICKNESS_MM = 25.4 * 0.125 # 3.175 -TUBE_TOP_PLATE_INNER_X_MM = CASE_TOP_FACE_X_MM + HORN_FACE_GAP_MM # 15.75 -TUBE_TOP_PLATE_OUTER_X_MM = TUBE_TOP_PLATE_INNER_X_MM + TUBE_PLATE_THICKNESS_MM -TUBE_AXIS_X_MM = TUBE_TOP_PLATE_INNER_X_MM - SK10_CENTER_HEIGHT_MM # -4.25 -BOTTOM_PLATE_TOP_Z_MM = FOOT_BOTTOM_Z_MM + TUBE_PLATE_THICKNESS_MM # 3.425 -TUBE_BOTTOM_Z_MM = 5.0 -TUBE_TOP_Z_MM = 95.0 -SK10_CENTER_Z_MM = 85.0 -TUBE_TOP_PLATE_BOTTOM_Z_MM = 70.0 -TUBE_TOP_PLATE_FLARE_HALF_WIDTH_MM = 19.5 -BOTTOM_PLATE_FLARE_HALF_WIDTH_MM = 21.0 -BOTTOM_PLATE_X_MIN_MM = -14.5 -BOTTOM_PLATE_X_MAX_MM = 35.5 -BOTTOM_PLATE_FLARE_X_MAX_MM = 6.0 - -# Row-major 4x4 transforms placing the servos in the link frame. -# Horn variation: body up (local x -> +Z), output horn toward +X. -TOP_SERVO_HORN_TRANSFORM = ( - 0.0, 1.0, 0.0, HORN_SPAN_CENTERING_OFFSET_MM, - 0.0, 0.0, 1.0, 0.0, - 1.0, 0.0, 0.0, TOP_BODY_UP_Z_OFFSET_MM, - 0.0, 0.0, 0.0, 1.0, -) -def top_servo_case_transform(case_span_centering_offset_mm: float = CASE_SPAN_CENTERING_OFFSET_MM) -> tuple[float, ...]: - # Case variation: body down (local x -> -Z), output horn toward +X. - return ( - 0.0, 1.0, 0.0, case_span_centering_offset_mm, - 0.0, 0.0, -1.0, 0.0, - -1.0, 0.0, 0.0, TOP_BODY_DOWN_Z_OFFSET_MM, - 0.0, 0.0, 0.0, 1.0, - ) - - -TOP_SERVO_CASE_TRANSFORM = top_servo_case_transform() -BOTTOM_SERVO_TRANSFORM = ( - 1.0, 0.0, 0.0, -SERVO_SHAFT_LOCAL_X_MM, - 0.0, 0.0, 1.0, 0.0, - 0.0, -1.0, 0.0, SERVO_CASE_BOTTOM_LOCAL_Y_MM, - 0.0, 0.0, 0.0, 1.0, -) - -# Expected hole centers in the link frame. -BOTTOM_SERVO_MOUNT_HOLES_XY_MM = tuple( - (x_offset, side * HOLE_SIDE_OFFSET_MM) - for x_offset in (HOLE_NEAR_OFFSET_MM, HOLE_FAR_OFFSET_MM) - for side in (-1.0, 1.0) -) -# Horn screw holes on each horn face, as (y, z) in the link frame. -HORN_SCREW_HOLES_YZ_MM = ( - (0.0, TOP_PIVOT_Z_MM + SERVO_HORN_SCREW_CIRCLE_RADIUS_MM), - (0.0, TOP_PIVOT_Z_MM - SERVO_HORN_SCREW_CIRCLE_RADIUS_MM), - (SERVO_HORN_SCREW_CIRCLE_RADIUS_MM, TOP_PIVOT_Z_MM), - (-SERVO_HORN_SCREW_CIRCLE_RADIUS_MM, TOP_PIVOT_Z_MM), -) -# Case-mount screw holes, as (y, z) in the link frame. -CASE_FLUSH_HOLES_YZ_MM = tuple( - (side * HOLE_SIDE_OFFSET_MM, CASE_FLUSH_HOLE_Z_MM) for side in (-1.0, 1.0) -) -CASE_OFFSET_HOLES_YZ_MM = tuple( - (side * HOLE_SIDE_OFFSET_MM, CASE_OFFSET_HOLE_Z_MM) for side in (-1.0, 1.0) -) - - -def make_box( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def make_z_cylinder( - *, - x: float, - y: float, - radius: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x, y, z_min), z_dir=(0.0, 0.0, 1.0)), - ) - - -def make_x_cylinder( - *, - y: float, - z: float, - radius: float, - x_min: float, - x_max: float, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - x_max - x_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y, z), z_dir=(1.0, 0.0, 0.0)), - ) - - -def make_y_cylinder( - *, - x: float, - z: float, - radius: float, - y_min: float, - y_max: float, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - y_max - y_min, - plane=build123d.Plane(origin=build123d.Vector(x, y_min, z), z_dir=(0.0, 1.0, 0.0)), - ) - - -def _transform_location(transform: tuple[float, ...]) -> build123d.Location: - return build123d.Location( - build123d.Plane( - origin=(transform[3], transform[7], transform[11]), - x_dir=(transform[0], transform[4], transform[8]), - z_dir=(transform[2], transform[6], transform[10]), - ) - ) - - -def _load_servo_shape(step_path: Path) -> build123d.Shape: - if not step_path.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {step_path}") - shape = import_as_shape(step_path) - bb = shape.bounding_box() - for measured, expected in ( - (bb.min.X, SERVO_BODY_X_MIN_MM), - (bb.max.X, SERVO_BODY_X_MAX_MM), - (bb.min.Z, -SERVO_HALF_WIDTH_MM), - (bb.max.Z, SERVO_HALF_WIDTH_MM), - (bb.min.Y, SERVO_REAR_EXTREME_LOCAL_Y_MM), - (bb.max.Y, SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM), - ): - if abs(measured - expected) > 0.01: - raise RuntimeError( - f"{step_path.name} no longer matches the v2 layout facts: " - f"measured {measured:.4f}, expected {expected:.4f}" - ) - return shape - - -def place_servo(shape: build123d.Shape, transform: tuple[float, ...]) -> build123d.Shape: - return shape.moved(_transform_location(transform)) - - -def _planar_hole_centers( - shape: build123d.Shape, - *, - axis: str, - plane_coordinate: float, -) -> list[tuple[float, float, float]]: - """Inner circular wire centers on planar faces at the given axis plane. - - Returns (a, b, radius) where (a, b) are the in-plane coordinates: (x, y) - for a Z-normal plane and (y, z) for an X-normal plane. - """ - centers: list[tuple[float, float, float]] = [] - for face in shape.faces(): - if face.geom_type.name != "PLANE": - continue - face_center = face.center() - coordinate = face_center.Z if axis == "z" else face_center.X - if abs(coordinate - plane_coordinate) > 0.05: - continue - normal = face.normal_at(face_center) - component = normal.Z if axis == "z" else normal.X - if abs(abs(component) - 1.0) > 1e-3: - continue - wires = list(face.wires()) - if len(wires) < 2: - continue - outer = max(wires, key=lambda wire: abs(build123d.Face(wire).area)) - for wire in wires: - if wire.is_same(outer): - continue - circle_edges = [edge for edge in wire.edges() if edge.geom_type.name == "CIRCLE"] - if not circle_edges: - continue - center = build123d.Face(wire).center() - radius = max(edge.radius for edge in circle_edges) - if axis == "z": - centers.append((center.X, center.Y, radius)) - else: - centers.append((center.Y, center.Z, radius)) - return centers - - -def _verify_holes( - found: list[tuple[float, float, float]], - *, - expected: tuple[tuple[float, float], ...], - radius: float, - label: str, -) -> None: - for expected_a, expected_b in expected: - if not any( - abs(a - expected_a) <= HOLE_MATCH_TOLERANCE_MM - and abs(b - expected_b) <= HOLE_MATCH_TOLERANCE_MM - and abs(found_radius - radius) <= HOLE_MATCH_TOLERANCE_MM - for a, b, found_radius in found - ): - raise RuntimeError( - f"Placed STS3250 is missing an expected {label} hole at " - f"({expected_a:.3f}, {expected_b:.3f}); found {found!r}" - ) - - -def verify_bottom_servo_mount_holes(placed_servo: build123d.Shape) -> None: - _verify_holes( - _planar_hole_centers(placed_servo, axis="z", plane_coordinate=BOTTOM_FACE_Z_MM), - expected=BOTTOM_SERVO_MOUNT_HOLES_XY_MM, - radius=SERVO_MOUNT_HOLE_RADIUS_MM, - label="bottom-servo case-bottom", - ) - - -def verify_top_servo_horn_holes(placed_servo: build123d.Shape, *, side: float) -> None: - _verify_holes( - _planar_hole_centers(placed_servo, axis="x", plane_coordinate=side * HORN_HALF_SPAN_MM), - expected=HORN_SCREW_HOLES_YZ_MM, - radius=SERVO_HORN_SCREW_HOLE_RADIUS_MM, - label=f"horn-face (side {side:+.0f})", - ) - - -def verify_top_servo_case_holes( - placed_servo: build123d.Shape, - *, - case_span_centering_offset_mm: float = CASE_SPAN_CENTERING_OFFSET_MM, -) -> None: - _verify_holes( - _planar_hole_centers( - placed_servo, - axis="x", - plane_coordinate=SERVO_CASE_BOTTOM_LOCAL_Y_MM + case_span_centering_offset_mm, - ), - expected=CASE_FLUSH_HOLES_YZ_MM, - radius=SERVO_MOUNT_HOLE_RADIUS_MM, - label="case-bottom-face", - ) - _verify_holes( - _planar_hole_centers( - placed_servo, - axis="x", - plane_coordinate=SERVO_CASE_TOP_LOCAL_Y_MM + case_span_centering_offset_mm, - ), - expected=CASE_OFFSET_HOLES_YZ_MM, - radius=SERVO_MOUNT_HOLE_RADIUS_MM, - label="case-top-face", - ) - - -def verify_no_interference( - bracket: build123d.Shape, - placed_servo: build123d.Shape, - *, - label: str, -) -> float: - volume = placed_servo.intersect(bracket).volume - if volume > INTERFERENCE_VOLUME_LIMIT_MM3: - raise RuntimeError(f"Bracket intersects {label} by {volume:.6f} mm^3") - return volume - - -def placed_link_servos_horn() -> tuple[build123d.Shape, build123d.Shape]: - top = place_servo(_load_servo_shape(SERVO_STEP), TOP_SERVO_HORN_TRANSFORM) - bottom = place_servo(_load_servo_shape(SERVO_NO_REAR_HORN_STEP), BOTTOM_SERVO_TRANSFORM) - verify_top_servo_horn_holes(top, side=1.0) - verify_top_servo_horn_holes(top, side=-1.0) - verify_bottom_servo_mount_holes(bottom) - return top, bottom - - -def placed_link_servos_case( - *, - case_span_centering_offset_mm: float = CASE_SPAN_CENTERING_OFFSET_MM, -) -> tuple[build123d.Shape, build123d.Shape]: - top = place_servo( - _load_servo_shape(SERVO_STEP), - top_servo_case_transform(case_span_centering_offset_mm), - ) - bottom = place_servo(_load_servo_shape(SERVO_NO_REAR_HORN_STEP), BOTTOM_SERVO_TRANSFORM) - verify_top_servo_case_holes( - top, - case_span_centering_offset_mm=case_span_centering_offset_mm, - ) - verify_bottom_servo_mount_holes(bottom) - return top, bottom - - -def horn_plate(*, side: float, z_min: float = SHELF_BOTTOM_Z_MM) -> build123d.Shape: - """Vertical horn plate for one side: a sheet panel bolted to the horn - face at X = side * 18.3, carrying the four 7 mm-circle screw holes and - a center clearance hole on the pitch axis.""" - x_inner = side * HORN_PLATE_INNER_X_MM - x_outer = side * HORN_PLATE_OUTER_X_MM - plate: build123d.Shape = make_box( - x_min=min(x_inner, x_outer), - x_max=max(x_inner, x_outer), - y_min=-PLATE_HALF_SPAN_MM, - y_max=PLATE_HALF_SPAN_MM, - z_min=z_min, - z_max=HORN_PLATE_TOP_Z_MM, - ) - cut_x_min = min(x_inner, x_outer) - 2.0 - cut_x_max = max(x_inner, x_outer) + 2.0 - for hole_y, hole_z in HORN_SCREW_HOLES_YZ_MM: - plate = plate.cut( - make_x_cylinder( - y=hole_y, - z=hole_z, - radius=SERVO_HORN_SCREW_HOLE_RADIUS_MM, - x_min=cut_x_min, - x_max=cut_x_max, - ) - ) - # The rear (-X) side needs a larger center hole: the case's rear - # journal/screw stub protrudes 0.8 mm past the rear horn face, through - # the plate plane. The output (+X) side only needs access to the horn's - # center screw. - center_clearance_radius = ( - REAR_JOURNAL_CLEARANCE_RADIUS_MM if side < 0.0 else SERVO_HORN_SCREW_HOLE_RADIUS_MM - ) - plate = plate.cut( - make_x_cylinder( - y=0.0, - z=TOP_PIVOT_Z_MM, - radius=center_clearance_radius, - x_min=cut_x_min, - x_max=cut_x_max, - ) - ) - return plate diff --git a/models/robots/tom/v2/parts/link_standoff_m3_35.py b/models/robots/tom/v2/parts/link_standoff_m3_35.py deleted file mode 100644 index 82521fea..00000000 --- a/models/robots/tom/v2/parts/link_standoff_m3_35.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env python3 -""" -35 mm M3 female-female round aluminum spacer for the tom v2 link bracket pair. - -The part is authored centered on the link frame Y axis so assemblies can place -it by translating to the bracket web hole center. The simplified thread is a -continuous M3 minor-diameter bore. Outside dimensions match the MECCANIXITY -M3x35 round aluminum female-threaded spacer: 5 mm OD, 35 mm length. -""" - -from __future__ import annotations - -import os -import sys -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parent -if str(V2_DIR) not in sys.path: - sys.path.insert(0, str(V2_DIR)) - -import build123d - -import link_common as lc - - -PART_NAME = Path(__file__).stem - -STANDOFF_LENGTH_MM = float(os.environ.get("TOM_V2_LINK_STANDOFF_LENGTH_MM", "35.0")) -ROUND_OUTER_DIAMETER_MM = float(os.environ.get("TOM_V2_LINK_STANDOFF_OD_MM", "5.0")) -M3_THREAD_MINOR_RADIUS_MM = float(os.environ.get("TOM_V2_LINK_STANDOFF_THREAD_MINOR_RADIUS_MM", "1.25")) -END_ENTRY_RADIUS_MM = float(os.environ.get("TOM_V2_LINK_STANDOFF_THREAD_ENTRY_RADIUS_MM", "1.65")) -END_ENTRY_DEPTH_MM = 1.2 -CUT_EXTENSION_MM = 1.0 -SILVER_ALUMINUM_COLOR = build123d.Color(0.78, 0.80, 0.82, 1.0) - - -def build_step() -> build123d.Shape: - if STANDOFF_LENGTH_MM <= 0.0: - raise ValueError("TOM_V2_LINK_STANDOFF_LENGTH_MM must be positive") - if ROUND_OUTER_DIAMETER_MM <= 2.0 * END_ENTRY_RADIUS_MM: - raise ValueError("Round body OD is too small for the requested M3 thread entry") - - body: build123d.Shape = lc.make_y_cylinder( - x=0.0, - z=0.0, - radius=0.5 * ROUND_OUTER_DIAMETER_MM, - y_min=-0.5 * STANDOFF_LENGTH_MM, - y_max=0.5 * STANDOFF_LENGTH_MM, - ) - body = body.cut( - lc.make_y_cylinder( - x=0.0, - z=0.0, - radius=M3_THREAD_MINOR_RADIUS_MM, - y_min=-0.5 * STANDOFF_LENGTH_MM - CUT_EXTENSION_MM, - y_max=0.5 * STANDOFF_LENGTH_MM + CUT_EXTENSION_MM, - ) - ) - for end_y in (-0.5 * STANDOFF_LENGTH_MM, 0.5 * STANDOFF_LENGTH_MM - END_ENTRY_DEPTH_MM): - body = body.cut( - lc.make_y_cylinder( - x=0.0, - z=0.0, - radius=END_ENTRY_RADIUS_MM, - y_min=end_y - CUT_EXTENSION_MM, - y_max=end_y + END_ENTRY_DEPTH_MM + CUT_EXTENSION_MM, - ) - ) - - solids = body.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one standoff solid, found {len(solids)}") - - body.label = PART_NAME - body.color = SILVER_ALUMINUM_COLOR - bb = body.bounding_box() - print( - "M3 female-female round aluminum link spacer " - f"length={bb.size.Y:.3f} mm, outer_diameter={ROUND_OUTER_DIAMETER_MM:.3f} mm, " - f"thread_minor_diameter={2.0 * M3_THREAD_MINOR_RADIUS_MM:.3f} mm" - ) - return body - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/parts/link_standoff_m3_35.step b/models/robots/tom/v2/parts/link_standoff_m3_35.step deleted file mode 100644 index 42f4cc27..00000000 --- a/models/robots/tom/v2/parts/link_standoff_m3_35.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:30b1c634c4891c04cb96ae03415da18a2e0cc2bd17f4d1db56bd00122af9289f -size 18551 diff --git a/models/robots/tom/v2/parts/servo_end_mount.py b/models/robots/tom/v2/parts/servo_end_mount.py deleted file mode 100644 index 8528be2a..00000000 --- a/models/robots/tom/v2/parts/servo_end_mount.py +++ /dev/null @@ -1,1719 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate the localized end mount. - -The part is authored in the imported STS3250 local frame so assemblies can place -it with the same transform as the reference servo geometry. - -Usage: - python STEP/servo_end_mount.py -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -# Keep CAD library caches inside the workspace to avoid permission issues. -TOM_DIR = Path(__file__).resolve().parents[2] -V2_DIR = Path(__file__).resolve().parents[1] -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import ezdxf -import build123d -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.materials import GRAY_ALUMINUM_COLOR -from robot_common.step_import import import_as_shape -import dxf_topology - - -CAD_DIR = Path(__file__).resolve().parent -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Servo End Mount" - -SERVO_STEP = V2_DIR / "parts" / "imports" / "sts3250.step" - - -def _env_float(name: str, default: float) -> float: - value = os.environ.get(name) - if value is None: - return default - try: - parsed = float(value) - except ValueError as exc: - raise ValueError(f"{name} must be a number, got {value!r}") from exc - if parsed <= 0.0: - raise ValueError(f"{name} must be positive, got {parsed!r}") - return parsed - - -def _env_nonnegative_float(name: str, default: float) -> float: - value = os.environ.get(name) - if value is None: - return default - try: - parsed = float(value) - except ValueError as exc: - raise ValueError(f"{name} must be a number, got {value!r}") from exc - if parsed < 0.0: - raise ValueError(f"{name} must be non-negative, got {parsed!r}") - return parsed - - -def front_horn_mount_face_center_local_mm() -> tuple[float, float, float]: - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - servo_shape = import_as_shape(SERVO_STEP) - layout = _build_layout(servo_shape, include_top_center_bridge=True) - return ( - layout.bracket_face_outer_x, - layout.front_face_center_y, - 0.0, - ) - - -SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM = 25.4 * 0.255 -SENDCUTSEND_SHEET_THICKNESS_MM = _env_float("TOM_V2_SHEET_THICKNESS_MM", 25.4 * 0.063) -CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM = 25.4 * 0.010 -CENTER_BRIDGE_FLANGE_LENGTH_EXTENSION_MM = _env_nonnegative_float( - "TOM_V2_SERVO_END_MOUNT_CENTER_BRIDGE_FLANGE_LENGTH_EXTENSION_MM", - 7.9, -) -CENTER_BRIDGE_SIDE_FACE_LENGTH_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_CENTER_BRIDGE_SIDE_FACE_LENGTH_MM", - 8.0, -) -CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM = ( - SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM - + CENTER_BRIDGE_BEND_LINE_EDGE_BUFFER_MM - + CENTER_BRIDGE_FLANGE_LENGTH_EXTENSION_MM -) -SENDCUTSEND_CENTER_BRIDGE_MIN_AFTER_BEND_MM = 25.4 * 0.303 -CENTER_BRIDGE_BEND_EDGE_DISTANCE_BUFFER_MM = 0.035 -CENTER_BRIDGE_BEND_EDGE_DISTANCE_TARGET_MM = ( - SENDCUTSEND_CENTER_BRIDGE_MIN_AFTER_BEND_MM - + CENTER_BRIDGE_BEND_EDGE_DISTANCE_BUFFER_MM -) -BRACKET_FACE_CLEARANCE_MM = ( - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - - SENDCUTSEND_SHEET_THICKNESS_MM -) -SENDCUTSEND_REFERENCE_SHEET_THICKNESS_MM = 25.4 * 0.080 -SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM = 25.4 * 0.035 -SENDCUTSEND_K_FACTOR = 0.42 -SENDCUTSEND_BEND_DEDUCTION_90_MM = 25.4 * 0.096 -SENDCUTSEND_DIE_WIDTH_MM = 25.4 * 0.472 -SENDCUTSEND_HALF_DIE_WIDTH_MM = 0.5 * SENDCUTSEND_DIE_WIDTH_MM -SENDCUTSEND_MIN_FLANGE_FLAT_MM = SENDCUTSEND_MIN_BEND_LINE_EDGE_CLEARANCE_MM -SENDCUTSEND_MIN_FLANGE_FORMED_MM = ( - 25.4 * 0.303 -) -SENDCUTSEND_BEND_RELIEF_DEPTH_MM = 25.4 * 0.118 -SENDCUTSEND_MIN_CORNER_RELIEF_DISTANCE_MM = 25.4 * 0.030 -SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM = 25.4 -BEND_DIE_REGION_TOLERANCE_MM = 0.1 -TOPOLOGY_FLAT_PANEL_JOIN_TOLERANCE_MM = 0.02 -# Display the formed bracket with sharp 90-degree bends. The SendCutSend bend -# allowance/radius constants above still drive the flat-pattern math. -BEND_OUTER_RADIUS_MM = 0.0 -BEND_INNER_RADIUS_MM = 0.0 -FLANGE_FREE_CORNER_RADIUS_MM = 1.0 -FLANGE_FREE_CORNER_SAMPLE_COUNT = 8 -YOKE_MATCHED_SLOT_WIDTH_MM = 3.0 -YOKE_MATCHED_SLOT_LENGTH_MM = 13.0 -SLOT_WIDTH_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_SLOT_WIDTH_MM", - YOKE_MATCHED_SLOT_WIDTH_MM, -) -SLOT_LENGTH_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_SLOT_LENGTH_MM", - YOKE_MATCHED_SLOT_LENGTH_MM, -) -SLOT_SIDE_END_GAP_MM = _env_float("TOM_V2_SERVO_END_MOUNT_SLOT_SIDE_END_GAP_MM", 4.0) -SLOT_EDGE_TO_BEND_MM = _env_float("TOM_V2_SERVO_END_MOUNT_SLOT_EDGE_TO_BEND_MM", 6.0) -SLOT_CENTER_BRIDGE_WEB_MIN_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_SLOT_CENTER_BRIDGE_WEB_MIN_MM", - 2.0, -) -# Keep the servo-hole axes fixed; the default model seats the wrap directly -# against the servo faces unless an environment override adds clearance. -MOUNT_PLANE_CLEARANCE_MM = _env_float("TOM_V2_SERVO_END_MOUNT_WRAP_CLEARANCE_MM", 0.0) -TOP_MOUNT_PLANE_CLEARANCE_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_TOP_CLEARANCE_MM", - MOUNT_PLANE_CLEARANCE_MM, -) -BOTTOM_MOUNT_PLANE_CLEARANCE_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_BOTTOM_CLEARANCE_MM", - MOUNT_PLANE_CLEARANCE_MM, -) -SIDE_WRAP_CLEARANCE_MM = _env_nonnegative_float("TOM_V2_SERVO_END_MOUNT_SIDE_CLEARANCE_MM", 0.0) -TOP_CENTER_BRIDGE_SERVO_END_CLEARANCE_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_CENTER_BRIDGE_END_CLEARANCE_MM", - 1.0, -) -TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM = _env_float( - "TOM_V2_SERVO_END_MOUNT_CENTER_RELIEF_SIDE_CLEARANCE_MM", - 0.0, -) -FLANGE_LENGTH_EXTENSION_MM = _env_nonnegative_float("TOM_V2_SERVO_END_MOUNT_FLANGE_EXTENSION_MM", 0.0) -TOP_FLANGE_LENGTH_EXTENSION_MM = _env_nonnegative_float( - "TOM_V2_SERVO_END_MOUNT_TOP_FLANGE_EXTENSION_MM", - FLANGE_LENGTH_EXTENSION_MM, -) -BOTTOM_FLANGE_LENGTH_EXTENSION_MM = _env_nonnegative_float( - "TOM_V2_SERVO_END_MOUNT_BOTTOM_FLANGE_EXTENSION_MM", - FLANGE_LENGTH_EXTENSION_MM, -) -MOUNT_HOLE_EDGE_X_WEB_MM = 1.55 -MOUNT_HOLE_OUTBOARD_Z_WEB_MM = 2.15 -MOUNT_HOLE_INBOARD_Z_WEB_MM = 2.15 -MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM = 1.0 -FRONT_HORN_SCREW_CIRCLE_RADIUS_MM = 7.0 -FRONT_HORN_M3_CLEARANCE_RADIUS_MM = 1.7 -FRONT_HORN_CENTER_CLEARANCE_RADIUS_MM = 3.25 -CUT_EXTENSION_MM = 2.0 -MAX_FACE_TARGET_ERROR_MM = 1.0 -FACE_TARGET_CENTERS_MM: dict[str, tuple[float, float, float]] = { - "end_face": (9.61, -7.65, 0.0), - "end_wrap_neg": (8.843, -7.65, -11.593), - "end_wrap_pos": (8.843, -7.65, 11.593), - "upper_top_neg": (-6.561, 6.4, -9.484), - "upper_top_pos": (-6.561, 6.4, 9.484), - "main_bottom": (-0.881, -25.6, 0.0), -} -@dataclass(frozen=True) -class BracketLayout: - z_min: float - z_max: float - top_holes: tuple[tuple[build123d.Vector, float], ...] - bottom_holes: tuple[tuple[build123d.Vector, float], ...] - top_tab_x_min: float - bottom_tab_x_min: float - bracket_face_inner_x: float - bracket_face_outer_x: float - top_tab_y_min: float - top_tab_y_max: float - bottom_tab_y_min: float - bottom_tab_y_max: float - tab_bands: tuple[tuple[float, float], ...] - bend_bands: tuple[tuple[float, float], ...] - center_bridge_x_min: float | None - front_face_center_y: float - outer_flush_gap: float - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - top_flat_length_mm: float - web_flat_start_mm: float - web_flat_end_mm: float - bottom_flat_start_mm: float - bend_line_positions_mm: tuple[float, float] - bend_line_segments_mm: tuple[tuple[float, float, float], ...] - cut_circles: tuple[tuple[float, float, float], ...] - cut_segments: tuple[tuple[str, tuple[tuple[float, float], ...]], ...] - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _face_from_wire( - exterior: build123d.Wire, - interior_wires: list[build123d.Wire] | None = None, -) -> build123d.Face: - holes = list(interior_wires or []) - return build123d.Face(exterior, holes) if holes else build123d.Face(exterior) - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _vector_to_tuple(vector: build123d.Vector) -> tuple[float, float, float]: - return (vector.X, vector.Y, vector.Z) - - -def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _slot_profile_points( - *, - center_x: float, - center_y: float, - length: float, - width: float, - segments_per_cap: int = 12, -) -> list[tuple[float, float]]: - radius = 0.5 * width - half_straight = 0.5 * length - radius - bottom_y = center_y - half_straight - top_y = center_y + half_straight - - points: list[tuple[float, float]] = [] - for index in range(segments_per_cap + 1): - angle = 0.0 + (180.0 * index / segments_per_cap) - points.append( - ( - center_x + radius * math.cos(math.radians(angle)), - top_y + radius * math.sin(math.radians(angle)), - ) - ) - for index in range(segments_per_cap + 1): - angle = 180.0 + (180.0 * index / segments_per_cap) - points.append( - ( - center_x + radius * math.cos(math.radians(angle)), - bottom_y + radius * math.sin(math.radians(angle)), - ) - ) - return points - - -def _z_slot_cut( - *, - center_x: float, - center_z: float, - length: float, - y_min: float, - y_max: float, -) -> build123d.Shape: - radius = 0.5 * SLOT_WIDTH_MM - half_straight = 0.5 * length - radius - y_start = y_min - CUT_EXTENSION_MM - y_height = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - body: build123d.Shape = _make_box( - x_min=center_x - radius, - x_max=center_x + radius, - y_min=y_start, - y_max=y_start + y_height, - z_min=center_z - half_straight, - z_max=center_z + half_straight, - ) - for end_z in (center_z - half_straight, center_z + half_straight): - cap = _make_cylinder_along( - radius=radius, - height=y_height, - origin=build123d.Vector(center_x, y_start, end_z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - body = body.fuse(cap) - return body - - -def _slot_length(layout: BracketLayout) -> float: - available_length = (layout.z_max - layout.z_min) - (2.0 * SLOT_SIDE_END_GAP_MM) - if SLOT_LENGTH_MM > available_length + 1e-6: - raise RuntimeError( - "Servo end mount slot does not fit across the flange width with the " - "configured end gaps: " - f"{SLOT_LENGTH_MM:.3f} mm > {available_length:.3f} mm" - ) - if SLOT_LENGTH_MM <= SLOT_WIDTH_MM: - raise RuntimeError( - "Servo end mount slot does not fit across the flange width: " - f"{SLOT_LENGTH_MM:.3f} mm <= {SLOT_WIDTH_MM:.3f} mm" - ) - return SLOT_LENGTH_MM - - -def _slot_center_x(layout: BracketLayout) -> float: - return layout.bracket_face_inner_x - SLOT_EDGE_TO_BEND_MM - (0.5 * SLOT_WIDTH_MM) - - -def _slot_specs(layout: BracketLayout) -> tuple[tuple[str, float, float, float], ...]: - center_x = _slot_center_x(layout) - center_z = 0.5 * (layout.z_min + layout.z_max) - length = _slot_length(layout) - return ( - ("top", center_x, center_z, length), - ("bottom", center_x, center_z, length), - ) - - -def _cut_slots(shape: build123d.Shape, layout: BracketLayout) -> build123d.Shape: - for name, center_x, center_z, length in _slot_specs(layout): - if name == "top": - y_min = layout.top_tab_y_min - y_max = layout.top_tab_y_max - else: - y_min = layout.bottom_tab_y_min - y_max = layout.bottom_tab_y_max - shape = shape.cut( - _z_slot_cut( - center_x=center_x, - center_z=center_z, - length=length, - y_min=y_min, - y_max=y_max, - ) - ) - return shape - - -def _slot_clearance_metrics(layout: BracketLayout) -> dict[str, float]: - specs = _slot_specs(layout) - min_slot_edge_to_bend = min( - layout.bracket_face_inner_x - (center_x + (0.5 * SLOT_WIDTH_MM)) - for _name, center_x, _center_z, _length in specs - ) - min_side_end_gap = min( - min( - (center_z - (0.5 * length)) - layout.z_min, - layout.z_max - (center_z + (0.5 * length)), - ) - for _name, _center_x, center_z, length in specs - ) - min_slot_to_mount_hole_x = min( - abs(center.X - _slot_center_x(layout)) - (radius + (0.5 * SLOT_WIDTH_MM)) - for center, radius in [*layout.top_holes, *layout.bottom_holes] - ) - center_bridge_margin = ( - (_slot_center_x(layout) - (0.5 * SLOT_WIDTH_MM)) - - (layout.center_bridge_x_min or layout.bracket_face_inner_x) - ) - return { - "slot_side_end_gap_min_mm": min_side_end_gap, - "slot_to_mount_hole_x_min_mm": min_slot_to_mount_hole_x, - "slot_center_bridge_margin_mm": center_bridge_margin, - "slot_edge_to_bend_min_mm": min_slot_edge_to_bend, - "slot_edge_to_bend_leeway_mm": min_slot_edge_to_bend - SENDCUTSEND_HALF_DIE_WIDTH_MM, - } - - -def _validate_slot_clearances(layout: BracketLayout) -> None: - metrics = _slot_clearance_metrics(layout) - if metrics["slot_edge_to_bend_leeway_mm"] < -1e-6: - raise RuntimeError( - "Slot is too close to the bend for the SendCutSend die clearance: " - f"{metrics['slot_edge_to_bend_min_mm']:.3f} mm < {SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - if metrics["slot_center_bridge_margin_mm"] + 1e-6 < SLOT_CENTER_BRIDGE_WEB_MIN_MM: - raise RuntimeError( - "Slot is not sufficiently supported by the center bridge: " - f"{metrics['slot_center_bridge_margin_mm']:.3f} mm < " - f"{SLOT_CENTER_BRIDGE_WEB_MIN_MM:.3f} mm" - ) - - -def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire: - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}") - return wires[0] - - -def _arc_angles_xz( - *, - center_x: float, - center_z: float, - start_point: tuple[float, float], - end_point: tuple[float, float], -) -> tuple[float, float, build123d.AngularDirection]: - start_angle = math.degrees(math.atan2(start_point[1] - center_z, start_point[0] - center_x)) - end_angle = math.degrees(math.atan2(end_point[1] - center_z, end_point[0] - center_x)) - ccw_span = (end_angle - start_angle) % 360.0 - if ccw_span <= 180.0: - return start_angle, end_angle, build123d.AngularDirection.COUNTER_CLOCKWISE - return start_angle, end_angle, build123d.AngularDirection.CLOCKWISE - - -def _make_arc_edge_xz( - *, - center_x: float, - center_z: float, - radius: float, - start_point: tuple[float, float], - end_point: tuple[float, float], -) -> build123d.Edge: - start_angle, end_angle, angular_direction = _arc_angles_xz( - center_x=center_x, - center_z=center_z, - start_point=start_point, - end_point=end_point, - ) - return build123d.Edge.make_circle( - radius, - plane=build123d.Plane(origin=(center_x, 0.0, center_z), x_dir=(1.0, 0.0, 0.0), z_dir=(0.0, -1.0, 0.0)), - start_angle=start_angle, - end_angle=end_angle, - angular_direction=angular_direction, - ) - - -def _make_wire_prism_y( - *, - edges_xz: list[build123d.Edge], - y_min: float, - y_max: float, -) -> build123d.Solid: - face = _face_from_wire(_assemble_wire(edges_xz)) - solid = build123d.Solid.extrude(face, build123d.Vector(0.0, y_max - y_min, 0.0)) - return solid.translate((0.0, y_min, 0.0)) - - -def _make_rounded_flange_tab( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, - radius: float, -) -> build123d.Shape: - radius = min(radius, 0.5 * (z_max - z_min), x_max - x_min) - if radius <= 1e-6: - return _make_box(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, z_min=z_min, z_max=z_max) - - lower_tangent = (x_min + radius, z_min) - right_lower = (x_max, z_min) - right_upper = (x_max, z_max) - upper_tangent = (x_min + radius, z_max) - left_upper = (x_min, z_max - radius) - left_lower = (x_min, z_min + radius) - - return _make_wire_prism_y( - edges_xz=[ - build123d.Edge.make_line((lower_tangent[0], 0.0, lower_tangent[1]), (right_lower[0], 0.0, right_lower[1])), - build123d.Edge.make_line((right_lower[0], 0.0, right_lower[1]), (right_upper[0], 0.0, right_upper[1])), - build123d.Edge.make_line((right_upper[0], 0.0, right_upper[1]), (upper_tangent[0], 0.0, upper_tangent[1])), - _make_arc_edge_xz( - center_x=x_min + radius, - center_z=z_max - radius, - radius=radius, - start_point=upper_tangent, - end_point=left_upper, - ), - build123d.Edge.make_line((left_upper[0], 0.0, left_upper[1]), (left_lower[0], 0.0, left_lower[1])), - _make_arc_edge_xz( - center_x=x_min + radius, - center_z=z_min + radius, - radius=radius, - start_point=left_lower, - end_point=lower_tangent, - ), - ], - y_min=y_min, - y_max=y_max, - ) - - -def _find_face_near(shape: build123d.Shape, target: tuple[float, float, float], *, geom_type: str | None = None) -> build123d.Face: - target_vec = build123d.Vector(*target) - best: tuple[float, build123d.Face] | None = None - for face in shape.faces(): - if geom_type is not None and _geom_type_name(face) != geom_type: - continue - dist = face.center().sub(target_vec).length - if best is None or dist < best[0]: - best = (dist, face) - - if best is None: - raise RuntimeError(f"No planar face found near {target}") - if best[0] > MAX_FACE_TARGET_ERROR_MM: - raise RuntimeError( - f"Resolved face near {target} was {best[0]:.3f} mm away, which exceeds the tolerance" - ) - return best[1] - - -def _wire_area(wire: build123d.Wire) -> float: - return abs(_face_from_wire(wire).area) - - -def _outer_wire_and_inner_wires(face: build123d.Face) -> tuple[build123d.Wire, list[build123d.Wire]]: - wires = list(face.wires()) - if not wires: - raise RuntimeError("Face has no wires") - - outer_wire = max(wires, key=_wire_area) - inner_wires = [wire for wire in wires if not wire.is_same(outer_wire)] - return outer_wire, inner_wires - - -def _extract_inner_hole_profiles(face: build123d.Face) -> list[tuple[build123d.Vector, float]]: - _outer_wire, inner_wires = _outer_wire_and_inner_wires(face) - profiles: list[tuple[build123d.Vector, float]] = [] - for wire in inner_wires: - circle_edges = [edge for edge in wire.edges() if _geom_type_name(edge) == "CIRCLE"] - if not circle_edges: - continue - center = _face_from_wire(wire).center() - radius = max(edge.radius for edge in circle_edges) - profiles.append((center, radius)) - return profiles - - -def _select_near_end_holes( - profiles: list[tuple[build123d.Vector, float]], - *, - count: int, -) -> list[tuple[build123d.Vector, float]]: - if len(profiles) < count: - raise RuntimeError(f"Expected at least {count} mount holes, found {len(profiles)}") - - selected = sorted(profiles, key=lambda item: (item[0].X, abs(item[0].Z)), reverse=True)[:count] - selected.sort(key=lambda item: item[0].Z) - return selected - - -def _cut_mount_holes( - body: build123d.Shape, - *, - profiles: list[tuple[build123d.Vector, float]], - start_y: float, - direction_y: float, -) -> build123d.Shape: - hole_depth = SENDCUTSEND_SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - for center, radius in profiles: - cutter = _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(center.X, start_y, center.Z), - direction=build123d.Vector(0.0, direction_y, 0.0), - ) - body = body.cut(cutter) - return body - - -def _front_horn_mount_hole_profiles(layout: BracketLayout) -> tuple[tuple[float, float, float], ...]: - center_y = layout.front_face_center_y - center_z = 0.0 - radius = FRONT_HORN_SCREW_CIRCLE_RADIUS_MM - return ( - (center_y, center_z, FRONT_HORN_CENTER_CLEARANCE_RADIUS_MM), - (center_y, center_z + radius, FRONT_HORN_M3_CLEARANCE_RADIUS_MM), - (center_y, center_z - radius, FRONT_HORN_M3_CLEARANCE_RADIUS_MM), - (center_y + radius, center_z, FRONT_HORN_M3_CLEARANCE_RADIUS_MM), - (center_y - radius, center_z, FRONT_HORN_M3_CLEARANCE_RADIUS_MM), - ) - - -def _cut_front_horn_mount_holes(body: build123d.Shape, *, layout: BracketLayout) -> build123d.Shape: - hole_depth = SENDCUTSEND_SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - start_x = layout.bracket_face_outer_x + CUT_EXTENSION_MM - for center_y, center_z, radius in _front_horn_mount_hole_profiles(layout): - cutter = _make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(start_x, center_y, center_z), - direction=build123d.Vector(-1.0, 0.0, 0.0), - ) - body = body.cut(cutter) - return body - - -def _apply_bends_and_end_rounds( - bracket: build123d.Shape, - *, - bracket_face_inner_x: float, - bracket_face_outer_x: float, - top_tab_x_min: float, - bottom_tab_x_min: float, - top_tab_y_min: float, - top_tab_y_max: float, - bottom_tab_y_min: float, - bottom_tab_y_max: float, - bend_bands: tuple[tuple[float, float], ...], -) -> build123d.Shape: - if BEND_INNER_RADIUS_MM < 0.0 or BEND_OUTER_RADIUS_MM < 0.0: - raise ValueError("Bend radii must be >= 0") - if BEND_INNER_RADIUS_MM == 0.0 and BEND_OUTER_RADIUS_MM == 0.0: - return bracket - - def _long_z_edge_near(*, x: float, y: float, tolerance: float = 0.08) -> build123d.ShapeList: - return bracket.edges().filter_by(build123d.Axis.Z).filter_by( - lambda edge: abs(edge.center().X - x) <= tolerance - and abs(edge.center().Y - y) <= tolerance - and edge.length > 0.5 * (max(z_max - z_min for z_min, z_max in bend_bands)) - ) - - inner_bend_edges = build123d.ShapeList() - for bend_y in (top_tab_y_min, bottom_tab_y_max): - inner_bend_edges.extend(_long_z_edge_near(x=bracket_face_inner_x, y=bend_y)) - if len(inner_bend_edges) != 2: - raise RuntimeError(f"Expected two inside bend edges to fillet, found {len(inner_bend_edges)}") - - if BEND_INNER_RADIUS_MM > 0.0: - bracket = bracket.fillet(BEND_INNER_RADIUS_MM, inner_bend_edges) - - def _outer_long_z_edge_near(*, x: float, y: float, tolerance: float = 0.65) -> build123d.ShapeList: - return bracket.edges().filter_by(build123d.Axis.Z).filter_by( - lambda edge: abs(edge.center().X - x) <= tolerance - and abs(edge.center().Y - y) <= tolerance - and edge.length > 0.5 * (max(z_max - z_min for z_min, z_max in bend_bands)) - ) - - outer_bend_edges = build123d.ShapeList() - for bend_y in (top_tab_y_max, bottom_tab_y_min): - outer_bend_edges.extend(_outer_long_z_edge_near(x=bracket_face_outer_x, y=bend_y)) - if len(outer_bend_edges) != 2: - raise RuntimeError(f"Expected two outside bend edges to fillet, found {len(outer_bend_edges)}") - - if BEND_OUTER_RADIUS_MM > 0.0: - bracket = bracket.fillet(BEND_OUTER_RADIUS_MM, outer_bend_edges) - - return bracket - - -def _min_mount_hole_distance_to_bend_line( - profiles: list[tuple[build123d.Vector, float]], - *, - bend_line_x: float, -) -> float: - return min(abs(center.X - bend_line_x) for center, _radius in profiles) - - -def _merge_z_bands( - bands: list[tuple[float, float]] | tuple[tuple[float, float], ...], - *, - tolerance: float = 1e-6, -) -> tuple[tuple[float, float], ...]: - merged: list[tuple[float, float]] = [] - for start, end in sorted(bands, key=lambda band: band[0]): - if end - start <= tolerance: - continue - if not merged or start > merged[-1][1] + tolerance: - merged.append((start, end)) - continue - merged[-1] = (merged[-1][0], max(merged[-1][1], end)) - return tuple(merged) - - -def _build_layout( - servo_shape: build123d.Shape, - *, - include_top_center_bridge: bool = True, - validate_bend_rule: bool = True, -) -> BracketLayout: - faces = { - "end_face": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE"), - "end_wrap_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_neg"]), - "end_wrap_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_wrap_pos"]), - "upper_top_neg": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_neg"], geom_type="PLANE"), - "upper_top_pos": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["upper_top_pos"], geom_type="PLANE"), - "main_bottom": _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["main_bottom"], geom_type="PLANE"), - } - - servo_bb = servo_shape.bounding_box() - end_face_bb = faces["end_face"].bounding_box() - servo_z_center = 0.5 * (servo_bb.min.Z + servo_bb.max.Z) - servo_half_width_z = 0.5 * (servo_bb.max.Z - servo_bb.min.Z) - mount_half_width_z = servo_half_width_z + SIDE_WRAP_CLEARANCE_MM - z_min = servo_z_center - mount_half_width_z - z_max = servo_z_center + mount_half_width_z - - top_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_neg"]), count=1) - top_holes += _select_near_end_holes(_extract_inner_hole_profiles(faces["upper_top_pos"]), count=1) - top_holes.sort(key=lambda item: item[0].Z) - - bottom_holes = _select_near_end_holes(_extract_inner_hole_profiles(faces["main_bottom"]), count=2) - - top_mount_y = sum(center.Y for center, _radius in top_holes) / len(top_holes) - bottom_mount_y = sum(center.Y for center, _radius in bottom_holes) / len(bottom_holes) - top_tab_y_min = top_mount_y + TOP_MOUNT_PLANE_CLEARANCE_MM - top_tab_y_max = top_tab_y_min + SENDCUTSEND_SHEET_THICKNESS_MM - bottom_tab_y_max = bottom_mount_y - BOTTOM_MOUNT_PLANE_CLEARANCE_MM - bottom_tab_y_min = bottom_tab_y_max - SENDCUTSEND_SHEET_THICKNESS_MM - - side_profiles = { - "neg": [top_holes[0], bottom_holes[0]], - "pos": [top_holes[1], bottom_holes[1]], - } - tab_bands: list[tuple[float, float]] = [] - for top_profile, bottom_profile in side_profiles.values(): - profiles = (top_profile, bottom_profile) - if sum(center.Z for center, _radius in profiles) < 0.0: - z_band_min = min(center.Z - radius - MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles) - z_band_max = max(center.Z + radius + MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles) - else: - z_band_min = min(center.Z - radius - MOUNT_HOLE_INBOARD_Z_WEB_MM for center, radius in profiles) - z_band_max = max(center.Z + radius + MOUNT_HOLE_OUTBOARD_Z_WEB_MM for center, radius in profiles) - z_band_min = max(z_band_min, z_min) - z_band_max = min(z_band_max, z_max) - tab_bands.append((z_band_min, z_band_max)) - - sorted_tab_bands = sorted(tab_bands, key=lambda band: band[0]) - if include_top_center_bridge and len(sorted_tab_bands) >= 2: - lower_z_min, lower_z_max = sorted_tab_bands[0] - upper_z_min, upper_z_max = sorted_tab_bands[-1] - adjusted_lower_z_max = lower_z_max - TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM - adjusted_upper_z_min = upper_z_min + TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM - if adjusted_lower_z_max <= lower_z_min or adjusted_upper_z_min >= upper_z_max: - raise RuntimeError( - "Top center relief side clearance consumes the mount tab bands: " - f"clearance={TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM:.3f} mm" - ) - sorted_tab_bands = [ - (lower_z_min, adjusted_lower_z_max), - *sorted_tab_bands[1:-1], - (adjusted_upper_z_min, upper_z_max), - ] - bend_bands = _merge_z_bands(sorted_tab_bands) - - top_tab_x_min = ( - min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in top_holes) - - TOP_FLANGE_LENGTH_EXTENSION_MM - ) - bottom_tab_x_min = ( - min(center.X - radius - MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in bottom_holes) - - BOTTOM_FLANGE_LENGTH_EXTENSION_MM - ) - side_face_target_center_bridge_x_min = ( - top_tab_x_min - + FLANGE_FREE_CORNER_RADIUS_MM - + CENTER_BRIDGE_SIDE_FACE_LENGTH_MM - ) - base_bracket_face_outer_x = ( - end_face_bb.max.X - + BRACKET_FACE_CLEARANCE_MM - + SENDCUTSEND_SHEET_THICKNESS_MM - ) - side_face_bracket_face_outer_x = ( - side_face_target_center_bridge_x_min - + CENTER_BRIDGE_BEND_EDGE_DISTANCE_TARGET_MM - ) - bracket_face_outer_x = max(base_bracket_face_outer_x, side_face_bracket_face_outer_x) - bracket_face_inner_x = bracket_face_outer_x - SENDCUTSEND_SHEET_THICKNESS_MM - center_bridge_x_min: float | None = None - if include_top_center_bridge and len(sorted_tab_bands) >= 2: - center_gap_z_min = sorted_tab_bands[0][1] - center_gap_z_max = sorted_tab_bands[-1][0] - if center_gap_z_max - center_gap_z_min > 1e-3: - candidate_center_bridge_x_min = max( - bracket_face_outer_x - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM, - top_tab_x_min, - end_face_bb.max.X + TOP_CENTER_BRIDGE_SERVO_END_CLEARANCE_MM, - side_face_target_center_bridge_x_min, - ) - if candidate_center_bridge_x_min < bracket_face_outer_x - 1e-3: - center_bridge_x_min = candidate_center_bridge_x_min - bend_bands = _merge_z_bands((*sorted_tab_bands, (center_gap_z_min, center_gap_z_max))) - - min_hole_to_bend_line = min( - _min_mount_hole_distance_to_bend_line(top_holes, bend_line_x=bracket_face_inner_x), - _min_mount_hole_distance_to_bend_line(bottom_holes, bend_line_x=bracket_face_inner_x), - ) - if ( - validate_bend_rule - and min_hole_to_bend_line + BEND_DIE_REGION_TOLERANCE_MM < SENDCUTSEND_HALF_DIE_WIDTH_MM - ): - raise RuntimeError( - "Mount holes are too close to the bend line for the configured sheet-metal bend rule: " - f"{min_hole_to_bend_line:.3f} mm < {SENDCUTSEND_HALF_DIE_WIDTH_MM:.3f} mm" - ) - - front_face_center_y = 0.5 * (bottom_tab_y_min + top_tab_y_max) - outer_flush_gap = min( - min( - z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min - for center, radius in top_holes - ), - min( - z_max - (center.Z + radius) if center.Z >= 0.0 else (center.Z - radius) - z_min - for center, radius in bottom_holes - ), - ) - if outer_flush_gap <= 0.0: - raise RuntimeError(f"Trimmed mount width cuts into a mounting hole: outboard web {outer_flush_gap:.3f} mm") - - return BracketLayout( - z_min=z_min, - z_max=z_max, - top_holes=tuple(top_holes), - bottom_holes=tuple(bottom_holes), - top_tab_x_min=top_tab_x_min, - bottom_tab_x_min=bottom_tab_x_min, - bracket_face_inner_x=bracket_face_inner_x, - bracket_face_outer_x=bracket_face_outer_x, - top_tab_y_min=top_tab_y_min, - top_tab_y_max=top_tab_y_max, - bottom_tab_y_min=bottom_tab_y_min, - bottom_tab_y_max=bottom_tab_y_max, - tab_bands=tuple(sorted_tab_bands), - bend_bands=bend_bands, - center_bridge_x_min=center_bridge_x_min, - front_face_center_y=front_face_center_y, - outer_flush_gap=outer_flush_gap, - ) - - -def build_bracket( - servo_shape: build123d.Shape, - *, - include_top_center_bridge: bool = True, - validate_bend_rule: bool = True, -) -> tuple[build123d.Shape, BracketLayout]: - layout = _build_layout( - servo_shape, - include_top_center_bridge=include_top_center_bridge, - validate_bend_rule=validate_bend_rule, - ) - - bracket: build123d.Shape = _make_box( - x_min=layout.bracket_face_inner_x, - x_max=layout.bracket_face_outer_x, - y_min=layout.bottom_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=layout.z_min, - z_max=layout.z_max, - ) - - for z_band_min, z_band_max in layout.tab_bands: - bracket = bracket.fuse( - _make_rounded_flange_tab( - x_min=layout.top_tab_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.top_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=z_band_min, - z_max=z_band_max, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - ) - - if layout.center_bridge_x_min is not None and len(layout.tab_bands) >= 2: - center_gap_z_min = layout.tab_bands[0][1] - center_gap_z_max = layout.tab_bands[-1][0] - bracket = bracket.fuse( - _make_box( - x_min=layout.center_bridge_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.top_tab_y_min, - y_max=layout.top_tab_y_max, - z_min=center_gap_z_min, - z_max=center_gap_z_max, - ) - ) - - for z_band_min, z_band_max in layout.tab_bands: - bracket = bracket.fuse( - _make_rounded_flange_tab( - x_min=layout.bottom_tab_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.bottom_tab_y_min, - y_max=layout.bottom_tab_y_max, - z_min=z_band_min, - z_max=z_band_max, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - ) - - if layout.center_bridge_x_min is not None and len(layout.tab_bands) >= 2: - center_gap_z_min = layout.tab_bands[0][1] - center_gap_z_max = layout.tab_bands[-1][0] - bracket = bracket.fuse( - _make_box( - x_min=layout.center_bridge_x_min, - x_max=layout.bracket_face_outer_x, - y_min=layout.bottom_tab_y_min, - y_max=layout.bottom_tab_y_max, - z_min=center_gap_z_min, - z_max=center_gap_z_max, - ) - ) - - bracket = _apply_bends_and_end_rounds( - bracket, - bracket_face_inner_x=layout.bracket_face_inner_x, - bracket_face_outer_x=layout.bracket_face_outer_x, - top_tab_x_min=layout.top_tab_x_min, - bottom_tab_x_min=layout.bottom_tab_x_min, - top_tab_y_min=layout.top_tab_y_min, - top_tab_y_max=layout.top_tab_y_max, - bottom_tab_y_min=layout.bottom_tab_y_min, - bottom_tab_y_max=layout.bottom_tab_y_max, - bend_bands=layout.bend_bands, - ) - - bracket = _cut_mount_holes( - bracket, - profiles=list(layout.top_holes), - start_y=layout.top_tab_y_max + CUT_EXTENSION_MM, - direction_y=-1.0, - ) - bracket = _cut_mount_holes( - bracket, - profiles=list(layout.bottom_holes), - start_y=layout.bottom_tab_y_min - CUT_EXTENSION_MM, - direction_y=1.0, - ) - bracket = _cut_front_horn_mount_holes(bracket, layout=layout) - - return bracket, layout - - -def _sendcutsend_outside_setback_90_mm() -> float: - return SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM + SENDCUTSEND_SHEET_THICKNESS_MM - - -def _sendcutsend_bend_allowance_90_mm() -> float: - return (2.0 * _sendcutsend_outside_setback_90_mm()) - SENDCUTSEND_BEND_DEDUCTION_90_MM - - -def _validate_sendcutsend_bend_rule() -> None: - bend_deduction_error = abs( - SENDCUTSEND_BEND_DEDUCTION_90_MM - - ((2.0 * _sendcutsend_outside_setback_90_mm()) - _sendcutsend_bend_allowance_90_mm()) - ) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _min_flange_outside_length(layout: BracketLayout) -> float: - return min( - layout.bracket_face_outer_x - layout.top_tab_x_min, - layout.bracket_face_outer_x - layout.bottom_tab_x_min, - ) - - -def _supported_bend_fraction(layout: BracketLayout) -> float: - bend_width = layout.z_max - layout.z_min - if bend_width <= 0.0: - return 0.0 - supported_width = sum(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands) - return supported_width / bend_width - - -def _min_bend_segment_length(layout: BracketLayout) -> float: - return min(z_band_max - z_band_min for z_band_min, z_band_max in layout.bend_bands) - - -def _center_bridge_z_range(layout: BracketLayout) -> tuple[float, float] | None: - if layout.center_bridge_x_min is None or len(layout.tab_bands) < 2: - return None - - z_min = layout.tab_bands[0][1] - z_max = layout.tab_bands[-1][0] - if z_max - z_min <= 1e-6: - return None - return z_min, z_max - - -def _center_bridge_formed_length(layout: BracketLayout) -> float | None: - if layout.center_bridge_x_min is None: - return None - return layout.bracket_face_inner_x - layout.center_bridge_x_min - - -def _center_bridge_bend_edge_distance(layout: BracketLayout) -> float | None: - if layout.center_bridge_x_min is None: - return None - return layout.bracket_face_outer_x - layout.center_bridge_x_min - - -def _center_bridge_side_face_length(layout: BracketLayout) -> float | None: - if layout.center_bridge_x_min is None: - return None - return layout.center_bridge_x_min - (layout.top_tab_x_min + FLANGE_FREE_CORNER_RADIUS_MM) - - -def _center_bridge_flat_length(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None: - formed_length = _center_bridge_formed_length(layout) - if formed_length is None: - return None - - # Keep the flat-pattern edge at least the required distance from the drawn - # bend line while preserving the formed center-bridge length in the 3D part. - bend_line_clearance_flat_length = ( - CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - - (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm) - ) - return max(formed_length, bend_line_clearance_flat_length) - - -def _center_bridge_flat_bend_line_to_edge_distance(layout: BracketLayout, flat_pattern: FlatPattern) -> float | None: - flat_length = _center_bridge_flat_length(layout, flat_pattern) - if flat_length is None: - return None - return flat_length + (flat_pattern.bend_line_positions_mm[0] - flat_pattern.top_flat_length_mm) - - -def _build_flat_pattern( - layout: BracketLayout, -) -> FlatPattern: - _validate_sendcutsend_bend_rule() - - outside_setback = _sendcutsend_outside_setback_90_mm() - bend_allowance = _sendcutsend_bend_allowance_90_mm() - top_outside_length = layout.bracket_face_outer_x - layout.top_tab_x_min - bottom_outside_length = layout.bracket_face_outer_x - layout.bottom_tab_x_min - top_flat_length = top_outside_length - outside_setback - bottom_flat_length = bottom_outside_length - outside_setback - if top_flat_length <= 0.0 or bottom_flat_length <= 0.0: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"top={top_flat_length:.3f} mm, bottom={bottom_flat_length:.3f} mm" - ) - flange_outside_min = _min_flange_outside_length(layout) - flange_flat_min = flange_outside_min - outside_setback - if flange_outside_min < SENDCUTSEND_MIN_FLANGE_FORMED_MM: - raise RuntimeError( - "Formed flange length is below the configured bend-rule minimum: " - f"{flange_outside_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FORMED_MM:.3f} mm" - ) - if flange_flat_min < SENDCUTSEND_MIN_FLANGE_FLAT_MM: - raise RuntimeError( - "Flat flange length is below the configured bend-rule minimum: " - f"{flange_flat_min:.3f} mm < {SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm" - ) - - top_web_tangent_y = layout.top_tab_y_min - SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - bottom_web_tangent_y = layout.bottom_tab_y_max + SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM - web_flat_length = top_web_tangent_y - bottom_web_tangent_y - if web_flat_length <= 0.0: - raise RuntimeError(f"Web flat length implied by the bend rule is too short: {web_flat_length:.3f} mm") - - web_flat_start = top_flat_length + bend_allowance - web_flat_end = web_flat_start + web_flat_length - bottom_flat_start = web_flat_end + bend_allowance - total_length = bottom_flat_start + bottom_flat_length - width = layout.z_max - layout.z_min - top_bend_line_x = top_flat_length + (0.5 * bend_allowance) - bottom_bend_line_x = web_flat_end + (0.5 * bend_allowance) - bend_line_segments = ( - (top_bend_line_x, 0.0, width), - (bottom_bend_line_x, 0.0, width), - ) - - cut_circles: list[tuple[float, float, float]] = [] - for center, radius in layout.top_holes: - cut_circles.append( - ( - center.X - layout.top_tab_x_min, - center.Z - layout.z_min, - radius, - ) - ) - for center, radius in layout.bottom_holes: - cut_circles.append( - ( - total_length - (center.X - layout.bottom_tab_x_min), - center.Z - layout.z_min, - radius, - ) - ) - for center_y, center_z, radius in _front_horn_mount_hole_profiles(layout): - cut_circles.append( - ( - web_flat_start + (top_web_tangent_y - center_y), - center_z - layout.z_min, - radius, - ) - ) - flat_pattern = FlatPattern( - total_length_mm=total_length, - width_mm=width, - top_flat_length_mm=top_flat_length, - web_flat_start_mm=web_flat_start, - web_flat_end_mm=web_flat_end, - bottom_flat_start_mm=bottom_flat_start, - bend_line_positions_mm=(top_bend_line_x, bottom_bend_line_x), - bend_line_segments_mm=bend_line_segments, - cut_circles=tuple(cut_circles), - cut_segments=(), - ) - center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern) - if ( - center_bridge_flat_clearance is not None - and center_bridge_flat_clearance + 1e-6 < CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM - ): - raise RuntimeError( - "Top center bridge flat bend-line clearance is below the configured target: " - f"{center_bridge_flat_clearance:.3f} mm < {CENTER_BRIDGE_BEND_LINE_EDGE_TARGET_MM:.3f} mm" - ) - return flat_pattern - - -def _flat_rectangles(layout: BracketLayout, flat_pattern: FlatPattern) -> tuple[tuple[float, float, float, float], ...]: - rectangles: list[tuple[float, float, float, float]] = [ - ( - flat_pattern.top_flat_length_mm, - 0.0, - flat_pattern.bottom_flat_start_mm, - flat_pattern.width_mm, - ) - ] - - for z_band_min, z_band_max in layout.tab_bands: - y_min = z_band_min - layout.z_min - y_max = z_band_max - layout.z_min - rectangles.append((0.0, y_min, flat_pattern.top_flat_length_mm, y_max)) - - center_bridge_z_range = _center_bridge_z_range(layout) - center_bridge_flat_length = _center_bridge_flat_length(layout, flat_pattern) - if center_bridge_z_range is not None and center_bridge_flat_length is not None: - y_min = center_bridge_z_range[0] - layout.z_min - y_max = center_bridge_z_range[1] - layout.z_min - x_min = flat_pattern.top_flat_length_mm - center_bridge_flat_length - rectangles.append((x_min, y_min, flat_pattern.top_flat_length_mm, y_max)) - - for z_band_min, z_band_max in layout.tab_bands: - y_min = z_band_min - layout.z_min - y_max = z_band_max - layout.z_min - rectangles.append((flat_pattern.bottom_flat_start_mm, y_min, flat_pattern.total_length_mm, y_max)) - - if center_bridge_z_range is not None and center_bridge_flat_length is not None: - y_min = center_bridge_z_range[0] - layout.z_min - y_max = center_bridge_z_range[1] - layout.z_min - x_max = flat_pattern.bottom_flat_start_mm + center_bridge_flat_length - rectangles.append((flat_pattern.bottom_flat_start_mm, y_min, x_max, y_max)) - - return tuple(rectangle for rectangle in rectangles if rectangle[2] - rectangle[0] > 1e-6 and rectangle[3] - rectangle[1] > 1e-6) - - -def _flange_free_corner_points( - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> set[tuple[float, float]]: - points: set[tuple[float, float]] = set() - for z_band_min, z_band_max in layout.tab_bands: - flat_z_min = z_band_min - layout.z_min - flat_z_max = z_band_max - layout.z_min - points.add((0.0, flat_z_min)) - points.add((0.0, flat_z_max)) - for z_band_min, z_band_max in layout.tab_bands: - flat_z_min = z_band_min - layout.z_min - flat_z_max = z_band_max - layout.z_min - points.add((flat_pattern.total_length_mm, flat_z_min)) - points.add((flat_pattern.total_length_mm, flat_z_max)) - return points - - -def _sample_short_arc_2d( - *, - center: tuple[float, float], - start: tuple[float, float], - end: tuple[float, float], - count: int, -) -> list[tuple[float, float]]: - start_angle = math.atan2(start[1] - center[1], start[0] - center[0]) - end_angle = math.atan2(end[1] - center[1], end[0] - center[0]) - ccw_span = (end_angle - start_angle) % (2.0 * math.pi) - if ccw_span <= math.pi: - span = ccw_span - else: - span = -((start_angle - end_angle) % (2.0 * math.pi)) - - samples = max(count, 2) - radius = math.hypot(start[0] - center[0], start[1] - center[1]) - return [ - ( - center[0] + (radius * math.cos(start_angle + (span * index / (samples - 1)))), - center[1] + (radius * math.sin(start_angle + (span * index / (samples - 1)))), - ) - for index in range(samples) - ] - - -def _rounded_outline_loop_points( - loop: list[tuple[float, float]], - *, - rounded_corners: set[tuple[float, float]], - radius: float, -) -> list[tuple[float, float]]: - if radius <= 1e-6: - return loop - - rounded_loop: list[tuple[float, float]] = [] - for index, point in enumerate(loop): - if point not in rounded_corners: - rounded_loop.append(point) - continue - - previous_point = loop[index - 1] - next_point = loop[(index + 1) % len(loop)] - prev_vec = (previous_point[0] - point[0], previous_point[1] - point[1]) - next_vec = (next_point[0] - point[0], next_point[1] - point[1]) - prev_len = math.hypot(*prev_vec) - next_len = math.hypot(*next_vec) - corner_radius = min(radius, 0.5 * prev_len, 0.5 * next_len) - if corner_radius <= 1e-6: - rounded_loop.append(point) - continue - - prev_unit = (prev_vec[0] / prev_len, prev_vec[1] / prev_len) - next_unit = (next_vec[0] / next_len, next_vec[1] / next_len) - tangent_in = ( - point[0] + (prev_unit[0] * corner_radius), - point[1] + (prev_unit[1] * corner_radius), - ) - tangent_out = ( - point[0] + (next_unit[0] * corner_radius), - point[1] + (next_unit[1] * corner_radius), - ) - center = ( - point[0] + ((prev_unit[0] + next_unit[0]) * corner_radius), - point[1] + ((prev_unit[1] + next_unit[1]) * corner_radius), - ) - arc_points = _sample_short_arc_2d( - center=center, - start=tangent_in, - end=tangent_out, - count=FLANGE_FREE_CORNER_SAMPLE_COUNT, - ) - rounded_loop.extend(arc_points) - - return rounded_loop - - -def _outline_loops_from_rectangles( - rectangles: tuple[tuple[float, float, float, float], ...], -) -> list[list[tuple[float, float]]]: - x_coords = sorted({coord for x0, _y0, x1, _y1 in rectangles for coord in (x0, x1)}) - y_coords = sorted({coord for _x0, y0, _x1, y1 in rectangles for coord in (y0, y1)}) - filled_cells: set[tuple[int, int]] = set() - - for x_index in range(len(x_coords) - 1): - for y_index in range(len(y_coords) - 1): - x_mid = 0.5 * (x_coords[x_index] + x_coords[x_index + 1]) - y_mid = 0.5 * (y_coords[y_index] + y_coords[y_index + 1]) - if any(x0 < x_mid < x1 and y0 < y_mid < y1 for x0, y0, x1, y1 in rectangles): - filled_cells.add((x_index, y_index)) - - edges: dict[tuple[tuple[float, float], tuple[float, float]], None] = {} - - def add_edge(start: tuple[float, float], end: tuple[float, float]) -> None: - reverse = (end, start) - if reverse in edges: - del edges[reverse] - return - edges[(start, end)] = None - - for x_index, y_index in filled_cells: - x0 = x_coords[x_index] - x1 = x_coords[x_index + 1] - y0 = y_coords[y_index] - y1 = y_coords[y_index + 1] - add_edge((x0, y0), (x1, y0)) - add_edge((x1, y0), (x1, y1)) - add_edge((x1, y1), (x0, y1)) - add_edge((x0, y1), (x0, y0)) - - outgoing: dict[tuple[float, float], list[tuple[float, float]]] = {} - for start, end in edges: - outgoing.setdefault(start, []).append(end) - - loops: list[list[tuple[float, float]]] = [] - while outgoing: - start = min(outgoing) - loop = [start] - current = start - while True: - next_points = outgoing.get(current) - if not next_points: - raise RuntimeError("Flat-pattern outline could not be assembled from panel rectangles") - next_point = next_points.pop() - if not next_points: - del outgoing[current] - if next_point == start: - break - loop.append(next_point) - current = next_point - - area = _polygon_area(loop) - if area < 0.0: - loop.reverse() - loops.append(loop) - - return loops - - -def _polygon_area(points: list[tuple[float, float]]) -> float: - return 0.5 * sum( - (points[index][0] * points[(index + 1) % len(points)][1]) - - (points[(index + 1) % len(points)][0] * points[index][1]) - for index in range(len(points)) - ) - - -def _add_reference_arc( - msp: ezdxf.layouts.Modelspace, - *, - start: tuple[float, float], - midpoint: tuple[float, float], - end: tuple[float, float], -) -> None: - from ezdxf.math import ConstructionArc - - arc = ConstructionArc.from_3p(start, end, def_point=midpoint, ccw=True) - center = (arc.center.x, arc.center.y) - start_angle = math.atan2(start[1] - center[1], start[0] - center[0]) - mid_angle = math.atan2(midpoint[1] - center[1], midpoint[0] - center[0]) - end_angle = math.atan2(end[1] - center[1], end[0] - center[0]) - ccw_sweep = (end_angle - start_angle) % (2.0 * math.pi) - mid_sweep = (mid_angle - start_angle) % (2.0 * math.pi) - if mid_sweep <= ccw_sweep + 1e-6: - msp.add_arc( - center=center, - radius=arc.radius, - start_angle=math.degrees(start_angle), - end_angle=math.degrees(end_angle), - dxfattribs={"layer": "CUT"}, - ) - return - - msp.add_arc( - center=center, - radius=arc.radius, - start_angle=math.degrees(end_angle), - end_angle=math.degrees(start_angle), - dxfattribs={"layer": "CUT"}, - ) - - -def _build_flat_pattern_dxf(flat_pattern: FlatPattern, layout: BracketLayout) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - rounded_corners = _flange_free_corner_points(layout, flat_pattern) - for loop in _outline_loops_from_rectangles(_flat_rectangles(layout, flat_pattern)): - rounded_loop = _rounded_outline_loop_points( - loop, - rounded_corners=rounded_corners, - radius=FLANGE_FREE_CORNER_RADIUS_MM, - ) - for index, point in enumerate(rounded_loop): - msp.add_line(point, rounded_loop[(index + 1) % len(rounded_loop)], dxfattribs={"layer": "CUT"}) - - for x, y, cut_radius in flat_pattern.cut_circles: - msp.add_circle((x, y), cut_radius, dxfattribs={"layer": "CUT"}) - - for segment_type, points in flat_pattern.cut_segments: - if segment_type == "LINE": - msp.add_line(points[0], points[-1], dxfattribs={"layer": "CUT"}) - elif segment_type == "ARC": - _add_reference_arc(msp, start=points[0], midpoint=points[1], end=points[-1]) - else: - raise RuntimeError(f"Unsupported flat-pattern cut segment type: {segment_type}") - - for bend_line_x, bend_y_min, bend_y_max in flat_pattern.bend_line_segments_mm: - msp.add_line( - (bend_line_x, bend_y_min), - (bend_line_x, bend_y_max), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def _topology_flat_pattern_dxf( - bracket: build123d.Shape, - layout: BracketLayout, - flat_pattern: FlatPattern, -) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - web_source_span = layout.top_tab_y_min - layout.bottom_tab_y_max - web_flat_span = flat_pattern.bottom_flat_start_mm - flat_pattern.top_flat_length_mm - if web_source_span <= 0.0 or web_flat_span <= 0.0: - raise RuntimeError("Servo end mount topology DXF has invalid web span") - - def project_top(point: build123d.Vector) -> tuple[float, float]: - if abs(point.X - layout.bracket_face_outer_x) <= 1e-3: - u = flat_pattern.top_flat_length_mm - else: - u = point.X - layout.top_tab_x_min - return (u, point.Z - layout.z_min) - - def project_web(point: build123d.Vector) -> tuple[float, float]: - t = (layout.top_tab_y_min - point.Y) / web_source_span - return ( - flat_pattern.top_flat_length_mm + (t * web_flat_span), - point.Z - layout.z_min, - ) - - def project_bottom(point: build123d.Vector) -> tuple[float, float]: - if abs(point.X - layout.bracket_face_outer_x) <= 1e-3: - u = flat_pattern.bottom_flat_start_mm - else: - u = flat_pattern.total_length_mm - (point.X - layout.bottom_tab_x_min) - return (u, point.Z - layout.z_min) - - top_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=1.0, - coordinate_axis="y", - coordinate=layout.top_tab_y_max, - ) - web_faces = dxf_topology.planar_faces( - bracket, - normal_axis="x", - normal_sign=-1.0, - coordinate_axis="x", - coordinate=layout.bracket_face_inner_x, - ) - bottom_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=-1.0, - coordinate_axis="y", - coordinate=layout.bottom_tab_y_min, - ) - geometry = dxf_topology.union_projected_faces( - ( - (top_faces, project_top), - (web_faces, project_web), - (bottom_faces, project_bottom), - ), - merge_touching_tolerance_mm=TOPOLOGY_FLAT_PANEL_JOIN_TOLERANCE_MM, - ) - dxf_topology.add_shapely_geometry(doc.modelspace(), geometry, layer="CUT") - - for bend_line_x, bend_y_min, bend_y_max in flat_pattern.bend_line_segments_mm: - doc.modelspace().add_line( - (bend_line_x, bend_y_min), - (bend_line_x, bend_y_max), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def build_step( - *, - include_top_center_bridge: bool = True, -) -> build123d.Shape: - CAD_DIR.mkdir(parents=True, exist_ok=True) - - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - bracket, layout = build_bracket(servo_shape, include_top_center_bridge=include_top_center_bridge) - _validate_slot_clearances(layout) - bracket = _cut_slots(bracket, layout) - flat_pattern = _build_flat_pattern(layout) - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected bracket solid, found {len(solids)}") - - intersection_volume = servo_shape.intersect(bracket).volume - if intersection_volume > 1e-3: - raise RuntimeError(f"Bracket intersects the servo by {intersection_volume:.6f} mm^3") - - bracket.label = PART_NAME - bracket.color = GRAY_ALUMINUM_COLOR - - bracket_bb = bracket.bounding_box() - slot_metrics = _slot_clearance_metrics(layout) - print( - "Bracket envelope " - f"X={bracket_bb.size.X:.3f} mm, " - f"Y={bracket_bb.size.Y:.3f} mm, " - f"Z={bracket_bb.size.Z:.3f} mm" - ) - print( - "SendCutSend setup " - f"material=5052-H32, thickness={SENDCUTSEND_SHEET_THICKNESS_MM:.3f} mm, " - f"bend_radius={SENDCUTSEND_EFFECTIVE_BEND_RADIUS_MM:.3f} mm, " - f"k_factor={SENDCUTSEND_K_FACTOR:.2f}, " - f"bend_deduction_90={SENDCUTSEND_BEND_DEDUCTION_90_MM:.3f} mm, " - f"anodize_min_short_dim={SENDCUTSEND_MIN_ANODIZE_SHORTEST_DIM_MM:.3f} mm" - ) - sendcutsend_checks = ( - "SendCutSend checks " - f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, " - f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, " - f"bend_support={_supported_bend_fraction(layout):.1%}, " - f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm, " - f"flange_free_corner_radius={FLANGE_FREE_CORNER_RADIUS_MM:.3f} mm" - ) - print(sendcutsend_checks) - print( - "Slot checks " - f"slot_count={len(_slot_specs(layout))}, " - f"slot={_slot_length(layout):.3f}x{SLOT_WIDTH_MM:.3f} mm, " - f"side_end_gap_min={slot_metrics['slot_side_end_gap_min_mm']:.3f} mm, " - f"slot_to_mount_hole_x_min={slot_metrics['slot_to_mount_hole_x_min_mm']:.3f} mm, " - f"center_bridge_margin={slot_metrics['slot_center_bridge_margin_mm']:.3f} mm, " - f"slot_edge_to_bend_min={slot_metrics['slot_edge_to_bend_min_mm']:.3f} mm, " - f"slot_edge_to_bend_leeway={slot_metrics['slot_edge_to_bend_leeway_mm']:.3f} mm" - ) - if layout.outer_flush_gap < MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM: - print( - "Warning: trimmed outer side leaves a small hole edge web: " - f"{layout.outer_flush_gap:.3f} mm < {MOUNT_HOLE_TRIMMED_OUTBOARD_Z_WEB_WARN_MM:.3f} mm" - ) - end_face_x = _find_face_near(servo_shape, FACE_TARGET_CENTERS_MM["end_face"], geom_type="PLANE").bounding_box().max.X - servo_bb = servo_shape.bounding_box() - side_gap_neg = servo_bb.min.Z - layout.z_min - side_gap_pos = layout.z_max - servo_bb.max.Z - print(f"Bracket face clearance: {layout.bracket_face_inner_x - end_face_x:.3f} mm") - print( - "Servo width check " - f"servo_z_width={servo_bb.size.Z:.3f} mm, " - f"mount_z_width={layout.z_max - layout.z_min:.3f} mm, " - f"side_gaps={side_gap_neg:.3f}/{side_gap_pos:.3f} mm" - ) - print( - "Wrap clearances " - f"top={TOP_MOUNT_PLANE_CLEARANCE_MM:.3f} mm, " - f"bottom={BOTTOM_MOUNT_PLANE_CLEARANCE_MM:.3f} mm, " - f"side_each={SIDE_WRAP_CLEARANCE_MM:.3f} mm, " - f"center_bridge_end={TOP_CENTER_BRIDGE_SERVO_END_CLEARANCE_MM:.3f} mm, " - f"center_bridge_flange_extension={CENTER_BRIDGE_FLANGE_LENGTH_EXTENSION_MM:.3f} mm, " - f"center_bridge_side_face_target={CENTER_BRIDGE_SIDE_FACE_LENGTH_MM:.3f} mm, " - f"center_relief_side_extra={TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM:.3f} mm, " - f"top_flange_extension={TOP_FLANGE_LENGTH_EXTENSION_MM:.3f} mm, " - f"bottom_flange_extension={BOTTOM_FLANGE_LENGTH_EXTENSION_MM:.3f} mm" - ) - center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout) - center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern) - center_bridge_side_face_length = _center_bridge_side_face_length(layout) - if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None: - print( - "Top center bridge " - f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, " - f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm, " - f"side_face_length={center_bridge_side_face_length:.3f} mm, " - f"servo_end_clearance={layout.center_bridge_x_min - end_face_x:.3f} mm" - ) - print(f"Bracket face inner X: {layout.bracket_face_inner_x:.3f} mm") - print( - "Top mount holes used (x, y, z) mm: " - + ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.top_holes) - ) - print( - "Bottom mount holes used (x, y, z) mm: " - + ", ".join(str(tuple(round(value, 6) for value in _vector_to_tuple(center))) for center, _radius in layout.bottom_holes) - ) - print( - "Front horn face holes (y, z, radius) mm: " - + ", ".join( - str(tuple(round(value, 6) for value in profile)) - for profile in _front_horn_mount_hole_profiles(layout) - ) - ) - print(f"Servo/bracket interference volume (mm^3): {intersection_volume:.6f}") - return bracket - - -def build_dxf( - *, - include_top_center_bridge: bool = True, -) -> ezdxf.document.Drawing: - CAD_DIR.mkdir(parents=True, exist_ok=True) - - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - bracket, layout = build_bracket( - servo_shape, - include_top_center_bridge=include_top_center_bridge, - ) - _validate_slot_clearances(layout) - bracket = _cut_slots(bracket, layout) - flat_pattern = _build_flat_pattern(layout) - doc = _topology_flat_pattern_dxf(bracket, layout, flat_pattern) - - outside_setback = _sendcutsend_outside_setback_90_mm() - flange_outside_min = _min_flange_outside_length(layout) - slot_metrics = _slot_clearance_metrics(layout) - print( - "Flat pattern " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"bend_segments={len(flat_pattern.bend_line_segments_mm)}, " - f"cut_circles={len(flat_pattern.cut_circles)}, " - f"front_cut_segments={len(flat_pattern.cut_segments)}" - ) - dxf_bend_checks = ( - "DXF bend checks " - f"flange_outside_min={flange_outside_min:.3f} mm, " - f"flange_flat_min={flange_outside_min - outside_setback:.3f} mm, " - f"flange_flat_required={SENDCUTSEND_MIN_FLANGE_FLAT_MM:.3f} mm, " - f"bend_segment_min={_min_bend_segment_length(layout):.3f} mm, " - f"bend_support={_supported_bend_fraction(layout):.1%}, " - f"hole_to_bend_min={min(abs(center.X - layout.bracket_face_inner_x) for center, _radius in [*layout.top_holes, *layout.bottom_holes]):.3f} mm, " - f"outboard_hole_edge_web={layout.outer_flush_gap:.3f} mm, " - f"center_bridge_end_clearance={TOP_CENTER_BRIDGE_SERVO_END_CLEARANCE_MM:.3f} mm, " - f"center_relief_side_extra={TOP_CENTER_RELIEF_SIDE_CLEARANCE_MM:.3f} mm" - ) - print(dxf_bend_checks) - print( - "DXF slot checks " - f"slot_count={len(_slot_specs(layout))}, " - f"slot={_slot_length(layout):.3f}x{SLOT_WIDTH_MM:.3f} mm, " - f"side_end_gap_min={slot_metrics['slot_side_end_gap_min_mm']:.3f} mm, " - f"slot_to_mount_hole_x_min={slot_metrics['slot_to_mount_hole_x_min_mm']:.3f} mm, " - f"center_bridge_margin={slot_metrics['slot_center_bridge_margin_mm']:.3f} mm, " - f"slot_edge_to_bend_min={slot_metrics['slot_edge_to_bend_min_mm']:.3f} mm" - ) - center_bridge_bend_edge_distance = _center_bridge_bend_edge_distance(layout) - center_bridge_flat_clearance = _center_bridge_flat_bend_line_to_edge_distance(layout, flat_pattern) - center_bridge_side_face_length = _center_bridge_side_face_length(layout) - if center_bridge_bend_edge_distance is not None and center_bridge_flat_clearance is not None: - print( - "DXF center bridge " - f"bend_edge_distance={center_bridge_bend_edge_distance:.3f} mm, " - f"flat_bend_line_to_edge={center_bridge_flat_clearance:.3f} mm, " - f"side_face_length={center_bridge_side_face_length:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/parts/servo_end_mount.step b/models/robots/tom/v2/parts/servo_end_mount.step deleted file mode 100644 index fe91e622..00000000 --- a/models/robots/tom/v2/parts/servo_end_mount.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8cc83029027f32a492331fbb2cf81c86643ad763463245b3a11a3b00e897f3a6 -size 174315 diff --git a/models/robots/tom/v2/parts/servo_end_mount_double.py b/models/robots/tom/v2/parts/servo_end_mount_double.py deleted file mode 100644 index 82d306a1..00000000 --- a/models/robots/tom/v2/parts/servo_end_mount_double.py +++ /dev/null @@ -1,444 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -import build123d - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc -import servo_end_mount as sem - - -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Double Servo End Mount" - -SIDE_BY_SIDE_CLEARANCE_MM = 2.0 -SERVO_CENTER_SPACING_Y_MM = (2.0 * abs(lc.SERVO_REAR_EXTREME_LOCAL_Y_MM)) + SIDE_BY_SIDE_CLEARANCE_MM -SERVO_CENTER_OFFSET_Y_MM = 0.5 * SERVO_CENTER_SPACING_Y_MM -REAR_SERVO_CENTER_Y_MM = -SERVO_CENTER_OFFSET_Y_MM -FRONT_SERVO_CENTER_Y_MM = SERVO_CENTER_OFFSET_Y_MM -OUTER_FLANGE_WIDTH_SCALE = 2.0 -SLOT_WIDTH_SCALE = 1.0 -# Run the slot end-to-end between the vertical midlines of the two mount-hole -# rows so the slot grows with the widened double-servo side plate. -SLOT_LENGTH_MM = 2.0 * lc.SERVO_MOUNT_HOLE_LOCAL_Z_MM -CENTER_RELIEF_EDGE_CLEARANCE_MM = 1.0 -# Match the output-horn relief used by link_bracket.py on link_assembly -# #o1.4.f4. This radius keeps the near M2 screw web at the same distance -# from the horn relief while clearing the rotating horn boss. -OUTPUT_HORN_RELIEF_RADIUS_MM = 11.0 -EXTENDED_TOP_MOUNT_HOLES = tuple( - ( - build123d.Vector(x_mm, 0.0, z_mm), - lc.SERVO_MOUNT_HOLE_RADIUS_MM, - ) - for x_mm in lc.SERVO_TOP_MOUNT_HOLE_LOCAL_X_MM - for z_mm in (-lc.SERVO_MOUNT_HOLE_LOCAL_Z_MM, lc.SERVO_MOUNT_HOLE_LOCAL_Z_MM) -) - - -def _load_single_layout() -> sem.BracketLayout: - if not sem.SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing STS3250 servo STEP: {sem.SERVO_STEP}") - servo_shape = sem.import_as_shape(sem.SERVO_STEP) - return sem._build_layout(servo_shape, include_top_center_bridge=True) - - -def _translated_band(y_min: float, y_max: float, y_offset: float) -> tuple[float, float]: - return y_min + y_offset, y_max + y_offset - - -def _x_flipped_band(y_min: float, y_max: float, y_offset: float) -> tuple[float, float]: - return y_offset - y_max, y_offset - y_min - - -def _extended_top_tab_x_min() -> float: - return ( - min(center.X - radius - sem.MOUNT_HOLE_EDGE_X_WEB_MM for center, radius in EXTENDED_TOP_MOUNT_HOLES) - - sem.TOP_FLANGE_LENGTH_EXTENSION_MM - ) - - -def _expanded_z_bounds(layout: sem.BracketLayout) -> tuple[float, float]: - lower_z_min, lower_z_inner = layout.tab_bands[0] - _upper_z_inner, upper_z_max = layout.tab_bands[-1] - lower_width = lower_z_inner - lower_z_min - width_extension = (OUTER_FLANGE_WIDTH_SCALE - 1.0) * lower_width - return ( - lower_z_min - width_extension, - upper_z_max + width_extension, - ) - - -def _slot_center_z(layout: sem.BracketLayout) -> float: - z_min, z_max = _expanded_z_bounds(layout) - return 0.5 * (z_min + z_max) - - -def _expanded_tab_bands(layout: sem.BracketLayout) -> tuple[tuple[float, float], ...]: - z_min, z_max = _expanded_z_bounds(layout) - _lower_z_min, lower_z_max = layout.tab_bands[0] - upper_z_min, _upper_z_max = layout.tab_bands[-1] - lower_z_max -= CENTER_RELIEF_EDGE_CLEARANCE_MM - upper_z_min += CENTER_RELIEF_EDGE_CLEARANCE_MM - return ( - (z_min, lower_z_max), - (upper_z_min, z_max), - ) - - -def _z_slot_cut( - *, - center_x: float, - center_z: float, - length: float, - width: float, - y_min: float, - y_max: float, -) -> build123d.Shape: - radius = 0.5 * width - half_straight = 0.5 * length - radius - y_start = y_min - sem.CUT_EXTENSION_MM - y_height = (y_max - y_min) + (2.0 * sem.CUT_EXTENSION_MM) - body: build123d.Shape = sem._make_box( - x_min=center_x - radius, - x_max=center_x + radius, - y_min=y_start, - y_max=y_start + y_height, - z_min=center_z - half_straight, - z_max=center_z + half_straight, - ) - for end_z in (center_z - half_straight, center_z + half_straight): - cap = sem._make_cylinder_along( - radius=radius, - height=y_height, - origin=build123d.Vector(center_x, y_start, end_z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - body = body.fuse(cap) - return body - - -def _fuse_outer_flange( - bracket: build123d.Shape, - *, - layout: sem.BracketLayout, - x_min: float, - y_min: float, - y_max: float, -) -> build123d.Shape: - for z_band_min, z_band_max in _expanded_tab_bands(layout): - bracket = bracket.fuse( - sem._make_rounded_flange_tab( - x_min=x_min, - x_max=layout.bracket_face_outer_x, - y_min=y_min, - y_max=y_max, - z_min=z_band_min, - z_max=z_band_max, - radius=sem.FLANGE_FREE_CORNER_RADIUS_MM, - ) - ) - - if layout.center_bridge_x_min is not None and len(layout.tab_bands) >= 2: - center_gap_z_min = layout.tab_bands[0][1] - CENTER_RELIEF_EDGE_CLEARANCE_MM - center_gap_z_max = layout.tab_bands[-1][0] + CENTER_RELIEF_EDGE_CLEARANCE_MM - bracket = bracket.fuse( - sem._make_box( - x_min=layout.center_bridge_x_min, - x_max=layout.bracket_face_outer_x, - y_min=y_min, - y_max=y_max, - z_min=center_gap_z_min, - z_max=center_gap_z_max, - ) - ) - return bracket - - -def _cut_outer_flange_slots( - bracket: build123d.Shape, - *, - layout: sem.BracketLayout, - rear_y_min: float, - rear_y_max: float, - front_y_min: float, - front_y_max: float, -) -> build123d.Shape: - scaled_slot_width = sem.SLOT_WIDTH_MM * SLOT_WIDTH_SCALE - slot_center_z = _slot_center_z(layout) - for name, center_x, _center_z, _length in sem._slot_specs(layout): - if name == "top": - y_min, y_max = front_y_min, front_y_max - else: - y_min, y_max = rear_y_min, rear_y_max - bracket = bracket.cut( - _z_slot_cut( - center_x=center_x, - center_z=slot_center_z, - length=SLOT_LENGTH_MM, - width=scaled_slot_width, - y_min=y_min, - y_max=y_max, - ) - ) - return bracket - - -def _cut_rear_output_horn_relief( - bracket: build123d.Shape, - *, - rear_y_min: float, - rear_y_max: float, -) -> build123d.Shape: - cutter = sem._make_cylinder_along( - radius=OUTPUT_HORN_RELIEF_RADIUS_MM, - height=(rear_y_max - rear_y_min) + (2.0 * sem.CUT_EXTENSION_MM), - origin=build123d.Vector( - lc.SERVO_SHAFT_LOCAL_X_MM, - rear_y_min - sem.CUT_EXTENSION_MM, - 0.0, - ), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - return bracket.cut(cutter) - - -def _cut_centered_front_horn_holes( - bracket: build123d.Shape, - *, - layout: sem.BracketLayout, -) -> build123d.Shape: - hole_depth = sem.SENDCUTSEND_SHEET_THICKNESS_MM + (2.0 * sem.CUT_EXTENSION_MM) - start_x = layout.bracket_face_outer_x + sem.CUT_EXTENSION_MM - rear_outer_horn_y = REAR_SERVO_CENTER_Y_MM - lc.SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - front_outer_horn_y = FRONT_SERVO_CENTER_Y_MM + lc.SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - front_horn_center_y = 0.5 * (rear_outer_horn_y + front_outer_horn_y) - center_y_delta = front_horn_center_y - layout.front_face_center_y - for center_y, center_z, radius in sem._front_horn_mount_hole_profiles(layout): - cutter = sem._make_cylinder_along( - radius=radius, - height=hole_depth, - origin=build123d.Vector(start_x, center_y + center_y_delta, center_z), - direction=build123d.Vector(-1.0, 0.0, 0.0), - ) - bracket = bracket.cut(cutter) - return bracket - - -def _front_horn_center_y(layout: sem.BracketLayout) -> float: - rear_outer_horn_y = REAR_SERVO_CENTER_Y_MM - lc.SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - front_outer_horn_y = FRONT_SERVO_CENTER_Y_MM + lc.SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - return 0.5 * (rear_outer_horn_y + front_outer_horn_y) - - -def _build_double_flat_layout() -> sem.BracketLayout: - single = _load_single_layout() - extended_top_tab_x_min = _extended_top_tab_x_min() - z_min, z_max = _expanded_z_bounds(single) - rear_y_min, rear_y_max = _x_flipped_band( - single.top_tab_y_min, - single.top_tab_y_max, - REAR_SERVO_CENTER_Y_MM, - ) - front_y_min, front_y_max = _translated_band( - single.top_tab_y_min, - single.top_tab_y_max, - FRONT_SERVO_CENTER_Y_MM, - ) - tab_bands = _expanded_tab_bands(single) - center_bridge_x_min = single.center_bridge_x_min - bend_bands = sem._merge_z_bands( - ( - *tab_bands, - ( - tab_bands[0][1], - tab_bands[-1][0], - ), - ) - ) - return sem.BracketLayout( - z_min=z_min, - z_max=z_max, - top_holes=EXTENDED_TOP_MOUNT_HOLES, - bottom_holes=EXTENDED_TOP_MOUNT_HOLES, - top_tab_x_min=extended_top_tab_x_min, - bottom_tab_x_min=extended_top_tab_x_min, - bracket_face_inner_x=single.bracket_face_inner_x, - bracket_face_outer_x=single.bracket_face_outer_x, - top_tab_y_min=front_y_min, - top_tab_y_max=front_y_max, - bottom_tab_y_min=rear_y_min, - bottom_tab_y_max=rear_y_max, - tab_bands=tab_bands, - bend_bands=bend_bands, - center_bridge_x_min=center_bridge_x_min, - front_face_center_y=_front_horn_center_y(single), - outer_flush_gap=single.outer_flush_gap, - ) - - -def _add_double_slots_to_dxf(doc, flat_pattern: sem.FlatPattern, layout: sem.BracketLayout) -> None: - msp = doc.modelspace() - slot_center_x = sem._slot_center_x(layout) - slot_center_z = _slot_center_z(layout) - slot_width = sem.SLOT_WIDTH_MM * SLOT_WIDTH_SCALE - for name in ("top", "bottom"): - if name == "top": - flat_center_x = slot_center_x - layout.top_tab_x_min - else: - flat_center_x = flat_pattern.total_length_mm - (slot_center_x - layout.bottom_tab_x_min) - flat_center_y = slot_center_z - layout.z_min - msp.add_lwpolyline( - sem._slot_profile_points( - center_x=flat_center_x, - center_y=flat_center_y, - length=SLOT_LENGTH_MM, - width=slot_width, - ), - close=True, - dxfattribs={"layer": "CUT"}, - ) - - -def build_step() -> build123d.Shape: - layout = _load_single_layout() - extended_top_tab_x_min = _extended_top_tab_x_min() - z_min, z_max = _expanded_z_bounds(layout) - rear_y_min, rear_y_max = _x_flipped_band( - layout.top_tab_y_min, - layout.top_tab_y_max, - REAR_SERVO_CENTER_Y_MM, - ) - front_y_min, front_y_max = _translated_band( - layout.top_tab_y_min, - layout.top_tab_y_max, - FRONT_SERVO_CENTER_Y_MM, - ) - - bracket: build123d.Shape = sem._make_box( - x_min=layout.bracket_face_inner_x, - x_max=layout.bracket_face_outer_x, - y_min=rear_y_min, - y_max=front_y_max, - z_min=z_min, - z_max=z_max, - ) - bracket = _fuse_outer_flange( - bracket, - layout=layout, - x_min=extended_top_tab_x_min, - y_min=rear_y_min, - y_max=rear_y_max, - ) - bracket = _fuse_outer_flange( - bracket, - layout=layout, - x_min=extended_top_tab_x_min, - y_min=front_y_min, - y_max=front_y_max, - ) - - bracket = sem._cut_mount_holes( - bracket, - profiles=list(EXTENDED_TOP_MOUNT_HOLES), - start_y=rear_y_min - sem.CUT_EXTENSION_MM, - direction_y=1.0, - ) - bracket = sem._cut_mount_holes( - bracket, - profiles=list(EXTENDED_TOP_MOUNT_HOLES), - start_y=front_y_max + sem.CUT_EXTENSION_MM, - direction_y=-1.0, - ) - bracket = _cut_rear_output_horn_relief( - bracket, - rear_y_min=rear_y_min, - rear_y_max=rear_y_max, - ) - bracket = _cut_centered_front_horn_holes(bracket, layout=layout) - bracket = _cut_outer_flange_slots( - bracket, - layout=layout, - rear_y_min=rear_y_min, - rear_y_max=rear_y_max, - front_y_min=front_y_min, - front_y_max=front_y_max, - ) - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected double servo end mount solid, found {len(solids)}") - - bracket.label = PART_NAME - bracket.color = lc.BRACKET_COLOR - bb = bracket.bounding_box() - print( - f"{DISPLAY_NAME}: " - f"servo_center_spacing_y={SERVO_CENTER_SPACING_Y_MM:.3f} mm, " - f"span_y={bb.size.Y:.3f} mm, " - f"span_z={bb.size.Z:.3f} mm, " - f"flange_x_min={extended_top_tab_x_min:.3f} mm, " - f"slot={SLOT_LENGTH_MM:.3f}x" - f"{sem.SLOT_WIDTH_MM * SLOT_WIDTH_SCALE:.3f} mm, " - f"slot_center_z={_slot_center_z(layout):.3f} mm, " - f"center_relief_edge_clearance={CENTER_RELIEF_EDGE_CLEARANCE_MM:.3f} mm, " - f"rear_horn_relief_radius={OUTPUT_HORN_RELIEF_RADIUS_MM:.3f} mm, " - f"mount_face_x={layout.bracket_face_outer_x:.4f} mm, " - f"thickness={sem.SENDCUTSEND_SHEET_THICKNESS_MM:.4f} mm, " - f"front_horn_patterns=1" - ) - return bracket - - -def build_dxf(): - layout = _build_double_flat_layout() - flat_pattern = sem._build_flat_pattern(layout) - bracket = build_step() - doc = sem._topology_flat_pattern_dxf(bracket, layout, flat_pattern) - slot_edge_to_bend_min = layout.bracket_face_inner_x - ( - sem._slot_center_x(layout) + (0.5 * sem.SLOT_WIDTH_MM * SLOT_WIDTH_SCALE) - ) - slot_side_end_gap = min( - (_slot_center_z(layout) - (0.5 * SLOT_LENGTH_MM)) - layout.z_min, - layout.z_max - (_slot_center_z(layout) + (0.5 * SLOT_LENGTH_MM)), - ) - print( - f"{DISPLAY_NAME} DXF: " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, " - f"{flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"cut_circles={len(flat_pattern.cut_circles)}, " - f"slots=2x{SLOT_LENGTH_MM:.3f}x{sem.SLOT_WIDTH_MM * SLOT_WIDTH_SCALE:.3f} mm, " - f"slot_edge_to_bend_min={slot_edge_to_bend_min:.3f} mm, " - f"slot_side_end_gap={slot_side_end_gap:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/parts/servo_end_mount_double.step b/models/robots/tom/v2/parts/servo_end_mount_double.step deleted file mode 100644 index 9a4c2828..00000000 --- a/models/robots/tom/v2/parts/servo_end_mount_double.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4859a62e7641eae3c08b290bd424fdc5ff831dc686aeaba99b36c72bae94d151 -size 193997 diff --git a/models/robots/tom/v2/parts/servo_horn_yoke.py b/models/robots/tom/v2/parts/servo_horn_yoke.py deleted file mode 100644 index cda2ecba..00000000 --- a/models/robots/tom/v2/parts/servo_horn_yoke.py +++ /dev/null @@ -1,1616 +0,0 @@ -#!/usr/bin/env python3 -""" -Generate the localized horn-mounted yoke. - -This variant preserves the existing horn interface geometry while shortening -the horn-side end into a single flange that forks into two circular cap -flanges. The horn flanges carry the two outer M3 holes plus the inboard horn -hole, and intentionally omit the previous center clearance and far-side M3 -hole. - -Usage: - python STEP/servo_horn_yoke.py -""" - -from __future__ import annotations - -import math -import os -import sys -from dataclasses import dataclass -from pathlib import Path - -# Keep CAD library caches inside the workspace to avoid permission issues. -TOM_DIR = Path(__file__).resolve().parents[2] -V2_DIR = Path(__file__).resolve().parents[1] -_CACHE_HOME = V2_DIR / ".cache" -_CACHE_HOME.mkdir(parents=True, exist_ok=True) -os.environ.setdefault("XDG_CACHE_HOME", str(_CACHE_HOME)) -os.environ.setdefault("EZDXF_CACHE_HOME", str(_CACHE_HOME / "ezdxf")) - -import ezdxf -import build123d -from ezdxf.units import MM as DXF_UNIT_MM - -if str(TOM_DIR) not in sys.path: - sys.path.insert(0, str(TOM_DIR)) - -from robot_common.materials import GRAY_ALUMINUM_COLOR -from robot_common.step_import import import_as_shape -import dxf_topology - - -CAD_DIR = Path(__file__).resolve().parent -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Servo Horn Yoke" - -SERVO_STEP = V2_DIR / "parts" / "imports" / "sts3250.step" - -UPPER_HORN_SELECTOR = "xcaf:1.4" -LOWER_HORN_SELECTOR = "xcaf:1.5" -UPPER_HORN_NAME = "4" -LOWER_HORN_NAME = "5" - -SERVO_HORN_AXIS_X_MM = -25.5 -YOKE_HORN_SPAN_CENTER_Y_MM = -9.1 -SERVO_REFERENCE_FACE_TARGET_CENTER_MM = (-35.61, -7.65, 0.0) -SERVO_REFERENCE_FACE_MAX_ERROR_MM = 0.5 -HORN_FACE_GAP_MM = 0.0 -MOUNT_HOLE_MIN_OFFSET_MM = 3.0 -CUT_EXTENSION_MM = 2.0 -M3_CLEARANCE_RADIUS_MM = 1.7 -HORN_CENTER_CLEARANCE_RADIUS_MM = 3.25 -YOKE_LENGTH_REDUCTION_MM = 6.5 -HORN_CAP_FLANGE_RADIUS_MM = 10.0 - YOKE_LENGTH_REDUCTION_MM -YOKE_FULL_FLANGE_OUTER_LENGTH_MM = 30.0 -# Keep the rotating web outside the primary servo's left-side corner sweep. -# This is measured from the flange servo horn axis to the yoke web's outer face. -YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM = ( - YOKE_FULL_FLANGE_OUTER_LENGTH_MM - HORN_CAP_FLANGE_RADIUS_MM -) -HORN_CAP_FLANGE_HOLE_X_TOLERANCE_MM = 0.25 -HORN_FORK_SLOT_RADIUS_MM = HORN_CAP_FLANGE_RADIUS_MM -REFERENCE_SHEET_THICKNESS_MM = 25.4 * 0.080 -BOTTOM_FACE_SLOT_WIDTH_MM = 3.0 -# Keep the side-flange slot away from the bend/root so the yoke keeps a -# stronger uninterrupted load path into the base. -BOTTOM_FACE_SLOT_BEND_EDGE_CLEARANCE_MM = 10.0 -BOTTOM_FACE_SLOT_END_GAP_MM = 4.0 -STANDALONE_FLOOR_ROTATION_AXIS = build123d.Axis.Y -STANDALONE_FLOOR_ROTATION_DEG = -90.0 -DESIGN_OUTER_WEB_X_MM = SERVO_HORN_AXIS_X_MM - YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM - - -def multiply_transforms( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> tuple[float, ...]: - return tuple( - sum(float(left[row * 4 + inner]) * float(right[inner * 4 + col]) for inner in range(4)) - for row in range(4) - for col in range(4) - ) - - -# The standalone yoke STEP is rotated to sit on the floor for inspection. -# Assemblies still need the original horn/yoke design frame, so these matrices -# bridge standalone STEP coordinates and design coordinates. -STANDALONE_TO_DESIGN_TRANSFORM = ( - 0.0, 0.0, 1.0, DESIGN_OUTER_WEB_X_MM, - 0.0, 1.0, 0.0, 0.0, - -1.0, 0.0, 0.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) -DESIGN_TO_STANDALONE_TRANSFORM = ( - 0.0, 0.0, -1.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 1.0, 0.0, 0.0, -DESIGN_OUTER_WEB_X_MM, - 0.0, 0.0, 0.0, 1.0, -) -YOKE_180_ABOUT_WEB_AXIS_DESIGN_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, 2.0 * YOKE_HORN_SPAN_CENTER_Y_MM, - 0.0, 0.0, -1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) -YOKE_180_ABOUT_WEB_AXIS_STANDALONE_TRANSFORM = multiply_transforms( - DESIGN_TO_STANDALONE_TRANSFORM, - multiply_transforms( - YOKE_180_ABOUT_WEB_AXIS_DESIGN_TRANSFORM, - STANDALONE_TO_DESIGN_TRANSFORM, - ), -) -STANDALONE_YOKE_ON_SERVO_HORNS_TRANSFORM = multiply_transforms( - YOKE_180_ABOUT_WEB_AXIS_DESIGN_TRANSFORM, - STANDALONE_TO_DESIGN_TRANSFORM, -) - - -def _env_float(name: str, default: float) -> float: - value = os.environ.get(name) - if value is None: - return default - try: - parsed = float(value) - except ValueError as exc: - raise ValueError(f"{name} must be a number, got {value!r}") from exc - if parsed <= 0.0: - raise ValueError(f"{name} must be positive, got {parsed!r}") - return parsed - - -SHEET_THICKNESS_MM = _env_float("TOM_V2_SHEET_THICKNESS_MM", 25.4 * 0.063) -SHEET_RULE_SCALE = SHEET_THICKNESS_MM / REFERENCE_SHEET_THICKNESS_MM - - -@dataclass(frozen=True) -class BendRule: - material_name: str - thickness_mm: float - effective_inside_radius_mm: float - k_factor: float - bend_deduction_90_mm: float - half_die_width_mm: float - min_flange_flat_mm: float - min_flange_formed_mm: float - min_acute_centerline_length_mm: float - channel_one_to_one_max_flange_mm: float - channel_narrow_min_base_mm: float - channel_base_spacing_min_mm: float - max_bend_angle_deg: float - - @property - def outside_setback_90_mm(self) -> float: - return self.effective_inside_radius_mm + self.thickness_mm - - @property - def bend_allowance_90_mm(self) -> float: - return (2.0 * self.outside_setback_90_mm) - self.bend_deduction_90_mm - - @property - def outer_radius_mm(self) -> float: - return self.effective_inside_radius_mm + self.thickness_mm - - def flat_length_from_outside_length_90(self, outside_length_mm: float) -> float: - return outside_length_mm - self.outside_setback_90_mm - - -SHEET_RULE = BendRule( - material_name="5052-H32 ALU-063", - thickness_mm=SHEET_THICKNESS_MM, - effective_inside_radius_mm=25.4 * 0.035, - k_factor=0.42, - bend_deduction_90_mm=25.4 * 0.096, - half_die_width_mm=0.5 * (25.4 * 0.472), - min_flange_flat_mm=25.4 * 0.255, - min_flange_formed_mm=25.4 * 0.303, - min_acute_centerline_length_mm=(25.4 * 0.332) * SHEET_RULE_SCALE, - channel_one_to_one_max_flange_mm=25.4 * 3.000, - channel_narrow_min_base_mm=25.4 * 1.000, - channel_base_spacing_min_mm=2.0 * (25.4 * 0.255), - max_bend_angle_deg=130.0, -) - - -@dataclass(frozen=True) -class HornMountSpec: - name: str - mount_face_y: float - mount_face_center: build123d.Vector - mount_face_bbox: build123d.BoundBox - half_width_z: float - center_clearance_radius: float - mount_holes: tuple[tuple[build123d.Vector, float], ...] - - -@dataclass(frozen=True) -class BracketLayout: - web_inner_x: float - web_outer_x: float - top_y_min: float - top_y_max: float - bottom_y_min: float - bottom_y_max: float - top_z_min: float - top_z_max: float - bottom_z_min: float - bottom_z_max: float - global_z_min: float - global_z_max: float - - -@dataclass(frozen=True) -class BracketMetrics: - min_flange_outside_length_mm: float - min_flange_flat_length_mm: float - bend_line_spacing_mm: float - min_hole_edge_to_bend_mm: float - min_slot_edge_to_bend_mm: float - min_web_feature_edge_to_bend_mm: float - base_to_flange_ratio: float - bend_deduction_formula_mm: float - - -@dataclass(frozen=True) -class GeneratorInputs: - servo_shape: build123d.Shape - reference_face_x: float - sheet_half_width_z: float - upper_horn: HornMountSpec - lower_horn: HornMountSpec - - -@dataclass(frozen=True) -class WebHoleSpec: - center_y: float - center_z: float - radius: float - - -@dataclass(frozen=True) -class SlotSpec: - center_x: float - center_z: float - length: float - width: float - - -@dataclass(frozen=True) -class CircleProfileSpec: - center_x: float - center_y: float - radius: float - - -@dataclass(frozen=True) -class SlotProfileSpec: - center_x: float - center_y: float - length: float - width: float - - -@dataclass(frozen=True) -class FlatPattern: - total_length_mm: float - width_mm: float - cap_radius_mm: float - bend_line_positions_mm: tuple[float, float] - round_hole_profiles: tuple[CircleProfileSpec, ...] - slot_profiles: tuple[SlotProfileSpec, ...] - - -def _cast_shape(obj: object) -> build123d.Shape: - from build123d.topology import downcast - from build123d.topology.shape_core import shapetype - - occt_shape = downcast(obj) - shape_name = build123d.Shape.shape_LUT[shapetype(occt_shape)] - shape_type = getattr(build123d, shape_name) - return shape_type.cast(occt_shape) - - -def _geom_type_name(shape: build123d.Shape) -> str: - geom_type = shape.geom_type - return geom_type.name if hasattr(geom_type, "name") else str(geom_type) - - -def _face_from_wire( - exterior: build123d.Wire, - interior_wires: list[build123d.Wire] | None = None, -) -> build123d.Face: - holes = list(interior_wires or []) - return build123d.Face(exterior, holes) if holes else build123d.Face(exterior) - - -def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire: - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}") - return wires[0] - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _vector_to_tuple(vector: build123d.Vector) -> tuple[float, float, float]: - return (vector.X, vector.Y, vector.Z) - - -def _validate_bend_rule(rule: BendRule) -> None: - bend_deduction_error = abs(rule.bend_deduction_90_mm - ((2.0 * rule.outside_setback_90_mm) - rule.bend_allowance_90_mm)) - if bend_deduction_error > 0.05: - raise RuntimeError( - "SendCutSend bend-rule constants are inconsistent: " - f"bend deduction error {bend_deduction_error:.3f} mm is too large" - ) - - -def _extract_selector_shape(step_path: Path, selector: str) -> tuple[str, build123d.Shape]: - from OCP.STEPCAFControl import STEPCAFControl_Reader - from OCP.TCollection import TCollection_ExtendedString - from OCP.TDataStd import TDataStd_Name - from OCP.TDF import TDF_Label, TDF_LabelSequence - from OCP.TDocStd import TDocStd_Document - from OCP.XCAFApp import XCAFApp_Application - from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ShapeTool - - def _label_name(label: object) -> str: - name = TDataStd_Name() - if not label.FindAttribute(TDataStd_Name.GetID_s(), name): - return "" - return name.Get().ToExtString() - - def _resolve_label(shape_tool: object, label: object) -> object: - if not shape_tool.IsReference_s(label): - return label - referred = TDF_Label() - if shape_tool.GetReferredShape_s(label, referred): - return referred - return label - - def _child_components(shape_tool: object, label: object) -> list[object]: - children = TDF_LabelSequence() - ok = XCAFDoc_ShapeTool.GetComponents_s(label, children, False) - resolved = _resolve_label(shape_tool, label) - if (not ok or children.Length() <= 0) and resolved != label: - children = TDF_LabelSequence() - ok = XCAFDoc_ShapeTool.GetComponents_s(resolved, children, False) - if not ok or children.Length() <= 0: - return [] - return [children.Value(index) for index in range(1, children.Length() + 1)] - - def _shape_location(shape: object) -> build123d.Location: - try: - return build123d.Location(shape.Location().Transformation()) - except Exception: - return build123d.Location() - - def _located_shape(shape: object, location: build123d.Location | None) -> object: - if location is None: - return shape - return shape.Located(location.wrapped) - - app = XCAFApp_Application.GetApplication_s() - doc = TDocStd_Document(TCollection_ExtendedString("servo_horn_yoke_doc")) - app.NewDocument(TCollection_ExtendedString("MDTV-XCAF"), doc) - - reader = STEPCAFControl_Reader() - read_status = reader.ReadFile(str(step_path)) - if int(read_status) != 1: - raise RuntimeError(f"Could not read {step_path} (status={int(read_status)})") - if not reader.Transfer(doc): - raise RuntimeError(f"Could not transfer STEP document from {step_path}") - - shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) - free_labels = TDF_LabelSequence() - shape_tool.GetFreeShapes(free_labels) - if free_labels.Length() <= 0: - raise RuntimeError(f"No free shapes found in {step_path}") - - if not selector.startswith("xcaf:"): - raise ValueError(f"Unsupported selector format: {selector}") - segments = [int(value) for value in selector.split(":", 1)[1].split(".") if value] - if not segments: - raise ValueError(f"Selector does not contain XCAF path segments: {selector}") - if segments[0] < 1 or segments[0] > free_labels.Length(): - raise RuntimeError(f"Selector root index is out of range for {step_path}: {selector}") - - label = free_labels.Value(segments[0]) - current_location: build123d.Location | None = None - for index in segments[1:]: - parent_location = _shape_location(shape_tool.GetShape_s(label)) - current_location = parent_location if current_location is None else current_location * parent_location - children = _child_components(shape_tool, label) - if index < 1 or index > len(children): - raise RuntimeError(f"Selector segment {index} is out of range for {selector}") - label = children[index - 1] - - resolved = _resolve_label(shape_tool, label) - instance_shape = shape_tool.GetShape_s(label) - resolved_shape = shape_tool.GetShape_s(resolved) - base_shape = instance_shape if not instance_shape.IsNull() else resolved_shape - if base_shape.IsNull(): - raise RuntimeError(f"Selector {selector} did not resolve to a shape") - - base_location = _shape_location(base_shape) - current_location = base_location if current_location is None else current_location * base_location - extracted = _cast_shape(_located_shape(base_shape, current_location)) - return _label_name(label), extracted - - -def _wire_area(wire: build123d.Wire) -> float: - return abs(_face_from_wire(wire).area) - - -def _outer_wire_and_inner_wires(face: build123d.Face) -> tuple[build123d.Wire, list[build123d.Wire]]: - wires = list(face.wires()) - if not wires: - raise RuntimeError("Face has no wires") - - outer_wire = max(wires, key=_wire_area) - inner_wires = [wire for wire in wires if not wire.is_same(outer_wire)] - return outer_wire, inner_wires - - -def _select_mount_face(shape: build123d.Shape, *, outward_sign: float) -> build123d.Face: - shape_bb = shape.bounding_box() - target_y = shape_bb.max.Y if outward_sign > 0.0 else shape_bb.min.Y - candidates: list[tuple[float, build123d.Face]] = [] - for face in shape.faces(): - if face.geom_type.name != "PLANE": - continue - normal = face.normal_at() - if outward_sign > 0.0 and normal.Y < 0.99: - continue - if outward_sign < 0.0 and normal.Y > -0.99: - continue - if abs(face.center().Y - target_y) > 0.25: - continue - candidates.append((face.area, face)) - - if not candidates: - raise RuntimeError(f"Could not find a horn mount face on y sign {outward_sign}") - return max(candidates, key=lambda item: item[0])[1] - - -def _extract_circular_wire_profiles(face: build123d.Face) -> list[tuple[build123d.Vector, float]]: - _outer_wire, inner_wires = _outer_wire_and_inner_wires(face) - profiles: list[tuple[build123d.Vector, float]] = [] - for wire in inner_wires: - circle_edges = [edge for edge in wire.edges() if edge.geom_type.name == "CIRCLE"] - if not circle_edges: - continue - center = _face_from_wire(wire).center() - radius = max(edge.radius for edge in circle_edges) - profiles.append((center, radius)) - return profiles - - -def _extract_horn_mount_spec(shape: build123d.Shape, *, name: str, outward_sign: float) -> HornMountSpec: - mount_face = _select_mount_face(shape, outward_sign=outward_sign) - face_center = mount_face.center() - shape_bbox = shape.bounding_box() - profiles = _extract_circular_wire_profiles(mount_face) - if not profiles: - raise RuntimeError(f"No circular profiles found on horn mount face for {name}") - - center_profiles = [ - (center, radius) - for center, radius in profiles - if center.sub(face_center).length <= MOUNT_HOLE_MIN_OFFSET_MM - ] - if not center_profiles: - raise RuntimeError(f"No center clearance profile found on horn mount face for {name}") - center_clearance_radius = max(radius for _center, radius in center_profiles) - - mount_holes = [ - (center, radius) - for center, radius in profiles - if center.sub(face_center).length > MOUNT_HOLE_MIN_OFFSET_MM - ] - if len(mount_holes) != 4: - raise RuntimeError(f"Expected 4 horn mount holes on {name}, found {len(mount_holes)}") - - mount_holes.sort(key=lambda item: (item[0].X, item[0].Z)) - return HornMountSpec( - name=name, - mount_face_y=face_center.Y, - mount_face_center=face_center, - mount_face_bbox=mount_face.bounding_box(), - half_width_z=max(face_center.Z - shape_bbox.min.Z, shape_bbox.max.Z - face_center.Z), - center_clearance_radius=center_clearance_radius, - mount_holes=tuple(mount_holes), - ) - - -def _find_reference_face_x(shape: build123d.Shape) -> float: - target = build123d.Vector(*SERVO_REFERENCE_FACE_TARGET_CENTER_MM) - candidates: list[tuple[float, float, build123d.Face]] = [] - for face in shape.faces(): - if face.geom_type.name != "PLANE": - continue - normal = face.normal_at() - if normal.X > -0.99: - continue - center = face.center() - dist = center.sub(target).length - candidates.append((dist, -face.area, face)) - - if not candidates: - raise RuntimeError("Could not find the requested negative-X reference face on the servo") - - dist, _neg_area, face = min(candidates, key=lambda item: (item[0], item[1])) - if dist > SERVO_REFERENCE_FACE_MAX_ERROR_MM: - raise RuntimeError( - "Resolved servo reference face is too far from the requested target center: " - f"{dist:.3f} mm > {SERVO_REFERENCE_FACE_MAX_ERROR_MM:.3f} mm" - ) - return face.bounding_box().min.X - - -def _shape_half_width_z(shape: build123d.Shape) -> float: - bbox = shape.bounding_box() - return max(abs(bbox.min.Z), abs(bbox.max.Z)) - - -def _load_generator_inputs() -> GeneratorInputs: - _validate_bend_rule(SHEET_RULE) - if not SERVO_STEP.exists(): - raise FileNotFoundError(f"Missing servo STEP: {SERVO_STEP}") - - servo_shape = import_as_shape(SERVO_STEP) - reference_face_x = _find_reference_face_x(servo_shape) - - upper_label, upper_horn_shape = _extract_selector_shape(SERVO_STEP, UPPER_HORN_SELECTOR) - lower_label, lower_horn_shape = _extract_selector_shape(SERVO_STEP, LOWER_HORN_SELECTOR) - if upper_label != UPPER_HORN_NAME: - raise RuntimeError( - f"Expected upper horn selector {UPPER_HORN_SELECTOR} to resolve to {UPPER_HORN_NAME}, got {upper_label}" - ) - if lower_label != LOWER_HORN_NAME: - raise RuntimeError( - f"Expected lower horn selector {LOWER_HORN_SELECTOR} to resolve to {LOWER_HORN_NAME}, got {lower_label}" - ) - - upper_horn = _extract_horn_mount_spec(upper_horn_shape, name=upper_label, outward_sign=1.0) - lower_horn = _extract_horn_mount_spec(lower_horn_shape, name=lower_label, outward_sign=-1.0) - return GeneratorInputs( - servo_shape=servo_shape, - reference_face_x=reference_face_x, - sheet_half_width_z=_shape_half_width_z(servo_shape), - upper_horn=upper_horn, - lower_horn=lower_horn, - ) - - -def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _cap_hole_specs(horn: HornMountSpec) -> tuple[tuple[build123d.Vector, float], ...]: - cap_holes = tuple( - (center, M3_CLEARANCE_RADIUS_MM) - for center, _radius in horn.mount_holes - if abs(center.X - horn.mount_face_center.X) <= HORN_CAP_FLANGE_HOLE_X_TOLERANCE_MM - and abs(center.Z - horn.mount_face_center.Z) > MOUNT_HOLE_MIN_OFFSET_MM - ) - if len(cap_holes) != 2: - raise RuntimeError(f"Expected 2 yoke cap holes on horn {horn.name}, found {len(cap_holes)}") - return tuple(sorted(cap_holes, key=lambda item: item[0].Z)) - - -def _web_side_hole_spec(horn: HornMountSpec) -> tuple[build123d.Vector, float]: - side_holes = tuple( - (center, radius) - for center, radius in horn.mount_holes - if center.X < horn.mount_face_center.X - HORN_CAP_FLANGE_HOLE_X_TOLERANCE_MM - and abs(center.Z - horn.mount_face_center.Z) <= MOUNT_HOLE_MIN_OFFSET_MM - ) - if len(side_holes) != 1: - raise RuntimeError(f"Expected 1 web-side horn hole on horn {horn.name}, found {len(side_holes)}") - return side_holes[0] - - -def _cap_z_bounds(horn: HornMountSpec, *, cap_radius: float) -> tuple[float, float]: - cap_holes = _cap_hole_specs(horn) - return ( - min(center.Z - cap_radius for center, _radius in cap_holes), - max(center.Z + cap_radius for center, _radius in cap_holes), - ) - - -def _cap_end_x(horn: HornMountSpec, *, cap_radius: float) -> float: - return max(center.X + cap_radius for center, _radius in _cap_hole_specs(horn)) - - -def _capped_hole_flange_shapes( - *, - horn: HornMountSpec, - web_inner_x: float, - y_min: float, - y_max: float, - cap_radius: float, -) -> tuple[build123d.Shape, ...]: - cap_holes = _cap_hole_specs(horn) - side_center, _side_radius = _web_side_hole_spec(horn) - slot_radius = HORN_FORK_SLOT_RADIUS_MM - slot_center_x = horn.mount_face_center.X - slot_left_x = slot_center_x - slot_radius - cap_center_x = max(center.X for center, _radius in cap_holes) - if abs(slot_center_x - cap_center_x) > HORN_CAP_FLANGE_HOLE_X_TOLERANCE_MM: - raise RuntimeError( - f"Expected yoke cap holes to share the fork slot center X on horn {horn.name}: " - f"slot_center_x={slot_center_x:.3f}, cap_center_x={cap_center_x:.3f}" - ) - if not web_inner_x < slot_left_x < slot_center_x: - raise RuntimeError( - f"Invalid yoke flange fork slot for horn {horn.name}: " - f"web_inner_x={web_inner_x:.3f}, slot_left_x={slot_left_x:.3f}, " - f"slot_center_x={slot_center_x:.3f}" - ) - - flange_shape: build123d.Shape = _make_box( - x_min=web_inner_x, - x_max=slot_center_x, - y_min=y_min, - y_max=y_max, - z_min=min(center.Z - cap_radius for center, _radius in cap_holes), - z_max=max(center.Z + cap_radius for center, _radius in cap_holes), - ) - for center, _radius in cap_holes: - cap = _make_cylinder_along( - radius=cap_radius, - height=y_max - y_min, - origin=build123d.Vector(center.X, y_min, center.Z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - flange_shape = flange_shape.fuse(cap) - - fork_slot = _make_cylinder_along( - radius=slot_radius, - height=(y_max - y_min) + (2.0 * CUT_EXTENSION_MM), - origin=build123d.Vector(slot_center_x, y_min - CUT_EXTENSION_MM, side_center.Z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - flange_shape = flange_shape.cut(fork_slot) - return (flange_shape,) - - -def _web_pattern_center_from_values( - *, - web_outer_x: float, - top_y_max: float, - bottom_y_min: float, - z_center: float, -) -> build123d.Vector: - return build123d.Vector( - web_outer_x, - 0.5 * (bottom_y_min + top_y_max), - z_center, - ) - - -def _web_pattern_center(layout: BracketLayout) -> build123d.Vector: - return _web_pattern_center_from_values( - web_outer_x=layout.web_outer_x, - top_y_max=layout.top_y_max, - bottom_y_min=layout.bottom_y_min, - z_center=0.5 * (layout.global_z_min + layout.global_z_max), - ) - - -def _center_hole_radius(horn: HornMountSpec) -> float: - return max(HORN_CENTER_CLEARANCE_RADIUS_MM, horn.center_clearance_radius) - - -def _horn_face_hole_specs(horn: HornMountSpec) -> tuple[tuple[build123d.Vector, float], ...]: - side_center, _side_radius = _web_side_hole_spec(horn) - return ( - (side_center, M3_CLEARANCE_RADIUS_MM), - *_cap_hole_specs(horn), - ) - - -def _web_round_hole_specs( - layout: BracketLayout, - *, - horn: HornMountSpec, -) -> tuple[WebHoleSpec, ...]: - center = _web_pattern_center(layout) - specs = [ - WebHoleSpec( - center_y=center.Y, - center_z=center.Z, - radius=_center_hole_radius(horn), - ) - ] - for mount_center, _radius in horn.mount_holes: - specs.append( - WebHoleSpec( - center_y=center.Y + (mount_center.X - horn.mount_face_center.X), - center_z=center.Z + (mount_center.Z - horn.mount_face_center.Z), - radius=M3_CLEARANCE_RADIUS_MM, - ) - ) - return tuple(specs) - - -def _bottom_face_slot_specs(layout: BracketLayout) -> tuple[SlotSpec, ...]: - center_x = ( - layout.web_inner_x - + BOTTOM_FACE_SLOT_BEND_EDGE_CLEARANCE_MM - + (0.5 * BOTTOM_FACE_SLOT_WIDTH_MM) - ) - slot_z_min = layout.global_z_min + BOTTOM_FACE_SLOT_END_GAP_MM - slot_z_max = layout.global_z_max - BOTTOM_FACE_SLOT_END_GAP_MM - slot_length = slot_z_max - slot_z_min - if slot_length <= BOTTOM_FACE_SLOT_WIDTH_MM: - raise ValueError( - "Bottom face slot does not have enough length across the flange width " - "after applying the flange-end gaps" - ) - - return ( - SlotSpec( - center_x=center_x, - center_z=0.5 * (slot_z_min + slot_z_max), - length=slot_length, - width=BOTTOM_FACE_SLOT_WIDTH_MM, - ), - ) - - -def _make_y_slot_cut( - *, - slot: SlotSpec, - y_min: float, - y_max: float, -) -> build123d.Shape: - radius = 0.5 * slot.width - half_straight = 0.5 * slot.length - radius - y_start = y_min - CUT_EXTENSION_MM - y_height = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - body: build123d.Shape = _make_box( - x_min=slot.center_x - radius, - x_max=slot.center_x + radius, - y_min=y_start, - y_max=y_start + y_height, - z_min=slot.center_z - half_straight, - z_max=slot.center_z + half_straight, - ) - for end_z in (slot.center_z - half_straight, slot.center_z + half_straight): - cap = _make_cylinder_along( - radius=radius, - height=y_height, - origin=build123d.Vector(slot.center_x, y_start, end_z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - body = body.fuse(cap) - return body - - -def _cut_round_horn_face_holes( - shape: build123d.Shape, - *, - horn: HornMountSpec, - start_y: float, - direction_y: float, - rule: BendRule, -) -> build123d.Shape: - for center, radius in _horn_face_hole_specs(horn): - cutter = _make_cylinder_along( - radius=radius, - height=rule.thickness_mm + (2.0 * CUT_EXTENSION_MM), - origin=build123d.Vector(center.X, start_y, center.Z), - direction=build123d.Vector(0.0, direction_y, 0.0), - ) - shape = shape.cut(cutter) - return shape - - -def _cut_bottom_face_slots( - shape: build123d.Shape, - *, - layout: BracketLayout, -) -> build123d.Shape: - for y_min, y_max in ( - (layout.top_y_min, layout.top_y_max), - (layout.bottom_y_min, layout.bottom_y_max), - ): - for slot in _bottom_face_slot_specs(layout): - shape = shape.cut(_make_y_slot_cut(slot=slot, y_min=y_min, y_max=y_max)) - return shape - - -def _cut_round_web_holes( - shape: build123d.Shape, - *, - layout: BracketLayout, - horn: HornMountSpec, - rule: BendRule, -) -> build123d.Shape: - start_x = layout.web_outer_x - CUT_EXTENSION_MM - for hole in _web_round_hole_specs(layout, horn=horn): - cutter = _make_cylinder_along( - radius=hole.radius, - height=rule.thickness_mm + (2.0 * CUT_EXTENSION_MM), - origin=build123d.Vector(start_x, hole.center_y, hole.center_z), - direction=build123d.Vector(1.0, 0.0, 0.0), - ) - shape = shape.cut(cutter) - return shape - - -def _make_bent_channel_body( - *, - top_end_x: float, - bottom_end_x: float, - layout: BracketLayout, -) -> build123d.Shape: - # Build the channel body with sharp corners instead of formed bend radii. - x_inner = layout.web_inner_x - x_outer = layout.web_outer_x - - profile_points = [ - build123d.Vector(top_end_x, layout.top_y_max, 0.0), - build123d.Vector(x_outer, layout.top_y_max, 0.0), - build123d.Vector(x_outer, layout.bottom_y_min, 0.0), - build123d.Vector(bottom_end_x, layout.bottom_y_min, 0.0), - build123d.Vector(bottom_end_x, layout.bottom_y_max, 0.0), - build123d.Vector(x_inner, layout.bottom_y_max, 0.0), - build123d.Vector(x_inner, layout.top_y_min, 0.0), - build123d.Vector(top_end_x, layout.top_y_min, 0.0), - ] - profile_edges = [ - build123d.Edge.make_line(profile_points[0], profile_points[1]), - build123d.Edge.make_line(profile_points[1], profile_points[2]), - build123d.Edge.make_line(profile_points[2], profile_points[3]), - build123d.Edge.make_line(profile_points[3], profile_points[4]), - build123d.Edge.make_line(profile_points[4], profile_points[5]), - build123d.Edge.make_line(profile_points[5], profile_points[6]), - build123d.Edge.make_line(profile_points[6], profile_points[7]), - build123d.Edge.make_line(profile_points[7], profile_points[0]), - ] - face = _face_from_wire(_assemble_wire(profile_edges)) - return build123d.Solid.extrude( - face, - build123d.Vector(0.0, 0.0, layout.global_z_max - layout.global_z_min), - ).translate((0.0, 0.0, layout.global_z_min)) - - -def _min_horn_face_feature_edge_to_bend_line( - horn: HornMountSpec, - *, - bend_line_u: float, -) -> float: - return min( - (center.X - radius) - bend_line_u - for center, radius in _horn_face_hole_specs(horn) - ) - - -def _min_web_feature_edge_to_bend_lines( - *, - layout: BracketLayout, - horn: HornMountSpec, -) -> float: - distances: list[float] = [] - for hole in _web_round_hole_specs(layout, horn=horn): - distances.append((hole.center_y - hole.radius) - layout.bottom_y_max) - distances.append(layout.top_y_min - (hole.center_y + hole.radius)) - return min(distances) - - -def _min_bottom_face_slot_edge_to_bend_line(layout: BracketLayout) -> float: - return min( - (slot.center_x - (0.5 * slot.width)) - layout.web_inner_x - for slot in _bottom_face_slot_specs(layout) - ) - - -def _build_metrics( - *, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - layout: BracketLayout, - rule: BendRule, -) -> BracketMetrics: - upper_outside_length = upper_horn.mount_face_center.X - layout.web_outer_x - lower_outside_length = lower_horn.mount_face_center.X - layout.web_outer_x - min_flange_outside_length = min(upper_outside_length, lower_outside_length) - min_flange_flat_length = rule.flat_length_from_outside_length_90(min_flange_outside_length) - bend_line_spacing = layout.top_y_min - layout.bottom_y_max - - min_hole_edge_to_bend = min( - _min_horn_face_feature_edge_to_bend_line(upper_horn, bend_line_u=layout.web_inner_x), - _min_horn_face_feature_edge_to_bend_line(lower_horn, bend_line_u=layout.web_inner_x), - ) - min_slot_edge_to_bend = _min_bottom_face_slot_edge_to_bend_line(layout) - min_web_feature_edge_to_bend = _min_web_feature_edge_to_bend_lines( - layout=layout, - horn=upper_horn, - ) - - max_flange_outside = max(upper_outside_length, lower_outside_length) - base_to_flange_ratio = bend_line_spacing / max_flange_outside - - return BracketMetrics( - min_flange_outside_length_mm=min_flange_outside_length, - min_flange_flat_length_mm=min_flange_flat_length, - bend_line_spacing_mm=bend_line_spacing, - min_hole_edge_to_bend_mm=min_hole_edge_to_bend, - min_slot_edge_to_bend_mm=min_slot_edge_to_bend, - min_web_feature_edge_to_bend_mm=min_web_feature_edge_to_bend, - base_to_flange_ratio=base_to_flange_ratio, - bend_deduction_formula_mm=(2.0 * rule.outside_setback_90_mm) - rule.bend_allowance_90_mm, - ) - - -def _validate_metrics(metrics: BracketMetrics, *, rule: BendRule) -> None: - if metrics.min_flange_outside_length_mm < rule.min_flange_formed_mm: - raise RuntimeError( - "Flange is shorter than the configured bend-rule formed minimum: " - f"{metrics.min_flange_outside_length_mm:.3f} mm < {rule.min_flange_formed_mm:.3f} mm" - ) - if metrics.min_flange_flat_length_mm < rule.min_flange_flat_mm: - raise RuntimeError( - "Flat flange length implied by the bend rule is too short: " - f"{metrics.min_flange_flat_length_mm:.3f} mm < {rule.min_flange_flat_mm:.3f} mm" - ) - if metrics.bend_line_spacing_mm < rule.channel_base_spacing_min_mm: - raise RuntimeError( - "Channel base spacing is below the SendCutSend consecutive-bend minimum: " - f"{metrics.bend_line_spacing_mm:.3f} mm < {rule.channel_base_spacing_min_mm:.3f} mm" - ) - if metrics.min_hole_edge_to_bend_mm < rule.half_die_width_mm: - raise RuntimeError( - "Horn flange features are too close to the bend die region: " - f"{metrics.min_hole_edge_to_bend_mm:.3f} mm < {rule.half_die_width_mm:.3f} mm" - ) - if metrics.min_slot_edge_to_bend_mm < rule.half_die_width_mm: - raise RuntimeError( - "Horn flange bottom slots are too close to the bend die region: " - f"{metrics.min_slot_edge_to_bend_mm:.3f} mm < {rule.half_die_width_mm:.3f} mm" - ) - if metrics.min_web_feature_edge_to_bend_mm < rule.half_die_width_mm: - raise RuntimeError( - "Closing-face horn pattern is too close to a bend die region: " - f"{metrics.min_web_feature_edge_to_bend_mm:.3f} mm < {rule.half_die_width_mm:.3f} mm" - ) - if metrics.min_flange_outside_length_mm <= rule.channel_one_to_one_max_flange_mm and metrics.base_to_flange_ratio < 1.0: - raise RuntimeError( - "U-channel base-to-flange ratio is below the configured 1:1 manufacturing check for this stock: " - f"{metrics.base_to_flange_ratio:.3f} < 1.000" - ) - - -def _build_layout( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> tuple[BracketLayout, float]: - web_outer_x = SERVO_HORN_AXIS_X_MM - YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM - web_inner_x = web_outer_x + rule.thickness_mm - - top_y_min = upper_horn.mount_face_y + HORN_FACE_GAP_MM - top_y_max = top_y_min + rule.thickness_mm - bottom_y_max = lower_horn.mount_face_y - HORN_FACE_GAP_MM - bottom_y_min = bottom_y_max - rule.thickness_mm - - cap_radius = HORN_CAP_FLANGE_RADIUS_MM - - top_z_min, top_z_max = _cap_z_bounds(upper_horn, cap_radius=cap_radius) - bottom_z_min, bottom_z_max = _cap_z_bounds(lower_horn, cap_radius=cap_radius) - global_z_min = min(top_z_min, bottom_z_min) - global_z_max = max(top_z_max, bottom_z_max) - - layout = BracketLayout( - web_inner_x=web_inner_x, - web_outer_x=web_outer_x, - top_y_min=top_y_min, - top_y_max=top_y_max, - bottom_y_min=bottom_y_min, - bottom_y_max=bottom_y_max, - top_z_min=top_z_min, - top_z_max=top_z_max, - bottom_z_min=bottom_z_min, - bottom_z_max=bottom_z_max, - global_z_min=global_z_min, - global_z_max=global_z_max, - ) - return layout, cap_radius - - -def build_bracket( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, - validate_metrics: bool = True, -) -> tuple[build123d.Shape, BracketLayout, BracketMetrics]: - layout, cap_radius = _build_layout( - reference_face_x, - sheet_half_width_z=sheet_half_width_z, - upper_horn=upper_horn, - lower_horn=lower_horn, - rule=rule, - ) - web_inner_x = layout.web_inner_x - web_outer_x = layout.web_outer_x - top_y_min = layout.top_y_min - top_y_max = layout.top_y_max - bottom_y_min = layout.bottom_y_min - bottom_y_max = layout.bottom_y_max - global_z_min = layout.global_z_min - global_z_max = layout.global_z_max - - bracket = _make_bent_channel_body( - top_end_x=_cap_end_x(upper_horn, cap_radius=cap_radius), - bottom_end_x=_cap_end_x(lower_horn, cap_radius=cap_radius), - layout=layout, - ) - cap_mask: build123d.Shape = _make_box( - x_min=web_outer_x, - x_max=web_inner_x, - y_min=bottom_y_min, - y_max=top_y_max, - z_min=global_z_min, - z_max=global_z_max, - ) - for flange_shape in ( - *_capped_hole_flange_shapes( - horn=upper_horn, - web_inner_x=web_inner_x, - y_min=top_y_min, - y_max=top_y_max, - cap_radius=cap_radius, - ), - *_capped_hole_flange_shapes( - horn=lower_horn, - web_inner_x=web_inner_x, - y_min=bottom_y_min, - y_max=bottom_y_max, - cap_radius=cap_radius, - ), - ): - cap_mask = cap_mask.fuse(flange_shape) - bracket = bracket.intersect(cap_mask) - - top_start_y = top_y_max + CUT_EXTENSION_MM - bracket = _cut_round_horn_face_holes( - bracket, - horn=upper_horn, - start_y=top_start_y, - direction_y=-1.0, - rule=rule, - ) - - bottom_start_y = bottom_y_min - CUT_EXTENSION_MM - bracket = _cut_round_horn_face_holes( - bracket, - horn=lower_horn, - start_y=bottom_start_y, - direction_y=1.0, - rule=rule, - ) - - bracket = _cut_bottom_face_slots( - bracket, - layout=layout, - ) - - bracket = _cut_round_web_holes( - bracket, - layout=layout, - horn=upper_horn, - rule=rule, - ) - metrics = _build_metrics( - upper_horn=upper_horn, - lower_horn=lower_horn, - layout=layout, - rule=rule, - ) - if validate_metrics: - _validate_metrics(metrics, rule=rule) - return bracket, layout, metrics - - -def build_flat_pattern( - reference_face_x: float, - *, - sheet_half_width_z: float, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> tuple[FlatPattern, BracketMetrics]: - if abs(upper_horn.mount_face_center.Z - lower_horn.mount_face_center.Z) > 1e-6: - raise RuntimeError("Flat-pattern export currently requires the horn faces to share the same Z centerline") - - layout, cap_radius = _build_layout( - reference_face_x, - sheet_half_width_z=sheet_half_width_z, - upper_horn=upper_horn, - lower_horn=lower_horn, - rule=rule, - ) - metrics = _build_metrics( - upper_horn=upper_horn, - lower_horn=lower_horn, - layout=layout, - rule=rule, - ) - _validate_metrics(metrics, rule=rule) - - top_end_x = _cap_end_x(upper_horn, cap_radius=cap_radius) - bottom_end_x = _cap_end_x(lower_horn, cap_radius=cap_radius) - top_outside_length = top_end_x - layout.web_outer_x - bottom_outside_length = bottom_end_x - layout.web_outer_x - top_flat_length = top_outside_length - rule.outside_setback_90_mm - bottom_flat_length = bottom_outside_length - rule.outside_setback_90_mm - - bend_allowance = rule.bend_allowance_90_mm - top_web_tangent_y = layout.top_y_min - rule.effective_inside_radius_mm - bottom_web_tangent_y = layout.bottom_y_max + rule.effective_inside_radius_mm - web_flat_length = top_web_tangent_y - bottom_web_tangent_y - total_flat_length = top_flat_length + bend_allowance + web_flat_length + bend_allowance + bottom_flat_length - flat_width = layout.global_z_max - layout.global_z_min - top_bend_line_x = top_flat_length + (0.5 * bend_allowance) - bottom_bend_line_x = top_flat_length + bend_allowance + web_flat_length + (0.5 * bend_allowance) - - upper_hole_profiles = tuple( - CircleProfileSpec( - center_x=top_end_x - center.X, - center_y=center.Z - layout.global_z_min, - radius=radius, - ) - for center, radius in _horn_face_hole_specs(upper_horn) - ) - web_hole_profiles = tuple( - CircleProfileSpec( - center_x=top_flat_length + bend_allowance + (top_web_tangent_y - hole.center_y), - center_y=hole.center_z - layout.global_z_min, - radius=hole.radius, - ) - for hole in _web_round_hole_specs(layout, horn=upper_horn) - ) - lower_hole_profiles = tuple( - CircleProfileSpec( - center_x=total_flat_length - (bottom_end_x - center.X), - center_y=center.Z - layout.global_z_min, - radius=radius, - ) - for center, radius in _horn_face_hole_specs(lower_horn) - ) - round_hole_profiles = upper_hole_profiles + web_hole_profiles + lower_hole_profiles - upper_slot_profiles = tuple( - SlotProfileSpec( - center_x=top_end_x - slot.center_x, - center_y=slot.center_z - layout.global_z_min, - length=slot.length, - width=slot.width, - ) - for slot in _bottom_face_slot_specs(layout) - ) - lower_slot_profiles = tuple( - SlotProfileSpec( - center_x=total_flat_length - (bottom_end_x - slot.center_x), - center_y=slot.center_z - layout.global_z_min, - length=slot.length, - width=slot.width, - ) - for slot in _bottom_face_slot_specs(layout) - ) - - return ( - FlatPattern( - total_length_mm=total_flat_length, - width_mm=flat_width, - cap_radius_mm=cap_radius, - bend_line_positions_mm=(top_bend_line_x, bottom_bend_line_x), - round_hole_profiles=round_hole_profiles, - slot_profiles=upper_slot_profiles + lower_slot_profiles, - ), - metrics, - ) - - -def _slot_profile_points(slot: SlotProfileSpec, *, segments_per_cap: int = 12) -> list[tuple[float, float]]: - radius = 0.5 * slot.width - half_straight = 0.5 * slot.length - radius - bottom_y = slot.center_y - half_straight - top_y = slot.center_y + half_straight - - points: list[tuple[float, float]] = [] - for index in range(segments_per_cap + 1): - angle = 0.0 + (180.0 * index / segments_per_cap) - points.append( - ( - slot.center_x + radius * math.cos(math.radians(angle)), - top_y + radius * math.sin(math.radians(angle)), - ) - ) - for index in range(segments_per_cap + 1): - angle = 180.0 + (180.0 * index / segments_per_cap) - points.append( - ( - slot.center_x + radius * math.cos(math.radians(angle)), - bottom_y + radius * math.sin(math.radians(angle)), - ) - ) - return points - - -def _arc_points_2d( - *, - center: tuple[float, float], - radius: float, - start_deg: float, - end_deg: float, - segments: int, - include_start: bool = False, -) -> list[tuple[float, float]]: - start_index = 0 if include_start else 1 - return [ - ( - center[0] + radius * math.cos(math.radians(start_deg + ((end_deg - start_deg) * index / segments))), - center[1] + radius * math.sin(math.radians(start_deg + ((end_deg - start_deg) * index / segments))), - ) - for index in range(start_index, segments + 1) - ] - - -def _outer_outline_points(flat_pattern: FlatPattern, *, segments_per_lobe: int = 24) -> list[tuple[float, float]]: - radius = flat_pattern.cap_radius_mm - width = flat_pattern.width_mm - total_length = flat_pattern.total_length_mm - lower_lobe_top_y = 2.0 * radius - upper_lobe_bottom_y = width - (2.0 * radius) - if upper_lobe_bottom_y < lower_lobe_top_y - 1e-6: - raise RuntimeError( - "Yoke flat-pattern cap lobes overlap; closed outline export needs a wider flat pattern " - f"or smaller cap radius (width={width:.3f}, radius={radius:.3f})" - ) - - right_x = total_length - radius - left_x = radius - points: list[tuple[float, float]] = [ - (left_x, 0.0), - (right_x, 0.0), - ] - points.extend( - _arc_points_2d( - center=(right_x, radius), - radius=radius, - start_deg=-90.0, - end_deg=90.0, - segments=segments_per_lobe, - ) - ) - if upper_lobe_bottom_y > lower_lobe_top_y + 1e-6: - points.append((right_x, upper_lobe_bottom_y)) - points.extend( - _arc_points_2d( - center=(right_x, width - radius), - radius=radius, - start_deg=-90.0, - end_deg=90.0, - segments=segments_per_lobe, - ) - ) - points.append((left_x, width)) - points.extend( - _arc_points_2d( - center=(left_x, width - radius), - radius=radius, - start_deg=90.0, - end_deg=270.0, - segments=segments_per_lobe, - ) - ) - if upper_lobe_bottom_y > lower_lobe_top_y + 1e-6: - points.append((left_x, lower_lobe_top_y)) - points.extend( - _arc_points_2d( - center=(left_x, radius), - radius=radius, - start_deg=90.0, - end_deg=270.0, - segments=segments_per_lobe, - ) - ) - if points[-1] == points[0]: - points.pop() - return points - - -def _build_flat_pattern_dxf(flat_pattern: FlatPattern) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - msp = doc.modelspace() - msp.add_lwpolyline( - _outer_outline_points(flat_pattern), - close=True, - dxfattribs={"layer": "CUT"}, - ) - - for round_hole in flat_pattern.round_hole_profiles: - msp.add_circle( - (round_hole.center_x, round_hole.center_y), - radius=round_hole.radius, - dxfattribs={"layer": "CUT"}, - ) - for slot in flat_pattern.slot_profiles: - msp.add_lwpolyline( - _slot_profile_points(slot), - close=True, - dxfattribs={"layer": "CUT"}, - ) - - for bend_line_x in flat_pattern.bend_line_positions_mm: - msp.add_line( - (bend_line_x, 0.0), - (bend_line_x, flat_pattern.width_mm), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def _topology_flat_pattern_dxf( - bracket: build123d.Shape, - layout: BracketLayout, - flat_pattern: FlatPattern, - *, - upper_horn: HornMountSpec, - lower_horn: HornMountSpec, - rule: BendRule, -) -> ezdxf.document.Drawing: - doc = ezdxf.new("R2010", setup=True) - doc.units = DXF_UNIT_MM - if "CUT" not in doc.layers: - doc.layers.add("CUT") - if "BEND" not in doc.layers: - doc.layers.add("BEND", linetype="DASHED") - - top_end_x = _cap_end_x(upper_horn, cap_radius=flat_pattern.cap_radius_mm) - bottom_end_x = _cap_end_x(lower_horn, cap_radius=flat_pattern.cap_radius_mm) - top_flat_length = (top_end_x - layout.web_outer_x) - rule.outside_setback_90_mm - bottom_flat_length = (bottom_end_x - layout.web_outer_x) - rule.outside_setback_90_mm - bottom_flat_start = flat_pattern.total_length_mm - bottom_flat_length - web_source_span = layout.top_y_min - layout.bottom_y_max - web_flat_span = bottom_flat_start - top_flat_length - if top_flat_length <= 0.0 or bottom_flat_length <= 0.0 or web_source_span <= 0.0 or web_flat_span <= 0.0: - raise RuntimeError("Yoke topology DXF has invalid flat-pattern spans") - - def project_top(point: build123d.Vector) -> tuple[float, float]: - if abs(point.X - layout.web_outer_x) <= 1e-3: - u = top_flat_length - else: - u = top_end_x - point.X - return (u, point.Z - layout.global_z_min) - - def project_web(point: build123d.Vector) -> tuple[float, float]: - t = (layout.top_y_min - point.Y) / web_source_span - return ( - top_flat_length + (t * web_flat_span), - point.Z - layout.global_z_min, - ) - - def project_bottom(point: build123d.Vector) -> tuple[float, float]: - if abs(point.X - layout.web_outer_x) <= 1e-3: - u = bottom_flat_start - else: - u = flat_pattern.total_length_mm - (bottom_end_x - point.X) - return (u, point.Z - layout.global_z_min) - - top_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=1.0, - coordinate_axis="y", - coordinate=layout.top_y_max, - ) - web_faces = dxf_topology.planar_faces( - bracket, - normal_axis="x", - normal_sign=1.0, - coordinate_axis="x", - coordinate=layout.web_inner_x, - ) - bottom_faces = dxf_topology.planar_faces( - bracket, - normal_axis="y", - normal_sign=-1.0, - coordinate_axis="y", - coordinate=layout.bottom_y_min, - ) - geometry = dxf_topology.union_projected_faces( - ( - (top_faces, project_top), - (web_faces, project_web), - (bottom_faces, project_bottom), - ) - ) - dxf_topology.add_shapely_geometry(doc.modelspace(), geometry, layer="CUT") - - for bend_line_x in flat_pattern.bend_line_positions_mm: - doc.modelspace().add_line( - (bend_line_x, 0.0), - (bend_line_x, flat_pattern.width_mm), - dxfattribs={"layer": "BEND", "linetype": "DASHED"}, - ) - return doc - - -def _orient_for_standalone_floor(shape: build123d.Shape) -> build123d.Shape: - rotated = shape.rotate(STANDALONE_FLOOR_ROTATION_AXIS, STANDALONE_FLOOR_ROTATION_DEG) - rotated_bb = rotated.bounding_box() - return rotated.translate((0.0, 0.0, -rotated_bb.min.Z)) - - -def build_step() -> build123d.Shape: - CAD_DIR.mkdir(parents=True, exist_ok=True) - inputs = _load_generator_inputs() - bracket, layout, metrics = build_bracket( - inputs.reference_face_x, - sheet_half_width_z=inputs.sheet_half_width_z, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - - solids = bracket.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected bracket solid, found {len(solids)}") - - interference_volume = inputs.servo_shape.intersect(bracket).volume - if interference_volume > 1e-3: - raise RuntimeError(f"Bracket intersects the servo by {interference_volume:.6f} mm^3") - - bracket_bb = bracket.bounding_box() - bottom_slots = _bottom_face_slot_specs(layout) - bottom_slot_length = bottom_slots[0].length if bottom_slots else 0.0 - print( - "Bracket envelope " - f"X={bracket_bb.size.X:.3f} mm, " - f"Y={bracket_bb.size.Y:.3f} mm, " - f"Z={bracket_bb.size.Z:.3f} mm" - ) - print( - "Connection features " - f"face_patterns=3, " - f"horn_flange_m3_holes_per_pattern={len(_horn_face_hole_specs(inputs.upper_horn))}x" - f"{2.0 * M3_CLEARANCE_RADIUS_MM:.3f} mm, " - f"cap_flange_radius={HORN_CAP_FLANGE_RADIUS_MM:.3f} mm, " - f"fork_slot_radius={HORN_FORK_SLOT_RADIUS_MM:.3f} mm, " - f"bottom_slots={2 * len(bottom_slots)}x" - f"{bottom_slot_length:.3f}x{BOTTOM_FACE_SLOT_WIDTH_MM:.3f} mm, " - f"removed_center_and_far_side_hole=true" - ) - print( - "SendCutSend bend rule " - f"material={SHEET_RULE.material_name}, " - f"thickness={SHEET_RULE.thickness_mm:.3f} mm, " - f"inside_radius={SHEET_RULE.effective_inside_radius_mm:.3f} mm, " - f"outer_radius={SHEET_RULE.outer_radius_mm:.3f} mm, " - f"k_factor={SHEET_RULE.k_factor:.2f}, " - f"bend_deduction_90={SHEET_RULE.bend_deduction_90_mm:.3f} mm" - ) - print( - "SendCutSend checks " - f"flange_outside_min={metrics.min_flange_outside_length_mm:.3f} mm, " - f"flange_flat_min={metrics.min_flange_flat_length_mm:.3f} mm, " - f"bend_spacing={metrics.bend_line_spacing_mm:.3f} mm, " - f"hole_edge_to_bend_min={metrics.min_hole_edge_to_bend_mm:.3f} mm, " - f"slot_edge_to_bend_min={metrics.min_slot_edge_to_bend_mm:.3f} mm, " - f"web_feature_edge_to_bend_min={metrics.min_web_feature_edge_to_bend_mm:.3f} mm, " - f"base_to_flange_ratio={metrics.base_to_flange_ratio:.3f}" - ) - print( - "Bracket placement " - f"inner_web_x={layout.web_inner_x:.3f} mm, " - f"outer_web_x={layout.web_outer_x:.3f} mm, " - f"outer_web_to_horn_axis={YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM:.3f} mm, " - f"outer_web_to_cap_tip={bracket_bb.size.X:.3f} mm, " - f"reference_face_x={inputs.reference_face_x:.3f} mm, " - f"reference_clearance={inputs.reference_face_x - layout.web_inner_x:.3f} mm" - ) - print( - "Upper horn " - f"selector={UPPER_HORN_SELECTOR}, face_y={inputs.upper_horn.mount_face_y:.3f} mm, " - f"m3_holes={len(_horn_face_hole_specs(inputs.upper_horn))}, " - f"cap_flange_radius={HORN_CAP_FLANGE_RADIUS_MM:.3f} mm" - ) - print( - "Lower horn " - f"selector={LOWER_HORN_SELECTOR}, face_y={inputs.lower_horn.mount_face_y:.3f} mm, " - f"m3_holes={len(_horn_face_hole_specs(inputs.lower_horn))}, " - f"cap_flange_radius={HORN_CAP_FLANGE_RADIUS_MM:.3f} mm" - ) - print(f"Servo/bracket interference volume (mm^3): {interference_volume:.6f}") - - oriented_bracket = _orient_for_standalone_floor(bracket) - oriented_bb = oriented_bracket.bounding_box() - print( - "Standalone orientation " - f"rotate_axis=Y, rotate={STANDALONE_FLOOR_ROTATION_DEG:.1f} deg, " - f"floor_z={oriented_bb.min.Z:.3f} mm, " - f"oriented_envelope X={oriented_bb.size.X:.3f} mm, " - f"Y={oriented_bb.size.Y:.3f} mm, " - f"Z={oriented_bb.size.Z:.3f} mm" - ) - oriented_bracket.label = PART_NAME - oriented_bracket.color = GRAY_ALUMINUM_COLOR - return oriented_bracket - - -def build_dxf() -> ezdxf.document.Drawing: - CAD_DIR.mkdir(parents=True, exist_ok=True) - inputs = _load_generator_inputs() - bracket, layout, metrics = build_bracket( - inputs.reference_face_x, - sheet_half_width_z=inputs.sheet_half_width_z, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - flat_pattern, metrics = build_flat_pattern( - inputs.reference_face_x, - sheet_half_width_z=inputs.sheet_half_width_z, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - doc = _topology_flat_pattern_dxf( - bracket, - layout, - flat_pattern, - upper_horn=inputs.upper_horn, - lower_horn=inputs.lower_horn, - rule=SHEET_RULE, - ) - print( - "Flat pattern " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, {flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"round_holes={len(flat_pattern.round_hole_profiles)}, " - f"slots={len(flat_pattern.slot_profiles)}" - ) - print( - "DXF checks " - f"flange_flat_min={metrics.min_flange_flat_length_mm:.3f} mm, " - f"bend_spacing={metrics.bend_line_spacing_mm:.3f} mm, " - f"hole_edge_to_bend_min={metrics.min_hole_edge_to_bend_mm:.3f} mm, " - f"slot_edge_to_bend_min={metrics.min_slot_edge_to_bend_mm:.3f} mm, " - f"web_feature_edge_to_bend_min={metrics.min_web_feature_edge_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/parts/servo_horn_yoke.step b/models/robots/tom/v2/parts/servo_horn_yoke.step deleted file mode 100644 index 5a740120..00000000 --- a/models/robots/tom/v2/parts/servo_horn_yoke.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6f53f29df90f58a95066963ba8e99e3372bcca21dccdd1c8dcfca5bc1c38f65 -size 132697 diff --git a/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.py b/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.py deleted file mode 100644 index e09fce38..00000000 --- a/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.py +++ /dev/null @@ -1,444 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -import build123d - -V2_DIR = Path(__file__).resolve().parents[1] -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc -import servo_horn_yoke - - -PART_NAME = Path(__file__).stem -DISPLAY_NAME = "Double Servo Horn Yoke Horn" - -SIDE_BY_SIDE_CLEARANCE_MM = 2.0 -SERVO_CENTER_SPACING_Y_MM = (2.0 * abs(lc.SERVO_REAR_EXTREME_LOCAL_Y_MM)) + SIDE_BY_SIDE_CLEARANCE_MM -SERVO_CENTER_OFFSET_Y_MM = 0.5 * SERVO_CENTER_SPACING_Y_MM -YOKE_SINGLE_HALF_WIDTH_Z_MM = 10.5 - -SHEET_THICKNESS_MM = lc.SHEET_THICKNESS_MM -CUT_EXTENSION_MM = 2.0 -M3_CLEARANCE_RADIUS_MM = servo_horn_yoke.M3_CLEARANCE_RADIUS_MM -HORN_CENTER_CLEARANCE_RADIUS_MM = servo_horn_yoke.HORN_CENTER_CLEARANCE_RADIUS_MM -HORN_SCREW_CIRCLE_RADIUS_MM = lc.SERVO_HORN_SCREW_CIRCLE_RADIUS_MM -BOTTOM_FACE_SLOT_WIDTH_MM = servo_horn_yoke.BOTTOM_FACE_SLOT_WIDTH_MM -BOTTOM_FACE_SLOT_BEND_EDGE_CLEARANCE_MM = servo_horn_yoke.BOTTOM_FACE_SLOT_BEND_EDGE_CLEARANCE_MM -BOTTOM_FACE_SLOT_END_GAP_MM = servo_horn_yoke.BOTTOM_FACE_SLOT_END_GAP_MM - -WEB_OUTER_X_MM = ( - lc.SERVO_SHAFT_LOCAL_X_MM - - servo_horn_yoke.YOKE_HORN_AXIS_TO_WEB_OUTER_LENGTH_MM -) -WEB_INNER_X_MM = WEB_OUTER_X_MM + SHEET_THICKNESS_MM -HORN_AXIS_X_MM = lc.SERVO_SHAFT_LOCAL_X_MM -HORN_FLANGE_END_X_MM = HORN_AXIS_X_MM + servo_horn_yoke.HORN_CAP_FLANGE_RADIUS_MM - -OUTPUT_HORN_FACE_Y_MM = lc.SERVO_OUTPUT_HORN_FACE_LOCAL_Y_MM - -REAR_SERVO_CENTER_Y_MM = -SERVO_CENTER_OFFSET_Y_MM -FRONT_SERVO_CENTER_Y_MM = SERVO_CENTER_OFFSET_Y_MM -REAR_OUTER_HORN_FACE_Y_MM = REAR_SERVO_CENTER_Y_MM - OUTPUT_HORN_FACE_Y_MM -FRONT_OUTER_HORN_FACE_Y_MM = FRONT_SERVO_CENTER_Y_MM + OUTPUT_HORN_FACE_Y_MM -WEB_PATTERN_CENTER_Y_MM = 0.5 * (REAR_OUTER_HORN_FACE_Y_MM + FRONT_OUTER_HORN_FACE_Y_MM) - -Z_MIN_MM = -YOKE_SINGLE_HALF_WIDTH_Z_MM -Z_MAX_MM = YOKE_SINGLE_HALF_WIDTH_Z_MM -HORN_PATTERN_CENTER_Z_MM = 0.0 - - -def _make_cylinder_along( - *, - radius: float, - height: float, - origin: build123d.Vector, - direction: build123d.Vector, -) -> build123d.Solid: - return build123d.Solid.make_cylinder( - radius, - height, - plane=build123d.Plane(origin=origin, z_dir=direction), - ) - - -def _make_box(*, x_min: float, x_max: float, y_min: float, y_max: float, z_min: float, z_max: float) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _face_from_wire(wire: build123d.Wire) -> build123d.Face: - return build123d.Face(wire) - - -def _assemble_wire(edges: list[build123d.Edge]) -> build123d.Wire: - wires = list(build123d.Wire.combine(edges)) - if len(wires) != 1: - raise RuntimeError(f"Expected a single assembled wire, found {len(wires)}") - return wires[0] - - -def _make_channel_body() -> build123d.Shape: - rear_outer_y = REAR_OUTER_HORN_FACE_Y_MM - SHEET_THICKNESS_MM - output_outer_y = FRONT_OUTER_HORN_FACE_Y_MM + SHEET_THICKNESS_MM - - body: build123d.Shape = _make_box( - x_min=WEB_OUTER_X_MM, - x_max=WEB_INNER_X_MM, - y_min=rear_outer_y, - y_max=output_outer_y, - z_min=Z_MIN_MM, - z_max=Z_MAX_MM, - ) - body = body.fuse( - _make_capped_horn_flange( - y_min=rear_outer_y, - y_max=REAR_OUTER_HORN_FACE_Y_MM, - ) - ) - body = body.fuse( - _make_capped_horn_flange( - y_min=FRONT_OUTER_HORN_FACE_Y_MM, - y_max=output_outer_y, - ) - ) - return body - - -def _make_capped_horn_flange(*, y_min: float, y_max: float) -> build123d.Shape: - cap_radius = servo_horn_yoke.HORN_CAP_FLANGE_RADIUS_MM - slot_radius = servo_horn_yoke.HORN_FORK_SLOT_RADIUS_MM - cap_z_min = HORN_PATTERN_CENTER_Z_MM - HORN_SCREW_CIRCLE_RADIUS_MM - cap_radius - cap_z_max = HORN_PATTERN_CENTER_Z_MM + HORN_SCREW_CIRCLE_RADIUS_MM + cap_radius - - flange: build123d.Shape = _make_box( - x_min=WEB_INNER_X_MM, - x_max=HORN_AXIS_X_MM, - y_min=y_min, - y_max=y_max, - z_min=cap_z_min, - z_max=cap_z_max, - ) - for cap_z in ( - HORN_PATTERN_CENTER_Z_MM - HORN_SCREW_CIRCLE_RADIUS_MM, - HORN_PATTERN_CENTER_Z_MM + HORN_SCREW_CIRCLE_RADIUS_MM, - ): - cap = _make_cylinder_along( - radius=cap_radius, - height=y_max - y_min, - origin=build123d.Vector(HORN_AXIS_X_MM, y_min, cap_z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - flange = flange.fuse(cap) - - fork_slot = _make_cylinder_along( - radius=slot_radius, - height=(y_max - y_min) + (2.0 * CUT_EXTENSION_MM), - origin=build123d.Vector( - HORN_AXIS_X_MM, - y_min - CUT_EXTENSION_MM, - HORN_PATTERN_CENTER_Z_MM, - ), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - return flange.cut(fork_slot) - - -def _horn_face_m3_holes() -> tuple[tuple[float, float, float], ...]: - return ( - ( - HORN_AXIS_X_MM - HORN_SCREW_CIRCLE_RADIUS_MM, - HORN_PATTERN_CENTER_Z_MM, - M3_CLEARANCE_RADIUS_MM, - ), - ( - HORN_AXIS_X_MM, - HORN_PATTERN_CENTER_Z_MM - HORN_SCREW_CIRCLE_RADIUS_MM, - M3_CLEARANCE_RADIUS_MM, - ), - ( - HORN_AXIS_X_MM, - HORN_PATTERN_CENTER_Z_MM + HORN_SCREW_CIRCLE_RADIUS_MM, - M3_CLEARANCE_RADIUS_MM, - ), - ) - - -def _web_holes(center_y: float) -> tuple[tuple[float, float, float], ...]: - circle = HORN_SCREW_CIRCLE_RADIUS_MM - return ( - ( - center_y, - HORN_PATTERN_CENTER_Z_MM, - HORN_CENTER_CLEARANCE_RADIUS_MM, - ), - ( - center_y - circle, - HORN_PATTERN_CENTER_Z_MM, - M3_CLEARANCE_RADIUS_MM, - ), - ( - center_y + circle, - HORN_PATTERN_CENTER_Z_MM, - M3_CLEARANCE_RADIUS_MM, - ), - ( - center_y, - HORN_PATTERN_CENTER_Z_MM - circle, - M3_CLEARANCE_RADIUS_MM, - ), - ( - center_y, - HORN_PATTERN_CENTER_Z_MM + circle, - M3_CLEARANCE_RADIUS_MM, - ), - ) - - -def _double_horn_mount_holes(face_y: float) -> tuple[tuple[build123d.Vector, float], ...]: - circle = HORN_SCREW_CIRCLE_RADIUS_MM - return ( - ( - build123d.Vector(HORN_AXIS_X_MM - circle, face_y, HORN_PATTERN_CENTER_Z_MM), - M3_CLEARANCE_RADIUS_MM, - ), - ( - build123d.Vector(HORN_AXIS_X_MM + circle, face_y, HORN_PATTERN_CENTER_Z_MM), - M3_CLEARANCE_RADIUS_MM, - ), - ( - build123d.Vector(HORN_AXIS_X_MM, face_y, HORN_PATTERN_CENTER_Z_MM - circle), - M3_CLEARANCE_RADIUS_MM, - ), - ( - build123d.Vector(HORN_AXIS_X_MM, face_y, HORN_PATTERN_CENTER_Z_MM + circle), - M3_CLEARANCE_RADIUS_MM, - ), - ) - - -def _horn_mount_spec(name: str, face_y: float) -> servo_horn_yoke.HornMountSpec: - bbox = _make_box( - x_min=WEB_OUTER_X_MM, - x_max=HORN_FLANGE_END_X_MM, - y_min=face_y, - y_max=face_y + SHEET_THICKNESS_MM, - z_min=Z_MIN_MM, - z_max=Z_MAX_MM, - ).bounding_box() - return servo_horn_yoke.HornMountSpec( - name=name, - mount_face_y=face_y, - mount_face_center=build123d.Vector(HORN_AXIS_X_MM, face_y, HORN_PATTERN_CENTER_Z_MM), - mount_face_bbox=bbox, - half_width_z=YOKE_SINGLE_HALF_WIDTH_Z_MM, - center_clearance_radius=HORN_CENTER_CLEARANCE_RADIUS_MM, - mount_holes=_double_horn_mount_holes(face_y), - ) - - -def _bottom_face_slot() -> tuple[float, float, float, float]: - center_x = ( - WEB_INNER_X_MM - + BOTTOM_FACE_SLOT_BEND_EDGE_CLEARANCE_MM - + (0.5 * BOTTOM_FACE_SLOT_WIDTH_MM) - ) - slot_z_min = Z_MIN_MM + BOTTOM_FACE_SLOT_END_GAP_MM - slot_z_max = Z_MAX_MM - BOTTOM_FACE_SLOT_END_GAP_MM - slot_length = slot_z_max - slot_z_min - if slot_length <= BOTTOM_FACE_SLOT_WIDTH_MM: - raise ValueError( - "Double yoke bottom-face slot does not have enough length across " - "the flange width after applying the flange-end gaps" - ) - return ( - center_x, - 0.5 * (slot_z_min + slot_z_max), - slot_length, - BOTTOM_FACE_SLOT_WIDTH_MM, - ) - - -def _make_y_slot_cut( - *, - center_x: float, - center_z: float, - length: float, - width: float, - y_min: float, - y_max: float, -) -> build123d.Shape: - radius = 0.5 * width - half_straight = 0.5 * length - radius - y_start = y_min - CUT_EXTENSION_MM - y_height = (y_max - y_min) + (2.0 * CUT_EXTENSION_MM) - body: build123d.Shape = _make_box( - x_min=center_x - radius, - x_max=center_x + radius, - y_min=y_start, - y_max=y_start + y_height, - z_min=center_z - half_straight, - z_max=center_z + half_straight, - ) - for end_z in (center_z - half_straight, center_z + half_straight): - cap = _make_cylinder_along( - radius=radius, - height=y_height, - origin=build123d.Vector(center_x, y_start, end_z), - direction=build123d.Vector(0.0, 1.0, 0.0), - ) - body = body.fuse(cap) - return body - - -def _cut_horn_face_holes(shape: build123d.Shape) -> build123d.Shape: - cut_height = SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - for face_y, direction_y in ( - (REAR_OUTER_HORN_FACE_Y_MM - SHEET_THICKNESS_MM - CUT_EXTENSION_MM, 1.0), - (FRONT_OUTER_HORN_FACE_Y_MM - CUT_EXTENSION_MM, 1.0), - ): - for center_x, center_z, radius in _horn_face_m3_holes(): - cutter = _make_cylinder_along( - radius=radius, - height=cut_height, - origin=build123d.Vector(center_x, face_y, center_z), - direction=build123d.Vector(0.0, direction_y, 0.0), - ) - shape = shape.cut(cutter) - return shape - - -def _cut_outer_horn_slots(shape: build123d.Shape) -> build123d.Shape: - center_x, center_z, length, width = _bottom_face_slot() - for y_min, y_max in ( - ( - REAR_OUTER_HORN_FACE_Y_MM - SHEET_THICKNESS_MM, - REAR_OUTER_HORN_FACE_Y_MM, - ), - ( - FRONT_OUTER_HORN_FACE_Y_MM, - FRONT_OUTER_HORN_FACE_Y_MM + SHEET_THICKNESS_MM, - ), - ): - shape = shape.cut( - _make_y_slot_cut( - center_x=center_x, - center_z=center_z, - length=length, - width=width, - y_min=y_min, - y_max=y_max, - ) - ) - return shape - - -def _cut_web_holes(shape: build123d.Shape) -> build123d.Shape: - cut_height = SHEET_THICKNESS_MM + (2.0 * CUT_EXTENSION_MM) - start_x = WEB_OUTER_X_MM - CUT_EXTENSION_MM - for center_y, center_z, radius in _web_holes(WEB_PATTERN_CENTER_Y_MM): - cutter = _make_cylinder_along( - radius=radius, - height=cut_height, - origin=build123d.Vector(start_x, center_y, center_z), - direction=build123d.Vector(1.0, 0.0, 0.0), - ) - shape = shape.cut(cutter) - return shape - - -def build_step() -> build123d.Shape: - horn = _make_channel_body() - horn = _cut_horn_face_holes(horn) - horn = _cut_outer_horn_slots(horn) - horn = _cut_web_holes(horn) - - solids = horn.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one connected double horn solid, found {len(solids)}") - - horn.label = PART_NAME - horn.color = lc.BRACKET_COLOR - - bb = horn.bounding_box() - _slot_center_x, _slot_center_z, slot_length, slot_width = _bottom_face_slot() - print( - f"{DISPLAY_NAME}: " - f"servo_center_spacing_y={SERVO_CENTER_SPACING_Y_MM:.3f} mm, " - f"span_y={bb.size.Y:.3f} mm, " - f"span_z={bb.size.Z:.3f} mm, " - f"web_face_x={WEB_INNER_X_MM:.4f} mm, " - f"thickness={SHEET_THICKNESS_MM:.4f} mm, " - f"horn_patterns=2, " - f"outer_horn_slots=2x{slot_length:.3f}x{slot_width:.3f} mm" - ) - return horn - - -def build_dxf(): - front_horn = _horn_mount_spec("double_front_horn", FRONT_OUTER_HORN_FACE_Y_MM) - rear_horn = _horn_mount_spec("double_rear_horn", REAR_OUTER_HORN_FACE_Y_MM) - layout, _cap_radius = servo_horn_yoke._build_layout( - WEB_INNER_X_MM, - sheet_half_width_z=YOKE_SINGLE_HALF_WIDTH_Z_MM, - upper_horn=front_horn, - lower_horn=rear_horn, - rule=servo_horn_yoke.SHEET_RULE, - ) - flat_pattern, metrics = servo_horn_yoke.build_flat_pattern( - WEB_INNER_X_MM, - sheet_half_width_z=YOKE_SINGLE_HALF_WIDTH_Z_MM, - upper_horn=front_horn, - lower_horn=rear_horn, - rule=servo_horn_yoke.SHEET_RULE, - ) - doc = servo_horn_yoke._topology_flat_pattern_dxf( - build_step(), - layout, - flat_pattern, - upper_horn=front_horn, - lower_horn=rear_horn, - rule=servo_horn_yoke.SHEET_RULE, - ) - print( - f"{DISPLAY_NAME} DXF: " - f"length={flat_pattern.total_length_mm:.3f} mm, " - f"width={flat_pattern.width_mm:.3f} mm, " - f"bend_lines=({flat_pattern.bend_line_positions_mm[0]:.3f}, " - f"{flat_pattern.bend_line_positions_mm[1]:.3f}) mm, " - f"round_holes={len(flat_pattern.round_hole_profiles)}, " - f"slots={len(flat_pattern.slot_profiles)}, " - f"flange_flat_min={metrics.min_flange_flat_length_mm:.3f} mm, " - f"slot_edge_to_bend_min={metrics.min_slot_edge_to_bend_mm:.3f} mm" - ) - return doc - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -def gen_dxf() -> dict[str, object]: - return { - "document": build_dxf(), - } - - -def gen() -> None: - gen_step() - gen_dxf() - - -if __name__ == "__main__": - gen() diff --git a/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.step b/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.step deleted file mode 100644 index 6b36cbf5..00000000 --- a/models/robots/tom/v2/parts/servo_horn_yoke_double_horn.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a6e2766e98316d481253f56d646b8b22b647ffacbf6dc22729ddb1ae5453bdb0 -size 119137 diff --git a/models/robots/tom/v2/parts/v2_variant_common.py b/models/robots/tom/v2/parts/v2_variant_common.py deleted file mode 100644 index 69d70a47..00000000 --- a/models/robots/tom/v2/parts/v2_variant_common.py +++ /dev/null @@ -1,142 +0,0 @@ -from __future__ import annotations - -from contextlib import contextmanager -import copy -import importlib.util -import math -import os -import sys -from pathlib import Path - -import build123d - - -PARTS_DIR = Path(__file__).resolve().parent -V2_DIR = PARTS_DIR.parent -TOM_DIR = V2_DIR.parent - - -def _env_float(name: str, default: float) -> float: - raw = os.environ.get(name) - if raw is None: - return default - try: - return float(raw) - except ValueError as exc: - raise ValueError(f"{name} must be a floating point number, got {raw!r}") from exc - - -ALUMINUM_5052_SHEET_THICKNESS_MM = _env_float( - "TOM_V2_ALUMINUM_5052_SHEET_THICKNESS_MM", - 25.4 * 0.063, -) -ALUMINUM_5052_YIELD_MPA = _env_float("TOM_V2_ALUMINUM_5052_YIELD_MPA", 193.0) -PRINTABLE_EFFECTIVE_STRENGTH_MPA = _env_float( - "TOM_V2_PRINTABLE_EFFECTIVE_STRENGTH_MPA", - 21.449805890625, -) -PRINTABLE_BENDING_STRENGTH_MATCH_THICKNESS_MM = ( - ALUMINUM_5052_SHEET_THICKNESS_MM - * math.sqrt(ALUMINUM_5052_YIELD_MPA / PRINTABLE_EFFECTIVE_STRENGTH_MPA) -) -PRINTABLE_EQUIVALENT_THICKNESS_MM = float( - os.environ.get( - "TOM_V2_PRINTABLE_THICKNESS_MM", - f"{PRINTABLE_BENDING_STRENGTH_MATCH_THICKNESS_MM:.9f}", - ) -) -PRINTABLE_COLOR = build123d.Color(0.72, 0.74, 0.72, 1.0) - - -def _module_path(module_name: str) -> Path: - return PARTS_DIR / f"{module_name}.py" - - -@contextmanager -def loaded_v2_module( - module_name: str, - *, - sheet_thickness_mm: float | None = None, - case_span_centering_offset_mm: float | None = None, - link_bracket_height_mm: float | None = None, -): - old_env = os.environ.get("TOM_V2_SHEET_THICKNESS_MM") - old_case_offset_env = os.environ.get("TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM") - old_link_height_env = os.environ.get("TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM") - if sheet_thickness_mm is None: - os.environ.pop("TOM_V2_SHEET_THICKNESS_MM", None) - else: - os.environ["TOM_V2_SHEET_THICKNESS_MM"] = f"{sheet_thickness_mm:.9f}" - if case_span_centering_offset_mm is None: - os.environ.pop("TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM", None) - else: - os.environ["TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM"] = ( - f"{case_span_centering_offset_mm:.9f}" - ) - if link_bracket_height_mm is None: - os.environ.pop("TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM", None) - else: - os.environ["TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM"] = ( - f"{link_bracket_height_mm:.9f}" - ) - - stale_modules = {module_name} - if module_name == "link_bracket": - stale_modules.add("link_common") - previous_modules = {name: sys.modules.get(name) for name in stale_modules} - for name in stale_modules: - sys.modules.pop(name, None) - - old_sys_path = list(sys.path) - for path in (TOM_DIR, V2_DIR, PARTS_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - - module_file = _module_path(module_name) - synthetic_name = ( - f"_tom_v2_variant_{module_name}_" - f"{abs(hash((module_file, sheet_thickness_mm, case_span_centering_offset_mm, link_bracket_height_mm)))}" - ) - spec = importlib.util.spec_from_file_location(synthetic_name, module_file) - if spec is None or spec.loader is None: - raise RuntimeError(f"Unable to load {module_file}") - module = importlib.util.module_from_spec(spec) - - try: - sys.modules[module_name] = module - sys.modules[synthetic_name] = module - spec.loader.exec_module(module) - yield module - finally: - sys.path[:] = old_sys_path - sys.modules.pop(synthetic_name, None) - for name in stale_modules: - sys.modules.pop(name, None) - previous = previous_modules[name] - if previous is not None: - sys.modules[name] = previous - if old_env is None: - os.environ.pop("TOM_V2_SHEET_THICKNESS_MM", None) - else: - os.environ["TOM_V2_SHEET_THICKNESS_MM"] = old_env - if old_case_offset_env is None: - os.environ.pop("TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM", None) - else: - os.environ["TOM_V2_CASE_SPAN_CENTERING_OFFSET_MM"] = old_case_offset_env - if old_link_height_env is None: - os.environ.pop("TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM", None) - else: - os.environ["TOM_V2_LINK_BRACKET_OVERALL_HEIGHT_MM"] = old_link_height_env - - -def mirror_y(shape: build123d.Shape) -> build123d.Shape: - mirrored = copy.deepcopy(shape) - mirrored = mirrored.mirror(build123d.Plane.XZ) - return mirrored - - -def finish_part(shape: build123d.Shape, label: str, *, printable: bool = False) -> build123d.Shape: - shape.label = label - if printable: - shape.color = PRINTABLE_COLOR - return shape diff --git a/models/robots/tom/v2/printable/link_assembly.py b/models/robots/tom/v2/printable/link_assembly.py deleted file mode 100644 index 5dca92a8..00000000 --- a/models/robots/tom/v2/printable/link_assembly.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -"""Printable-side tom v2 link assembly with straight tongue shell halves added. - -This is a working copy of the regular link assembly in the printable folder. -It keeps the original servo, bracket, standoff, and yoke layout, then adds the -current straight four-tongue plastic shell halves on both link brackets. The -right shell has printed male dovetail rails; the left shell has matching -female dovetail channels. Future printable connector iterations can target this -assembly without keeping assemblies/link_assembly.py in sync. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -PRINTABLE_DIR = Path(__file__).resolve().parent -V2_DIR = PRINTABLE_DIR.parent -ASSEMBLIES_DIR = V2_DIR / "assemblies" -PARTS_DIR = V2_DIR / "parts" -for path in (V2_DIR, ASSEMBLIES_DIR, PARTS_DIR, PRINTABLE_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import link_common as lc -import link_bracket -import link_bracket_slot_pair_straight_sample as connector -import pitch_link_sts3250 -import servo_horn_yoke - - -YOKE_HORN_SPAN_CENTER_LOCAL_Y_MM = -9.1 -IDENTITY_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - -MIRROR_Y_TRANSFORM = ( - 1.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0, -) - -YOKE_ON_SERVO_HORNS_TRANSFORM = tuple( - servo_horn_yoke.STANDALONE_YOKE_ON_SERVO_HORNS_TRANSFORM -) - - -def _matrix(transform: list[float] | tuple[float, ...]) -> list[list[float]]: - return [list(transform[index : index + 4]) for index in range(0, 16, 4)] - - -def _flatten(matrix: list[list[float]]) -> list[float]: - return [value for row in matrix for value in row] - - -def _matmul( - left: list[float] | tuple[float, ...], - right: list[float] | tuple[float, ...], -) -> list[float]: - left_matrix = _matrix(left) - right_matrix = _matrix(right) - return _flatten( - [ - [ - sum(left_matrix[row][inner] * right_matrix[inner][col] for inner in range(4)) - for col in range(4) - ] - for row in range(4) - ] - ) - - -def _invert_rigid_transform(transform: list[float] | tuple[float, ...]) -> list[float]: - matrix = _matrix(transform) - rotation = [row[:3] for row in matrix[:3]] - translation = [matrix[row][3] for row in range(3)] - inverse_rotation = [[rotation[col][row] for col in range(3)] for row in range(3)] - inverse_translation = [ - -sum(inverse_rotation[row][col] * translation[col] for col in range(3)) - for row in range(3) - ] - return _flatten( - [ - [*inverse_rotation[0], inverse_translation[0]], - [*inverse_rotation[1], inverse_translation[1]], - [*inverse_rotation[2], inverse_translation[2]], - [0.0, 0.0, 0.0, 1.0], - ] - ) - - -YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM = _matmul( - lc.BOTTOM_SERVO_TRANSFORM, - _invert_rigid_transform(pitch_link_sts3250.STS3250_TRANSFORM), -) - -THIRD_SERVO_HORNS_IN_YOKE_TRANSFORM = _matmul( - YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM, - _invert_rigid_transform(YOKE_ON_SERVO_HORNS_TRANSFORM), -) - -TOP_SERVO_FACE_TO_FLANGE_TRANSFORM = link_bracket.top_servo_case_transform( - case_span_centering_offset_mm=link_bracket.TOP_SERVO_MATED_CASE_SPAN_CENTERING_OFFSET_MM, -) - -LOWER_SLOT_PAIR_CENTER_X_MM = 0.5 * ( - link_bracket.LOWER_SLOT_CENTER_X_MM[0] + link_bracket.LOWER_SLOT_CENTER_X_MM[1] -) - -# Map shell local coordinates into the right bracket slot frame: -# connector X (section offset and tongue length) -> bracket Z -# connector Y (slot-pair spacing and upper-section offset) -> bracket -X -# connector Z (insertion depth) -> bracket -Y, seated from the right bracket outside face -SHELL_ON_RIGHT_BRACKET_TRANSFORM = [ - 0.0, -1.0, 0.0, LOWER_SLOT_PAIR_CENTER_X_MM, - 0.0, 0.0, -1.0, link_bracket.LEG_OUTER_Y_MM + connector.BASE_THICKNESS_MM, - 1.0, 0.0, 0.0, link_bracket.LOWER_SLOT_CENTER_Z_MM, - 0.0, 0.0, 0.0, 1.0, -] - -# Same shell half mirrored to the other bracket. Local Z still runs from the -# outside plastic face through the bracket slots and on to the Y=0 center plane. -SHELL_ON_LEFT_BRACKET_TRANSFORM = [ - 0.0, -1.0, 0.0, LOWER_SLOT_PAIR_CENTER_X_MM, - 0.0, 0.0, 1.0, -(link_bracket.LEG_OUTER_Y_MM + connector.BASE_THICKNESS_MM), - 1.0, 0.0, 0.0, link_bracket.LOWER_SLOT_CENTER_Z_MM, - 0.0, 0.0, 0.0, 1.0, -] - - -def _translation_transform(x: float, y: float, z: float) -> list[float]: - return [ - 1.0, 0.0, 0.0, x, - 0.0, 1.0, 0.0, y, - 0.0, 0.0, 1.0, z, - 0.0, 0.0, 0.0, 1.0, - ] - - -def _standoff_children() -> list[dict[str, object]]: - return [ - { - "path": "../parts/link_standoff_m3_35.step", - "name": f"link_standoff_{label}", - "transform": _translation_transform(x, 0.0, z), - "use_source_colors": True, - } - for label, x, z in link_bracket.STANDOFF_CENTER_XZ_MM - ] - - -def _mate( - source_label: str, - *, - fixed: str, - moving: str, - relation: str = "rigid", -) -> dict[str, object]: - fixed_part, fixed_frame = fixed.split(":", 1) - moving_part, moving_frame = moving.split(":", 1) - return { - "sourceLabel": source_label, - "type": relation, - "relation": relation, - "fixed": fixed, - "moving": moving, - "parameters": {}, - "fixedEndpoint": { - "part": fixed_part, - "frame": fixed_frame, - }, - "movingEndpoint": { - "part": moving_part, - "frame": moving_frame, - }, - } - - -def _standoff_mates() -> list[dict[str, object]]: - mates: list[dict[str, object]] = [] - for label, _x, _z in link_bracket.STANDOFF_CENTER_XZ_MM: - mates.append( - _mate( - f"{label}_standoff_to_right_bracket", - fixed=f"link_bracket_right:{label}_standoff_hole", - moving=f"link_standoff_{label}:positive_y_thread", - ) - ) - mates.append( - _mate( - f"{label}_standoff_to_left_bracket", - fixed=f"link_bracket_left:{label}_standoff_hole", - moving=f"link_standoff_{label}:negative_y_thread", - ) - ) - return mates - - -def _children() -> list[dict[str, object]]: - return [ - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_top", - "transform": list(TOP_SERVO_FACE_TO_FLANGE_TRANSFORM), - }, - { - "path": "../parts/imports/sts3250.step", - "name": "sts3250_bottom", - "transform": list(lc.BOTTOM_SERVO_TRANSFORM), - "use_source_colors": True, - }, - { - "path": "../parts/link_bracket_right.step", - "name": "link_bracket_right", - "transform": list(IDENTITY_TRANSFORM), - }, - { - "path": "../parts/link_bracket_left.step", - "name": "link_bracket_left", - "transform": list(IDENTITY_TRANSFORM), - }, - *_standoff_children(), - { - "path": "../parts/servo_horn_yoke.step", - "name": "servo_horn_yoke", - "transform": YOKE_WEB_ON_BOTTOM_SERVO_OUTPUT_HORN_TRANSFORM, - }, - { - "path": "../parts/imports/sts3215.step", - "name": "sts3215_yoke_servo", - "transform": THIRD_SERVO_HORNS_IN_YOKE_TRANSFORM, - "use_source_colors": True, - }, - { - "path": "link_bracket_slot_pair_straight_sample.step", - "name": "straight_slot_shell_right", - "transform": SHELL_ON_RIGHT_BRACKET_TRANSFORM, - "use_source_colors": True, - }, - { - "path": "link_bracket_slot_pair_straight_socket_sample.step", - "name": "straight_slot_shell_left", - "transform": SHELL_ON_LEFT_BRACKET_TRANSFORM, - "use_source_colors": True, - }, - ] - - -def _assembly_mates() -> list[dict[str, object]]: - return [ - _mate( - "bottom_servo_to_right_bracket", - fixed="sts3250_bottom:upstream_case", - moving="link_bracket_right:bottom_servo_mount", - ), - _mate( - "bottom_servo_to_left_bracket", - fixed="sts3250_bottom:upstream_case", - moving="link_bracket_left:bottom_servo_mount", - ), - _mate( - "right_bracket_to_top_servo", - fixed="link_bracket_right:top_servo_mount", - moving="sts3250_top:case_mount", - ), - _mate( - "left_bracket_to_top_servo", - fixed="link_bracket_left:top_servo_mount", - moving="sts3250_top:case_mount", - ), - _mate( - "bottom_servo_output_horn_to_yoke_web", - fixed="sts3250_bottom:horn_axis", - moving="servo_horn_yoke:web_horn_axis", - ), - _mate( - "third_servo_horns_to_yoke", - fixed="servo_horn_yoke:horn_axis", - moving="sts3215_yoke_servo:horn_axis", - ), - *_standoff_mates(), - ] - - -def gen_step() -> dict[str, object]: - children = _children() - print( - "Printable link assembly with straight shell halves " - f"children={len(children)}; connector lower slot center x={LOWER_SLOT_PAIR_CENTER_X_MM:.3f} mm, " - f"lower slot center z={link_bracket.LOWER_SLOT_CENTER_Z_MM:.3f} mm, " - f"tongue depth={connector.TONGUE_INSERTION_DEPTH_MM:.3f} mm, " - f"base sheet={connector.BASE_LENGTH_MM:.3f} x {connector.BASE_WIDTH_MM:.3f} mm, " - f"shell half depth={connector.SHELL_HALF_DEPTH_MM:.3f} mm, " - "printed dovetail slide seam" - ) - return { - "children": children, - "assembly_mates": _assembly_mates(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/printable/link_assembly.step b/models/robots/tom/v2/printable/link_assembly.step deleted file mode 100644 index a41da6aa..00000000 --- a/models/robots/tom/v2/printable/link_assembly.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b9f775d1eb4e4acecdbe1ee86480e53bfd3d2ab2f66ae31e4f772095cc1fce0 -size 26274057 diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.py b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.py deleted file mode 100644 index 2a709e0a..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env python3 -"""Printable four-slot straight tongue dovetail half-shell for tom v2 link brackets. - -This intentionally keeps the slot interface simple: raised rounded tongues -locate in the nominal lower and upper lightening slots, while side return rails -wrap around the bracket plate to the link center plane. The default generated -part is the male half of an all-printed PLA dovetail; a matching female half is -generated by link_bracket_slot_pair_straight_socket_sample.py. -""" - -from __future__ import annotations - -import sys -from pathlib import Path - -import build123d - -PRINTABLE_DIR = Path(__file__).resolve().parent -V2_DIR = PRINTABLE_DIR.parent -PARTS_DIR = V2_DIR / "parts" -if str(PARTS_DIR) not in sys.path: - sys.path.insert(0, str(PARTS_DIR)) - -import link_bracket as lb -import link_common as lc -from v2_variant_common import finish_part - - -PART_NAME = Path(__file__).stem -PLASTIC_COLOR = build123d.Color(1.0, 1.0, 1.0, 1.0) - -FIT_CLEARANCE_TOTAL_MM = 0.0 -SLOT_LENGTH_CLEARANCE_TOTAL_MM = 0.0 -ORIGINAL_EXTRA_THROUGH_DEPTH_MM = 0.40 - -SLOT_WIDTH_MM = lb.SLOT_WIDTH_MM -TOP_SLOT_WIDTH_MM = lb.TOP_SLOT_WIDTH_MM -LOWER_SLOT_LENGTH_MM = lb.LOWER_SLOT_LENGTH_MM -TOP_SLOT_LENGTH_MM = lb.TOP_SLOT_LENGTH_MM -SHEET_THICKNESS_MM = lc.SHEET_THICKNESS_MM -LOWER_SLOT_PAIR_CENTER_X_MM = 0.5 * ( - lb.LOWER_SLOT_CENTER_X_MM[0] + lb.LOWER_SLOT_CENTER_X_MM[1] -) -TOP_SLOT_PAIR_CENTER_X_MM = 0.5 * ( - lb.TOP_SLOT_CENTER_X_MM[0] + lb.TOP_SLOT_CENTER_X_MM[1] -) -LOWER_SLOT_PAIR_CENTER_SPACING_MM = ( - lb.LOWER_SLOT_CENTER_X_MM[1] - lb.LOWER_SLOT_CENTER_X_MM[0] -) -TOP_SLOT_PAIR_CENTER_SPACING_MM = ( - lb.TOP_SLOT_CENTER_X_MM[1] - lb.TOP_SLOT_CENTER_X_MM[0] -) -TOP_SECTION_OFFSET_X_MM = lb.TOP_SLOT_CENTER_Z_MM - lb.LOWER_SLOT_CENTER_Z_MM -TOP_SECTION_OFFSET_Y_MM = LOWER_SLOT_PAIR_CENTER_X_MM - TOP_SLOT_PAIR_CENTER_X_MM - -TONGUE_WIDTH_MM = SLOT_WIDTH_MM - FIT_CLEARANCE_TOTAL_MM -TOP_TONGUE_WIDTH_MM = TOP_SLOT_WIDTH_MM - FIT_CLEARANCE_TOTAL_MM -ORIGINAL_TONGUE_INSERTION_DEPTH_MM = SHEET_THICKNESS_MM + ORIGINAL_EXTRA_THROUGH_DEPTH_MM -TONGUE_INSERTION_DEPTH_MM = ORIGINAL_TONGUE_INSERTION_DEPTH_MM -EXTRA_THROUGH_DEPTH_MM = TONGUE_INSERTION_DEPTH_MM - SHEET_THICKNESS_MM -BASE_END_MARGIN_MM = 2.0 -BASE_SIDE_MARGIN_MM = 6.0 -LOWER_TONGUE_LENGTH_MM = LOWER_SLOT_LENGTH_MM - SLOT_LENGTH_CLEARANCE_TOTAL_MM -TOP_TONGUE_LENGTH_MM = TOP_SLOT_LENGTH_MM - SLOT_LENGTH_CLEARANCE_TOTAL_MM -LOWER_BASE_LENGTH_MM = LOWER_TONGUE_LENGTH_MM + 2.0 * BASE_END_MARGIN_MM -TOP_BASE_LENGTH_MM = TOP_TONGUE_LENGTH_MM + 2.0 * BASE_END_MARGIN_MM -LOWER_BASE_WIDTH_MM = ( - LOWER_SLOT_PAIR_CENTER_SPACING_MM + TONGUE_WIDTH_MM + 2.0 * BASE_SIDE_MARGIN_MM -) -TOP_BASE_WIDTH_MM = ( - TOP_SLOT_PAIR_CENTER_SPACING_MM + TOP_TONGUE_WIDTH_MM + 2.0 * BASE_SIDE_MARGIN_MM -) -# In the printable link assembly, connector local X maps to assembly world Z. -# These world-Z limits match the link bracket plate extents in the printable -# assembly: link_bracket_right/left #o1.3 and #o1.4. -BASE_TARGET_WORLD_Z_MIN_MM = 0.0 -BASE_TARGET_WORLD_Z_MAX_MM = 144.538516 -BASE_TARGET_LOCAL_X_MIN_MM = BASE_TARGET_WORLD_Z_MIN_MM - lb.LOWER_SLOT_CENTER_Z_MM -BASE_TARGET_LOCAL_X_MAX_MM = BASE_TARGET_WORLD_Z_MAX_MM - lb.LOWER_SLOT_CENTER_Z_MM -BASE_X_MIN_MM = min( - -0.5 * LOWER_BASE_LENGTH_MM, - TOP_SECTION_OFFSET_X_MM - 0.5 * TOP_BASE_LENGTH_MM, - BASE_TARGET_LOCAL_X_MIN_MM, -) -BASE_X_MAX_MM = max( - 0.5 * LOWER_BASE_LENGTH_MM, - TOP_SECTION_OFFSET_X_MM + 0.5 * TOP_BASE_LENGTH_MM, - BASE_TARGET_LOCAL_X_MAX_MM, -) -# Connector local Y maps to bracket/assembly -X. Keep the wrap rails just -# outside the nearby servo envelopes so the shell wraps the plates instead of -# intersecting the servo bodies. -BASE_TARGET_WORLD_X_MIN_MM = -20.0 -BASE_TARGET_WORLD_X_MAX_MM = 36.0 -BASE_TARGET_LOCAL_Y_MIN_MM = LOWER_SLOT_PAIR_CENTER_X_MM - BASE_TARGET_WORLD_X_MAX_MM -BASE_TARGET_LOCAL_Y_MAX_MM = LOWER_SLOT_PAIR_CENTER_X_MM - BASE_TARGET_WORLD_X_MIN_MM -BASE_Y_MIN_MM = min( - -0.5 * LOWER_BASE_WIDTH_MM, - TOP_SECTION_OFFSET_Y_MM - 0.5 * TOP_BASE_WIDTH_MM, - BASE_TARGET_LOCAL_Y_MIN_MM, -) -BASE_Y_MAX_MM = max( - 0.5 * LOWER_BASE_WIDTH_MM, - TOP_SECTION_OFFSET_Y_MM + 0.5 * TOP_BASE_WIDTH_MM, - BASE_TARGET_LOCAL_Y_MAX_MM, -) -BASE_LENGTH_MM = BASE_X_MAX_MM - BASE_X_MIN_MM -BASE_WIDTH_MM = BASE_Y_MAX_MM - BASE_Y_MIN_MM -BASE_THICKNESS_MM = 1.8 -SHELL_HALF_DEPTH_MM = lb.LEG_OUTER_Y_MM + BASE_THICKNESS_MM -WRAP_RAIL_WIDTH_MM = 7.0 -WRAP_RAIL_OVERLAP_MM = 0.05 -OUTER_CORNER_FILLET_MM = 7.0 -END_COVERAGE_THICKNESS_MM = 2.2 -SERVO_HALF_Y_CLEARANCE_MM = lb.LEG_OUTER_Y_MM -END_COVERAGE_DEPTH_MM = SHELL_HALF_DEPTH_MM - SERVO_HALF_Y_CLEARANCE_MM -DOVETAIL_Y_CENTERS_MM = ( - BASE_Y_MIN_MM - 0.5 * WRAP_RAIL_WIDTH_MM, - BASE_Y_MAX_MM + 0.5 * WRAP_RAIL_WIDTH_MM, -) -DOVETAIL_X_END_RELIEF_MM = 0.0 -DOVETAIL_NECK_WIDTH_MM = 3.2 -DOVETAIL_HEAD_WIDTH_MM = 4.8 -DOVETAIL_ENGAGEMENT_DEPTH_MM = 2.2 -DOVETAIL_ROOT_OVERLAP_MM = 0.15 -DOVETAIL_CLEARANCE_MM = 0.0 -DOVETAIL_MALE_X_MIN_MM = BASE_X_MIN_MM + DOVETAIL_X_END_RELIEF_MM -DOVETAIL_MALE_X_MAX_MM = BASE_X_MAX_MM - DOVETAIL_X_END_RELIEF_MM -DOVETAIL_SOCKET_X_MIN_MM = BASE_X_MIN_MM - 1.0 -DOVETAIL_SOCKET_X_MAX_MM = BASE_X_MAX_MM + 1.0 -LOWER_TONGUE_CENTERS_MM = ( - (0.0, -0.5 * LOWER_SLOT_PAIR_CENTER_SPACING_MM), - (0.0, 0.5 * LOWER_SLOT_PAIR_CENTER_SPACING_MM), -) -TOP_TONGUE_CENTERS_MM = ( - (TOP_SECTION_OFFSET_X_MM, TOP_SECTION_OFFSET_Y_MM - 0.5 * TOP_SLOT_PAIR_CENTER_SPACING_MM), - (TOP_SECTION_OFFSET_X_MM, TOP_SECTION_OFFSET_Y_MM + 0.5 * TOP_SLOT_PAIR_CENTER_SPACING_MM), -) - - -def _slot_prism_xy( - *, - length: float, - width: float, - z_min: float, - z_max: float, - rotation_deg: float = 0.0, -) -> build123d.Shape: - with build123d.BuildPart() as part: - with build123d.BuildSketch(build123d.Plane.XY): - build123d.SlotOverall(width=length, height=width, rotation=rotation_deg) - build123d.extrude(amount=z_max - z_min) - return part.part.moved(build123d.Location((0.0, 0.0, z_min))) - - -def _box( - *, - x_min: float, - x_max: float, - y_min: float, - y_max: float, - z_min: float, - z_max: float, -) -> build123d.Solid: - return build123d.Solid.make_box( - x_max - x_min, - y_max - y_min, - z_max - z_min, - plane=build123d.Plane(origin=build123d.Vector(x_min, y_min, z_min)), - ) - - -def _base_sheet() -> build123d.Shape: - return _box( - x_min=BASE_X_MIN_MM, - x_max=BASE_X_MAX_MM, - y_min=BASE_Y_MIN_MM, - y_max=BASE_Y_MAX_MM, - z_min=0.0, - z_max=BASE_THICKNESS_MM, - ) - - -def _end_coverage_caps() -> list[build123d.Shape]: - y_min = BASE_Y_MIN_MM - WRAP_RAIL_WIDTH_MM - y_max = BASE_Y_MAX_MM + WRAP_RAIL_WIDTH_MM - return [ - _box( - x_min=BASE_X_MIN_MM, - x_max=BASE_X_MIN_MM + END_COVERAGE_THICKNESS_MM, - y_min=y_min, - y_max=y_max, - z_min=0.0, - z_max=END_COVERAGE_DEPTH_MM, - ), - _box( - x_min=BASE_X_MAX_MM - END_COVERAGE_THICKNESS_MM, - x_max=BASE_X_MAX_MM, - y_min=y_min, - y_max=y_max, - z_min=0.0, - z_max=END_COVERAGE_DEPTH_MM, - ), - ] - - -def _wrap_rails() -> list[build123d.Shape]: - return [ - _box( - x_min=BASE_X_MIN_MM, - x_max=BASE_X_MAX_MM, - y_min=BASE_Y_MIN_MM - WRAP_RAIL_WIDTH_MM, - y_max=BASE_Y_MIN_MM + WRAP_RAIL_OVERLAP_MM, - z_min=0.0, - z_max=SHELL_HALF_DEPTH_MM, - ), - _box( - x_min=BASE_X_MIN_MM, - x_max=BASE_X_MAX_MM, - y_min=BASE_Y_MAX_MM - WRAP_RAIL_OVERLAP_MM, - y_max=BASE_Y_MAX_MM + WRAP_RAIL_WIDTH_MM, - z_min=0.0, - z_max=SHELL_HALF_DEPTH_MM, - ), - ] - - -def _straight_tongue(*, length: float, width: float) -> build123d.Shape: - return _slot_prism_xy( - length=length, - width=width, - z_min=BASE_THICKNESS_MM, - z_max=BASE_THICKNESS_MM + TONGUE_INSERTION_DEPTH_MM, - ) - - -def _dovetail_prism_x( - *, - x_min: float, - x_max: float, - y_center: float, - z_root: float, - z_tip: float, - root_width: float, - tip_width: float, -) -> build123d.Shape: - with build123d.BuildPart() as part: - with build123d.BuildSketch(build123d.Plane.YZ): - build123d.Polygon( - (y_center - 0.5 * root_width, z_root), - (y_center + 0.5 * root_width, z_root), - (y_center + 0.5 * tip_width, z_tip), - (y_center - 0.5 * tip_width, z_tip), - align=None, - ) - build123d.extrude(amount=x_max - x_min) - return part.part.moved(build123d.Location((x_min, 0.0, 0.0))) - - -def _dovetail_male_rails() -> list[build123d.Shape]: - return [ - _dovetail_prism_x( - x_min=DOVETAIL_MALE_X_MIN_MM, - x_max=DOVETAIL_MALE_X_MAX_MM, - y_center=center_y, - z_root=SHELL_HALF_DEPTH_MM - DOVETAIL_ROOT_OVERLAP_MM, - z_tip=SHELL_HALF_DEPTH_MM + DOVETAIL_ENGAGEMENT_DEPTH_MM, - root_width=DOVETAIL_NECK_WIDTH_MM, - tip_width=DOVETAIL_HEAD_WIDTH_MM, - ) - for center_y in DOVETAIL_Y_CENTERS_MM - ] - - -def _dovetail_socket_cutters() -> list[build123d.Shape]: - cutters: list[build123d.Shape] = [] - opening_width = DOVETAIL_NECK_WIDTH_MM + 2.0 * DOVETAIL_CLEARANCE_MM - captured_width = DOVETAIL_HEAD_WIDTH_MM + 2.0 * DOVETAIL_CLEARANCE_MM - for center_y in DOVETAIL_Y_CENTERS_MM: - cutters.append( - _dovetail_prism_x( - x_min=DOVETAIL_SOCKET_X_MIN_MM, - x_max=DOVETAIL_SOCKET_X_MAX_MM, - y_center=center_y, - z_root=SHELL_HALF_DEPTH_MM + DOVETAIL_CLEARANCE_MM, - z_tip=( - SHELL_HALF_DEPTH_MM - - DOVETAIL_ENGAGEMENT_DEPTH_MM - - DOVETAIL_CLEARANCE_MM - ), - root_width=opening_width, - tip_width=captured_width, - ) - ) - return cutters - - -def _outside_face_corner_edges(shell: build123d.Shape) -> build123d.ShapeList[build123d.Edge]: - outer_y_values = ( - BASE_Y_MIN_MM - WRAP_RAIL_WIDTH_MM, - BASE_Y_MAX_MM + WRAP_RAIL_WIDTH_MM, - ) - edges = shell.edges().filter_by(build123d.Axis.X).filter_by( - lambda edge: abs(edge.center().Z) < 0.05 - and abs(edge.length - BASE_LENGTH_MM) < 0.05 - and any(abs(edge.center().Y - y) < 0.05 for y in outer_y_values) - ) - if len(edges) != 2: - raise RuntimeError( - "Expected two outside face corner edges to fillet, " - f"found {len(edges)}" - ) - return edges - - -def build_step(*, dovetail_role: str = "male") -> build123d.Shape: - if DOVETAIL_MALE_X_MIN_MM >= DOVETAIL_MALE_X_MAX_MM: - raise ValueError("Dovetail end relief is longer than the connector") - if DOVETAIL_HEAD_WIDTH_MM >= WRAP_RAIL_WIDTH_MM: - raise ValueError("Dovetail head must fit inside the wrap rail width") - if DOVETAIL_NECK_WIDTH_MM >= DOVETAIL_HEAD_WIDTH_MM: - raise ValueError("Dovetail neck must be narrower than the captured head") - if ( - DOVETAIL_HEAD_WIDTH_MM + 2.0 * DOVETAIL_CLEARANCE_MM - >= WRAP_RAIL_WIDTH_MM - 1.0 - ): - raise ValueError("Dovetail socket leaves too little rail wall for PLA") - if DOVETAIL_ENGAGEMENT_DEPTH_MM + DOVETAIL_CLEARANCE_MM >= SHELL_HALF_DEPTH_MM: - raise ValueError("Dovetail engagement is deeper than the shell half") - if TONGUE_WIDTH_MM <= 0.0: - raise ValueError("Fit clearance is larger than the bracket slot width") - if TOP_TONGUE_WIDTH_MM <= 0.0: - raise ValueError("Fit clearance is larger than the top bracket slot width") - if TONGUE_WIDTH_MM > SLOT_WIDTH_MM: - raise ValueError("Tongue width must not exceed the bracket slot") - if TOP_TONGUE_WIDTH_MM > TOP_SLOT_WIDTH_MM: - raise ValueError("Top tongue width must not exceed the top bracket slot") - if LOWER_TONGUE_LENGTH_MM > LOWER_SLOT_LENGTH_MM: - raise ValueError("Lower tongue length must not exceed the lower bracket slot") - if TOP_TONGUE_LENGTH_MM > TOP_SLOT_LENGTH_MM: - raise ValueError("Top tongue length must not exceed the top bracket slot") - if TONGUE_INSERTION_DEPTH_MM <= SHEET_THICKNESS_MM: - raise ValueError("Tongue insertion depth must pass through the bracket sheet") - if abs(TOP_SLOT_PAIR_CENTER_SPACING_MM - LOWER_SLOT_PAIR_CENTER_SPACING_MM) > 0.01: - raise ValueError( - f"Top slot pair spacing {TOP_SLOT_PAIR_CENTER_SPACING_MM:.3f} mm " - f"does not match lower slot spacing {LOWER_SLOT_PAIR_CENTER_SPACING_MM:.3f} mm" - ) - - sample: build123d.Shape = _base_sheet() - for rail in _wrap_rails(): - sample = sample.fuse(rail) - for cap in _end_coverage_caps(): - sample = sample.fuse(cap) - - for center_x, center_y in LOWER_TONGUE_CENTERS_MM: - tongue = _straight_tongue( - length=LOWER_TONGUE_LENGTH_MM, - width=TONGUE_WIDTH_MM, - ).moved(build123d.Location((center_x, center_y, 0.0))) - sample = sample.fuse(tongue) - for center_x, center_y in TOP_TONGUE_CENTERS_MM: - tongue = _straight_tongue( - length=TOP_TONGUE_LENGTH_MM, - width=TOP_TONGUE_WIDTH_MM, - ).moved(build123d.Location((center_x, center_y, 0.0))) - sample = sample.fuse(tongue) - - if dovetail_role == "male": - for rail in _dovetail_male_rails(): - sample = sample.fuse(rail) - elif dovetail_role == "female": - for cutter in _dovetail_socket_cutters(): - sample = sample.cut(cutter) - else: - raise ValueError(f"Unsupported dovetail_role {dovetail_role!r}") - - sample = sample.fillet( - OUTER_CORNER_FILLET_MM, - _outside_face_corner_edges(sample), - ) - - solids = sample.solids() - if len(solids) != 1: - raise RuntimeError(f"Expected one printable sample solid, found {len(solids)}") - - sample = finish_part(sample, PART_NAME, printable=True) - sample.color = PLASTIC_COLOR - bb = sample.bounding_box() - print( - "Link bracket four-slot straight sample " - f"envelope={bb.size.X:.3f} x {bb.size.Y:.3f} x {bb.size.Z:.3f} mm; " - f"lower_tongues=2x {LOWER_TONGUE_LENGTH_MM:.3f} x {TONGUE_WIDTH_MM:.3f} x " - f"{TONGUE_INSERTION_DEPTH_MM:.3f} mm, " - f"top_tongues=2x {TOP_TONGUE_LENGTH_MM:.3f} x {TOP_TONGUE_WIDTH_MM:.3f} x " - f"{TONGUE_INSERTION_DEPTH_MM:.3f} mm, " - f"slot_width={SLOT_WIDTH_MM:.3f} mm, total_clearance={FIT_CLEARANCE_TOTAL_MM:.3f} mm, " - f"lower_slot_length={LOWER_SLOT_LENGTH_MM:.3f} mm, " - f"top_slot_length={TOP_SLOT_LENGTH_MM:.3f} mm, " - f"base_sheet={BASE_LENGTH_MM:.3f} x {BASE_WIDTH_MM:.3f} mm, " - f"assembly_z_span={BASE_TARGET_WORLD_Z_MIN_MM:.3f}..{BASE_TARGET_WORLD_Z_MAX_MM:.3f} mm, " - f"shell_half_depth={SHELL_HALF_DEPTH_MM:.3f} mm, " - f"outer_corner_fillet={OUTER_CORNER_FILLET_MM:.3f} mm, " - f"dovetail_role={dovetail_role}" - ) - print( - "Straight insertion depth " - f"{TONGUE_INSERTION_DEPTH_MM:.3f} mm from sheet_thickness={SHEET_THICKNESS_MM:.4f} mm " - f"+ extra_depth={EXTRA_THROUGH_DEPTH_MM:.3f} mm; " - f"top section offset=({TOP_SECTION_OFFSET_X_MM:.3f}, {TOP_SECTION_OFFSET_Y_MM:.3f}) mm; " - f"seam rails={WRAP_RAIL_WIDTH_MM:.3f} mm wide with " - f"{len(DOVETAIL_Y_CENTERS_MM)} continuous dovetail slide rails, " - f"neck={DOVETAIL_NECK_WIDTH_MM:.3f} mm, " - f"head={DOVETAIL_HEAD_WIDTH_MM:.3f} mm, " - f"engagement={DOVETAIL_ENGAGEMENT_DEPTH_MM:.3f} mm, " - f"clearance={DOVETAIL_CLEARANCE_MM:.3f} mm" - ) - return sample - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.step b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.step deleted file mode 100644 index 61f9ab69..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:45bc32c736b292ca1a2c9c6ace411b605ff857af677d16f53883b3040cbe870b -size 140214 diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.stl b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.stl deleted file mode 100644 index d3a3bc97..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d3bacf52f76202cc5f21116d17706c8de8b7346da1cdec84ee0683b7b78f669a -size 382684 diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.py b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.py deleted file mode 100644 index 54cfb93e..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -"""Matching female dovetail half for the printable straight tongue shell.""" - -from __future__ import annotations - -import sys -from pathlib import Path - -PRINTABLE_DIR = Path(__file__).resolve().parent -if str(PRINTABLE_DIR) not in sys.path: - sys.path.insert(0, str(PRINTABLE_DIR)) - -import link_bracket_slot_pair_straight_sample as connector_shell - - -PART_NAME = Path(__file__).stem - - -def build_step(): - shell = connector_shell.build_step(dovetail_role="female") - shell.label = PART_NAME - shell.color = connector_shell.PLASTIC_COLOR - return shell - - -def gen_step() -> dict[str, object]: - return { - "shape": build_step(), - } - - -if __name__ == "__main__": - gen_step() diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.step b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.step deleted file mode 100644 index 6ac94e42..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f599f93afc7f6f07d38bb8d4ce4218cc97cc5d4adadf459f2dd66e38ffc9c19 -size 141560 diff --git a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.stl b/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.stl deleted file mode 100644 index 51fd4053..00000000 --- a/models/robots/tom/v2/printable/link_bracket_slot_pair_straight_socket_sample.stl +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6bccfd39a27e7dbe55f4facdd542ecfa3d25ad97f4f8e4939cfcf837f3b31276 -size 382684 diff --git a/models/robots/tom/v2/srdf_planning_ledger.md b/models/robots/tom/v2/srdf_planning_ledger.md deleted file mode 100644 index 1737b9d6..00000000 --- a/models/robots/tom/v2/srdf_planning_ledger.md +++ /dev/null @@ -1,76 +0,0 @@ -# TOM v2 SRDF planning ledger - -## URDF dependency - -| Field | tom | tom_double | tom_with_gripper | -|---|---|---|---| -| URDF path | `tom.urdf` | `tom_double.urdf` | `tom_with_gripper.urdf` | -| SRDF output path | `tom.srdf` | `tom_double.srdf` | `tom_with_gripper.srdf` | -| Robot name | `tom_v2` | `tom_v2_double` | `tom_v2_with_gripper` | -| Root link | `base_footprint` | `base_footprint` | `base_footprint` | -| Active arm joints | `base_yaw`, `shoulder_pitch`, `shoulder_roll`, `elbow_pitch`, `elbow_roll`, `wrist_pitch`, `wrist_roll`; v2 rebases the named home pose at `elbow_pitch=-90 deg` | same, with double STS3250 effort on `shoulder_pitch` in URDF | same | -| Fixed joints | `base_footprint_to_base_link` | `base_footprint_to_base_link` | `base_footprint_to_base_link`, `wrist_roll_to_gripper_base` | -| Mimic joints | none | none | `gripper_right_claw_slide`, `gripper_left_claw_slide` mimic `gripper_servo` | -| Passive joints | none | none | none | -| Known URDF limitations | Arm collision geometry is intentionally omitted pending simplified collision models. | same | Gripper collision meshes are present; arm collision geometry is intentionally omitted. | - -## Planning task - -| Field | Value | -|---|---| -| Main task | Fixed-base arm plan-to-pose / named-state review, plus gripper states for `tom_with_gripper`. | -| Primary planning group | `arm` | -| Expected end-effector or TCP | `wrist_roll_link` for no-gripper variants; `gripper_base_link` via `gripper_tcp` for `tom_with_gripper`. | -| Required solver or planner | Not specified; MoveIt setup remains downstream. | -| Position-only IK | Not specified. | -| Orientation constraints | Not specified. | - -## Virtual joints - -| Name | Type | Parent frame | Child link | Required? | Rationale | -|---|---|---|---|---|---| -| `fixed_base` | fixed | `world` | `base_footprint` | yes | Matches v1 fixed-base semantics and gives MoveIt an explicit world attachment. | - -## Planning groups - -| Group | Representation | Members | Base link | Tip link | Purpose | -|---|---|---|---|---|---| -| `arm` | chain | `base_link` to `wrist_roll_link` | `base_link` | `wrist_roll_link` | Main serial manipulator group. | -| `tcp` | link | `gripper_base_link` | n/a | n/a | TCP/end-effector target group for `tom_with_gripper` only. | -| `gripper` | joint | `gripper_servo` | n/a | n/a | Active gripper actuator for `tom_with_gripper` only. | -| `arm_with_gripper` | subgroups | `arm`, `gripper` | n/a | n/a | Convenience group for `tom_with_gripper` only. | - -## End effectors - -| Name | End-effector group | Parent group | Parent link | Target/TCP link | Notes | -|---|---|---|---|---|---| -| `gripper_tcp` | `tcp` | `arm` | `wrist_roll_link` | `gripper_base_link` | Used only by `tom_with_gripper`; `wrist_roll_to_gripper_base` is the adjacent fixed attachment. | - -## Group states - -| State | Group | Unit check | Limit check | Purpose | -|---|---|---|---|---| -| `home` | `arm` | revolute/continuous values generated in radians; `elbow_pitch=-90 deg` | within URDF limits | V2 calibrated home pose. | -| `reach_forward` | `arm` | radians; explicit v2 review pose uses `base_yaw=0 deg`, `shoulder_pitch=90 deg`, `shoulder_roll=0 deg`, `elbow_pitch=0 deg`, `elbow_roll=0 deg`, `wrist_pitch=0 deg`, `wrist_roll=0 deg` | within URDF limits | Forward reach review pose selected from viewer review. | -| `inspection` | `arm` | radians; explicit v2 review pose uses `base_yaw=45 deg`, `shoulder_pitch=-27 deg`, `shoulder_roll=66 deg`, `elbow_pitch=-87 deg`, `elbow_roll=121.995 deg`, `wrist_pitch=83 deg`, `wrist_roll=-99 deg` | within URDF limits | Angled inspection pose selected from viewer review. | -| `inspection_mirrored` | `arm` | radians; mirrors the inspection roll joints to `shoulder_roll=-66 deg`, `elbow_roll=-121.995 deg`, `wrist_roll=99 deg` while keeping the other inspection values | within URDF limits | Mirrored inspection pose using the same roll-sign convention as v1. | -| `open` | `gripper` | radians | within URDF limits | Gripper open state for `tom_with_gripper`. | -| `half_closed` | `gripper` | radians | within URDF limits | Gripper mid travel state for `tom_with_gripper`. | -| `closed` | `gripper` | radians | within URDF limits | Gripper closed state for `tom_with_gripper`. | - -## Disabled collisions - -Disabled collisions are generated from the linked URDF kinematic adjacency list only, excluding the world/base-footprint fixed joint. No sampled self-collision matrix is available yet. - -## MoveIt smoke tests - -MoveIt Setup Assistant and runtime IK/path planning were not run in this change. The generated SRDFs are validated by the SRDF generator against their linked URDFs and handed to CAD Viewer for review links. - -## Assumptions to report - -- The v2 arm planning group intentionally mirrors the v1 `arm` chain from `base_link` to `wrist_roll_link`. -- `home` keeps the v2-only `elbow_pitch=-90 deg` home offset. -- `reach_forward` is an explicit v2 viewer-selected pose with `elbow_roll=0 deg`. -- `inspection` is an explicit v2 viewer-selected pose, and `inspection_mirrored` flips the roll-joint signs from that pose. -- Collision disables are adjacency-only; no broad manual or sampled-safe collision matrix was added. -- `tom_with_gripper` uses `gripper_base_link` as the TCP target link, matching v1 semantics. diff --git a/models/robots/tom/v2/tom.py b/models/robots/tom/v2/tom.py deleted file mode 100644 index f5e19f63..00000000 --- a/models/robots/tom/v2/tom.py +++ /dev/null @@ -1,1731 +0,0 @@ -from __future__ import annotations - -from functools import lru_cache -import importlib.util -import math -import sys -import xml.etree.ElementTree as ET -from pathlib import Path - -V2_DIR = Path(__file__).resolve().parent -TOM_DIR = V2_DIR.parent -ASSEMBLIES_DIR = V2_DIR / "assemblies" -PARTS_DIR = V2_DIR / "parts" -for path in (TOM_DIR, V2_DIR, PARTS_DIR, ASSEMBLIES_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -from robot_common import robot_arm -from robot_common.materials import BLACK_ALUMINUM_RGBA, GRAY_ALUMINUM_RGBA -import link_bracket -import servo_end_mount -import servo_horn_yoke -from v2_roll_link_common import ( - invert_rigid_transform, - multiply_transforms, - roll_link_mates, - roll_link_instances, -) - - -LINK_ASSEMBLY_MODULES = { - "base_link": ASSEMBLIES_DIR / "base_link.py", - "shoulder_yaw_link": ASSEMBLIES_DIR / "shoulder_yaw_link.py", - "shoulder_pitch_link": ASSEMBLIES_DIR / "pitch_link_sts3250.py", - "elbow_pitch_link": ASSEMBLIES_DIR / "pitch_link_sts3215.py", - "wrist_pitch_link": ASSEMBLIES_DIR / "pitch_link_sts3215.py", -} - -ROLL_LINK_REPLACEMENTS = { - "shoulder_roll_link": "shoulder", - "elbow_roll_link": "elbow", -} - -GRIPPER_CHILD_NAME = "parallel_gripper" -V2_GRIPPER_SERVO_INSTANCE_NAME = "wrist_pitch_link__sts3215" - -NEXT_PITCH_LINK_BY_ROLL_LINK = { - "shoulder": "elbow_pitch_link", - "elbow": "wrist_pitch_link", -} - -YOKE_HORN_SPAN_CENTER_LOCAL_Y_MM = -9.1 -# Source STEP face datums for the terminal gripper mate: -# - sts3215 #o1.16.f14 is the rear horn face at local y=-27.4. -# - gripper #o1.5.f82 is the mounting face at local z=-52. -STS3215_HORN_AXIS_LOCAL_X_MM = -25.5 -STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM = 9.2 -STS3215_REAR_HORN_FACE_LOCAL_Y_MM = -27.4 -GRIPPER_MOUNT_FACE_CENTER_LOCAL_MM = (0.032382, 0.002077, -52.0) -BASE_TO_SHOULDER_YAW_HORN_CLEARANCE_MM = 0.0 -SERVO_END_MOUNT_FRONT_HORN_FACE_CENTER_LOCAL_MM = ( - servo_end_mount.front_horn_mount_face_center_local_mm() -) -YOKE_180_ABOUT_WEB_AXIS_TRANSFORM = tuple( - servo_horn_yoke.YOKE_180_ABOUT_WEB_AXIS_DESIGN_TRANSFORM -) -PITCH_MODULE_STANDALONE_TO_DESIGN_TRANSFORM = tuple( - servo_horn_yoke.STANDALONE_TO_DESIGN_TRANSFORM -) - -URDF_MATERIALS = { - "aluminum_5052": GRAY_ALUMINUM_RGBA, - "dark_servo": BLACK_ALUMINUM_RGBA, - "silver_aluminum_alloy": (0.78, 0.80, 0.82, 1.0), -} - -URDF_MESH_BY_STEP_BASENAME = { - "base_plate.step": "3MF/base_plate.3mf", - "servo_end_mount.step": "3MF/servo_end_mount.3mf", - "servo_horn_yoke.step": "3MF/servo_horn_yoke.3mf", - "link_bracket_right.step": "3MF/link_bracket_right.3mf", - "link_bracket_left.step": "3MF/link_bracket_left.3mf", - "link_standoff_m3_35.step": "3MF/link_standoff_m3_35.3mf", - "sts3250.step": "3MF/sts3250.3mf", - "sts3215.step": "3MF/sts3215.3mf", -} - -URDF_MATERIAL_BY_STEP_BASENAME = { - "base_plate.step": "aluminum_5052", - "servo_end_mount.step": "aluminum_5052", - "servo_horn_yoke.step": "aluminum_5052", - "link_bracket_right.step": "aluminum_5052", - "link_bracket_left.step": "aluminum_5052", - "link_standoff_m3_35.step": "silver_aluminum_alloy", - "sts3250.step": "dark_servo", - "sts3215.step": "dark_servo", -} - -KGF_CM_TO_NM = robot_arm.KGF_CM_TO_NM -STS3250_MASS_KG = robot_arm.STS3250_MASS_KG -STS3215_MASS_KG = robot_arm.STS3215_MASS_KG -STS3250_STALL_TORQUE_NM = 50.0 * KGF_CM_TO_NM -STS3215_STALL_TORQUE_NM = 30.0 * KGF_CM_TO_NM -STS3250_NO_LOAD_SPEED_RAD_S = math.radians(60.0) / 0.133 -STS3215_NO_LOAD_SPEED_RAD_S = math.radians(60.0) / 0.222 -ALUMINUM_5052_DENSITY_KG_PER_MM3 = robot_arm.ALUMINUM_5052_H32_DENSITY_KG_PER_MM3 -SERVO_MASS_KG_BY_STEP_BASENAME = { - "sts3250.step": STS3250_MASS_KG, - "sts3215.step": STS3215_MASS_KG, -} -MATERIAL_DENSITY_KG_PER_MM3 = { - "aluminum_5052": ALUMINUM_5052_DENSITY_KG_PER_MM3, - "silver_aluminum_alloy": ALUMINUM_5052_DENSITY_KG_PER_MM3, -} -URDF_JOINT_EFFORT_NM_BY_NAME = { - "base_yaw": STS3250_STALL_TORQUE_NM, - "shoulder_pitch": STS3250_STALL_TORQUE_NM, - "shoulder_roll": STS3250_STALL_TORQUE_NM, - "elbow_pitch": STS3250_STALL_TORQUE_NM, - "elbow_roll": STS3215_STALL_TORQUE_NM, - "wrist_pitch": STS3215_STALL_TORQUE_NM, - "wrist_roll": STS3215_STALL_TORQUE_NM, -} -URDF_JOINT_VELOCITY_RAD_S_BY_NAME = { - "base_yaw": STS3250_NO_LOAD_SPEED_RAD_S, - "shoulder_pitch": STS3250_NO_LOAD_SPEED_RAD_S, - "shoulder_roll": STS3250_NO_LOAD_SPEED_RAD_S, - "elbow_pitch": STS3250_NO_LOAD_SPEED_RAD_S, - "elbow_roll": STS3215_NO_LOAD_SPEED_RAD_S, - "wrist_pitch": STS3215_NO_LOAD_SPEED_RAD_S, - "wrist_roll": STS3215_NO_LOAD_SPEED_RAD_S, -} -V2_HOME_ELBOW_PITCH_DEG = -90.0 -V2_SRDF_REACH_ARM_GROUP_STATE_DEG = { - "base_yaw": 0.0, - "shoulder_pitch": 90.0, - "shoulder_roll": 0.0, - "elbow_pitch": 0.0, - "elbow_roll": 0.0, - "wrist_pitch": 0.0, - "wrist_roll": 0.0, -} -V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG = { - "base_yaw": 45.0, - "shoulder_pitch": -27.0, - "shoulder_roll": 66.0, - "elbow_pitch": -87.0, - "elbow_roll": 121.995, - "wrist_pitch": 83.0, - "wrist_roll": -99.0, -} -V2_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG = { - **V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG, - "shoulder_roll": -V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG["shoulder_roll"], - "elbow_roll": -V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG["elbow_roll"], - "wrist_roll": -V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG["wrist_roll"], -} - -NO_GRIPPER_LINK_NAMES = ( - "base_footprint", - "base_link", - "shoulder_yaw_link", - "shoulder_pitch_link", - "shoulder_roll_link", - "elbow_pitch_link", - "elbow_roll_link", - "wrist_pitch_link", - "wrist_roll_link", -) -GRIPPER_LINK_NAMES = ( - "gripper_base_link", - "gripper_servo_link", - "gripper_right_claw_link", - "gripper_left_claw_link", -) -WITH_GRIPPER_LINK_NAMES = (*NO_GRIPPER_LINK_NAMES, *GRIPPER_LINK_NAMES) - -URDF_SERVO_AXIS_INSTANCE_BY_JOINT = { - "base_yaw": "base_link__sts3250_3", - "shoulder_pitch": "shoulder_yaw_link__sts3250_1", - "shoulder_roll": "shoulder_pitch_link__sts3250", - "elbow_pitch": "shoulder_roll_link__sts3250_4", - "elbow_roll": "elbow_pitch_link__sts3215", - "wrist_pitch": "elbow_roll_link__sts3215_6", - "wrist_roll": "wrist_pitch_link__sts3215", -} - -URDF_VISUAL_MODULE_BY_LINK = { - "base_link": LINK_ASSEMBLY_MODULES["base_link"], - "shoulder_yaw_link": LINK_ASSEMBLY_MODULES["shoulder_yaw_link"], - "shoulder_pitch_link": LINK_ASSEMBLY_MODULES["shoulder_pitch_link"], - "elbow_pitch_link": LINK_ASSEMBLY_MODULES["elbow_pitch_link"], - "wrist_pitch_link": LINK_ASSEMBLY_MODULES["wrist_pitch_link"], -} - -URDF_PITCH_MODULE_SERVO_BY_LINK = { - "shoulder_pitch_link": "sts3250", - "elbow_pitch_link": "sts3215", - "wrist_pitch_link": "sts3215", -} - -URDF_ROLL_BOTTOM_SERVO_SOURCE_BY_LINK = { - "shoulder_roll_link": ("shoulder_pitch_link", "sts3250"), - "elbow_roll_link": ("elbow_pitch_link", "sts3215"), - "wrist_roll_link": ("wrist_pitch_link", "sts3215"), -} - - -def _mate( - source_label: str, - *, - fixed: str, - moving: str, - relation: str = "rigid", - parameters: dict[str, object] | None = None, -) -> dict[str, object]: - fixed_part, fixed_frame = fixed.split(":", 1) - moving_part, moving_frame = moving.split(":", 1) - return { - "sourceLabel": source_label, - "type": relation, - "relation": relation, - "fixed": fixed, - "moving": moving, - "parameters": dict(parameters or {}), - "fixedEndpoint": { - "part": fixed_part, - "frame": fixed_frame, - }, - "movingEndpoint": { - "part": moving_part, - "frame": moving_frame, - }, - } - - -def _roll_link_standoff_mates(parent_name: str, kind: str) -> list[dict[str, object]]: - mates: list[dict[str, object]] = [] - for label, _x, _z in link_bracket.STANDOFF_CENTER_XZ_MM: - standoff = f"{parent_name}__{kind}_link_standoff_{label}" - mates.append( - _mate( - f"{parent_name}_{label}_standoff_to_right_bracket", - fixed=f"{parent_name}__{kind}_link_bracket_right:{label}_standoff_hole", - moving=f"{standoff}:positive_y_thread", - ) - ) - mates.append( - _mate( - f"{parent_name}_{label}_standoff_to_left_bracket", - fixed=f"{parent_name}__{kind}_link_bracket_left:{label}_standoff_hole", - moving=f"{standoff}:negative_y_thread", - ) - ) - return mates - - -def _assembly_mates(*, include_gripper: bool) -> list[dict[str, object]]: - mates = [ - _mate( - "base_servo_to_base_plate", - fixed="base_link__base_plate:top_plate", - moving="base_link__sts3250_3:case_mount", - ), - _mate( - "base_servo_horn_to_shoulder_yaw_mount", - fixed="base_link__sts3250_3:output_horn_face", - moving="shoulder_yaw_link__servo_end_mount:front_horn_mount_face", - parameters={"clearance_mm": BASE_TO_SHOULDER_YAW_HORN_CLEARANCE_MM}, - ), - _mate( - "shoulder_yaw_mount_to_servo", - fixed="shoulder_yaw_link__sts3250_1:rear_case", - moving="shoulder_yaw_link__servo_end_mount:servo_face", - ), - _mate( - "shoulder_pitch_yoke_to_servo", - fixed="shoulder_pitch_link__servo_horn_yoke:horn_axis", - moving="shoulder_pitch_link__sts3250:horn_axis", - ), - _mate( - "shoulder_pitch_servo_to_roll_bracket_right", - fixed="shoulder_pitch_link__sts3250:upstream_case", - moving="shoulder_roll_link__shoulder_link_bracket_right:bottom_servo_mount", - ), - _mate( - "shoulder_pitch_servo_to_roll_bracket_left", - fixed="shoulder_pitch_link__sts3250:upstream_case", - moving="shoulder_roll_link__shoulder_link_bracket_left:bottom_servo_mount", - ), - _mate( - "shoulder_roll_bracket_right_to_servo", - fixed="shoulder_roll_link__shoulder_link_bracket_right:top_servo_mount", - moving="shoulder_roll_link__sts3250_4:case_mount", - ), - _mate( - "shoulder_roll_bracket_left_to_servo", - fixed="shoulder_roll_link__shoulder_link_bracket_left:top_servo_mount", - moving="shoulder_roll_link__sts3250_4:case_mount", - ), - _mate( - "elbow_pitch_yoke_to_shoulder_roll_servo", - fixed="shoulder_roll_link__sts3250_4:horn_axis", - moving="elbow_pitch_link__servo_horn_yoke:horn_axis", - ), - _mate( - "elbow_pitch_servo_to_roll_bracket_right", - fixed="elbow_pitch_link__sts3215:upstream_case", - moving="elbow_roll_link__elbow_link_bracket_right:bottom_servo_mount", - ), - _mate( - "elbow_pitch_servo_to_roll_bracket_left", - fixed="elbow_pitch_link__sts3215:upstream_case", - moving="elbow_roll_link__elbow_link_bracket_left:bottom_servo_mount", - ), - _mate( - "elbow_roll_bracket_right_to_servo", - fixed="elbow_roll_link__elbow_link_bracket_right:top_servo_mount", - moving="elbow_roll_link__sts3215_6:case_mount", - ), - _mate( - "elbow_roll_bracket_left_to_servo", - fixed="elbow_roll_link__elbow_link_bracket_left:top_servo_mount", - moving="elbow_roll_link__sts3215_6:case_mount", - ), - _mate( - "wrist_pitch_yoke_to_elbow_roll_servo", - fixed="elbow_roll_link__sts3215_6:horn_axis", - moving="wrist_pitch_link__servo_horn_yoke:horn_axis", - ), - ] - if include_gripper: - mates.append( - _mate( - "gripper_mount_face_to_wrist_servo_horn", - fixed="wrist_pitch_link__sts3215:rear_horn_face", - moving="parallel_gripper:mount_face", - ) - ) - mates.extend(_roll_link_standoff_mates("shoulder_roll_link", "shoulder")) - mates.extend(_roll_link_standoff_mates("elbow_roll_link", "elbow")) - return mates - - -def _module_gen_step(module_path: Path) -> dict[str, object]: - module_name = f"_tom_v2_{module_path.stem}_{abs(hash(module_path))}" - spec = importlib.util.spec_from_file_location(module_name, module_path) - if spec is None or spec.loader is None: - raise RuntimeError(f"Unable to load {module_path}") - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - gen_step = getattr(module, "gen_step", None) - if not callable(gen_step): - raise RuntimeError(f"{module_path} does not define gen_step()") - envelope = gen_step() - if not isinstance(envelope, dict) or "instances" not in envelope: - raise RuntimeError(f"{module_path} must return an instances assembly") - instances = envelope["instances"] - if not isinstance(instances, list) or not instances: - raise RuntimeError(f"{module_path} returned no instances") - return envelope - - -def _instances_from_module(module_path: Path) -> list[dict[str, object]]: - return [ - dict(instance) - for instance in _module_gen_step(module_path)["instances"] # type: ignore[index] - ] - - -def _rebase_step_path(path: str, *, source_dir: Path) -> str: - resolved = (source_dir / path).resolve() - try: - return resolved.relative_to(V2_DIR.resolve()).as_posix() - except ValueError as exc: - raise RuntimeError(f"{path} resolves outside the v2 assembly folder") from exc - - -def _v2_source_child_path(path: str) -> str: - source_path = Path(path) - if source_path.parts and source_path.parts[0] in {"imports", "gripper"}: - return (Path("parts") / source_path).as_posix() - return source_path.as_posix() - - -def _flatten_instances( - *, - parent_name: str, - parent_transform: list[float], - local_instances: list[dict[str, object]], - local_source_dir: Path, - instance_transform_overrides: dict[str, list[float]] | None = None, -) -> list[dict[str, object]]: - flattened: list[dict[str, object]] = [] - transform_overrides = instance_transform_overrides or {} - for local in local_instances: - local_name = str(local["name"]) - local_transform = [float(value) for value in local["transform"]] - world_transform = transform_overrides.get(local_name) - if world_transform is None: - world_transform = multiply_transforms(parent_transform, local_transform) - flattened.append( - { - "path": _rebase_step_path(str(local["path"]), source_dir=local_source_dir), - "name": f"{parent_name}__{local_name}", - "transform": world_transform, - "use_source_colors": bool(local.get("use_source_colors", True)), - } - ) - return flattened - - -def _flat_child_instances( - *, - child_name: str, - child_transform: list[float], - instance_transform_overrides: dict[str, list[float]] | None = None, -) -> list[dict[str, object]]: - replacement_kind = ROLL_LINK_REPLACEMENTS.get(child_name) - if replacement_kind is not None: - return _flatten_instances( - parent_name=child_name, - parent_transform=child_transform, - local_instances=roll_link_instances(replacement_kind), - local_source_dir=ASSEMBLIES_DIR, - instance_transform_overrides=instance_transform_overrides, - ) - - module_path = LINK_ASSEMBLY_MODULES[child_name] - return _flatten_instances( - parent_name=child_name, - parent_transform=child_transform, - local_instances=_instances_from_module(module_path), - local_source_dir=module_path.parent, - instance_transform_overrides=instance_transform_overrides, - ) - - -def _module_transform_for_child( - *, - child_name: str, - design_child_transform: list[float], -) -> list[float]: - if child_name in URDF_PITCH_MODULE_SERVO_BY_LINK: - return list( - multiply_transforms( - design_child_transform, - PITCH_MODULE_STANDALONE_TO_DESIGN_TRANSFORM, - ) - ) - return design_child_transform - - -def _yoke_transform_for_servo_horn( - *, - upstream_servo_transform: list[float], -) -> list[float]: - return multiply_transforms( - upstream_servo_transform, - YOKE_180_ABOUT_WEB_AXIS_TRANSFORM, - ) - - -def _identity_transform() -> list[float]: - return [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1.0, - ] - - -def _transform_point( - transform: list[float], - point: tuple[float, float, float], -) -> tuple[float, float, float]: - x, y, z = point - return ( - transform[0] * x + transform[1] * y + transform[2] * z + transform[3], - transform[4] * x + transform[5] * y + transform[6] * z + transform[7], - transform[8] * x + transform[9] * y + transform[10] * z + transform[11], - ) - - -def _transform_direction( - transform: list[float], - direction: tuple[float, float, float], -) -> tuple[float, float, float]: - x, y, z = direction - return ( - transform[0] * x + transform[1] * y + transform[2] * z, - transform[4] * x + transform[5] * y + transform[6] * z, - transform[8] * x + transform[9] * y + transform[10] * z, - ) - - -def _axis_angle_transform( - *, - axis_point_mm: tuple[float, float, float], - axis_direction: tuple[float, float, float], - angle_deg: float, -) -> list[float]: - ux, uy, uz = _normalized_vector(axis_direction) - theta = math.radians(angle_deg) - c = math.cos(theta) - s = math.sin(theta) - one_minus_c = 1.0 - c - rotation = ( - ( - c + ux * ux * one_minus_c, - ux * uy * one_minus_c - uz * s, - ux * uz * one_minus_c + uy * s, - ), - ( - uy * ux * one_minus_c + uz * s, - c + uy * uy * one_minus_c, - uy * uz * one_minus_c - ux * s, - ), - ( - uz * ux * one_minus_c - uy * s, - uz * uy * one_minus_c + ux * s, - c + uz * uz * one_minus_c, - ), - ) - px, py, pz = axis_point_mm - rotated_point = ( - rotation[0][0] * px + rotation[0][1] * py + rotation[0][2] * pz, - rotation[1][0] * px + rotation[1][1] * py + rotation[1][2] * pz, - rotation[2][0] * px + rotation[2][1] * py + rotation[2][2] * pz, - ) - translation = ( - px - rotated_point[0], - py - rotated_point[1], - pz - rotated_point[2], - ) - return [ - rotation[0][0], rotation[0][1], rotation[0][2], translation[0], - rotation[1][0], rotation[1][1], rotation[1][2], translation[1], - rotation[2][0], rotation[2][1], rotation[2][2], translation[2], - 0.0, 0.0, 0.0, 1.0, - ] - - -def _rotate_transform_about_axis( - transform: list[float], - *, - axis_point_mm: tuple[float, float, float], - axis_direction: tuple[float, float, float], - angle_deg: float, -) -> list[float]: - if abs(angle_deg) <= 1e-9: - return list(transform) - return multiply_transforms( - _axis_angle_transform( - axis_point_mm=axis_point_mm, - axis_direction=axis_direction, - angle_deg=angle_deg, - ), - transform, - ) - - -def _apply_v2_step_home_pose_to_elbow_pitch( - *, - child_transform: list[float], - instance_transforms_by_name: dict[str, list[float]], -) -> list[float]: - elbow_pitch_servo = instance_transforms_by_name.get( - URDF_SERVO_AXIS_INSTANCE_BY_JOINT["elbow_pitch"] - ) - if elbow_pitch_servo is None: - raise RuntimeError("Cannot apply v2 STEP elbow home pose before elbow servo exists") - axis_center_world, axis_direction_world = _servo_horn_axis_from_transform( - elbow_pitch_servo - ) - return _rotate_transform_about_axis( - child_transform, - axis_point_mm=axis_center_world, - axis_direction=axis_direction_world, - angle_deg=V2_HOME_ELBOW_PITCH_DEG, - ) - - -def _translate_transform( - transform: list[float], - delta: tuple[float, float, float], -) -> list[float]: - adjusted = list(transform) - adjusted[3] += delta[0] - adjusted[7] += delta[1] - adjusted[11] += delta[2] - return adjusted - - -def _mate_shoulder_yaw_mount_to_base_servo_horn( - *, - shoulder_yaw_transform: list[float], - base_servo_transform: list[float], -) -> list[float]: - base_horn_face_center = _transform_point( - base_servo_transform, - ( - STS3215_HORN_AXIS_LOCAL_X_MM, - STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM, - 0.0, - ), - ) - base_horn_face_normal = _transform_direction(base_servo_transform, (0.0, 1.0, 0.0)) - target = tuple( - base_horn_face_center[index] - + (BASE_TO_SHOULDER_YAW_HORN_CLEARANCE_MM * base_horn_face_normal[index]) - for index in range(3) - ) - moving = _transform_point( - shoulder_yaw_transform, - SERVO_END_MOUNT_FRONT_HORN_FACE_CENTER_LOCAL_MM, - ) - return _translate_transform( - shoulder_yaw_transform, - ( - target[0] - moving[0], - target[1] - moving[1], - target[2] - moving[2], - ), - ) - - -def _mate_gripper_to_terminal_servo_horn( - *, - gripper_transform: list[float], - terminal_servo_transform: list[float], -) -> list[float]: - target = _transform_point( - terminal_servo_transform, - ( - STS3215_HORN_AXIS_LOCAL_X_MM, - STS3215_REAR_HORN_FACE_LOCAL_Y_MM, - 0.0, - ), - ) - moving = _transform_point(gripper_transform, GRIPPER_MOUNT_FACE_CENTER_LOCAL_MM) - return _translate_transform( - gripper_transform, - ( - target[0] - moving[0], - target[1] - moving[1], - target[2] - moving[2], - ), - ) - - -def gen_step_with_options(*, include_gripper: bool = False) -> dict[str, object]: - instances: list[dict[str, object]] = [] - instance_transforms_by_name: dict[str, list[float]] = {} - downstream_correction = _identity_transform() - pending_child_transform_overrides: dict[str, list[float]] = {} - - for source_child in robot_arm.robot_arm_assembly_children(): - child_name = str(source_child["name"]) - source_child_transform = [float(value) for value in source_child["transform"]] - child_transform = pending_child_transform_overrides.pop(child_name, None) - if child_transform is not None: - downstream_correction = multiply_transforms( - child_transform, - invert_rigid_transform(source_child_transform), - ) - else: - child_transform = multiply_transforms( - downstream_correction, - source_child_transform, - ) - - if child_name == "shoulder_yaw_link": - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is not None: - child_transform = _mate_shoulder_yaw_mount_to_base_servo_horn( - shoulder_yaw_transform=child_transform, - base_servo_transform=base_servo_transform, - ) - downstream_correction = multiply_transforms( - child_transform, - invert_rigid_transform(source_child_transform), - ) - - if child_name == "elbow_pitch_link": - child_transform = _apply_v2_step_home_pose_to_elbow_pitch( - child_transform=child_transform, - instance_transforms_by_name=instance_transforms_by_name, - ) - downstream_correction = multiply_transforms( - child_transform, - invert_rigid_transform(source_child_transform), - ) - - if child_name == GRIPPER_CHILD_NAME: - if not include_gripper: - continue - terminal_servo_transform = instance_transforms_by_name.get( - V2_GRIPPER_SERVO_INSTANCE_NAME - ) - if terminal_servo_transform is not None: - child_transform = _mate_gripper_to_terminal_servo_horn( - gripper_transform=child_transform, - terminal_servo_transform=terminal_servo_transform, - ) - instances.append( - { - "path": _v2_source_child_path(str(source_child["path"])), - "name": child_name, - "transform": child_transform, - "use_source_colors": bool(source_child.get("use_source_colors", True)), - } - ) - instance_transforms_by_name[child_name] = child_transform - continue - - module_child_transform = _module_transform_for_child( - child_name=child_name, - design_child_transform=child_transform, - ) - child_instances = _flat_child_instances( - child_name=child_name, - child_transform=module_child_transform, - ) - instances.extend(child_instances) - for instance in child_instances: - instance_transforms_by_name[str(instance["name"])] = [ - float(value) for value in instance["transform"] - ] - - replacement_kind = ROLL_LINK_REPLACEMENTS.get(child_name) - if replacement_kind is not None: - mates = roll_link_mates(replacement_kind) - next_pitch_child_name = NEXT_PITCH_LINK_BY_ROLL_LINK.get(replacement_kind) - if next_pitch_child_name is not None: - upstream_servo_transform = multiply_transforms( - child_transform, - mates.downstream_servo_local, - ) - pending_child_transform_overrides[next_pitch_child_name] = ( - _yoke_transform_for_servo_horn( - upstream_servo_transform=upstream_servo_transform, - ) - ) - - return { - "instances": instances, - "assembly_mates": _assembly_mates(include_gripper=include_gripper), - } - - -def gen_step() -> dict[str, object]: - envelope = gen_step_with_options(include_gripper=False) - return { - "instances": envelope["instances"], - "assembly_mates": envelope.get("assembly_mates", []), - } - - -def _xml( - tag: str, - attrs: dict[str, str] | None = None, - children: tuple[ET.Element, ...] = (), -) -> ET.Element: - element = ET.Element(tag, attrs or {}) - for child in children: - element.append(child) - return element - - -def _serialize_urdf(root: ET.Element) -> str: - ET.indent(root, space=" ") - return '\n' + ET.tostring(root, encoding="unicode") - - -def _urdf_number(value: float) -> str: - numeric = 0.0 if abs(value) < 5e-10 else float(value) - text = f"{numeric:.9f}".rstrip("0").rstrip(".") - return text if text and text != "-0" else "0" - - -def _rgba_text(rgba: tuple[float, float, float, float]) -> str: - return " ".join(_urdf_number(float(value)) for value in rgba) - - -def _rpy_from_transform(transform: list[float] | tuple[float, ...]) -> tuple[float, float, float]: - r00 = float(transform[0]) - r10 = float(transform[4]) - r20 = float(transform[8]) - r21 = float(transform[9]) - r22 = float(transform[10]) - r01 = float(transform[1]) - r11 = float(transform[5]) - - pitch = math.asin(max(-1.0, min(1.0, -r20))) - cp = math.cos(pitch) - if abs(cp) > 1e-9: - roll = math.atan2(r21, r22) - yaw = math.atan2(r10, r00) - else: - roll = 0.0 - yaw = math.atan2(-r01, r11) - return roll, pitch, yaw - - -def _urdf_origin_attrs_from_transform( - transform: list[float] | tuple[float, ...], -) -> dict[str, str]: - xyz = ( - float(transform[3]) * 0.001, - float(transform[7]) * 0.001, - float(transform[11]) * 0.001, - ) - rpy = _rpy_from_transform(transform) - return { - "xyz": " ".join(_urdf_number(value) for value in xyz), - "rpy": " ".join(_urdf_number(value) for value in rpy), - } - - -def _transform_from_urdf_origin_attrs(xyz: str, rpy: str) -> list[float]: - x, y, z = (float(value) * 1000.0 for value in xyz.split()) - roll, pitch, yaw = (float(value) for value in rpy.split()) - sr = math.sin(roll) - cr = math.cos(roll) - sp = math.sin(pitch) - cp = math.cos(pitch) - sy = math.sin(yaw) - cy = math.cos(yaw) - return [ - cy * cp, - cy * sp * sr - sy * cr, - cy * sp * cr + sy * sr, - x, - sy * cp, - sy * sp * sr + cy * cr, - sy * sp * cr - cy * sr, - y, - -sp, - cp * sr, - cp * cr, - z, - 0.0, - 0.0, - 0.0, - 1.0, - ] - - -def _world_vector_to_local( - frame_transform: list[float] | tuple[float, ...], - vector: tuple[float, float, float], -) -> tuple[float, float, float]: - x, y, z = vector - return ( - frame_transform[0] * x + frame_transform[4] * y + frame_transform[8] * z, - frame_transform[1] * x + frame_transform[5] * y + frame_transform[9] * z, - frame_transform[2] * x + frame_transform[6] * y + frame_transform[10] * z, - ) - - -def _normalized_vector(vector: tuple[float, float, float]) -> tuple[float, float, float]: - length = math.sqrt(sum(component * component for component in vector)) - if length <= 1e-9: - raise RuntimeError("Cannot normalize a zero-length vector") - return tuple(component / length for component in vector) # type: ignore[return-value] - - -def _axis_attrs(vector: tuple[float, float, float]) -> str: - axis = _normalized_vector(vector) - return " ".join(_urdf_number(value) for value in axis) - - -def _with_translation( - transform: list[float] | tuple[float, ...], - point: tuple[float, float, float], -) -> list[float]: - adjusted = [float(value) for value in transform] - adjusted[3], adjusted[7], adjusted[11] = point - return adjusted - - -def _servo_horn_axis_from_transform( - transform: list[float] | tuple[float, ...], -) -> tuple[tuple[float, float, float], tuple[float, float, float]]: - rear = _transform_point( - list(transform), - ( - STS3215_HORN_AXIS_LOCAL_X_MM, - STS3215_REAR_HORN_FACE_LOCAL_Y_MM, - 0.0, - ), - ) - front = _transform_point( - list(transform), - ( - STS3215_HORN_AXIS_LOCAL_X_MM, - STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM, - 0.0, - ), - ) - center = tuple(0.5 * (rear[index] + front[index]) for index in range(3)) - direction = _normalized_vector( - tuple(front[index] - rear[index] for index in range(3)) - ) - return center, direction - - -def _zero_pose_source_frames_and_instances( - *, - include_gripper: bool = False, -) -> tuple[dict[str, list[float]], dict[str, list[float]]]: - frames: dict[str, list[float]] = {"base_footprint": _identity_transform()} - instance_transforms_by_name: dict[str, list[float]] = {} - downstream_correction = _identity_transform() - pending_child_transform_overrides: dict[str, list[float]] = {} - - for source_child in robot_arm.robot_arm_assembly_children(): - child_name = str(source_child["name"]) - source_child_transform = [float(value) for value in source_child["transform"]] - child_transform = pending_child_transform_overrides.pop(child_name, None) - if child_transform is not None: - downstream_correction = multiply_transforms( - child_transform, - invert_rigid_transform(source_child_transform), - ) - else: - child_transform = multiply_transforms( - downstream_correction, - source_child_transform, - ) - - if child_name == "shoulder_yaw_link": - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is not None: - child_transform = _mate_shoulder_yaw_mount_to_base_servo_horn( - shoulder_yaw_transform=child_transform, - base_servo_transform=base_servo_transform, - ) - downstream_correction = multiply_transforms( - child_transform, - invert_rigid_transform(source_child_transform), - ) - - if child_name == GRIPPER_CHILD_NAME: - if include_gripper: - frames[child_name] = child_transform - continue - - module_child_transform = _module_transform_for_child( - child_name=child_name, - design_child_transform=child_transform, - ) - frames[child_name] = module_child_transform - child_instances = _flat_child_instances( - child_name=child_name, - child_transform=module_child_transform, - ) - for instance in child_instances: - instance_transforms_by_name[str(instance["name"])] = [ - float(value) for value in instance["transform"] - ] - - replacement_kind = ROLL_LINK_REPLACEMENTS.get(child_name) - if replacement_kind is not None: - mates = roll_link_mates(replacement_kind) - next_pitch_child_name = NEXT_PITCH_LINK_BY_ROLL_LINK.get(replacement_kind) - if next_pitch_child_name is not None: - upstream_servo_transform = multiply_transforms( - child_transform, - mates.downstream_servo_local, - ) - pending_child_transform_overrides[next_pitch_child_name] = ( - _yoke_transform_for_servo_horn( - upstream_servo_transform=upstream_servo_transform, - ) - ) - - wrist_pitch = frames.get("wrist_pitch_link") - if wrist_pitch is not None and "wrist_roll_link" not in frames: - wrist_roll_joint = next( - joint - for joint in robot_arm.URDF_JOINTS - if str(joint["name"]) == "wrist_roll" - ) - frames["wrist_roll_link"] = multiply_transforms( - wrist_pitch, - _transform_from_urdf_origin_attrs( - str(wrist_roll_joint["origin_xyz"]), - str(wrist_roll_joint["origin_rpy"]), - ), - ) - return frames, instance_transforms_by_name - - -def _urdf_link_frames_and_axes( - *, - include_gripper: bool = False, -) -> tuple[ - dict[str, list[float]], - dict[str, list[float]], - dict[str, str], -]: - source_frames, instance_transforms = _zero_pose_source_frames_and_instances( - include_gripper=include_gripper, - ) - if include_gripper and GRIPPER_CHILD_NAME in source_frames: - source_frames["gripper_base_link"] = list(source_frames[GRIPPER_CHILD_NAME]) - urdf_frames = {name: list(transform) for name, transform in source_frames.items()} - axes_by_joint: dict[str, str] = {} - - for joint in _no_gripper_joints(): - joint_name = str(joint["name"]) - servo_instance_name = URDF_SERVO_AXIS_INSTANCE_BY_JOINT.get(joint_name) - if not servo_instance_name: - continue - servo_transform = instance_transforms.get(servo_instance_name) - if servo_transform is None: - raise RuntimeError( - f"Missing servo instance {servo_instance_name!r} for URDF joint {joint_name!r}" - ) - - axis_center_world, axis_direction_world = _servo_horn_axis_from_transform( - servo_transform, - ) - child_link = str(joint["child"]) - if child_link not in urdf_frames: - parent_frame = urdf_frames.get(str(joint["parent"])) - if parent_frame is None: - raise RuntimeError(f"Missing parent frame for URDF joint {joint_name!r}") - urdf_frames[child_link] = list(parent_frame) - urdf_frames[child_link] = _with_translation( - urdf_frames[child_link], - axis_center_world, - ) - axis_in_joint_frame = _world_vector_to_local( - urdf_frames[child_link], - axis_direction_world, - ) - axes_by_joint[joint_name] = _axis_attrs(axis_in_joint_frame) - - return source_frames, urdf_frames, axes_by_joint - - -def _instances_for_urdf_link(link_name: str) -> list[dict[str, object]]: - instances: list[dict[str, object]] = [] - bottom_servo_source = URDF_ROLL_BOTTOM_SERVO_SOURCE_BY_LINK.get(link_name) - if bottom_servo_source is not None: - source_link_name, source_instance_name = bottom_servo_source - source_module_path = URDF_VISUAL_MODULE_BY_LINK[source_link_name] - source_instances = _module_gen_step(source_module_path)["instances"] # type: ignore[index] - for instance in source_instances: - if str(instance["name"]) == source_instance_name: - moved_instance = dict(instance) - moved_instance["source_link_name"] = source_link_name - instances.append(moved_instance) - break - else: - raise RuntimeError( - f"Missing bottom roll servo {source_instance_name!r} " - f"in source link {source_link_name!r}" - ) - - if link_name == "shoulder_roll_link": - instances.extend(dict(instance) for instance in roll_link_instances("shoulder")) - return instances - if link_name == "elbow_roll_link": - instances.extend(dict(instance) for instance in roll_link_instances("elbow")) - return instances - if link_name == "wrist_roll_link": - return instances - - module_path = URDF_VISUAL_MODULE_BY_LINK.get(link_name) - if module_path is None: - return instances - skipped_servo_name = URDF_PITCH_MODULE_SERVO_BY_LINK.get(link_name) - return [ - dict(instance) - for instance in _module_gen_step(module_path)["instances"] # type: ignore[index] - if str(instance["name"]) != skipped_servo_name - ] - - -def _mesh_filename_for_step_path(step_path: str) -> str: - basename = Path(step_path).name - mesh = URDF_MESH_BY_STEP_BASENAME.get(basename) - if mesh is None: - raise RuntimeError(f"No v2 URDF 3MF mesh mapping for {step_path!r}") - return mesh - - -def _material_name_for_step_path(step_path: str) -> str: - basename = Path(step_path).name - material = URDF_MATERIAL_BY_STEP_BASENAME.get(basename) - if material is None: - raise RuntimeError(f"No v2 URDF material mapping for {step_path!r}") - return material - - -def _source_dir_for_urdf_instance( - instance: dict[str, object], - *, - link_name: str, -) -> Path: - source_link_name = str(instance.get("source_link_name", link_name)) - module_path = URDF_VISUAL_MODULE_BY_LINK.get(source_link_name) - if module_path is not None: - return module_path.parent - if source_link_name in ROLL_LINK_REPLACEMENTS: - return ASSEMBLIES_DIR - return V2_DIR - - -def _resolved_step_path( - step_path: str, - *, - source_dir: Path, -) -> Path: - path = Path(step_path) - candidates = [ - (source_dir / path).resolve(), - (V2_DIR / path).resolve(), - (PARTS_DIR / path.name).resolve(), - (PARTS_DIR / "imports" / path.name).resolve(), - (PARTS_DIR / "gripper" / path.name).resolve(), - ] - for candidate in candidates: - if candidate.exists(): - return candidate - raise FileNotFoundError(f"Unable to resolve STEP path {step_path!r}") - - -def _build123d_vector_xyz(vector: object) -> tuple[float, float, float]: - return ( - float(getattr(vector, "X")), - float(getattr(vector, "Y")), - float(getattr(vector, "Z")), - ) - - -@lru_cache(maxsize=None) -def _step_geometry_properties(step_file: str) -> dict[str, object]: - from build123d import import_step - - shape = import_step(step_file) - bbox = shape.bounding_box() - return { - "volume_mm3": float(shape.volume), - "center_mm": _build123d_vector_xyz(shape.center()), - "size_mm": _build123d_vector_xyz(bbox.size), - } - - -def _part_mass_properties( - step_path: str, - *, - source_dir: Path, -) -> dict[str, object]: - step_file = _resolved_step_path(step_path, source_dir=source_dir) - basename = step_file.name - geometry = _step_geometry_properties(step_file.as_posix()) - mass_kg = SERVO_MASS_KG_BY_STEP_BASENAME.get(basename) - if mass_kg is None: - material = _material_name_for_step_path(basename) - density = MATERIAL_DENSITY_KG_PER_MM3.get(material) - if density is None: - raise RuntimeError(f"No density for v2 URDF material {material!r}") - mass_kg = float(geometry["volume_mm3"]) * density - return { - "mass_kg": mass_kg, - "center_mm": geometry["center_mm"], - "size_mm": geometry["size_mm"], - } - - -def _mat3_mul(left: list[list[float]], right: list[list[float]]) -> list[list[float]]: - return [ - [ - sum(left[row][inner] * right[inner][col] for inner in range(3)) - for col in range(3) - ] - for row in range(3) - ] - - -def _mat3_transpose(matrix: list[list[float]]) -> list[list[float]]: - return [[matrix[col][row] for col in range(3)] for row in range(3)] - - -def _rotation_matrix(transform: list[float] | tuple[float, ...]) -> list[list[float]]: - return [ - [float(transform[0]), float(transform[1]), float(transform[2])], - [float(transform[4]), float(transform[5]), float(transform[6])], - [float(transform[8]), float(transform[9]), float(transform[10])], - ] - - -def _rotated_box_inertia_kg_m2( - *, - mass_kg: float, - size_mm: tuple[float, float, float], - transform: list[float], -) -> list[list[float]]: - sx, sy, sz = (dimension * 0.001 for dimension in size_mm) - local = [ - [mass_kg * (sy * sy + sz * sz) / 12.0, 0.0, 0.0], - [0.0, mass_kg * (sx * sx + sz * sz) / 12.0, 0.0], - [0.0, 0.0, mass_kg * (sx * sx + sy * sy) / 12.0], - ] - rotation = _rotation_matrix(transform) - return _mat3_mul(_mat3_mul(rotation, local), _mat3_transpose(rotation)) - - -def _add_parallel_axis( - inertia: list[list[float]], - *, - mass_kg: float, - offset_m: tuple[float, float, float], -) -> None: - x, y, z = offset_m - distance_squared = x * x + y * y + z * z - offsets = (x, y, z) - for row in range(3): - for col in range(3): - inertia[row][col] += mass_kg * ( - (distance_squared if row == col else 0.0) - - offsets[row] * offsets[col] - ) - - -def _inertial_element_for_instances( - instances: list[dict[str, object]], - *, - link_name: str, - source_frames: dict[str, list[float]], - urdf_link_frame: list[float], -) -> ET.Element | None: - placed_parts: list[dict[str, object]] = [] - total_mass_kg = 0.0 - weighted_center_mm = [0.0, 0.0, 0.0] - - for instance in instances: - source_link_name = str(instance.get("source_link_name", link_name)) - source_link_frame = source_frames.get( - source_link_name, - _identity_transform(), - ) - source_local_transform = [float(value) for value in instance["transform"]] - world_transform = multiply_transforms(source_link_frame, source_local_transform) - link_transform = multiply_transforms( - invert_rigid_transform(urdf_link_frame), - world_transform, - ) - properties = _part_mass_properties( - str(instance["path"]), - source_dir=_source_dir_for_urdf_instance(instance, link_name=link_name), - ) - mass_kg = float(properties["mass_kg"]) - center_link_mm = _transform_point( - link_transform, - properties["center_mm"], # type: ignore[arg-type] - ) - total_mass_kg += mass_kg - for axis in range(3): - weighted_center_mm[axis] += mass_kg * center_link_mm[axis] - placed_parts.append( - { - "mass_kg": mass_kg, - "center_link_mm": center_link_mm, - "size_mm": properties["size_mm"], - "link_transform": link_transform, - } - ) - - if total_mass_kg <= 0.0: - return None - - center_m = tuple( - (weighted_center_mm[axis] / total_mass_kg) * 0.001 - for axis in range(3) - ) - inertia = [[0.0, 0.0, 0.0] for _ in range(3)] - for part in placed_parts: - part_inertia = _rotated_box_inertia_kg_m2( - mass_kg=float(part["mass_kg"]), - size_mm=part["size_mm"], # type: ignore[arg-type] - transform=part["link_transform"], # type: ignore[arg-type] - ) - center_link_m = tuple( - component * 0.001 - for component in part["center_link_mm"] # type: ignore[union-attr] - ) - offset_m = tuple(center_link_m[index] - center_m[index] for index in range(3)) - _add_parallel_axis( - part_inertia, - mass_kg=float(part["mass_kg"]), - offset_m=offset_m, # type: ignore[arg-type] - ) - for row in range(3): - for col in range(3): - inertia[row][col] += part_inertia[row][col] - - return _xml( - "inertial", - children=( - _xml( - "origin", - { - "xyz": " ".join(_urdf_number(value) for value in center_m), - "rpy": "0 0 0", - }, - ), - _xml("mass", {"value": _urdf_number(total_mass_kg)}), - _xml( - "inertia", - { - "ixx": _urdf_number(inertia[0][0]), - "ixy": _urdf_number(inertia[0][1]), - "ixz": _urdf_number(inertia[0][2]), - "iyy": _urdf_number(inertia[1][1]), - "iyz": _urdf_number(inertia[1][2]), - "izz": _urdf_number(inertia[2][2]), - }, - ), - ), - ) - - -def _visual_element_for_instance( - instance: dict[str, object], - *, - source_link_frame: list[float], - urdf_link_frame: list[float], -) -> ET.Element: - step_path = str(instance["path"]) - source_local_transform = [float(value) for value in instance["transform"]] - world_transform = multiply_transforms(source_link_frame, source_local_transform) - transform = multiply_transforms( - invert_rigid_transform(urdf_link_frame), - world_transform, - ) - return _xml( - "visual", - children=( - _xml("origin", _urdf_origin_attrs_from_transform(transform)), - _xml( - "geometry", - children=( - _xml( - "mesh", - { - "filename": _mesh_filename_for_step_path(step_path), - "scale": "0.001 0.001 0.001", - }, - ), - ), - ), - _xml("material", {"name": _material_name_for_step_path(step_path)}), - ), - ) - - -def _link_element( - link_name: str, - *, - source_frames: dict[str, list[float]], - urdf_frames: dict[str, list[float]], -) -> ET.Element: - owner_source_link_frame = source_frames.get( - link_name, - urdf_frames.get(link_name, _identity_transform()), - ) - urdf_link_frame = urdf_frames.get(link_name, owner_source_link_frame) - instances = _instances_for_urdf_link(link_name) - children: list[ET.Element] = [] - inertial = _inertial_element_for_instances( - instances, - link_name=link_name, - source_frames=source_frames, - urdf_link_frame=urdf_link_frame, - ) - if inertial is not None: - children.append(inertial) - children.extend( - _visual_element_for_instance( - instance, - source_link_frame=source_frames.get( - str(instance.get("source_link_name", link_name)), - urdf_frames.get( - str(instance.get("source_link_name", link_name)), - _identity_transform(), - ), - ), - urdf_link_frame=urdf_link_frame, - ) - for instance in instances - ) - return _xml( - "link", - {"name": link_name}, - tuple(children), - ) - - -def _joint_element( - joint: dict[str, object], - *, - link_frames: dict[str, list[float]], - axes_by_joint: dict[str, str], -) -> ET.Element: - parent = str(joint["parent"]) - child = str(joint["child"]) - attrs = {"name": str(joint["name"]), "type": str(joint["joint_type"])} - attrs.update({str(key): str(value) for key, value in dict(joint.get("urdf_attrs", {})).items()}) - - if parent in link_frames and child in link_frames: - relative = multiply_transforms( - invert_rigid_transform(link_frames[parent]), - link_frames[child], - ) - origin_attrs = _urdf_origin_attrs_from_transform(relative) - else: - origin_attrs = { - "xyz": str(joint["origin_xyz"]), - "rpy": str(joint["origin_rpy"]), - } - - children = [ - _xml("parent", {"link": parent}), - _xml("child", {"link": child}), - _xml("origin", origin_attrs), - ] - axis_xyz = axes_by_joint.get(str(joint["name"]), joint.get("axis_xyz")) - if axis_xyz is not None: - children.append(_xml("axis", {"xyz": str(axis_xyz)})) - limit = joint.get("limit") - if limit is not None: - limit_attrs = {str(key): str(value) for key, value in dict(limit).items()} - effort_nm = URDF_JOINT_EFFORT_NM_BY_NAME.get(str(joint["name"])) - if effort_nm is not None: - limit_attrs["effort"] = _urdf_number(effort_nm) - velocity_rad_s = URDF_JOINT_VELOCITY_RAD_S_BY_NAME.get(str(joint["name"])) - if velocity_rad_s is not None: - limit_attrs["velocity"] = _urdf_number(velocity_rad_s) - children.append(_xml("limit", limit_attrs)) - mimic = joint.get("mimic") - if mimic is not None: - children.append(_xml("mimic", {str(key): str(value) for key, value in dict(mimic).items()})) - return _xml("joint", attrs, tuple(children)) - - -def _joints_for_link_names(link_names: tuple[str, ...]) -> tuple[dict[str, object], ...]: - link_name_set = set(link_names) - return tuple( - dict(joint) - for joint in robot_arm.URDF_JOINTS - if str(joint["parent"]) in link_name_set and str(joint["child"]) in link_name_set - ) - - -def _no_gripper_joints() -> tuple[dict[str, object], ...]: - return _joints_for_link_names(NO_GRIPPER_LINK_NAMES) - - -def _robot_common_link(name: str) -> dict[str, object]: - for link in robot_arm.URDF_LINKS: - if str(link["name"]) == name: - return dict(link) - raise RuntimeError(f"Missing shared URDF link {name!r}") - - -def gen_urdf_with_options( - *, - include_gripper: bool = False, - robot_name: str = "tom_v2", - source_name: str = "models/robots/tom/v2/tom.py", -) -> dict[str, object]: - """Jointed v2 URDF using 3MF visual meshes. - - Design ledger: - - Robot name: tom_v2 or tom_v2_with_gripper. - - Target consumers: CAD Viewer, RViz/robot_state_publisher-style display. - - Units: URDF meters/radians; v2 STEP/3MF meshes are authored in millimeters. - - Frame convention: each moving child link frame is placed on its incoming - servo axis, measured as the line between the rear and output horn face - centers. Link visuals are rebased from the v2 source link frames into - those horn-axis URDF frames. - - Roll joints: the upstream yoke remains on the pitch link, while the - bottom servo body is owned by the child roll link so the servo case - rotates with the bracket/link body. - - Mesh source: 3MF visual meshes only. V2 brackets, mounts, standoffs, - stock servos, and optional gripper component meshes live in v2/3MF. - - Kinematics: the default tom.urdf stops at wrist_roll_link; the - tom_with_gripper.urdf variant adds the Robonine gripper base, servo gear, - claw slide links, and mimic joints. - - Inertials: link masses are assembled from the same STEP instances as the - visuals. Servos use Feetech published masses; aluminum plates and - standoffs use 5052 density and current STEP volumes. Gripper inertials - reuse the shared Robonine mesh-derived estimates. Arm inertia tensors use - transformed bounding-box approximations around the computed center of - mass. Collision elements are intentionally omitted for the arm pending - simplified collision geometry. - """ - source_frames, urdf_frames, axes_by_joint = _urdf_link_frames_and_axes( - include_gripper=include_gripper, - ) - link_names = WITH_GRIPPER_LINK_NAMES if include_gripper else NO_GRIPPER_LINK_NAMES - joints = _joints_for_link_names(link_names) - - robot = _xml("robot", {"name": robot_name}) - robot.append(ET.Comment(f" Generated by {source_name}. ")) - robot.append( - ET.Comment( - " Jointed v2 URDF; all visual meshes are 3MF in " - "millimeters with scale 0.001 0.001 0.001. Link inertials use " - "Feetech STS3250 74.5 g / STS3215 55 g masses and 5052 aluminum " - "density for plates/standoffs; gripper inertials use shared " - "Robonine mesh-derived estimates when included. Joint efforts use " - "50/30 kg-cm stall torque converted to N-m for arm joints; joint " - "velocities use Feetech no-load speed at 12 V converted to rad/s. " - ) - ) - for material_name, rgba in URDF_MATERIALS.items(): - robot.append( - _xml( - "material", - {"name": material_name}, - (_xml("color", {"rgba": _rgba_text(rgba)}),), - ) - ) - for link_name in link_names: - if link_name in GRIPPER_LINK_NAMES: - robot.append(robot_arm._urdf_link_element(_robot_common_link(link_name))) - else: - robot.append( - _link_element( - link_name, - source_frames=source_frames, - urdf_frames=urdf_frames, - ) - ) - for joint in joints: - robot.append( - _joint_element( - joint, - link_frames=urdf_frames, - axes_by_joint=axes_by_joint, - ) - ) - return {"xml": _serialize_urdf(robot)} - - -def gen_urdf() -> dict[str, object]: - """Jointed no-gripper URDF using 3MF visual meshes.""" - return gen_urdf_with_options(include_gripper=False) - - -def _srdf_joint_degrees_to_radians(joint_values_deg: dict[str, float]) -> dict[str, float]: - return { - joint_name: math.radians(float(value_deg)) - for joint_name, value_deg in joint_values_deg.items() - } - - -def _srdf_group_state_element( - *, - name: str, - group: str, - joint_values_rad: dict[str, float], -) -> ET.Element: - return _xml( - "group_state", - {"name": name, "group": group}, - tuple( - _xml("joint", {"name": joint_name, "value": _urdf_number(value_rad)}) - for joint_name, value_rad in joint_values_rad.items() - ), - ) - - -def _srdf_arm_group_states_deg() -> tuple[tuple[str, dict[str, float]], ...]: - old_home_elbow_pitch_deg = 0.0 - for state_name, joint_values_deg in robot_arm.ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG: - if state_name == "home": - old_home_elbow_pitch_deg = float(joint_values_deg.get("elbow_pitch", 0.0)) - break - elbow_pitch_offset_deg = V2_HOME_ELBOW_PITCH_DEG - old_home_elbow_pitch_deg - states: list[tuple[str, dict[str, float]]] = [] - for state_name, joint_values_deg in robot_arm.ROBOT_ARM_SRDF_ARM_GROUP_STATES_DEG: - next_values = dict(joint_values_deg) - next_values["elbow_pitch"] = float(next_values.get("elbow_pitch", 0.0)) + elbow_pitch_offset_deg - if state_name == "reach_forward": - next_values = dict(V2_SRDF_REACH_ARM_GROUP_STATE_DEG) - elif state_name == "inspection": - next_values = dict(V2_SRDF_INSPECTION_ARM_GROUP_STATE_DEG) - elif state_name == "inspection_mirrored": - next_values = dict(V2_SRDF_INSPECTION_MIRRORED_ARM_GROUP_STATE_DEG) - states.append((state_name, next_values)) - return tuple(states) - - -def _srdf_adjacent_collision_pairs_from_urdf(urdf: str) -> tuple[tuple[str, str], ...]: - urdf_root = ET.parse(V2_DIR / urdf).getroot() - pairs: list[tuple[str, str]] = [] - seen: set[tuple[str, str]] = set() - for joint in urdf_root.findall("joint"): - parent = joint.find("parent") - child = joint.find("child") - if parent is None or child is None: - continue - parent_link = str(parent.attrib.get("link", "")) - child_link = str(child.attrib.get("link", "")) - if not parent_link or not child_link or "base_footprint" in {parent_link, child_link}: - continue - key = tuple(sorted((parent_link, child_link))) - if key in seen: - continue - seen.add(key) - pairs.append((parent_link, child_link)) - return tuple(pairs) - - -def _srdf_root_element( - *, - robot_name: str, - urdf: str, - include_gripper: bool, -) -> ET.Element: - root = _xml("robot", {"name": robot_name}) - root.append(ET.Comment(f" Generated by models/robots/tom/v2/{Path(urdf).stem}.py for MoveIt2 and CAD Viewer test poses. ")) - root.append( - _xml( - "virtual_joint", - { - "name": "fixed_base", - "type": "fixed", - "parent_frame": "world", - "child_link": "base_footprint", - }, - ) - ) - - arm_group = _xml("group", {"name": "arm"}) - arm_group.append(_xml("chain", {"base_link": "base_link", "tip_link": "wrist_roll_link"})) - root.append(arm_group) - - if include_gripper: - tcp_group = _xml("group", {"name": "tcp"}) - tcp_group.append(_xml("link", {"name": "gripper_base_link"})) - root.append(tcp_group) - - gripper_group = _xml("group", {"name": "gripper"}) - gripper_group.append(_xml("joint", {"name": "gripper_servo"})) - root.append(gripper_group) - - arm_with_gripper_group = _xml("group", {"name": "arm_with_gripper"}) - arm_with_gripper_group.append(_xml("group", {"name": "arm"})) - arm_with_gripper_group.append(_xml("group", {"name": "gripper"})) - root.append(arm_with_gripper_group) - - root.append( - _xml( - "end_effector", - { - "name": "gripper_tcp", - "parent_link": "wrist_roll_link", - "group": "tcp", - "parent_group": "arm", - }, - ) - ) - - for name, joint_values_deg in _srdf_arm_group_states_deg(): - root.append( - _srdf_group_state_element( - name=name, - group="arm", - joint_values_rad=_srdf_joint_degrees_to_radians(joint_values_deg), - ) - ) - - if include_gripper: - for name, joint_values_rad in robot_arm.ROBOT_ARM_SRDF_GRIPPER_GROUP_STATES_RAD: - root.append( - _srdf_group_state_element( - name=name, - group="gripper", - joint_values_rad=joint_values_rad, - ) - ) - - for link1, link2 in _srdf_adjacent_collision_pairs_from_urdf(urdf): - root.append(_xml("disable_collisions", {"link1": link1, "link2": link2, "reason": "Adjacent"})) - return root - - -def gen_srdf_with_options( - *, - include_gripper: bool = False, - robot_name: str = "tom_v2", - urdf: str = "tom.urdf", -) -> dict[str, object]: - """MoveIt semantic groups and named states for the v2 URDF variants.""" - return { - "xml": _serialize_urdf( - _srdf_root_element( - robot_name=robot_name, - urdf=urdf, - include_gripper=include_gripper, - ) - ), - "urdf": urdf, - } - - -def gen_srdf() -> dict[str, object]: - """MoveIt semantic groups and named states for the no-gripper v2 arm.""" - return gen_srdf_with_options(include_gripper=False) diff --git a/models/robots/tom/v2/tom.srdf b/models/robots/tom/v2/tom.srdf deleted file mode 100644 index cf0fcd36..00000000 --- a/models/robots/tom/v2/tom.srdf +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/tom.step b/models/robots/tom/v2/tom.step deleted file mode 100644 index df643801..00000000 --- a/models/robots/tom/v2/tom.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0de4a3767dbd830f7bf3174b96d196870d6ffc4dff8d1b476d081dda3c9e2d37 -size 26232257 diff --git a/models/robots/tom/v2/tom.urdf b/models/robots/tom/v2/tom.urdf deleted file mode 100644 index a85bb674..00000000 --- a/models/robots/tom/v2/tom.urdf +++ /dev/null @@ -1,295 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/tom_double.py b/models/robots/tom/v2/tom_double.py deleted file mode 100644 index 8348cb0b..00000000 --- a/models/robots/tom/v2/tom_double.py +++ /dev/null @@ -1,662 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -from pathlib import Path - - -V2_DIR = Path(__file__).resolve().parent -TOM_SOURCE = V2_DIR / "tom.py" -DOUBLE_SOURCE = V2_DIR / "assemblies" / "servo_horn_yoke_double.py" -ASSEMBLIES_DIR = V2_DIR / "assemblies" -PARTS_DIR = V2_DIR / "parts" - -for path in (V2_DIR.parent, V2_DIR, PARTS_DIR, ASSEMBLIES_DIR): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -SHOULDER_DOUBLE_CHILD_NAME = "shoulder_yaw_link" -SHOULDER_DOUBLE_PRIMARY_SERVO = "sts3250_rear" -SHOULDER_DOUBLE_ORIGINAL_SERVO_INSTANCE = "shoulder_yaw_link__sts3250_1" -SHOULDER_DOUBLE_REPLACED_MATES = frozenset( - { - "base_servo_horn_to_shoulder_yaw_mount", - "shoulder_yaw_mount_to_servo", - "shoulder_pitch_yoke_to_servo", - } -) -SHOULDER_DOUBLE_REMOVED_INSTANCES = frozenset( - { - "shoulder_pitch_link__servo_horn_yoke", - } -) -SHOULDER_DOUBLE_URDF_MESH_BY_STEP_BASENAME = { - "servo_horn_yoke_double_horn.step": "3MF/servo_horn_yoke_double_horn.3mf", - "servo_end_mount_double.step": "3MF/servo_end_mount_double.3mf", -} -SHOULDER_DOUBLE_URDF_MATERIAL_BY_STEP_BASENAME = { - "servo_horn_yoke_double_horn.step": "aluminum_5052", - "servo_end_mount_double.step": "aluminum_5052", -} - - -def _load_module(path: Path, module_label: str): - module_name = f"_tom_v2_{module_label}_{abs(hash(path))}" - spec = importlib.util.spec_from_file_location(module_name, path) - if spec is None or spec.loader is None: - raise RuntimeError(f"Unable to load {path}") - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -def _prefix_mate(parent_name: str, mate: dict[str, object]) -> dict[str, object]: - fixed_part, fixed_frame = str(mate["fixed"]).split(":", 1) - moving_part, moving_frame = str(mate["moving"]).split(":", 1) - fixed = f"{parent_name}__{fixed_part}:{fixed_frame}" - moving = f"{parent_name}__{moving_part}:{moving_frame}" - return { - **mate, - "sourceLabel": f"{parent_name}_{mate['sourceLabel']}", - "fixed": fixed, - "moving": moving, - "fixedEndpoint": { - "part": fixed.split(":", 1)[0], - "frame": fixed_frame, - }, - "movingEndpoint": { - "part": moving.split(":", 1)[0], - "frame": moving_frame, - }, - } - - -def _replace_shoulder_double_mates( - *, - tom_module, - double_module_envelope: dict[str, object], - include_gripper: bool, -) -> list[dict[str, object]]: - mates = [ - mate - for mate in tom_module._assembly_mates(include_gripper=include_gripper) - if str(mate["sourceLabel"]) not in SHOULDER_DOUBLE_REPLACED_MATES - ] - mates.extend( - _prefix_mate(SHOULDER_DOUBLE_CHILD_NAME, mate) - for mate in double_module_envelope.get("assembly_mates", []) - ) - mates.append( - tom_module._mate( - "base_servo_horn_to_shoulder_yaw_double_mount", - fixed="base_link__sts3250_3:output_horn_face", - moving=f"{SHOULDER_DOUBLE_CHILD_NAME}__servo_end_mount_double:front_horn_mount_face", - parameters={"clearance_mm": tom_module.BASE_TO_SHOULDER_YAW_HORN_CLEARANCE_MM}, - ) - ) - return mates - - -def _local_transform_by_name( - instances: list[dict[str, object]], - name: str, -) -> list[float]: - for instance in instances: - if str(instance["name"]) == name: - return [float(value) for value in instance["transform"]] - raise RuntimeError(f"Unable to find local transform for {name}") - - -def _shoulder_double_parent_transform( - *, - tom_module, - child_name: str, - design_child_transform: list[float], - double_module_envelope: dict[str, object], - base_servo_transform: list[float], -) -> list[float]: - original_module_transform = tom_module._module_transform_for_child( - child_name=child_name, - design_child_transform=design_child_transform, - ) - original_instances = tom_module._flat_child_instances( - child_name=child_name, - child_transform=original_module_transform, - ) - original_servo_transform = _local_transform_by_name( - original_instances, - SHOULDER_DOUBLE_ORIGINAL_SERVO_INSTANCE, - ) - primary_servo_local = _local_transform_by_name( - double_module_envelope["instances"], - f"sts3250_{SHOULDER_DOUBLE_PRIMARY_SERVO.removeprefix('sts3250_')}", - ) - parent_transform = tom_module.multiply_transforms( - original_servo_transform, - tom_module.invert_rigid_transform(primary_servo_local), - ) - base_horn_face_center = tom_module._transform_point( - base_servo_transform, - ( - tom_module.STS3215_HORN_AXIS_LOCAL_X_MM, - tom_module.STS3250_OUTPUT_HORN_FACE_LOCAL_Y_MM, - 0.0, - ), - ) - double_mount_face_center = tom_module._transform_point( - parent_transform, - ( - tom_module.SERVO_END_MOUNT_FRONT_HORN_FACE_CENTER_LOCAL_MM[0], - 0.0, - 0.0, - ), - ) - return tom_module._translate_transform( - parent_transform, - ( - base_horn_face_center[0] - double_mount_face_center[0], - base_horn_face_center[1] - double_mount_face_center[1], - base_horn_face_center[2] - double_mount_face_center[2], - ), - ) - - -def gen_step_with_options(*, include_gripper: bool = False) -> dict[str, object]: - tom_module = _load_module(TOM_SOURCE, "tom_double_base") - double_module = _load_module(DOUBLE_SOURCE, "shoulder_double") - double_envelope = double_module.gen_step() - - instances: list[dict[str, object]] = [] - instance_transforms_by_name: dict[str, list[float]] = {} - downstream_correction = tom_module._identity_transform() - pending_child_transform_overrides: dict[str, list[float]] = {} - - for source_child in tom_module.robot_arm.robot_arm_assembly_children(): - child_name = str(source_child["name"]) - source_child_transform = [float(value) for value in source_child["transform"]] - child_transform = pending_child_transform_overrides.pop(child_name, None) - if child_transform is not None: - downstream_correction = tom_module.multiply_transforms( - child_transform, - tom_module.invert_rigid_transform(source_child_transform), - ) - else: - child_transform = tom_module.multiply_transforms( - downstream_correction, - source_child_transform, - ) - - if child_name == "shoulder_yaw_link": - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is not None: - child_transform = tom_module._mate_shoulder_yaw_mount_to_base_servo_horn( - shoulder_yaw_transform=child_transform, - base_servo_transform=base_servo_transform, - ) - downstream_correction = tom_module.multiply_transforms( - child_transform, - tom_module.invert_rigid_transform(source_child_transform), - ) - - if child_name == "elbow_pitch_link": - child_transform = tom_module._apply_v2_step_home_pose_to_elbow_pitch( - child_transform=child_transform, - instance_transforms_by_name=instance_transforms_by_name, - ) - downstream_correction = tom_module.multiply_transforms( - child_transform, - tom_module.invert_rigid_transform(source_child_transform), - ) - - if child_name == tom_module.GRIPPER_CHILD_NAME: - if not include_gripper: - continue - terminal_servo_transform = instance_transforms_by_name.get( - tom_module.V2_GRIPPER_SERVO_INSTANCE_NAME - ) - if terminal_servo_transform is not None: - child_transform = tom_module._mate_gripper_to_terminal_servo_horn( - gripper_transform=child_transform, - terminal_servo_transform=terminal_servo_transform, - ) - instances.append( - { - "path": str(source_child["path"]), - "name": child_name, - "transform": child_transform, - "use_source_colors": bool(source_child.get("use_source_colors", True)), - } - ) - instance_transforms_by_name[child_name] = child_transform - continue - - if child_name == SHOULDER_DOUBLE_CHILD_NAME: - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is None: - raise RuntimeError("Cannot place shoulder double before base servo is available") - double_parent_transform = _shoulder_double_parent_transform( - tom_module=tom_module, - child_name=child_name, - design_child_transform=child_transform, - double_module_envelope=double_envelope, - base_servo_transform=base_servo_transform, - ) - child_instances = tom_module._flatten_instances( - parent_name=child_name, - parent_transform=double_parent_transform, - local_instances=double_envelope["instances"], - local_source_dir=ASSEMBLIES_DIR, - ) - instances.extend(child_instances) - for instance in child_instances: - instance_transforms_by_name[str(instance["name"])] = [ - float(value) for value in instance["transform"] - ] - continue - - module_child_transform = tom_module._module_transform_for_child( - child_name=child_name, - design_child_transform=child_transform, - ) - child_instances = tom_module._flat_child_instances( - child_name=child_name, - child_transform=module_child_transform, - ) - child_instances = [ - instance - for instance in child_instances - if str(instance["name"]) not in SHOULDER_DOUBLE_REMOVED_INSTANCES - ] - instances.extend(child_instances) - for instance in child_instances: - instance_transforms_by_name[str(instance["name"])] = [ - float(value) for value in instance["transform"] - ] - - replacement_kind = tom_module.ROLL_LINK_REPLACEMENTS.get(child_name) - if replacement_kind is not None: - mates = tom_module.roll_link_mates(replacement_kind) - next_pitch_child_name = tom_module.NEXT_PITCH_LINK_BY_ROLL_LINK.get(replacement_kind) - if next_pitch_child_name is not None: - upstream_servo_transform = tom_module.multiply_transforms( - child_transform, - mates.downstream_servo_local, - ) - pending_child_transform_overrides[next_pitch_child_name] = ( - tom_module._yoke_transform_for_servo_horn( - upstream_servo_transform=upstream_servo_transform, - ) - ) - - return { - "instances": instances, - "assembly_mates": _replace_shoulder_double_mates( - tom_module=tom_module, - double_module_envelope=double_envelope, - include_gripper=include_gripper, - ), - } - - -def gen_step() -> dict[str, object]: - envelope = gen_step_with_options(include_gripper=False) - return { - "instances": envelope["instances"], - "assembly_mates": envelope.get("assembly_mates", []), - } - - -def _zero_pose_source_frames_instances_and_double_visuals( - *, - tom_module, - double_module_envelope: dict[str, object], -) -> tuple[ - dict[str, list[float]], - dict[str, list[float]], - dict[str, list[dict[str, object]]], -]: - frames: dict[str, list[float]] = {"base_footprint": tom_module._identity_transform()} - instance_transforms_by_name: dict[str, list[float]] = {} - visual_instances_by_link: dict[str, list[dict[str, object]]] = {} - downstream_correction = tom_module._identity_transform() - pending_child_transform_overrides: dict[str, list[float]] = {} - - for source_child in tom_module.robot_arm.robot_arm_assembly_children(): - child_name = str(source_child["name"]) - source_child_transform = [float(value) for value in source_child["transform"]] - child_transform = pending_child_transform_overrides.pop(child_name, None) - if child_transform is not None: - downstream_correction = tom_module.multiply_transforms( - child_transform, - tom_module.invert_rigid_transform(source_child_transform), - ) - else: - child_transform = tom_module.multiply_transforms( - downstream_correction, - source_child_transform, - ) - - if child_name == "shoulder_yaw_link": - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is not None: - child_transform = tom_module._mate_shoulder_yaw_mount_to_base_servo_horn( - shoulder_yaw_transform=child_transform, - base_servo_transform=base_servo_transform, - ) - downstream_correction = tom_module.multiply_transforms( - child_transform, - tom_module.invert_rigid_transform(source_child_transform), - ) - - if child_name == tom_module.GRIPPER_CHILD_NAME: - continue - - if child_name == SHOULDER_DOUBLE_CHILD_NAME: - base_servo_transform = instance_transforms_by_name.get("base_link__sts3250_3") - if base_servo_transform is None: - raise RuntimeError("Cannot place shoulder double before base servo is available") - frames[child_name] = child_transform - double_parent_transform = _shoulder_double_parent_transform( - tom_module=tom_module, - child_name=child_name, - design_child_transform=child_transform, - double_module_envelope=double_module_envelope, - base_servo_transform=base_servo_transform, - ) - child_instances = tom_module._flatten_instances( - parent_name=child_name, - parent_transform=double_parent_transform, - local_instances=double_module_envelope["instances"], - local_source_dir=ASSEMBLIES_DIR, - ) - visual_instances_by_link[child_name] = [] - inverse_source_frame = tom_module.invert_rigid_transform(child_transform) - for instance in child_instances: - world_transform = [float(value) for value in instance["transform"]] - instance_transforms_by_name[str(instance["name"])] = world_transform - visual_instance = dict(instance) - visual_instance["transform"] = tom_module.multiply_transforms( - inverse_source_frame, - world_transform, - ) - visual_instances_by_link[child_name].append(visual_instance) - continue - - module_child_transform = tom_module._module_transform_for_child( - child_name=child_name, - design_child_transform=child_transform, - ) - frames[child_name] = module_child_transform - child_instances = tom_module._flat_child_instances( - child_name=child_name, - child_transform=module_child_transform, - ) - child_instances = [ - instance - for instance in child_instances - if str(instance["name"]) not in SHOULDER_DOUBLE_REMOVED_INSTANCES - ] - for instance in child_instances: - instance_transforms_by_name[str(instance["name"])] = [ - float(value) for value in instance["transform"] - ] - - replacement_kind = tom_module.ROLL_LINK_REPLACEMENTS.get(child_name) - if replacement_kind is not None: - mates = tom_module.roll_link_mates(replacement_kind) - next_pitch_child_name = tom_module.NEXT_PITCH_LINK_BY_ROLL_LINK.get(replacement_kind) - if next_pitch_child_name is not None: - upstream_servo_transform = tom_module.multiply_transforms( - child_transform, - mates.downstream_servo_local, - ) - pending_child_transform_overrides[next_pitch_child_name] = ( - tom_module._yoke_transform_for_servo_horn( - upstream_servo_transform=upstream_servo_transform, - ) - ) - - wrist_pitch = frames.get("wrist_pitch_link") - if wrist_pitch is not None and "wrist_roll_link" not in frames: - wrist_roll_joint = next( - joint - for joint in tom_module.robot_arm.URDF_JOINTS - if str(joint["name"]) == "wrist_roll" - ) - frames["wrist_roll_link"] = tom_module.multiply_transforms( - wrist_pitch, - tom_module._transform_from_urdf_origin_attrs( - str(wrist_roll_joint["origin_xyz"]), - str(wrist_roll_joint["origin_rpy"]), - ), - ) - - return frames, instance_transforms_by_name, visual_instances_by_link - - -def _double_horn_axis( - tom_module, - instance_transforms_by_name: dict[str, list[float]], -) -> tuple[tuple[float, float, float], tuple[float, float, float]]: - horn_transform = instance_transforms_by_name.get( - f"{SHOULDER_DOUBLE_CHILD_NAME}__servo_horn_yoke_double_horn" - ) - if horn_transform is None: - raise RuntimeError("Missing double horn transform for shoulder_pitch URDF axis") - center = tom_module._transform_point( - horn_transform, - ( - tom_module.STS3215_HORN_AXIS_LOCAL_X_MM, - 0.0, - 0.0, - ), - ) - direction = tom_module._normalized_vector( - tom_module._transform_direction(horn_transform, (0.0, 1.0, 0.0)) - ) - return center, direction - - -def _urdf_link_frames_axes_and_double_visuals( - *, - tom_module, - double_module_envelope: dict[str, object], -) -> tuple[ - dict[str, list[float]], - dict[str, list[float]], - dict[str, str], - dict[str, list[dict[str, object]]], -]: - source_frames, instance_transforms, double_visuals = ( - _zero_pose_source_frames_instances_and_double_visuals( - tom_module=tom_module, - double_module_envelope=double_module_envelope, - ) - ) - urdf_frames = {name: list(transform) for name, transform in source_frames.items()} - axes_by_joint: dict[str, str] = {} - servo_axis_instance_by_joint = dict(tom_module.URDF_SERVO_AXIS_INSTANCE_BY_JOINT) - - for joint in tom_module._no_gripper_joints(): - joint_name = str(joint["name"]) - if joint_name == "shoulder_pitch": - axis_center_world, axis_direction_world = _double_horn_axis( - tom_module, - instance_transforms, - ) - else: - servo_instance_name = servo_axis_instance_by_joint.get(joint_name) - if not servo_instance_name: - continue - servo_transform = instance_transforms.get(servo_instance_name) - if servo_transform is None: - raise RuntimeError( - f"Missing servo instance {servo_instance_name!r} " - f"for URDF joint {joint_name!r}" - ) - axis_center_world, axis_direction_world = tom_module._servo_horn_axis_from_transform( - servo_transform, - ) - - child_link = str(joint["child"]) - if child_link not in urdf_frames: - parent_frame = urdf_frames.get(str(joint["parent"])) - if parent_frame is None: - raise RuntimeError(f"Missing parent frame for URDF joint {joint_name!r}") - urdf_frames[child_link] = list(parent_frame) - urdf_frames[child_link] = tom_module._with_translation( - urdf_frames[child_link], - axis_center_world, - ) - axis_in_joint_frame = tom_module._world_vector_to_local( - urdf_frames[child_link], - axis_direction_world, - ) - axes_by_joint[joint_name] = tom_module._axis_attrs(axis_in_joint_frame) - - return source_frames, urdf_frames, axes_by_joint, double_visuals - - -def _instances_for_urdf_link_double( - *, - tom_module, - link_name: str, - double_visuals: dict[str, list[dict[str, object]]], -) -> list[dict[str, object]]: - if link_name == SHOULDER_DOUBLE_CHILD_NAME: - return [dict(instance) for instance in double_visuals.get(link_name, [])] - if link_name == "shoulder_pitch_link": - return [] - return tom_module._instances_for_urdf_link(link_name) - - -def _link_element_double( - *, - tom_module, - link_name: str, - source_frames: dict[str, list[float]], - urdf_frames: dict[str, list[float]], - double_visuals: dict[str, list[dict[str, object]]], -): - owner_source_link_frame = source_frames.get( - link_name, - urdf_frames.get(link_name, tom_module._identity_transform()), - ) - urdf_link_frame = urdf_frames.get(link_name, owner_source_link_frame) - instances = _instances_for_urdf_link_double( - tom_module=tom_module, - link_name=link_name, - double_visuals=double_visuals, - ) - children = [] - inertial = tom_module._inertial_element_for_instances( - instances, - link_name=link_name, - source_frames=source_frames, - urdf_link_frame=urdf_link_frame, - ) - if inertial is not None: - children.append(inertial) - children.extend( - tom_module._visual_element_for_instance( - instance, - source_link_frame=source_frames.get( - str(instance.get("source_link_name", link_name)), - urdf_frames.get( - str(instance.get("source_link_name", link_name)), - tom_module._identity_transform(), - ), - ), - urdf_link_frame=urdf_link_frame, - ) - for instance in instances - ) - return tom_module._xml( - "link", - {"name": link_name}, - tuple(children), - ) - - -def gen_urdf() -> dict[str, object]: - """Jointed no-gripper URDF for the tom_double shoulder-yaw variant. - - Design ledger: - - Based on tom_v2's no-gripper URDF and 3MF visual mesh convention. - - The shoulder-yaw single STS3250/end-mount visual module is replaced by - the side-by-side double servo horn/mount assembly. - - The shoulder_pitch joint frame/axis is derived from the double horn plate - centerline, not from either offset servo body. - - Inertials use the same vendor servo masses and 5052 STEP-volume - plate/standoff mass model as tom.urdf. Gripper links and collision - elements are intentionally omitted to match tom.urdf's current scope. - """ - tom_module = _load_module(TOM_SOURCE, "tom_double_urdf_base") - double_module = _load_module(DOUBLE_SOURCE, "tom_double_urdf_shoulder_double") - double_envelope = double_module.gen_step() - tom_module.URDF_JOINT_EFFORT_NM_BY_NAME = dict( - tom_module.URDF_JOINT_EFFORT_NM_BY_NAME - ) - tom_module.URDF_JOINT_EFFORT_NM_BY_NAME["shoulder_pitch"] = ( - 2.0 * tom_module.STS3250_STALL_TORQUE_NM - ) - tom_module.URDF_MESH_BY_STEP_BASENAME.update(SHOULDER_DOUBLE_URDF_MESH_BY_STEP_BASENAME) - tom_module.URDF_MATERIAL_BY_STEP_BASENAME.update( - SHOULDER_DOUBLE_URDF_MATERIAL_BY_STEP_BASENAME - ) - - source_frames, urdf_frames, axes_by_joint, double_visuals = ( - _urdf_link_frames_axes_and_double_visuals( - tom_module=tom_module, - double_module_envelope=double_envelope, - ) - ) - robot = tom_module._xml("robot", {"name": "tom_v2_double"}) - robot.append(tom_module.ET.Comment(" Generated by models/robots/tom/v2/tom_double.py. ")) - robot.append( - tom_module.ET.Comment( - " Jointed no-gripper URDF for the double shoulder-yaw variant; " - "all visual meshes are 3MF in millimeters with scale 0.001 0.001 0.001. " - "Link inertials use Feetech STS3250 74.5 g / STS3215 55 g masses, " - "5052 aluminum density for plates/standoffs, 2x STS3250 stall torque " - "on shoulder_pitch, and single-servo stall torque plus no-load speeds " - "elsewhere converted to SI units. " - ) - ) - for material_name, rgba in tom_module.URDF_MATERIALS.items(): - robot.append( - tom_module._xml( - "material", - {"name": material_name}, - (tom_module._xml("color", {"rgba": tom_module._rgba_text(rgba)}),), - ) - ) - for link_name in tom_module.NO_GRIPPER_LINK_NAMES: - robot.append( - _link_element_double( - tom_module=tom_module, - link_name=link_name, - source_frames=source_frames, - urdf_frames=urdf_frames, - double_visuals=double_visuals, - ) - ) - for joint in tom_module._no_gripper_joints(): - robot.append( - tom_module._joint_element( - joint, - link_frames=urdf_frames, - axes_by_joint=axes_by_joint, - ) - ) - return {"xml": tom_module._serialize_urdf(robot)} - - -def gen_srdf() -> dict[str, object]: - """MoveIt semantic groups and named states for the double shoulder v2 arm.""" - return _load_module(TOM_SOURCE, "tom_double_srdf_base").gen_srdf_with_options( - include_gripper=False, - robot_name="tom_v2_double", - urdf="tom_double.urdf", - ) diff --git a/models/robots/tom/v2/tom_double.srdf b/models/robots/tom/v2/tom_double.srdf deleted file mode 100644 index 6c5c223a..00000000 --- a/models/robots/tom/v2/tom_double.srdf +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/tom_double.step b/models/robots/tom/v2/tom_double.step deleted file mode 100644 index 97cc8652..00000000 --- a/models/robots/tom/v2/tom_double.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b14ad4c9e331e13ae371abbc6abf75ede32bce52dc6255b7deb3fce74c73d6c3 -size 26383560 diff --git a/models/robots/tom/v2/tom_double.urdf b/models/robots/tom/v2/tom_double.urdf deleted file mode 100644 index fbb4715c..00000000 --- a/models/robots/tom/v2/tom_double.urdf +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/tom_with_gripper.py b/models/robots/tom/v2/tom_with_gripper.py deleted file mode 100644 index 511c264e..00000000 --- a/models/robots/tom/v2/tom_with_gripper.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -from pathlib import Path - - -V2_DIR = Path(__file__).resolve().parent -TOM_SOURCE = V2_DIR / "tom.py" - - -def _load_tom_module(): - module_name = f"_tom_v2_with_gripper_source_{abs(hash(TOM_SOURCE))}" - spec = importlib.util.spec_from_file_location(module_name, TOM_SOURCE) - if spec is None or spec.loader is None: - raise RuntimeError(f"Unable to load {TOM_SOURCE}") - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -def gen_step() -> dict[str, object]: - envelope = _load_tom_module().gen_step_with_options(include_gripper=True) - return { - "instances": envelope["instances"], - "assembly_mates": envelope.get("assembly_mates", []), - } - - -def gen_urdf() -> dict[str, object]: - return _load_tom_module().gen_urdf_with_options( - include_gripper=True, - robot_name="tom_v2_with_gripper", - source_name="models/robots/tom/v2/tom_with_gripper.py", - ) - - -def gen_srdf() -> dict[str, object]: - return _load_tom_module().gen_srdf_with_options( - include_gripper=True, - robot_name="tom_v2_with_gripper", - urdf="tom_with_gripper.urdf", - ) diff --git a/models/robots/tom/v2/tom_with_gripper.srdf b/models/robots/tom/v2/tom_with_gripper.srdf deleted file mode 100644 index 19c33022..00000000 --- a/models/robots/tom/v2/tom_with_gripper.srdf +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/robots/tom/v2/tom_with_gripper.step b/models/robots/tom/v2/tom_with_gripper.step deleted file mode 100644 index 0af5b0b4..00000000 --- a/models/robots/tom/v2/tom_with_gripper.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42d79ef155eea6875318bf3932c7b89a1f27d491dc7195f80506eedd322c2bb3 -size 38275110 diff --git a/models/robots/tom/v2/tom_with_gripper.urdf b/models/robots/tom/v2/tom_with_gripper.urdf deleted file mode 100644 index 1abe3146..00000000 --- a/models/robots/tom/v2/tom_with_gripper.urdf +++ /dev/null @@ -1,399 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/models/simple/.basic_shape_mating_test_fixture.step.glb b/models/simple/.basic_shape_mating_test_fixture.step.glb deleted file mode 100644 index 7e9801f4..00000000 --- a/models/simple/.basic_shape_mating_test_fixture.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8f4df75c0f4f6cd38fe02f3cc79b7e4151d165c7d0b76145353678f2002a15d5 -size 1070752 diff --git a/models/simple/.cam_follower_roller.step.glb b/models/simple/.cam_follower_roller.step.glb deleted file mode 100644 index 58fb6365..00000000 --- a/models/simple/.cam_follower_roller.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:10b25e3aaa66c507d367e18d0ba7e2da55a21887356fc4bbfc862546974bd9e3 -size 1179932 diff --git a/models/simple/.cylindrical_cap.step.glb b/models/simple/.cylindrical_cap.step.glb deleted file mode 100644 index 60ac10f0..00000000 --- a/models/simple/.cylindrical_cap.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1cc9f3b1fa5108f65dce99581d07a4479f2a8831cb07bfd94c275760b206fe95 -size 1760780 diff --git a/models/simple/.cylindrical_spacer_sleeve.step.glb b/models/simple/.cylindrical_spacer_sleeve.step.glb deleted file mode 100644 index 8a7121b8..00000000 --- a/models/simple/.cylindrical_spacer_sleeve.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f2a50b0aa6f28f28e0ef99b8652301eae348e453810b3ff6b0972c4e6cbe4030 -size 1180000 diff --git a/models/simple/.flywheel_disk.step.glb b/models/simple/.flywheel_disk.step.glb deleted file mode 100644 index b2bbb317..00000000 --- a/models/simple/.flywheel_disk.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c5fadf028df0361f955b7c839796fad9143c10aedca955603e02e43929fd8ccd -size 5827452 diff --git a/models/simple/.gusset_plate.step.glb b/models/simple/.gusset_plate.step.glb deleted file mode 100644 index 3e375c35..00000000 --- a/models/simple/.gusset_plate.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:72bc380cfe24cf8c63733f403d7edb24f42b10cc55753f6587e06f6fc4fb40f3 -size 996276 diff --git a/models/simple/.keyed_shaft_hub.step.glb b/models/simple/.keyed_shaft_hub.step.glb deleted file mode 100644 index 3af31ac4..00000000 --- a/models/simple/.keyed_shaft_hub.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ec141e719fdef643c9a1d5261fb439402f535e0ed421e1ea402d19629832adc0 -size 639108 diff --git a/models/simple/.mounting_plate.step.glb b/models/simple/.mounting_plate.step.glb deleted file mode 100644 index 80222cd2..00000000 --- a/models/simple/.mounting_plate.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47645f13c3ff9ce40cac7d8264252ec95af5d39bd8a50ef92ea11add4ad0bb8a -size 1552184 diff --git a/models/simple/.pulley_wheel.step.glb b/models/simple/.pulley_wheel.step.glb deleted file mode 100644 index 82521c3e..00000000 --- a/models/simple/.pulley_wheel.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:425fbdfbe89901f26975a062a195f06160aef236895914b5c2be5a974545fd02 -size 4643012 diff --git a/models/simple/.rectangular_clamp_block.step.glb b/models/simple/.rectangular_clamp_block.step.glb deleted file mode 100644 index 5605209d..00000000 --- a/models/simple/.rectangular_clamp_block.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d040616b1e17f22251b85031c229e7ffcc6973fc2e38bdc80d8d4300cbade0c7 -size 287492 diff --git a/models/simple/.retainer_plate.step.glb b/models/simple/.retainer_plate.step.glb deleted file mode 100644 index aa3dac19..00000000 --- a/models/simple/.retainer_plate.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5de2215e803975d87e8e38e5374d874e38d44f7c1be615053143987fe60dd2ae -size 261472 diff --git a/models/simple/.shaft_collar.step.glb b/models/simple/.shaft_collar.step.glb deleted file mode 100644 index 2d782035..00000000 --- a/models/simple/.shaft_collar.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4658b472e230f2d9bcb046fb5aa895e075313296d262e9a1dfb5eca46f63dcfd -size 272736 diff --git a/models/simple/.small_enclosure_cover.step.glb b/models/simple/.small_enclosure_cover.step.glb deleted file mode 100644 index f77a2903..00000000 --- a/models/simple/.small_enclosure_cover.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:61f5bc9582d4725b78055113d7ab3712fb214af3747314106cea09e07e16f55c -size 271080 diff --git a/models/simple/.spur_gear_blank.step.glb b/models/simple/.spur_gear_blank.step.glb deleted file mode 100644 index bcd2be48..00000000 --- a/models/simple/.spur_gear_blank.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e01e534f679feac9e7cd467fbf74e79ade2399108f90a8170f910b0913e9199f -size 573312 diff --git a/models/simple/.square_mounting_block.step.glb b/models/simple/.square_mounting_block.step.glb deleted file mode 100644 index 57cdd3f5..00000000 --- a/models/simple/.square_mounting_block.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64cadf39cec734c011b802d814b6756c5265888f3d16abe4318f657b533be10d -size 197860 diff --git a/models/simple/.t_slot_slider_block.step.glb b/models/simple/.t_slot_slider_block.step.glb deleted file mode 100644 index c3a90c59..00000000 --- a/models/simple/.t_slot_slider_block.step.glb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d8213a54a17e9c4eaa0438318fa02b6f98d8609b823d27e1ada3d04802caa57 -size 297444 diff --git a/models/simple/README.md b/models/simple/README.md deleted file mode 100644 index fb3ae322..00000000 --- a/models/simple/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Simple CAD Examples - -These examples cover compact part generators that are not already represented -by the benchmark suite. - -The remaining prompts are: - -1. Cylindrical spacer sleeve with a central through-bore and rounded rim edges. -2. Square mounting block with a vertical through-hole and two side clearance holes. -3. Gusset plate with a triangular web, base holes, and softened perimeter edges. -4. Rectangular clamp block with a split slot and two transverse screw holes. -5. Shaft collar with a central bore, radial set-screw hole, and chamfered faces. -6. Pulley wheel with a central hub, outer groove, and circular through-bore. -7. Spur gear blank with central bore, raised hub, and simplified perimeter teeth. -8. Flywheel disk with central bore, annular rim, and lightening holes. -9. Cam follower roller with central bearing bore and rounded outer profile. -10. Small enclosure cover with raised rim, corner screw holes, and shallow recessed center. -11. Cylindrical cap with hollow interior, top boss, and rounded external edges. -12. Retainer plate with elongated slot, two circular holes, and chamfered perimeter. -13. Keyed shaft hub with central bore, keyway slot, and bolt-hole pattern. -14. T-slot slider block with central channel, side relief cuts, and mounting holes. -15. Mounting plate with central circular cutout, elongated side slot, four corner holes, and rounded edges. -16. Basic shape mating test fixture for assembly-helper surface and collision checks. - -The flat rectangular plate, circular flange, L-bracket, U/clevis bracket, and -open-top electronics enclosure examples are intentionally omitted here because -`models/benchmarks/` already carries richer versions. - -## Files - -- `*.step`: Primary STEP files for each simple example. -- `*.py`: Python generators for each example. -- `simple_model_library.py`: Shared build123d implementation helpers used by the generators. diff --git a/models/simple/basic_shape_mating_test_fixture.py b/models/simple/basic_shape_mating_test_fixture.py deleted file mode 100644 index fff1d715..00000000 --- a/models/simple/basic_shape_mating_test_fixture.py +++ /dev/null @@ -1,307 +0,0 @@ -from __future__ import annotations - -from math import sqrt - -from build123d import * -from cadpy.assembly import AssemblyHelper - - -# Units: millimeters. -# Origin: base plate center, XY on plate center plane, +Z upward. -# The cone uses a shallow visible seat because it is taller than the 10 mm base. -# The triangular prism uses a full-depth V-groove so the matching angled faces -# visibly nest while the part remains a separate component. -# Two seats are intentionally over-inserted to exercise the viewer's collision -# analysis overlay. The square block is exactly seated to exercise contact -# surface highlighting. - -BASE_LENGTH = 120.0 -BASE_WIDTH = 70.0 -BASE_THICKNESS = 10.0 -BASE_TOP_Z = BASE_THICKNESS / 2 -CLEARANCE = 0.2 - -PEG_DIAMETER = 16.0 -PEG_HEIGHT = 25.0 -BLOCK_SIZE = 18.0 -CONE_BASE_DIAMETER = 22.0 -CONE_HEIGHT = 25.0 -SPHERE_DIAMETER = 20.0 -PRISM_LENGTH = 35.0 -PRISM_SIDE = 18.0 -PRISM_HEIGHT = sqrt(3.0) * PRISM_SIDE / 2.0 - -POCKET_DEPTH = 3.0 -CONE_SEAT_DEPTH = 5.0 -SPHERE_CUP_RADIUS = SPHERE_DIAMETER / 2.0 + CLEARANCE / 2.0 -V_GROOVE_DEPTH = BASE_THICKNESS -V_GROOVE_SIDE_CLEARANCE = CLEARANCE / 2.0 -V_GROOVE_BOTTOM_WIDTH = 4.0 * V_GROOVE_SIDE_CLEARANCE / sqrt(3.0) -V_GROOVE_TOP_WIDTH = 2.0 * (V_GROOVE_DEPTH + 2.0 * V_GROOVE_SIDE_CLEARANCE) / sqrt(3.0) -PEG_COLLISION_DEPTH = 0.8 -PRISM_COLLISION_DEPTH = 1.0 -BLOCK_CONTACT_OFFSET = 0.0 - -CUBE_TOP_PAD_SIZE = 14.0 -CUBE_TOP_PAD_HEIGHT = 2.5 -CUBE_TOP_POST_RADIUS = 1.8 -CUBE_TOP_POST_HEIGHT = 7.0 -CUBE_TOP_POST_SPACING = 8.0 -CUBE_TOP_CAP_LENGTH = 13.0 -CUBE_TOP_CAP_WIDTH = 4.5 -CUBE_TOP_CAP_HEIGHT = 2.5 -CUBE_TOP_BEAD_DIAMETER = 4.5 - -STATIONS = { - "cylindrical_peg": (-48.0, 0.0), - "square_block": (-24.0, 0.0), - "cone": (0.0, 0.0), - "sphere": (24.0, 0.0), - "triangular_prism": (48.0, 0.0), -} - - -def _triangular_prism(side: float, length: float): - height = sqrt(3.0) * side / 2.0 - with BuildPart() as prism: - with BuildSketch(Plane.XZ): - Polygon( - (-side / 2.0, height / 2.0), - (side / 2.0, height / 2.0), - (0.0, -height / 2.0), - ) - extrude(amount=length) - return prism.part.moved(Location((0.0, length / 2.0, 0.0))) - - -def _v_groove_tool(depth: float, length: float, side_clearance: float): - top_half_width = (depth + 2.0 * side_clearance) / sqrt(3.0) - bottom_half_width = 2.0 * side_clearance / sqrt(3.0) - with BuildPart() as groove: - with BuildSketch(Plane.XZ): - Polygon( - (-top_half_width, 0.0), - (top_half_width, 0.0), - (bottom_half_width, -depth), - (-bottom_half_width, -depth), - ) - extrude(amount=length) - return groove.part.moved(Location((0.0, length / 2.0, 0.0))) - - -def _make_base(): - base = Box(BASE_LENGTH, BASE_WIDTH, BASE_THICKNESS) - - peg_x, peg_y = STATIONS["cylindrical_peg"] - peg_pocket = Cylinder( - radius=(PEG_DIAMETER + CLEARANCE) / 2.0, - height=POCKET_DEPTH + 0.4, - ).moved(Location((peg_x, peg_y, BASE_TOP_Z - POCKET_DEPTH / 2.0 + 0.2))) - base = base - peg_pocket - - block_x, block_y = STATIONS["square_block"] - block_pocket = Box( - BLOCK_SIZE + CLEARANCE, - BLOCK_SIZE + CLEARANCE, - POCKET_DEPTH + 0.4, - ).moved(Location((block_x, block_y, BASE_TOP_Z - POCKET_DEPTH / 2.0 + 0.2))) - base = base - block_pocket - - cone_x, cone_y = STATIONS["cone"] - cone_seat = Cone( - 0.0, - (CONE_BASE_DIAMETER + CLEARANCE) / 2.0, - CONE_SEAT_DEPTH, - ).moved(Location((cone_x, cone_y, BASE_TOP_Z - CONE_SEAT_DEPTH / 2.0))) - base = base - cone_seat - - sphere_x, sphere_y = STATIONS["sphere"] - sphere_cup = Sphere(SPHERE_CUP_RADIUS).moved(Location((sphere_x, sphere_y, BASE_TOP_Z))) - base = base - sphere_cup - - prism_x, prism_y = STATIONS["triangular_prism"] - groove = _v_groove_tool( - V_GROOVE_DEPTH, - PRISM_LENGTH + CLEARANCE, - V_GROOVE_SIDE_CLEARANCE, - ).moved(Location((prism_x, prism_y, 0.0))) - base = base - groove - - base.label = "base_plate" - base.color = Color(0.58, 0.62, 0.66, 1.0) - return base - - -def _make_peg(): - peg = Cylinder(radius=PEG_DIAMETER / 2.0, height=PEG_HEIGHT) - peg.label = "cylindrical_peg" - peg.color = Color(0.86, 0.24, 0.20, 1.0) - return peg - - -def _make_block(): - block = Box(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE) - block.label = "square_block" - block.color = Color(0.18, 0.53, 0.88, 1.0) - return block - - -def _make_cone(): - cone = Cone(0.0, CONE_BASE_DIAMETER / 2.0, CONE_HEIGHT) - cone.label = "cone" - cone.color = Color(0.95, 0.64, 0.16, 1.0) - return cone - - -def _make_sphere(): - sphere = Sphere(SPHERE_DIAMETER / 2.0) - sphere.label = "sphere" - sphere.color = Color(0.45, 0.72, 0.34, 1.0) - return sphere - - -def _make_prism(): - prism = _triangular_prism(PRISM_SIDE, PRISM_LENGTH) - prism.label = "triangular_prism" - prism.color = Color(0.58, 0.34, 0.74, 1.0) - return prism - - -def _make_cube_top_pad(): - pad = Box(CUBE_TOP_PAD_SIZE, CUBE_TOP_PAD_SIZE, CUBE_TOP_PAD_HEIGHT).moved( - Location((0.0, 0.0, CUBE_TOP_PAD_HEIGHT / 2.0)) - ) - pad.label = "cube_top_pad" - pad.color = Color(0.10, 0.14, 0.20, 1.0) - return pad - - -def _make_cube_top_post(x_offset: float, name: str): - post = Cylinder(radius=CUBE_TOP_POST_RADIUS, height=CUBE_TOP_POST_HEIGHT).moved( - Location((x_offset, 0.0, CUBE_TOP_PAD_HEIGHT + CUBE_TOP_POST_HEIGHT / 2.0)) - ) - post.label = name - post.color = Color(0.84, 0.30, 0.50, 1.0) - return post - - -def _make_cube_top_cap(): - cap = Box(CUBE_TOP_CAP_LENGTH, CUBE_TOP_CAP_WIDTH, CUBE_TOP_CAP_HEIGHT).moved( - Location( - ( - 0.0, - 0.0, - CUBE_TOP_PAD_HEIGHT + CUBE_TOP_POST_HEIGHT + CUBE_TOP_CAP_HEIGHT / 2.0, - ) - ) - ) - cap.label = "cube_top_bridge_cap" - cap.color = Color(0.12, 0.70, 0.74, 1.0) - return cap - - -def _make_cube_top_bead(): - bead_radius = CUBE_TOP_BEAD_DIAMETER / 2.0 - bead = Sphere(bead_radius).moved( - Location( - ( - 0.0, - 0.0, - CUBE_TOP_PAD_HEIGHT + CUBE_TOP_POST_HEIGHT + CUBE_TOP_CAP_HEIGHT + bead_radius, - ) - ) - ) - bead.label = "cube_top_bead" - bead.color = Color(0.98, 0.86, 0.20, 1.0) - return bead - - -def _make_cube_top_subassembly(): - subassembly = AssemblyHelper("cube_top_subassembly") - subassembly.add(_make_cube_top_pad(), "cube_top_pad") - subassembly.add( - _make_cube_top_post(-CUBE_TOP_POST_SPACING / 2.0, "cube_top_left_post"), - "cube_top_left_post", - ) - subassembly.add( - _make_cube_top_post(CUBE_TOP_POST_SPACING / 2.0, "cube_top_right_post"), - "cube_top_right_post", - ) - subassembly.add(_make_cube_top_cap(), "cube_top_bridge_cap") - subassembly.add(_make_cube_top_bead(), "cube_top_bead") - return subassembly.build() - - -def gen_step(): - asm = AssemblyHelper("basic_shape_mating_test_fixture") - - base = asm.add(_make_base(), "base_plate") - peg = asm.add(_make_peg(), "cylindrical_peg") - block = asm.add(_make_block(), "square_block") - cone = asm.add(_make_cone(), "cone") - sphere = asm.add(_make_sphere(), "sphere") - prism = asm.add(_make_prism(), "triangular_prism") - cube_top_subassembly = asm.add( - _make_cube_top_subassembly(), - "cube_top_subassembly", - ) - - peg_x, peg_y = STATIONS["cylindrical_peg"] - peg_seat = asm.rigid_frame(base, "peg_pocket_floor", Location((peg_x, peg_y, BASE_TOP_Z - POCKET_DEPTH))) - peg_bottom = asm.rigid_frame(peg, "bottom_center", Location((0.0, 0.0, -PEG_HEIGHT / 2.0))) - asm.face_to_face(peg_seat, peg_bottom, offset=-PEG_COLLISION_DEPTH) - - block_x, block_y = STATIONS["square_block"] - block_seat = asm.rigid_frame( - base, - "block_pocket_floor", - Location((block_x, block_y, BASE_TOP_Z - POCKET_DEPTH)), - ) - block_bottom = asm.rigid_frame(block, "bottom_center", Location((0.0, 0.0, -BLOCK_SIZE / 2.0))) - asm.face_to_face(block_seat, block_bottom, offset=BLOCK_CONTACT_OFFSET) - - asm.rigid_frame(block, "top_center", Location((0.0, 0.0, BLOCK_SIZE / 2.0))) - cube_top_seat = asm.rigid_frame( - base, - "cube_top_subassembly_seat", - Location((block_x, block_y, BASE_TOP_Z - POCKET_DEPTH + BLOCK_CONTACT_OFFSET + BLOCK_SIZE)), - ) - cube_top_subassembly_bottom = asm.rigid_frame( - cube_top_subassembly, - "bottom_center", - Location((0.0, 0.0, 0.0)), - ) - asm.face_to_face( - cube_top_seat, - cube_top_subassembly_bottom, - label="cube_top_subassembly_on_square_block", - ) - - cone_x, cone_y = STATIONS["cone"] - cone_tip_seat = asm.rigid_frame( - base, - "cone_countersink_tip", - Location((cone_x, cone_y, BASE_TOP_Z - CONE_SEAT_DEPTH)), - ) - cone_tip = asm.rigid_frame(cone, "tip", Location((0.0, 0.0, -CONE_HEIGHT / 2.0))) - asm.coaxial(cone_tip_seat, cone_tip, offset=0.1) - - sphere_x, sphere_y = STATIONS["sphere"] - sphere_cup_center = asm.rigid_frame(base, "sphere_cup_center", Location((sphere_x, sphere_y, BASE_TOP_Z))) - sphere_center = asm.rigid_frame(sphere, "center", Location((0.0, 0.0, 0.0))) - asm.coaxial(sphere_cup_center, sphere_center, offset=0.1) - - prism_x, prism_y = STATIONS["triangular_prism"] - prism_v_bottom = asm.rigid_frame( - base, - "v_groove_bottom", - Location((prism_x, prism_y, BASE_TOP_Z - V_GROOVE_DEPTH)), - ) - prism_bottom_vertex = asm.rigid_frame( - prism, - "bottom_vertex", - Location((0.0, 0.0, -PRISM_HEIGHT / 2.0)), - ) - asm.face_to_face(prism_v_bottom, prism_bottom_vertex, offset=-PRISM_COLLISION_DEPTH) - - return asm.build() diff --git a/models/simple/basic_shape_mating_test_fixture.step b/models/simple/basic_shape_mating_test_fixture.step deleted file mode 100644 index e35090b1..00000000 --- a/models/simple/basic_shape_mating_test_fixture.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:edcb73a7aeb10615b5449cc99bd726268b31ef238ffc359d0c4e52e5bdf5ff8b -size 156314 diff --git a/models/simple/cam_follower_roller.py b/models/simple/cam_follower_roller.py deleted file mode 100644 index 115bf91d..00000000 --- a/models/simple/cam_follower_roller.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Cam follower roller with central bearing bore and rounded outer profile. - -from simple_model_library import make_cam_follower_roller - - -def gen_step(): - return make_cam_follower_roller() diff --git a/models/simple/cam_follower_roller.step b/models/simple/cam_follower_roller.step deleted file mode 100644 index 6d18acd8..00000000 --- a/models/simple/cam_follower_roller.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da7988b2d483cc980a992c9587f23b98c1330ea3e6724a76810609088e199101 -size 23872 diff --git a/models/simple/cylindrical_cap.py b/models/simple/cylindrical_cap.py deleted file mode 100644 index 285634e6..00000000 --- a/models/simple/cylindrical_cap.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Cylindrical cap with hollow interior, top boss, and rounded external edges. - -from simple_model_library import make_cylindrical_cap - - -def gen_step(): - return make_cylindrical_cap() diff --git a/models/simple/cylindrical_cap.step b/models/simple/cylindrical_cap.step deleted file mode 100644 index 6a5413ba..00000000 --- a/models/simple/cylindrical_cap.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1b237279481b097db7793a53718d7bafd164324de2008d8c8947baf3f6b94fff -size 31035 diff --git a/models/simple/cylindrical_spacer_sleeve.py b/models/simple/cylindrical_spacer_sleeve.py deleted file mode 100644 index 26248175..00000000 --- a/models/simple/cylindrical_spacer_sleeve.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Cylindrical spacer sleeve with a central through-bore and rounded rim edges. - -from simple_model_library import make_cylindrical_spacer_sleeve - - -def gen_step(): - return make_cylindrical_spacer_sleeve() diff --git a/models/simple/cylindrical_spacer_sleeve.step b/models/simple/cylindrical_spacer_sleeve.step deleted file mode 100644 index 061a00ce..00000000 --- a/models/simple/cylindrical_spacer_sleeve.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d591885636e0d95af93b9987bb3d5f016878e92a26d66da6731a40da7297cb0 -size 23885 diff --git a/models/simple/flywheel_disk.py b/models/simple/flywheel_disk.py deleted file mode 100644 index 6b079b49..00000000 --- a/models/simple/flywheel_disk.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Flywheel disk with central bore, annular rim, and lightening holes. - -from simple_model_library import make_flywheel_disk - - -def gen_step(): - return make_flywheel_disk() diff --git a/models/simple/flywheel_disk.step b/models/simple/flywheel_disk.step deleted file mode 100644 index 20d5f3a2..00000000 --- a/models/simple/flywheel_disk.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb9623b0dc6d2e901a0d106497993561d98ebc6ca48b38fcfb046da5bea33973 -size 99180 diff --git a/models/simple/gusset_plate.py b/models/simple/gusset_plate.py deleted file mode 100644 index 630721f1..00000000 --- a/models/simple/gusset_plate.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Gusset plate with a triangular web, base holes, and softened perimeter edges. - -from simple_model_library import make_gusset_plate - - -def gen_step(): - return make_gusset_plate() diff --git a/models/simple/gusset_plate.step b/models/simple/gusset_plate.step deleted file mode 100644 index b73f1f60..00000000 --- a/models/simple/gusset_plate.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78043648cb7ed61b1a0837ee69506f1a463cd26fce62649906bd5e3a42c5eb92 -size 144041 diff --git a/models/simple/keyed_shaft_hub.py b/models/simple/keyed_shaft_hub.py deleted file mode 100644 index bf1857ca..00000000 --- a/models/simple/keyed_shaft_hub.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Keyed shaft hub with central bore, keyway slot, and bolt-hole pattern. - -from simple_model_library import make_keyed_shaft_hub - - -def gen_step(): - return make_keyed_shaft_hub() diff --git a/models/simple/keyed_shaft_hub.step b/models/simple/keyed_shaft_hub.step deleted file mode 100644 index d15a8dbd..00000000 --- a/models/simple/keyed_shaft_hub.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fdd480145af3a2666d3a5232d3fd44a5af2e1cc079ad01510ba30b6ef65977e4 -size 78311 diff --git a/models/simple/mounting_plate.py b/models/simple/mounting_plate.py deleted file mode 100644 index 707690b5..00000000 --- a/models/simple/mounting_plate.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Mounting plate with central circular cutout, elongated side slot, four corner holes, and rounded edges. - -from simple_model_library import make_mounting_plate - - -def gen_step(): - return make_mounting_plate() diff --git a/models/simple/mounting_plate.step b/models/simple/mounting_plate.step deleted file mode 100644 index cfd2090c..00000000 --- a/models/simple/mounting_plate.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:93702accf447872799ff117900d409d00667f6215564342d442650160f16e55e -size 137219 diff --git a/models/simple/pulley_wheel.py b/models/simple/pulley_wheel.py deleted file mode 100644 index b43ee934..00000000 --- a/models/simple/pulley_wheel.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Pulley wheel with a central hub, outer groove, and circular through-bore. - -from simple_model_library import make_pulley_wheel - - -def gen_step(): - return make_pulley_wheel() diff --git a/models/simple/pulley_wheel.step b/models/simple/pulley_wheel.step deleted file mode 100644 index ada86202..00000000 --- a/models/simple/pulley_wheel.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e5cde67cb0d6cf7008634b878a76ee30f2be3c652ff4fd91e0aef7d11cfb1de4 -size 78410 diff --git a/models/simple/rectangular_clamp_block.py b/models/simple/rectangular_clamp_block.py deleted file mode 100644 index b603b0fe..00000000 --- a/models/simple/rectangular_clamp_block.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Rectangular clamp block with a split slot and two transverse screw holes. - -from simple_model_library import make_rectangular_clamp_block - - -def gen_step(): - return make_rectangular_clamp_block() diff --git a/models/simple/rectangular_clamp_block.step b/models/simple/rectangular_clamp_block.step deleted file mode 100644 index bc55ba2b..00000000 --- a/models/simple/rectangular_clamp_block.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7249c7aaa98452b60101e6cd97616d80c35b53eb283f066364120bc44361b54b -size 150504 diff --git a/models/simple/retainer_plate.py b/models/simple/retainer_plate.py deleted file mode 100644 index bc3fee0c..00000000 --- a/models/simple/retainer_plate.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Retainer plate with elongated slot, two circular holes, and chamfered perimeter. - -from simple_model_library import make_retainer_plate - - -def gen_step(): - return make_retainer_plate() diff --git a/models/simple/retainer_plate.step b/models/simple/retainer_plate.step deleted file mode 100644 index b83e6ec8..00000000 --- a/models/simple/retainer_plate.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:09c1bca17ba3773d6f906a86bf5464a89559f8a66c7f5badaa9d406d643d4a39 -size 112168 diff --git a/models/simple/shaft_collar.py b/models/simple/shaft_collar.py deleted file mode 100644 index e4b8f428..00000000 --- a/models/simple/shaft_collar.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Shaft collar with a central bore, radial set-screw hole, and chamfered faces. - -from simple_model_library import make_shaft_collar - - -def gen_step(): - return make_shaft_collar() diff --git a/models/simple/shaft_collar.step b/models/simple/shaft_collar.step deleted file mode 100644 index e7adf30e..00000000 --- a/models/simple/shaft_collar.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:484ad2ba9bd41c56e74e01188489cd71ba793d53505689e4251dbd6111f739aa -size 52898 diff --git a/models/simple/simple_model_library.py b/models/simple/simple_model_library.py deleted file mode 100644 index 1f83dbe5..00000000 --- a/models/simple/simple_model_library.py +++ /dev/null @@ -1,451 +0,0 @@ -# Prompt: Shared implementation library for the simple CAD prompt scripts. - -from math import cos, sin, tau - -from build123d import ( - Align, - Box, - BuildPart, - BuildSketch, - Color, - Cylinder, - Location, - Locations, - Mode, - Plane, - Polygon, - RectangleRounded, - Torus, - add, - chamfer, - extrude, - fillet, -) - - -def _polar_point(radius: float, angle: float) -> tuple[float, float]: - return (radius * cos(angle), radius * sin(angle)) - - -def _safe_fillet(shape, edges, radius: float): - selected = list(edges) - if not selected: - return shape - try: - return fillet(selected, radius=radius) - except Exception: - return shape - - -def _safe_chamfer(shape, edges, length: float): - selected = list(edges) - if not selected: - return shape - try: - return chamfer(selected, length=length) - except Exception: - return shape - - -def _line_edges(shape): - return [edge for edge in shape.edges() if str(edge.geom_type).endswith("LINE")] - - -def _circular_edges(shape, *, radius: float | None = None, axis: str | None = None, coordinate: float | None = None): - edges = [] - for edge in shape.edges(): - try: - edge_radius = edge.radius - edge_center = edge.arc_center - edge_normal = edge.normal() - except Exception: - continue - - if radius is not None and abs(edge_radius - radius) > 0.08: - continue - - if axis is not None: - components = { - "x": abs(edge_normal.X), - "y": abs(edge_normal.Y), - "z": abs(edge_normal.Z), - } - if components[axis] < 1.0 - 1e-4: - continue - - if coordinate is not None: - coord = {"x": edge_center.X, "y": edge_center.Y, "z": edge_center.Z}[axis or "z"] - if abs(coord - coordinate) > 0.08: - continue - - edges.append(edge) - return edges - - -def _finish(shape, label: str, color: Color): - shape.label = label - shape.color = color - return shape - - -def _hole_z(diameter: float, height: float, x: float, y: float, z_min: float = -1.0): - with Locations(Location((x, y, z_min))): - Cylinder( - radius=diameter / 2.0, - height=height, - align=(Align.CENTER, Align.CENTER, Align.MIN), - mode=Mode.SUBTRACT, - ) - - -def _hole_x(diameter: float, length: float, x: float, y: float, z: float): - with Locations(Location((x, y, z))): - Cylinder( - radius=diameter / 2.0, - height=length, - rotation=(0.0, 90.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - - -def _hole_y(diameter: float, length: float, x: float, y: float, z: float): - with Locations(Location((x, y, z))): - Cylinder( - radius=diameter / 2.0, - height=length, - rotation=(90.0, 0.0, 0.0), - align=(Align.CENTER, Align.CENTER, Align.CENTER), - mode=Mode.SUBTRACT, - ) - - -def _slot_z(length: float, width: float, height: float, center: tuple[float, float], z_min: float, *, axis: str = "x"): - span = max(0.0, length - width) - if axis == "x": - if span > 0.01: - with Locations(Location((center[0], center[1], z_min))): - Box(span, width, height, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for x_offset in (-span / 2.0, span / 2.0): - _hole_z(width, height, center[0] + x_offset, center[1], z_min) - else: - if span > 0.01: - with Locations(Location((center[0], center[1], z_min))): - Box(width, span, height, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for y_offset in (-span / 2.0, span / 2.0): - _hole_z(width, height, center[0], center[1] + y_offset, z_min) - - -def _trapezoid_tooth_profile( - *, - teeth: int, - root_radius: float, - tip_radius: float, - phase: float, - root_span_fraction: float = 0.72, - tip_span_fraction: float = 0.38, -): - points = [] - pitch_angle = tau / teeth - for tooth_index in range(teeth): - center_angle = phase + tooth_index * pitch_angle - points.extend( - ( - _polar_point(root_radius, center_angle - root_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle - tip_span_fraction * pitch_angle / 2.0), - _polar_point(tip_radius, center_angle + tip_span_fraction * pitch_angle / 2.0), - _polar_point(root_radius, center_angle + root_span_fraction * pitch_angle / 2.0), - ) - ) - return points - - -def make_cylindrical_spacer_sleeve(): - outer_radius = 15.0 - bore_radius = 5.0 - height = 28.0 - - with BuildPart() as model: - Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _hole_z(bore_radius * 2.0, height + 2.0, 0.0, 0.0) - - rim_edges = [] - for z_pos in (0.0, height): - rim_edges.extend(_circular_edges(model.part, radius=outer_radius, axis="z", coordinate=z_pos)) - rim_edges.extend(_circular_edges(model.part, radius=bore_radius, axis="z", coordinate=z_pos)) - part = _safe_fillet(model.part, rim_edges, 1.1) - return _finish(part, "cylindrical_spacer_sleeve_through_bore_rounded_rims", Color(0.68, 0.68, 0.64, 1.0)) - - -def make_square_mounting_block(): - size = 48.0 - height = 28.0 - - with BuildPart() as model: - Box(size, size, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _hole_z(12.0, height + 2.0, 0.0, 0.0) - for y_pos in (-14.0, 14.0): - _hole_x(5.5, size + 2.0, 0.0, y_pos, height / 2.0) - - part = _safe_chamfer(model.part, _line_edges(model.part), 0.8) - return _finish(part, "square_mounting_block_vertical_and_side_clearance_holes", Color(0.50, 0.58, 0.64, 1.0)) - - -def make_gusset_plate(): - base_length = 88.0 - base_width = 30.0 - base_thickness = 6.0 - web_thickness = 8.0 - web_height = 48.0 - - with BuildPart() as web: - with BuildSketch(Plane.YZ): - Polygon( - [ - (-12.0, base_thickness), - (12.0, base_thickness), - (12.0, base_thickness + web_height), - ], - align=None, - ) - extrude(amount=web_thickness) - web_part = web.part.moved(Location((-web_thickness / 2.0, 0.0, 0.0))) - - with BuildPart() as model: - Box(base_length, base_width, base_thickness, align=(Align.CENTER, Align.CENTER, Align.MIN)) - add(web_part, mode=Mode.ADD) - for x_pos in (-28.0, 28.0): - _hole_z(6.0, base_thickness + 2.0, x_pos, -6.0) - - part = _safe_fillet(model.part, _line_edges(model.part), 0.9) - return _finish(part, "gusset_plate_triangular_web_base_holes_softened_edges", Color(0.58, 0.63, 0.58, 1.0)) - - -def make_rectangular_clamp_block(): - length = 70.0 - width = 34.0 - height = 28.0 - - with BuildPart() as model: - Box(length, width, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - with Locations(Location((0.0, 0.0, -1.0))): - Box(length + 2.0, 4.0, height + 2.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for x_pos in (-20.0, 20.0): - _hole_y(5.5, width + 2.0, x_pos, 0.0, height / 2.0) - - part = _safe_chamfer(model.part, _line_edges(model.part), 0.7) - return _finish(part, "rectangular_clamp_block_split_slot_two_transverse_screw_holes", Color(0.57, 0.60, 0.64, 1.0)) - - -def make_shaft_collar(): - outer_radius = 21.0 - bore_radius = 10.0 - height = 16.0 - - with BuildPart() as model: - Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _hole_z(bore_radius * 2.0, height + 2.0, 0.0, 0.0) - _hole_x(5.0, outer_radius + 3.0, outer_radius / 2.0, 0.0, height / 2.0) - - edge_set = [] - for z_pos in (0.0, height): - edge_set.extend(_circular_edges(model.part, axis="z", coordinate=z_pos)) - part = _safe_chamfer(model.part, edge_set, 0.8) - return _finish(part, "shaft_collar_bore_radial_set_screw_hole_chamfered_faces", Color(0.46, 0.50, 0.55, 1.0)) - - -def make_pulley_wheel(): - outer_radius = 35.0 - thickness = 16.0 - hub_radius = 18.0 - hub_height = 24.0 - - with BuildPart() as model: - Cylinder(outer_radius, thickness, align=(Align.CENTER, Align.CENTER, Align.MIN)) - with Locations(Location((0.0, 0.0, thickness / 2.0))): - Cylinder(hub_radius, hub_height, align=(Align.CENTER, Align.CENTER, Align.CENTER), mode=Mode.ADD) - Torus(outer_radius - 1.2, 3.0, mode=Mode.SUBTRACT) - _hole_z(10.0, hub_height + 4.0, 0.0, 0.0, z_min=-4.0) - - part = _safe_fillet(model.part, _circular_edges(model.part, axis="z"), 0.8) - return _finish(part, "pulley_wheel_central_hub_outer_groove_through_bore", Color(0.42, 0.46, 0.50, 1.0)) - - -def make_spur_gear_blank(): - thickness = 10.0 - teeth = 24 - root_radius = 35.0 - tip_radius = 39.0 - hub_radius = 17.0 - hub_height = 16.0 - - with BuildPart() as model: - with BuildSketch(Plane.XY): - Polygon( - _trapezoid_tooth_profile( - teeth=teeth, - root_radius=root_radius, - tip_radius=tip_radius, - phase=-tau / teeth / 2.0, - ), - align=None, - ) - extrude(amount=thickness) - with Locations(Location((0.0, 0.0, -3.0))): - Cylinder(hub_radius, hub_height, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.ADD) - _hole_z(18.0, hub_height + 4.0, 0.0, 0.0, z_min=-4.0) - - part = _safe_chamfer(model.part, _circular_edges(model.part, axis="z"), 0.6) - return _finish(part, "spur_gear_blank_central_bore_raised_hub_simplified_teeth", Color(0.70, 0.58, 0.36, 1.0)) - - -def make_flywheel_disk(): - radius = 45.0 - thickness = 12.0 - - with BuildPart() as model: - Cylinder(radius, thickness, align=(Align.CENTER, Align.CENTER, Align.MIN)) - with Locations(Location((0.0, 0.0, thickness - 2.2))): - Cylinder(34.0, 3.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - with Locations(Location((0.0, 0.0, -0.8))): - Cylinder(34.0, 3.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - _hole_z(18.0, thickness + 2.0, 0.0, 0.0) - for index in range(6): - x_pos, y_pos = _polar_point(25.0, tau * index / 6) - _hole_z(10.0, thickness + 2.0, x_pos, y_pos) - - part = _safe_fillet(model.part, _circular_edges(model.part, axis="z"), 0.8) - return _finish(part, "flywheel_disk_central_bore_annular_rim_lightening_holes", Color(0.59, 0.61, 0.63, 1.0)) - - -def make_cam_follower_roller(): - radius = 16.0 - width = 18.0 - - with BuildPart() as model: - Cylinder(radius, width, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _hole_z(12.0, width + 2.0, 0.0, 0.0) - - edge_set = [] - for z_pos in (0.0, width): - edge_set.extend(_circular_edges(model.part, radius=radius, axis="z", coordinate=z_pos)) - edge_set.extend(_circular_edges(model.part, radius=6.0, axis="z", coordinate=z_pos)) - part = _safe_fillet(model.part, edge_set, 1.8) - return _finish(part, "cam_follower_roller_bearing_bore_rounded_outer_profile", Color(0.60, 0.64, 0.67, 1.0)) - - -def make_small_enclosure_cover(): - length = 90.0 - width = 60.0 - base_thickness = 4.0 - rim_height = 3.0 - - with BuildPart() as model: - with BuildSketch(Plane.XY): - RectangleRounded(length, width, 4.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=base_thickness) - with Locations(Location((0.0, 0.0, base_thickness))): - with BuildSketch(Plane.XY): - RectangleRounded(length - 10.0, width - 10.0, 3.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=rim_height, mode=Mode.ADD) - with Locations(Location((0.0, 0.0, base_thickness - 0.8))): - Box(58.0, 28.0, rim_height + 1.2, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - with Locations(Location((0.0, 0.0, base_thickness))): - Box(66.0, 36.0, rim_height + 1.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for x_pos in (-35.0, 35.0): - for y_pos in (-22.0, 22.0): - _hole_z(4.2, base_thickness + 1.5, x_pos, y_pos) - - part = _safe_fillet(model.part, _line_edges(model.part), 0.5) - return _finish(part, "small_enclosure_cover_raised_rim_corner_screw_holes_recessed_center", Color(0.38, 0.51, 0.62, 1.0)) - - -def make_cylindrical_cap(): - outer_radius = 21.0 - height = 28.0 - wall = 3.0 - top_thickness = 4.0 - - with BuildPart() as model: - Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - with Locations(Location((0.0, 0.0, -1.0))): - Cylinder(outer_radius - wall, height - top_thickness + 1.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - with Locations(Location((0.0, 0.0, height))): - Cylinder(10.0, 6.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.ADD) - - part = _safe_fillet(model.part, _circular_edges(model.part, axis="z"), 1.0) - return _finish(part, "cylindrical_cap_hollow_interior_top_boss_rounded_external_edges", Color(0.52, 0.57, 0.61, 1.0)) - - -def make_retainer_plate(): - length = 82.0 - width = 34.0 - thickness = 6.0 - - with BuildPart() as model: - Box(length, width, thickness, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _slot_z(36.0, 10.0, thickness + 2.0, (0.0, 0.0), -1.0) - for x_pos in (-30.0, 30.0): - _hole_z(7.0, thickness + 2.0, x_pos, 0.0) - - part = _safe_chamfer(model.part, _line_edges(model.part), 0.8) - return _finish(part, "retainer_plate_elongated_slot_two_holes_chamfered_perimeter", Color(0.60, 0.62, 0.66, 1.0)) - - -def make_keyed_shaft_hub(): - outer_radius = 27.0 - height = 22.0 - bore_radius = 10.0 - - with BuildPart() as model: - Cylinder(outer_radius, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - _hole_z(bore_radius * 2.0, height + 2.0, 0.0, 0.0) - with Locations(Location((0.0, bore_radius + 2.0, -1.0))): - Box(6.0, 7.0, height + 2.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for index in range(4): - x_pos, y_pos = _polar_point(19.5, tau * index / 4.0 + tau / 8.0) - _hole_z(5.5, height + 2.0, x_pos, y_pos) - - part = _safe_chamfer(model.part, _circular_edges(model.part, axis="z"), 0.7) - return _finish(part, "keyed_shaft_hub_central_bore_keyway_bolt_hole_pattern", Color(0.49, 0.52, 0.56, 1.0)) - - -def make_t_slot_slider_block(): - length = 80.0 - width = 36.0 - height = 18.0 - - with BuildPart() as model: - Box(length, width, height, align=(Align.CENTER, Align.CENTER, Align.MIN)) - with Locations(Location((0.0, 0.0, 6.0))): - Box(length + 2.0, 24.0, 6.0, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - with Locations(Location((0.0, 0.0, 6.0))): - Box(length + 2.0, 10.0, height, align=(Align.CENTER, Align.CENTER, Align.MIN), mode=Mode.SUBTRACT) - for y_pos in (-18.0, 18.0): - with Locations(Location((0.0, y_pos, 8.0))): - Box(48.0, 5.0, 8.0, align=(Align.CENTER, Align.CENTER, Align.CENTER), mode=Mode.SUBTRACT) - for x_pos in (-28.0, 28.0): - for y_pos in (-14.0, 14.0): - _hole_z(4.5, height + 2.0, x_pos, y_pos) - - part = _safe_chamfer(model.part, _line_edges(model.part), 0.5) - return _finish(part, "t_slot_slider_block_central_channel_side_reliefs_mounting_holes", Color(0.47, 0.53, 0.55, 1.0)) - - -def make_mounting_plate(): - length = 100.0 - width = 70.0 - thickness = 8.0 - - with BuildPart() as model: - with BuildSketch(Plane.XY): - RectangleRounded(length, width, 5.0, align=(Align.CENTER, Align.CENTER)) - extrude(amount=thickness) - _hole_z(28.0, thickness + 2.0, 0.0, 0.0) - _slot_z(30.0, 8.0, thickness + 2.0, (32.0, 0.0), -1.0, axis="y") - for x_pos in (-38.0, 38.0): - for y_pos in (-24.0, 24.0): - _hole_z(5.5, thickness + 2.0, x_pos, y_pos) - - part = _safe_fillet(model.part, _line_edges(model.part), 0.8) - return _finish(part, "mounting_plate_central_cutout_side_slot_four_corner_holes_rounded_edges", Color(0.58, 0.65, 0.69, 1.0)) diff --git a/models/simple/small_enclosure_cover.py b/models/simple/small_enclosure_cover.py deleted file mode 100644 index cff6a209..00000000 --- a/models/simple/small_enclosure_cover.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Small enclosure cover with raised rim, corner screw holes, and shallow recessed center. - -from simple_model_library import make_small_enclosure_cover - - -def gen_step(): - return make_small_enclosure_cover() diff --git a/models/simple/small_enclosure_cover.step b/models/simple/small_enclosure_cover.step deleted file mode 100644 index c4db584a..00000000 --- a/models/simple/small_enclosure_cover.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b9b9283a21e60e9961f6276fd887093e7e180af38b58346ec73946ad4d78396 -size 62599 diff --git a/models/simple/spur_gear_blank.py b/models/simple/spur_gear_blank.py deleted file mode 100644 index 01b8399c..00000000 --- a/models/simple/spur_gear_blank.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Spur gear blank with central bore, raised hub, and simplified perimeter teeth. - -from simple_model_library import make_spur_gear_blank - - -def gen_step(): - return make_spur_gear_blank() diff --git a/models/simple/spur_gear_blank.step b/models/simple/spur_gear_blank.step deleted file mode 100644 index ad0f6b2c..00000000 --- a/models/simple/spur_gear_blank.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cb93dd5516f1e6ebe83febc8795a70cc05008e4217b43cbe9b1a87181df8d4c7 -size 400871 diff --git a/models/simple/square_mounting_block.py b/models/simple/square_mounting_block.py deleted file mode 100644 index d4670b92..00000000 --- a/models/simple/square_mounting_block.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: Square mounting block with a vertical through-hole and two side clearance holes. - -from simple_model_library import make_square_mounting_block - - -def gen_step(): - return make_square_mounting_block() diff --git a/models/simple/square_mounting_block.step b/models/simple/square_mounting_block.step deleted file mode 100644 index df96e7fb..00000000 --- a/models/simple/square_mounting_block.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0ad5e1f7227d0759dacf1184d310cdd4e5b3832a60b609a6e83ce1e6ad66f3e4 -size 77492 diff --git a/models/simple/t_slot_slider_block.py b/models/simple/t_slot_slider_block.py deleted file mode 100644 index a18af584..00000000 --- a/models/simple/t_slot_slider_block.py +++ /dev/null @@ -1,7 +0,0 @@ -# Prompt: T-slot slider block with central channel, side relief cuts, and mounting holes. - -from simple_model_library import make_t_slot_slider_block - - -def gen_step(): - return make_t_slot_slider_block() diff --git a/models/simple/t_slot_slider_block.step b/models/simple/t_slot_slider_block.step deleted file mode 100644 index 6b9119db..00000000 --- a/models/simple/t_slot_slider_block.step +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fd67187450ecd6d6ef769f07d853781482314e10b87791be02b13306d3c744b0 -size 122834 diff --git a/plugins/cad/skills/bambu-labs b/plugins/cad/skills/bambu-labs deleted file mode 120000 index 8ef6e683..00000000 --- a/plugins/cad/skills/bambu-labs +++ /dev/null @@ -1 +0,0 @@ -../../../skills/bambu-labs \ No newline at end of file diff --git a/plugins/cad/skills/bambu-labs/LICENSE b/plugins/cad/skills/bambu-labs/LICENSE new file mode 100644 index 00000000..eabcff2c --- /dev/null +++ b/plugins/cad/skills/bambu-labs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 earthtojake + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cad/skills/bambu-labs/SKILL.md b/plugins/cad/skills/bambu-labs/SKILL.md new file mode 100644 index 00000000..e9bc7154 --- /dev/null +++ b/plugins/cad/skills/bambu-labs/SKILL.md @@ -0,0 +1,209 @@ +--- +name: bambu-labs +description: Dry-run, upload, and cautiously initiate local Bambu Lab print jobs from validated plain `.gcode`, using Bambu LAN FTPS/MQTT handoffs. +--- + +# Bambu Labs + +Provenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad). +Use the installed local skill files as the runtime source of truth; the +repository link is only for provenance and release review. + +Use this skill for local-network Bambu Lab print handoffs after a plain `.gcode` +file already exists and has been validated. This skill does not slice models. + +## Safety Rules + +- Default to dry-run plans. Real printer traffic requires `--execute`. +- Never start a print without `--execute --confirm-start-print`. +- Pause and cancel controls are live printer requests; default to dry-run plans. + Canceling a print requires `--execute --confirm-cancel-print`. +- Treat an explicit user request to print or start a specific job as live-start + authorization; do not pause for a second confirmation solely for physical + checks. Still validate the G-code, inspect the dry-run payload, read printer + status, prefer upload-only before upload-start, state the physical checks, and + stop if validation/status/intent is unsafe or ambiguous. +- Do not ask for the printer serial by default; fetch it from the printer TLS certificate with `serial` or let `send` cache it. +- Prefer workspace-root `bambu-printers.json` over repeating access codes in commands. The file is local config and should be ignored by Git. +- Before a live start, state the physical checks: clear build plate, correct plate/filament/nozzle, safe surroundings, and operator nearby. +- Publishing MQTT is only a start request. Confirm acceptance with printer status/UI and physical observation. + +## CAD Viewer Handoff + +After completing Bambu work that creates or modifies a local supported print artifact such as `.gcode` or `.3mf`, you must ALWAYS hand the explicit file path to `$cad-viewer` when that skill is installed. `$cad-viewer` must start CAD Viewer if it is not already running and return link(s) to the relevant created or updated file(s); if `$cad-viewer` is unavailable or startup fails, report that instead of silently omitting the handoff. + +## Workflow + +1. Generate and validate plain G-code with `$gcode`. + If no slicer is installed, install OrcaSlicer and retry; do not treat the missing slicer as a blocker. On macOS, prefer `brew install --cask orcaslicer`. +2. Configure the printer. The user can either give the IP/access code in the thread and let the agent write JSON, or edit `bambu-printers.json` directly. + For a new printer setup or onboarding request, read + `references/new-printer-onboarding.md` first. Walk the user through the + model-specific touchscreen steps to find the IP and LAN access code, and make + **Enable LAN Only** plus **Enable Developer Mode** explicit before running + local start workflows. + +```bash +python scripts/bambu_lan_print.py config set \ + --printer a1-mini \ + --host 192.168.1.34 \ + --access-code 12345678 \ + --model a1-mini \ + --fetch-serial +``` + +Manual JSON shape: + +```json +{ + "printers": { + "a1-mini": { + "host": "192.168.1.34", + "access_code": "12345678", + "model": "a1-mini" + } + } +} +``` + +On A1/A1 Mini, find the IP and LAN access code on the printer touchscreen under +network/LAN settings. Enable LAN Only and Developer Mode when offered, then +power-cycle before retrying local start commands. + +3. Read status before live work: + +```bash +python scripts/bambu_lan_print.py status \ + --printer a1-mini \ + --push-all \ + --wait-seconds 10 +``` + +4. Dry-run the exact handoff, inspect the JSON payload, then run upload-only. +Only after upload succeeds should you run upload-start. If the user explicitly +asked to print or start the job, proceed to `upload-start --execute +--confirm-start-print` after the validation, status, and upload checks pass. If +the user only asked to prepare, slice, upload, or review, stop before the start +request. + +## Handoff Modes + +`--handoff template-project` is the validated A1 Mini path from this repo's LAN +debugging. It starts from validated plain `.gcode`, copies a known-good +same-printer `.gcode.3mf` template, replaces `Metadata/plate_N.gcode`, writes +the plate MD5, uploads the project to the FTPS root, and publishes +`print.project_file` with `url: ftp:///.gcode.3mf`. + +```bash +python scripts/bambu_lan_print.py send \ + --printer a1-mini \ + --gcode /tmp/job.gcode \ + --handoff template-project \ + --template-project /path/to/same-printer-template.gcode.3mf \ + --action upload-start +``` + +Execute after review when the user explicitly asked to print or start, or after +physical confirmation when intent is unclear: + +```bash +python scripts/bambu_lan_print.py send \ + --printer a1-mini \ + --gcode /tmp/job.gcode \ + --handoff template-project \ + --template-project /path/to/same-printer-template.gcode.3mf \ + --action upload-start \ + --execute \ + --confirm-start-print +``` + +`--handoff plain` uploads `cache/.gcode` and publishes +`print.gcode_file`. Keep it for diagnostics or printers/firmware where this is +known to work. On the tested A1 Mini, direct plain G-code was uploaded +successfully but `gcode_file` failed or was ignored, so do not use it as the +A1 Mini live-start path. + +`--handoff bambox-project` packages plain `.gcode` with `bambox`, uploads the +`.gcode.3mf` project to FTPS root, and publishes `print.project_file`. +Currently enabled only for `p1s-0.4` with `PLA`, `ASA`, or `PETG-CF`. +Known but disabled until validated profiles exist: `a1-mini-0.4`, `a1-0.4`, +`x1c-0.4`, and `p1p-0.4`. + +## Common Debugging Commands + +Fetch/cache serial: + +```bash +python scripts/bambu_lan_print.py serial \ + --printer a1-mini \ + --json +``` + +Clear a stale printer error after fixing the underlying cause: + +```bash +python scripts/bambu_lan_print.py clear-error \ + --printer a1-mini \ + --execute +``` + +Use `--mqtt-qos 1 --wait-after-publish 10` on `send` when debugging whether the +printer acknowledged the MQTT publish and what status it reported immediately +afterward. + +## Print Controls + +For a running print, use dedicated print-control commands rather than ad hoc +MQTT snippets. These commands publish only a control request; they do not upload +files or start a new job. Read status after execution to confirm the printer +state changed. + +Dry-run pause payload: + +```bash +python scripts/bambu_lan_print.py pause \ + --printer a1-mini +``` + +Execute pause and collect printer reports: + +```bash +python scripts/bambu_lan_print.py pause \ + --printer a1-mini \ + --execute \ + --mqtt-qos 1 \ + --wait-after-publish 10 +``` + +Dry-run cancel payload. The Bambu LAN command sent to the printer is `stop`: + +```bash +python scripts/bambu_lan_print.py cancel \ + --printer a1-mini +``` + +Execute cancel only when the user explicitly asks to cancel/stop the print or +after confirmation when intent is ambiguous: + +```bash +python scripts/bambu_lan_print.py cancel \ + --printer a1-mini \ + --execute \ + --confirm-cancel-print \ + --mqtt-qos 1 \ + --wait-after-publish 10 +``` + +## Failure Modes + +- `gcode_file` returns `result: fail` or leaves the printer `IDLE`: plain G-code upload worked, but the firmware rejected or ignored direct local start. For A1 Mini, switch to `template-project`. +- Project uploaded under `cache/` starts then fails with `print_error: 83935248` or `0500-C010`: clear the error, upload project handoffs to FTPS root, and use `ftp:///.gcode.3mf`. +- `file:///sdcard/cache/...` or local HTTP URLs appear accepted but nothing starts: stop using those URL forms for this workflow. +- Bambu Studio or OrcaSlicer project export crashes on macOS: do not keep retrying GUI-backed project export. Use OrcaSlicer for plain `.gcode`, then this skill for handoff. +- Stale `gcode_state: FAILED` or HMS after enabling Developer Mode: clear the printer error and power-cycle before retrying. +- FTPS login works but upload fails with `553` or missing `cache/`: check printer storage/SD card status before MQTT start. +- MQTT status works but start does not: confirm serial, access code, Developer Mode/LAN Only status, and the exact handoff payload before retrying. + +Read `references/new-printer-onboarding.md` for new printer setup, +`references/local-lan-protocol.md` for protocol details, and +`references/real-printer-checklist.md` before first live use on a new printer. diff --git a/plugins/cad/skills/bambu-labs/agents/openai.yaml b/plugins/cad/skills/bambu-labs/agents/openai.yaml new file mode 100644 index 00000000..53d49ceb --- /dev/null +++ b/plugins/cad/skills/bambu-labs/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Bambu Labs" + short_description: "Dry-run and start local Bambu G-code handoffs" + default_prompt: "Use $bambu-labs to dry-run a validated plain G-code handoff before starting it." diff --git a/plugins/cad/skills/bambu-labs/references/local-lan-protocol.md b/plugins/cad/skills/bambu-labs/references/local-lan-protocol.md new file mode 100644 index 00000000..fe4802a9 --- /dev/null +++ b/plugins/cad/skills/bambu-labs/references/local-lan-protocol.md @@ -0,0 +1,123 @@ +# Bambu Lab Local LAN Protocol Notes + +Use this reference only when planning or debugging against a real local printer. +Bambu does not provide this skill with a stable public local-print API +contract, so treat these as observed FTPS/MQTT behaviors. + +## Required Inputs + +- Validated plain `.gcode`. +- Printer LAN IP/hostname. +- Printer access code. +- Printer serial for MQTT topic `device/{serial}/request`; fetch it from the + printer TLS certificate with `serial` or let `send` fetch/cache it. +- Handoff mode: `template-project`, `plain`, or `bambox-project`. + +Workspace-root `bambu-printers.json` stores printer IDs, hostnames, access codes, models, +and cached serials. It is local config and should be ignored by Git. + +## Transport + +- FTPS upload: implicit TLS on port `990`. +- MQTT control/status: TLS on port `8883`. +- Username: `bblp`. +- Password: printer access code. +- TLS verification is off by default because local printers commonly use + device/self-signed certificates. +- The helper rejects public IPs/hostnames unless `--allow-nonprivate-host` is + set. +- FTPS data connections may require TLS session reuse. The helper reuses the + control TLS session for uploads and listings. + +## MQTT Topics + +- Request topic: `device/{serial}/request`. +- Report topic: `device/{serial}/report`. +- `status --push-all` subscribes to reports, then publishes `pushing.pushall` + to request a full state report. +- Publishing a start payload is a request, not proof of accepted motion. + +## Handoff Payloads + +### Template Project + +Validated on an A1 Mini during local LAN debugging: + +1. Start from a validated plain `.gcode`. +2. Copy a known-good same-printer `.gcode.3mf` template. +3. Replace `Metadata/plate_N.gcode` and update `Metadata/plate_N.gcode.md5`. +4. Upload the resulting `.gcode.3mf` to FTPS root, not `cache/`. +5. Publish `print.project_file` with root FTP URL. + +Representative payload: + +```json +{ + "print": { + "command": "project_file", + "param": "Metadata/plate_1.gcode", + "project_id": "0", + "profile_id": "0", + "task_id": "0", + "subtask_id": "0", + "subtask_name": "job", + "url": "ftp:///job.gcode.3mf", + "md5": "PROJECT_MD5_UPPERCASE", + "timelapse": false, + "bed_type": "auto", + "bed_levelling": true, + "flow_cali": true, + "vibration_cali": false, + "layer_inspect": true, + "use_ams": false, + "ams_mapping": "" + } +} +``` + +### Plain G-code + +The plain path uploads `cache/.gcode` and publishes: + +```json +{ + "print": { + "command": "gcode_file", + "param": "cache/job.gcode" + } +} +``` + +On the tested A1 Mini, byte-for-byte verified uploads still produced +`gcode_file` failure/idle behavior. Use this path only for diagnostics or +printer firmware where it has been validated. + +### Bambox Project + +The optional `bambox-project` path packages plain G-code into `.gcode.3mf` for +enabled profiles, validates the archive, uploads it to FTPS root, and publishes +the same `project_file` shape as template projects. Current enabled profile is +`p1s-0.4`; A1/A1 Mini are disabled until a validated bambox profile exists. + +## Observed Failure Modes + +- **Direct G-code rejected:** MQTT report after `gcode_file` may include + `{"command":"gcode_file","result":"fail","reason":"error string"}`. Stop and + use a project handoff. +- **Direct G-code ignored:** uploaded file exists, HMS is empty, target + temperatures stay at zero, and `gcode_state` remains `IDLE`. Do not keep + cycling path variants. +- **Project in `cache/` fails:** `project_file` can be accepted and then fail + with `print_error: 83935248` / `0500-C010`. Clear the error and upload project + files to FTPS root with `ftp:///.gcode.3mf`. +- **HTTP/file URLs inert:** `file:///sdcard/cache/...` and local HTTP URLs may + be accepted without fetching or starting. Do not use them for this workflow. +- **Bambu/Orca project export crash:** GUI-backed CLI project export on macOS + can crash inside AppKit/BambuStudio. Use slicer CLI only for plain `.gcode`. +- **Stale printer state:** after LAN Only/Developer Mode changes, clear errors + and power-cycle. Status can retain stale `FAILED`/HMS values. +- **Storage unavailable:** if FTPS auth works but upload fails with `553`, or + status reports no SD/storage, resolve storage before MQTT start. + +Use `clear-error --execute` only after the underlying cause is fixed. It +publishes `print.clean_print_error` and does not start motion. diff --git a/plugins/cad/skills/bambu-labs/references/new-printer-onboarding.md b/plugins/cad/skills/bambu-labs/references/new-printer-onboarding.md new file mode 100644 index 00000000..6538124c --- /dev/null +++ b/plugins/cad/skills/bambu-labs/references/new-printer-onboarding.md @@ -0,0 +1,139 @@ +# New Printer Onboarding + +Use this reference whenever the user asks to set up, add, pair, configure, or +test a new Bambu Lab printer for local LAN handoff. The goal is a calm, +step-by-step setup where the user always knows what is safe, what is private, +and what the agent will do next. + +## Agent Flow + +1. Start by confirming this is setup only. Say that no upload or print will + happen during onboarding unless the user explicitly asks later. +2. Ask for the printer model or family if it is not already known. Use a stable + local printer id such as `a1-mini`, `a1`, `p1s`, `p1p`, `x1c`, or `h2d`. +3. Tell the user you need two values from the printer touchscreen: + printer IP address and LAN access code. Explain that the serial is fetched + from the printer certificate and should not be requested by default. +4. Immediately give the model-specific touchscreen steps below. Do not just ask + for the IP and access code without helping the user find them. +5. Make the LAN prerequisites very explicit: the user must **Enable LAN Only** + and **Enable Developer Mode** before live local start commands. If either + option exists and is off, stop setup and ask them to enable it first. +6. After the user provides the IP and access code, store them with `config set` + in workspace-root `bambu-printers.json`, using `--fetch-serial`. Do not repeat the + access code in the final response. +7. Run `status --push-all --wait-seconds 10` and summarize the result in plain + language: reachable or not, idle/running/error, SD/storage present, and any + `print_error` or HMS entries. +8. If setup succeeds, offer the next safe step as a dry-run handoff plan. Do not + upload or start anything during onboarding unless the user explicitly asks. + +## What To Say First + +Use a short, reassuring prompt like this: + +```text +I can set this up safely. I need the printer's LAN IP address and LAN access +code from the touchscreen. Before you send them, please enable LAN Only and +Developer Mode; local starts often fail or become read-only if Developer Mode is +off. I will store the code only in the workspace-local ignored config and fetch the +serial from the printer automatically. +``` + +Then give the matching steps for their printer family. + +## Touchscreen Steps By Printer Family + +### A1 / A1 Mini + +1. On the printer touchscreen, open **Settings**. +2. Open **Network** or **WLAN**. +3. Connect the printer to the same trusted local network as this computer. +4. **Enable LAN Only**. +5. **Enable Developer Mode** in the same network/LAN area when shown. +6. Record the IPv4 address shown on the network screen. +7. Record the LAN access code shown near the LAN Only or Developer Mode + controls. If it is all zeros, refresh/regenerate it or toggle LAN Only off + and on, then record the new code. +8. Power-cycle the printer after changing LAN Only or Developer Mode. + +### P1P / P1S + +1. On the printer screen, open **Settings**. +2. Open **Network** or **WLAN**. +3. Confirm the printer is on the same trusted local network as this computer. +4. **Enable LAN Only**. +5. **Enable Developer Mode**. If it is not visible, update firmware or stop + before live local start workflows. +6. Record the printer IPv4 address. +7. Record the LAN access code. If the code is all zeros or blank, refresh it or + toggle LAN Only off and on, then record the new code. +8. Power-cycle the printer before the first local status check. + +### X1 / X1C / X1E + +1. On the printer touchscreen, open **Settings**. +2. Open **Network** or **WLAN**. +3. Confirm the printer is connected to the same trusted local network. +4. **Enable LAN Only**. +5. **Enable Developer Mode**. If it is not visible, update firmware or stop + before live local start workflows. +6. Record the IPv4 address. +7. Record the LAN access code from the LAN/network screen. +8. Power-cycle the printer after changing LAN settings. + +### H2D / Newer Bambu Printers + +1. Open **Settings** on the printer touchscreen. +2. Open **Network**, **WLAN**, or the printer's LAN settings panel. +3. Confirm local network connectivity. +4. **Enable LAN Only**. +5. **Enable Developer Mode**. +6. Record the IPv4 address and LAN access code. +7. Power-cycle after changing LAN settings. + +For an unknown Bambu model, guide the user to the printer's network/WLAN +settings, then require the same four items before continuing: same local +network, **Enable LAN Only**, **Enable Developer Mode**, and collect IP plus +LAN access code. + +## Agent Commands + +Use the active Python environment: + +```bash +python scripts/bambu_lan_print.py config set \ + --printer a1-mini \ + --host 192.168.1.34 \ + --access-code 12345678 \ + --model a1-mini \ + --fetch-serial +``` + +Then check status: + +```bash +python scripts/bambu_lan_print.py status \ + --printer a1-mini \ + --push-all \ + --wait-seconds 10 +``` + +Use the model-specific printer id in both commands. If sandboxing blocks local +network access, rerun the same command with permission to contact the printer. + +## Smooth Failure Handling + +- If the printer is unreachable, ask the user to confirm the IP, same Wi-Fi or + VLAN reachability, and that the printer is awake. +- If MQTT status fails but TLS serial fetch worked, first confirm **Developer + Mode** is enabled, then power-cycle and retry status. +- If the access code is rejected or all zeros, ask the user to refresh the LAN + access code on the printer and rerun `config set --fetch-serial`. +- If status returns stale `FAILED`, `print_error`, or HMS entries after enabling + LAN Only or Developer Mode, stop. Ask the user to resolve the physical issue, + power-cycle, then retry status. Use `clear-error --execute` only after the + underlying cause is fixed. +- If setup succeeds, do not imply that live printing is validated. Say that LAN + connectivity is configured and status works; live printing still requires the + real-printer checklist, a dry-run plan, upload-only, and supervised start. diff --git a/plugins/cad/skills/bambu-labs/references/real-printer-checklist.md b/plugins/cad/skills/bambu-labs/references/real-printer-checklist.md new file mode 100644 index 00000000..362ff4b3 --- /dev/null +++ b/plugins/cad/skills/bambu-labs/references/real-printer-checklist.md @@ -0,0 +1,56 @@ +# Real Printer Checklist + +Use this before any `--execute` run against a physical Bambu Lab printer. + +## Credentials And Network + +- Confirm the printer IP/hostname is on the trusted local network. +- Confirm LAN Only/Developer Mode is enabled when the printer exposes it, + especially on A1/A1 Mini. +- Store the access code in workspace-root `bambu-printers.json` with `config set`; do not + print it in final messages. +- Fetch/cache serial with `serial` or `config set --fetch-serial`. +- Run `status --push-all` and verify the intended printer responds. +- If LAN settings changed after failed starts, clear stale errors and + power-cycle before retrying. + +## Job + +- Confirm `$gcode` generated and validated the plain `.gcode`. +- Confirm scale, orientation, supports, material profile, nozzle, and bed type. +- For A1 Mini LAN start, prefer `--handoff template-project` with a known-good + same-printer `.gcode.3mf` template. +- Use `--handoff plain` only for diagnostics or firmware where `gcode_file` is + already validated. +- Use `--handoff bambox-project` only when the exact printer/nozzle profile is + enabled by the script. +- For project handoffs, upload to FTPS root and use `ftp:///.gcode.3mf`. + +## Physical Printer + +If the current user request explicitly asks to print or start the job, treat that +request as live-start authorization for this checklist. State these physical +checks before the live command and proceed if automated validation and printer +status are healthy. Ask for another confirmation only when the request intent is +ambiguous or a validation/status check raises concern. + +- Build plate is installed, clear, clean, and appropriate for the material. +- No failed-print remnants, loose tools, tape scraps, or debris are in the + printer. +- Filament is loaded and appropriate for the sliced file. +- Operator is nearby for heat-up, homing, and first-layer observation. +- Camera or direct observation is available. + +## First Live Sequence + +1. Run the exact `send` command without `--execute` and inspect the plan. +2. Run `send --action upload --execute` first. +3. Check status/UI/storage if possible. +4. Re-run the dry `send --action upload-start` plan. +5. Run `send --action upload-start --execute --confirm-start-print` when the + user explicitly asked to print/start, or after confirmation when intent is + unclear. +6. Poll status and watch the printer until the first layer is clearly normal. + +If status reports `print_error` or HMS after a failed attempt, stop. Resolve the +cause, optionally run `clear-error --execute`, then poll status before retrying. diff --git a/plugins/cad/skills/bambu-labs/scripts/bambu_lan_print.py b/plugins/cad/skills/bambu-labs/scripts/bambu_lan_print.py new file mode 100644 index 00000000..f5da8fd8 --- /dev/null +++ b/plugins/cad/skills/bambu-labs/scripts/bambu_lan_print.py @@ -0,0 +1,1771 @@ +#!/usr/bin/env python3 +"""Dry-run-first helpers for local Bambu Lab print workflows. + +The default behavior is non-networked. Start from a validated plain .gcode file, +then either upload/start it directly with the printer's gcode_file command or, +for explicitly supported printer profiles, package it with bambox into a +.gcode.3mf project and start that with project_file. +""" + +from __future__ import annotations + +import argparse +import ftplib +import hashlib +import ipaddress +import json +import os +import re +import shutil +import socket +import ssl +import subprocess +import sys +import tempfile +import time +import zipfile +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Union + + +BAMBU_USERNAME = "bblp" +DEFAULT_FTPS_PORT = 990 +DEFAULT_MQTT_PORT = 8883 +DEFAULT_REMOTE_DIR = "cache" +PLATE_RE = re.compile(r"^Metadata/plate_(\d+)\.gcode$") +IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] + + +def default_workspace_root() -> Path: + init_cwd = str(os.environ.get("INIT_CWD") or "").strip() + return Path(init_cwd).expanduser().resolve() if init_cwd else Path.cwd().resolve() + + +DEFAULT_CONFIG_PATH = default_workspace_root() / "bambu-printers.json" + + +class BambuPrintError(RuntimeError): + """Raised for expected user-facing workflow errors.""" + + +@dataclass(frozen=True) +class GCodeInspection: + path: str + size_bytes: int + md5: str + + +@dataclass(frozen=True) +class PlateInfo: + index: int + path: str + size_bytes: int + + +@dataclass(frozen=True) +class ProjectInspection: + path: str + size_bytes: int + md5: str + plates: list[PlateInfo] + + +@dataclass(frozen=True) +class BamboxProfile: + key: str + machine: str + nozzle_diameter: str + enabled: bool + reason: str + allowed_filaments: set[str] + + +BAMBOX_PROFILES: dict[str, BamboxProfile] = { + "p1s-0.4": BamboxProfile( + key="p1s-0.4", + machine="p1s", + nozzle_diameter="0.4", + enabled=True, + reason="bambox 0.5.0 bundles and hardware-validates the Bambu Lab P1S 0.4 profile.", + allowed_filaments={"PLA", "ASA", "PETG-CF"}, + ), + "a1-mini-0.4": BamboxProfile( + key="a1-mini-0.4", + machine="a1-mini", + nozzle_diameter="0.4", + enabled=False, + reason="no validated bambox A1 Mini profile is present.", + allowed_filaments=set(), + ), + "a1-0.4": BamboxProfile( + key="a1-0.4", + machine="a1", + nozzle_diameter="0.4", + enabled=False, + reason="no validated bambox A1 profile is present.", + allowed_filaments=set(), + ), + "x1c-0.4": BamboxProfile( + key="x1c-0.4", + machine="x1c", + nozzle_diameter="0.4", + enabled=False, + reason="no validated bambox X1C profile is present.", + allowed_filaments=set(), + ), + "p1p-0.4": BamboxProfile( + key="p1p-0.4", + machine="p1p", + nozzle_diameter="0.4", + enabled=False, + reason="no validated bambox P1P profile is present.", + allowed_filaments=set(), + ), +} + + +class ImplicitFTP_TLS(ftplib.FTP_TLS): + """FTP_TLS variant for implicit FTPS servers such as Bambu printers.""" + + def connect(self, host: str = "", port: int = 0, timeout: float | None = -999, source_address=None): + if host: + self.host = host + if port: + self.port = port + if timeout != -999: + self.timeout = timeout + if source_address is not None: + self.source_address = source_address + raw_sock = socket.create_connection( + (self.host, self.port), + self.timeout, + source_address=self.source_address, + ) + self.af = raw_sock.family + self.sock = self.context.wrap_socket(raw_sock, server_hostname=self.host) + self.file = self.sock.makefile("r", encoding=self.encoding) + self.welcome = self.getresp() + return self.welcome + + def ntransfercmd(self, cmd: str, rest: Any = None): + conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, server_hostname=self.host, session=self.sock.session) + return conn, size + + +def config_path_from_args(args: argparse.Namespace) -> Path: + return Path(getattr(args, "config", None) or DEFAULT_CONFIG_PATH).expanduser().resolve() + + +def load_config(path: Path) -> dict[str, Any]: + if not path.is_file(): + return {"printers": {}} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise BambuPrintError(f"Printer config is not valid JSON: {path}: {exc}") from exc + if not isinstance(data, dict): + raise BambuPrintError(f"Printer config must be a JSON object: {path}") + printers = data.setdefault("printers", {}) + if not isinstance(printers, dict): + raise BambuPrintError("Printer config field printers must be an object.") + return data + + +def save_config(path: Path, data: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def sanitized_printer_entry(entry: dict[str, Any]) -> dict[str, Any]: + return {key: value for key, value in entry.items() if key != "access_code"} + + +def printer_entry(data: dict[str, Any], printer_id: str) -> dict[str, Any]: + printers = data.get("printers", {}) + entry = printers.get(printer_id) + if not isinstance(entry, dict): + known = ", ".join(sorted(printers)) or "" + raise BambuPrintError(f"Printer {printer_id!r} is not configured. Known printers: {known}") + return entry + + +def apply_printer_config(args: argparse.Namespace) -> None: + printer_id = getattr(args, "printer", "") or "" + if not printer_id: + return + data = load_config(config_path_from_args(args)) + entry = printer_entry(data, printer_id) + for attr, key in ( + ("host", "host"), + ("access_code", "access_code"), + ("serial", "serial"), + ("model", "model"), + ): + if hasattr(args, attr) and not getattr(args, attr, "") and entry.get(key): + setattr(args, attr, str(entry[key])) + + +def cache_printer_serial(args: argparse.Namespace, serial: str) -> None: + printer_id = getattr(args, "printer", "") or "" + if not printer_id: + return + path = config_path_from_args(args) + data = load_config(path) + entry = printer_entry(data, printer_id) + if entry.get("serial") == serial: + return + entry["serial"] = serial + entry["serial_source"] = "printer_tls_certificate_common_name" + entry["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + save_config(path, data) + + +def md5_and_size(path: Path) -> tuple[str, int]: + digest = hashlib.md5() + size = 0 + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + size += len(chunk) + digest.update(chunk) + return digest.hexdigest(), size + + +def inspect_gcode_file(path: Path) -> GCodeInspection: + if not path.is_file(): + raise BambuPrintError(f"G-code input file does not exist: {path}") + name = path.name.lower() + if name.endswith(".gcode.3mf") or path.suffix.lower() == ".3mf": + raise BambuPrintError("Expected plain .gcode, not a Bambu .gcode.3mf or .3mf archive.") + if path.suffix.lower() != ".gcode": + raise BambuPrintError(f"Expected a plain .gcode input, got: {path.name}") + md5, size = md5_and_size(path) + if size == 0: + raise BambuPrintError(f"G-code input is empty: {path}") + return GCodeInspection(path=str(path), size_bytes=size, md5=md5) + + +def inspect_sliced_3mf(path: Path) -> ProjectInspection: + if not path.is_file(): + raise BambuPrintError(f"Packaged project file does not exist: {path}") + + md5, size = md5_and_size(path) + try: + archive = zipfile.ZipFile(path) + except zipfile.BadZipFile as exc: + raise BambuPrintError(f"Packaged project is not a readable 3MF/ZIP archive: {path}") from exc + + plates: list[PlateInfo] = [] + with archive: + for info in archive.infolist(): + match = PLATE_RE.match(info.filename) + if match: + plates.append( + PlateInfo( + index=int(match.group(1)), + path=info.filename, + size_bytes=info.file_size, + ) + ) + + if not plates: + raise BambuPrintError("Packaged project has no Metadata/plate_N.gcode entries.") + + plates.sort(key=lambda item: item.index) + return ProjectInspection(path=str(path), size_bytes=size, md5=md5, plates=plates) + + +def sanitize_remote_name(name: str) -> str: + base = Path(name).name.strip() + if not base: + raise BambuPrintError("Remote filename cannot be empty.") + return re.sub(r"[^A-Za-z0-9._+-]", "_", base) + + +def join_remote_path(remote_dir: str, remote_name: str) -> str: + clean_dir = remote_dir.strip("/") + clean_name = sanitize_remote_name(remote_name) + return f"{clean_dir}/{clean_name}" if clean_dir else clean_name + + +def default_project_name(gcode_path: Path) -> str: + if gcode_path.name.lower().endswith(".gcode"): + return f"{gcode_path.name}.3mf" + return f"{gcode_path.name}.gcode.3mf" + + +def project_subtask_name(remote_path: str) -> str: + name = Path(remote_path).name + for suffix in (".gcode.3mf", ".3mf", ".gcode"): + if name.lower().endswith(suffix): + name = name[: -len(suffix)] + return name or "local_print" + + +def is_local_address(address: IPAddress) -> bool: + return address.is_private or address.is_loopback or address.is_link_local + + +def resolve_host_addresses(host: str) -> list[IPAddress]: + clean_host = host.strip("[]") + try: + return [ipaddress.ip_address(clean_host)] + except ValueError: + pass + + try: + results = socket.getaddrinfo(clean_host, None, type=socket.SOCK_STREAM) + except socket.gaierror as exc: + raise BambuPrintError(f"Could not resolve printer host {host!r}: {exc}") from exc + + addresses = sorted({ipaddress.ip_address(item[4][0]) for item in results}, key=str) + if not addresses: + raise BambuPrintError(f"Could not resolve printer host {host!r}.") + return addresses + + +def validate_local_host(host: str, allow_nonprivate: bool) -> None: + if not host: + raise BambuPrintError("Printer host/IP is required for network actions.") + if allow_nonprivate: + return + + addresses = resolve_host_addresses(host) + nonprivate = [str(address) for address in addresses if not is_local_address(address)] + if nonprivate: + raise BambuPrintError( + f"{host} resolves to non-private address(es): {', '.join(nonprivate)}. " + "Refusing unless --allow-nonprivate-host is set." + ) + + +def common_name_from_decoded_cert(decoded: dict[str, Any]) -> str: + for subject_item in decoded.get("subject", ()): + for key, value in subject_item: + if key == "commonName": + return str(value) + return "" + + +def decode_der_certificate(der_cert: bytes) -> dict[str, Any]: + pem = ssl.DER_cert_to_PEM_cert(der_cert) + temp_path = "" + try: + with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".pem", delete=False) as handle: + temp_path = handle.name + handle.write(pem) + return ssl._ssl._test_decode_cert(temp_path) # type: ignore[attr-defined] + finally: + if temp_path: + try: + Path(temp_path).unlink() + except FileNotFoundError: + pass + + +def discover_printer_serial( + *, + host: str, + port: int = DEFAULT_MQTT_PORT, + timeout: float = 20.0, + tls_verify: bool = False, + allow_nonprivate_host: bool = False, +) -> str: + validate_local_host(host, allow_nonprivate_host) + context = ssl.create_default_context() + if not tls_verify: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket.create_connection((host, port), timeout=timeout) as raw_sock: + with context.wrap_socket(raw_sock, server_hostname=host) as sock: + der_cert = sock.getpeercert(binary_form=True) + if not der_cert: + raise BambuPrintError("Printer did not provide a TLS certificate for serial discovery.") + serial = common_name_from_decoded_cert(decode_der_certificate(der_cert)).strip() + if not serial: + raise BambuPrintError("Could not find printer serial in the local TLS certificate common name.") + return serial + + +def ensure_printer_serial(args: argparse.Namespace) -> str: + if getattr(args, "serial", ""): + return args.serial + if not getattr(args, "host", ""): + return "" + args.serial = discover_printer_serial( + host=args.host, + port=args.mqtt_port, + timeout=args.timeout, + tls_verify=args.tls_verify, + allow_nonprivate_host=args.allow_nonprivate_host, + ) + cache_printer_serial(args, args.serial) + return args.serial + + +def selected_plate(inspection: ProjectInspection, requested: int | None) -> PlateInfo: + if requested is None: + return inspection.plates[0] + for plate in inspection.plates: + if plate.index == requested: + return plate + available = ", ".join(str(plate.index) for plate in inspection.plates) + raise BambuPrintError(f"Plate {requested} not found. Available plates: {available}") + + +def sequence_id(value: str | None) -> str: + if value: + return value + return str(int(time.time() * 1000)) + + +def build_gcode_file_payload(*, remote_path: str, args: argparse.Namespace) -> dict[str, Any]: + return { + "print": { + "sequence_id": sequence_id(args.sequence_id), + "command": "gcode_file", + "param": args.gcode_param or remote_path, + } + } + + +def build_clean_print_error_payload(args: argparse.Namespace) -> dict[str, Any]: + return { + "print": { + "sequence_id": sequence_id(args.sequence_id), + "command": "clean_print_error", + } + } + + +def build_print_control_payload(args: argparse.Namespace) -> dict[str, Any]: + command = "stop" if args.action == "cancel" else args.action + payload = { + "print": { + "sequence_id": sequence_id(args.sequence_id), + "command": command, + } + } + if command == "stop": + payload["print"]["param"] = "" + return payload + + +def build_project_file_payload( + *, + plate: PlateInfo, + remote_path: str, + remote_url: str, + md5: str | None, + args: argparse.Namespace, +) -> dict[str, Any]: + return { + "print": { + "sequence_id": sequence_id(args.sequence_id), + "command": "project_file", + "param": plate.path, + "project_id": "0", + "profile_id": "0", + "task_id": "0", + "subtask_id": "0", + "subtask_name": project_subtask_name(remote_path), + "url": remote_url, + "md5": md5.upper() if md5 else None, + "timelapse": False, + "bed_type": "auto", + "bed_levelling": True, + "flow_cali": True, + "vibration_cali": False, + "layer_inspect": True, + "use_ams": False, + "ams_mapping": "", + } + } + + +def bambox_profile(profile_key: str) -> BamboxProfile: + try: + profile = BAMBOX_PROFILES[profile_key] + except KeyError as exc: + known = ", ".join(sorted(BAMBOX_PROFILES)) + raise BambuPrintError(f"Unknown bambox profile {profile_key!r}. Known profiles: {known}") from exc + if not profile.enabled: + raise BambuPrintError(f"{profile.key} is not enabled because {profile.reason}") + return profile + + +def bambox_profile_payload(profile: BamboxProfile) -> dict[str, Any]: + return { + "key": profile.key, + "machine": profile.machine, + "nozzle_diameter": profile.nozzle_diameter, + "enabled": profile.enabled, + "reason": profile.reason, + "allowed_filaments": sorted(profile.allowed_filaments), + } + + +def bambox_filament_type(spec: str) -> str: + parts = spec.split(":") + if len(parts) >= 2 and parts[0].isdigit(): + value = parts[1] + else: + value = parts[0] + return value.strip().upper() + + +def validate_bambox_filaments(profile: BamboxProfile, filaments: list[str] | None) -> list[str]: + if not filaments: + raise BambuPrintError("At least one --filament is required for bambox packaging.") + for spec in filaments: + filament_type = bambox_filament_type(spec) + if filament_type not in profile.allowed_filaments: + allowed = ", ".join(sorted(profile.allowed_filaments)) + raise BambuPrintError( + f"Filament {filament_type!r} is not enabled for {profile.key}. Allowed filaments: {allowed}" + ) + return filaments + + +def resolve_bambox_bin(requested: str | None, *, require_exists: bool) -> str: + candidate = requested or os.environ.get("BAMBOX_BIN") or "bambox" + if not require_exists: + return candidate + resolved = shutil.which(candidate) if Path(candidate).name == candidate else candidate + if not resolved or not Path(resolved).exists(): + raise BambuPrintError("bambox CLI was not found. Install it separately or set BAMBOX_BIN.") + return resolved + + +def build_bambox_pack_command( + *, + gcode_path: Path, + output_path: Path, + profile_key: str, + filaments: list[str] | None, + bambox_bin: str | None = None, + require_bambox: bool = False, +) -> list[str]: + profile = bambox_profile(profile_key) + safe_filaments = validate_bambox_filaments(profile, filaments) + command = [ + resolve_bambox_bin(bambox_bin, require_exists=require_bambox), + "pack", + str(gcode_path), + "-o", + str(output_path), + "-m", + profile.machine, + "--nozzle-diameter", + profile.nozzle_diameter, + ] + for filament in safe_filaments: + command.extend(["-f", filament]) + return command + + +def build_bambox_validate_command(*, project_path: Path, bambox_bin: str | None = None, require_bambox: bool = False) -> list[str]: + return [ + resolve_bambox_bin(bambox_bin, require_exists=require_bambox), + "validate", + str(project_path), + "--json", + "--strict", + ] + + +def run_checked_command(command: list[str], label: str) -> dict[str, Any]: + result = subprocess.run(command, check=False, capture_output=True, text=True) + payload: dict[str, Any] = {"command": command, "returncode": result.returncode} + if result.stdout: + payload["stdout_tail"] = tail_text(result.stdout) + if result.stderr: + payload["stderr_tail"] = tail_text(result.stderr) + if result.returncode != 0: + raise BambuPrintError(f"{label} failed: {json.dumps(payload, indent=2)}") + return payload + + +def package_with_bambox(args: argparse.Namespace, output_path: Path) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspect_gcode_file(gcode_path) + pack_command = build_bambox_pack_command( + gcode_path=gcode_path, + output_path=output_path, + profile_key=args.bambox_profile, + filaments=args.filament, + bambox_bin=args.bambox_bin, + require_bambox=True, + ) + validate_command = build_bambox_validate_command( + project_path=output_path, + bambox_bin=args.bambox_bin, + require_bambox=True, + ) + pack_result = run_checked_command(pack_command, "bambox pack") + if not output_path.is_file(): + raise BambuPrintError(f"bambox pack did not create expected output: {output_path}") + project_inspection = inspect_sliced_3mf(output_path) + validate_result = run_checked_command(validate_command, "bambox validate") + return { + "pack": pack_result, + "validate": validate_result, + "project": asdict(project_inspection), + } + + +def package_with_template_project(args: argparse.Namespace, output_path: Path) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspection = inspect_gcode_file(gcode_path) + if not args.template_project: + raise BambuPrintError("--template-project is required for template-project packaging.") + template_path = Path(args.template_project).expanduser().resolve() + template_inspection = inspect_sliced_3mf(template_path) + plate = selected_plate(template_inspection, args.plate) + + output_path.parent.mkdir(parents=True, exist_ok=True) + gcode_bytes = gcode_path.read_bytes() + gcode_md5 = hashlib.md5(gcode_bytes).hexdigest().upper() + md5_path = f"{plate.path}.md5" + found_plate = False + found_md5 = False + + with zipfile.ZipFile(template_path, "r") as source, zipfile.ZipFile( + output_path, "w", compression=zipfile.ZIP_DEFLATED + ) as target: + for info in source.infolist(): + data = source.read(info.filename) + if info.filename == plate.path: + data = gcode_bytes + found_plate = True + elif info.filename == md5_path: + data = gcode_md5.encode("ascii") + found_md5 = True + target.writestr(info, data) + if not found_md5: + target.writestr(md5_path, gcode_md5) + + if not found_plate: + raise BambuPrintError(f"Template project plate G-code entry not found: {plate.path}") + + project_inspection = inspect_sliced_3mf(output_path) + return { + "pack": { + "template": str(template_path), + "output": str(output_path), + "replaced_plate": plate.path, + "plate_gcode_md5": gcode_md5, + "input": asdict(inspection), + }, + "project": asdict(project_inspection), + } + + +def build_package_plan(args: argparse.Namespace) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspection = inspect_gcode_file(gcode_path) + output_path = Path(args.output).expanduser().resolve() + pack_command = build_bambox_pack_command( + gcode_path=gcode_path, + output_path=output_path, + profile_key=args.bambox_profile, + filaments=args.filament, + bambox_bin=args.bambox_bin, + require_bambox=False, + ) + validate_command = build_bambox_validate_command( + project_path=output_path, + bambox_bin=args.bambox_bin, + require_bambox=False, + ) + return { + "dry_run": not args.execute, + "action": "package", + "input": asdict(inspection), + "output": str(output_path), + "bambox": { + "profile": bambox_profile_payload(bambox_profile(args.bambox_profile)), + "filaments": args.filament, + "pack_command": pack_command, + "validate_command": validate_command, + }, + "notes": [ + "bambox is optional and must be installed separately.", + "Only enabled bambox profiles can be packaged.", + "A1 Mini is intentionally disabled until a validated bambox profile is available.", + ], + } + + +def build_plain_send_plan(args: argparse.Namespace) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspection = inspect_gcode_file(gcode_path) + remote_name = sanitize_remote_name(args.remote_name or gcode_path.name) + remote_path = join_remote_path(args.remote_dir or DEFAULT_REMOTE_DIR, remote_name) + payload = build_gcode_file_payload(remote_path=remote_path, args=args) + return build_common_send_plan( + args=args, + input_payload={**asdict(inspection), "kind": "plain_gcode"}, + local_path=str(gcode_path), + remote_path=remote_path, + remote_url=None, + payload=payload, + ) + + +def build_bambox_project_send_plan(args: argparse.Namespace, project_path: Path | None = None) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspection = inspect_gcode_file(gcode_path) + remote_name = sanitize_remote_name(args.remote_name or default_project_name(gcode_path)) + remote_path = join_remote_path(args.remote_dir or "", remote_name) + remote_url = f"ftp:///{remote_path}" + output_path = project_path or Path(tempfile.gettempdir()) / default_project_name(gcode_path) + pack_command = build_bambox_pack_command( + gcode_path=gcode_path, + output_path=output_path, + profile_key=args.bambox_profile, + filaments=args.filament, + bambox_bin=args.bambox_bin, + require_bambox=False, + ) + validate_command = build_bambox_validate_command( + project_path=output_path, + bambox_bin=args.bambox_bin, + require_bambox=False, + ) + + project: dict[str, Any] | None = None + payload: dict[str, Any] + if project_path is not None: + project_inspection = inspect_sliced_3mf(project_path) + plate = selected_plate(project_inspection, args.plate) + project = {**asdict(project_inspection), "selected_plate": asdict(plate)} + payload = build_project_file_payload( + plate=plate, + remote_path=remote_path, + remote_url=remote_url, + md5=project_inspection.md5, + args=args, + ) + else: + payload = build_project_file_payload( + plate=PlateInfo(index=1, path="Metadata/plate_1.gcode", size_bytes=0), + remote_path=remote_path, + remote_url=remote_url, + md5=None, + args=args, + ) + + return build_common_send_plan( + args=args, + input_payload={ + **asdict(inspection), + "kind": "plain_gcode", + "bambox_project": project, + "bambox": { + "profile": bambox_profile_payload(bambox_profile(args.bambox_profile)), + "filaments": args.filament, + "pack_command": pack_command, + "validate_command": validate_command, + }, + }, + local_path=str(project_path) if project_path is not None else None, + remote_path=remote_path, + remote_url=remote_url, + payload=payload, + ) + + +def build_template_project_send_plan(args: argparse.Namespace, project_path: Path | None = None) -> dict[str, Any]: + gcode_path = Path(args.gcode).expanduser().resolve() + inspection = inspect_gcode_file(gcode_path) + template_path = Path(args.template_project).expanduser().resolve() if args.template_project else None + if template_path is None: + raise BambuPrintError("--template-project is required for --handoff template-project.") + inspect_sliced_3mf(template_path) + remote_name = sanitize_remote_name(args.remote_name or default_project_name(gcode_path)) + remote_path = join_remote_path(args.remote_dir or "", remote_name) + remote_url = f"ftp:///{remote_path}" + output_path = project_path or Path(tempfile.gettempdir()) / default_project_name(gcode_path) + + project: dict[str, Any] | None = None + if project_path is not None: + project_inspection = inspect_sliced_3mf(project_path) + plate = selected_plate(project_inspection, args.plate) + project = {**asdict(project_inspection), "selected_plate": asdict(plate)} + payload = build_project_file_payload( + plate=plate, + remote_path=remote_path, + remote_url=remote_url, + md5=project_inspection.md5, + args=args, + ) + else: + payload = build_project_file_payload( + plate=PlateInfo(index=1, path="Metadata/plate_1.gcode", size_bytes=0), + remote_path=remote_path, + remote_url=remote_url, + md5=None, + args=args, + ) + + return build_common_send_plan( + args=args, + input_payload={ + **asdict(inspection), + "kind": "plain_gcode", + "template_project": { + "template": str(template_path), + "output": str(output_path), + "packaged_project": project, + "note": "Experimental A1/A1 Mini path: replaces Metadata/plate_1.gcode in a same-printer template project.", + }, + }, + local_path=str(project_path) if project_path is not None else None, + remote_path=remote_path, + remote_url=remote_url, + payload=payload, + ) + + +def build_common_send_plan( + *, + args: argparse.Namespace, + input_payload: dict[str, Any], + local_path: str | None, + remote_path: str, + remote_url: str | None, + payload: dict[str, Any], +) -> dict[str, Any]: + serial = getattr(args, "serial", "") or "" + topic = f"device/{serial}/request" if serial else None + will_upload = args.action in {"upload", "upload-start"} + will_publish = args.action in {"start", "upload-start"} + + ftps: dict[str, Any] = { + "host": args.host, + "port": args.ftps_port, + "username": BAMBU_USERNAME, + "remote_path": remote_path, + "will_upload": will_upload, + } + if remote_url: + ftps["url"] = remote_url + + return { + "dry_run": not args.execute, + "action": args.action, + "handoff": args.handoff, + "input": input_payload, + "local_upload_path": local_path, + "ftps": ftps, + "mqtt": { + "host": args.host, + "port": args.mqtt_port, + "username": BAMBU_USERNAME, + "topic": topic, + "payload": payload, + "will_publish": will_publish, + }, + "safety": [ + "Dry-run is the default; network actions require --execute.", + "Starting a print requires --confirm-start-print.", + "MQTT start publishes a request only; watch the printer UI/state for acceptance and progress.", + "Verify the build plate is clear, filament/path are correct, and the printer is physically safe before execution.", + ], + } + + +def build_clear_error_plan(args: argparse.Namespace) -> dict[str, Any]: + serial = getattr(args, "serial", "") or "" + topic = f"device/{serial}/request" if serial else None + return { + "dry_run": not args.execute, + "action": "clear-error", + "printer": args.printer or None, + "host": args.host, + "mqtt": { + "host": args.host, + "port": args.mqtt_port, + "username": BAMBU_USERNAME, + "topic": topic, + "payload": build_clean_print_error_payload(args), + "will_publish": bool(args.execute), + }, + "notes": [ + "This publishes clean_print_error only; it does not upload files or start a print.", + "Use after resolving the underlying cause of a stale printer error, then read status before retrying.", + ], + } + + +def build_print_control_plan(args: argparse.Namespace) -> dict[str, Any]: + serial = getattr(args, "serial", "") or "" + topic = f"device/{serial}/request" if serial else None + return { + "dry_run": not args.execute, + "action": args.action, + "printer": args.printer or None, + "host": args.host, + "mqtt": { + "host": args.host, + "port": args.mqtt_port, + "username": BAMBU_USERNAME, + "topic": topic, + "payload": build_print_control_payload(args), + "will_publish": bool(args.execute), + }, + "notes": [ + "This publishes a print-control request only; it does not upload files or start a print.", + "Read printer status after publishing to confirm the state changed as intended.", + ], + } + + +def build_send_plan(args: argparse.Namespace, project_path: Path | None = None) -> dict[str, Any]: + apply_printer_config(args) + if args.handoff == "plain": + return build_plain_send_plan(args) + if args.handoff == "bambox-project": + return build_bambox_project_send_plan(args, project_path=project_path) + if args.handoff == "template-project": + return build_template_project_send_plan(args, project_path=project_path) + raise BambuPrintError(f"Unsupported handoff: {args.handoff}") + + +def upload_ftps(args: argparse.Namespace, local_path: Path, remote_path: str) -> None: + access_code = access_code_from_args(args) + validate_local_host(args.host, args.allow_nonprivate_host) + context = ssl.create_default_context() + if not args.tls_verify: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + ftp = ImplicitFTP_TLS(context=context, timeout=args.timeout) + ftp.connect(args.host, args.ftps_port) + try: + ftp.login(BAMBU_USERNAME, access_code) + ftp.prot_p() + remote_dir = remote_path.rsplit("/", 1)[0] if "/" in remote_path else "" + remote_name = Path(remote_path).name + if remote_dir: + try: + ftp.cwd(remote_dir) + except ftplib.error_perm: + ftp.mkd(remote_dir) + ftp.cwd(remote_dir) + with local_path.open("rb") as handle: + storbinary_with_bambu_timeout_tolerance(ftp, f"STOR {remote_name}", handle) + finally: + try: + ftp.quit() + except Exception: + ftp.close() + + +def storbinary_with_bambu_timeout_tolerance(ftp: ftplib.FTP_TLS, command: str, handle: Any) -> None: + try: + ftp.storbinary(command, handle) + except socket.timeout as exc: + try: + ftp.voidresp() + except Exception as confirm_exc: + raise BambuPrintError( + "FTPS upload timed out before the printer confirmed completion. " + "The file may be partially uploaded; retry upload-only after checking printer storage." + ) from confirm_exc + # Some Bambu FTPS firmware completes STOR but does not close the TLS data + # channel the way Python's ftplib expects. A 226 control response is enough. + + +def encode_mqtt_string(value: str) -> bytes: + encoded = value.encode("utf-8") + if len(encoded) > 65535: + raise BambuPrintError("MQTT string is too long.") + return len(encoded).to_bytes(2, "big") + encoded + + +def encode_remaining_length(length: int) -> bytes: + out = bytearray() + while True: + byte = length % 128 + length //= 128 + if length: + byte |= 0x80 + out.append(byte) + if not length: + return bytes(out) + + +def read_exact(sock: ssl.SSLSocket, size: int) -> bytes: + chunks = bytearray() + while len(chunks) < size: + chunk = sock.recv(size - len(chunks)) + if not chunk: + raise BambuPrintError("MQTT connection closed unexpectedly.") + chunks.extend(chunk) + return bytes(chunks) + + +def read_mqtt_packet(sock: ssl.SSLSocket) -> tuple[int, bytes]: + header = read_exact(sock, 1)[0] + multiplier = 1 + remaining = 0 + while True: + digit = read_exact(sock, 1)[0] + remaining += (digit & 127) * multiplier + if not digit & 128: + break + multiplier *= 128 + if multiplier > 128 * 128 * 128: + raise BambuPrintError("Malformed MQTT remaining length.") + return header, read_exact(sock, remaining) + + +def mqtt_connect_packet(client_id: str, username: str, password: str, keepalive: int = 30) -> bytes: + variable = encode_mqtt_string("MQTT") + bytes([4, 0xC2]) + keepalive.to_bytes(2, "big") + payload = encode_mqtt_string(client_id) + encode_mqtt_string(username) + encode_mqtt_string(password) + remaining = variable + payload + return bytes([0x10]) + encode_remaining_length(len(remaining)) + remaining + + +def mqtt_publish_packet(topic: str, payload: bytes, *, qos: int = 0, packet_id: int = 1) -> bytes: + if qos not in {0, 1}: + raise BambuPrintError("Only MQTT QoS 0 and QoS 1 are supported.") + variable = encode_mqtt_string(topic) + if qos: + variable += packet_id.to_bytes(2, "big") + remaining = variable + payload + return bytes([0x30 | (qos << 1)]) + encode_remaining_length(len(remaining)) + remaining + + +def mqtt_subscribe_packet(packet_id: int, topic: str) -> bytes: + variable = packet_id.to_bytes(2, "big") + payload = encode_mqtt_string(topic) + bytes([0]) + remaining = variable + payload + return bytes([0x82]) + encode_remaining_length(len(remaining)) + remaining + + +def parse_mqtt_publish(header: int, body: bytes) -> tuple[str, bytes]: + if header & 0xF0 != 0x30: + raise BambuPrintError(f"Expected MQTT PUBLISH packet, got header=0x{header:02x}.") + if len(body) < 2: + raise BambuPrintError("Malformed MQTT PUBLISH packet.") + topic_length = int.from_bytes(body[:2], "big") + topic_start = 2 + topic_end = topic_start + topic_length + topic = body[topic_start:topic_end].decode("utf-8", errors="replace") + payload_start = topic_end + qos = (header & 0x06) >> 1 + if qos: + payload_start += 2 + return topic, body[payload_start:] + + +def format_hms_code(attr: Any, code: Any) -> str: + try: + attr_int = int(attr) + code_int = int(code) + except (TypeError, ValueError) as exc: + raise BambuPrintError("HMS attr/code values must be integers.") from exc + return f"{(attr_int >> 16) & 0xFFFF:04X}-{attr_int & 0xFFFF:04X}-{(code_int >> 16) & 0xFFFF:04X}-{code_int & 0xFFFF:04X}" + + +def annotate_hms_codes(message: dict[str, Any]) -> None: + payload = message.get("json") + if not isinstance(payload, dict): + return + print_payload = payload.get("print") + if not isinstance(print_payload, dict): + return + hms_items = print_payload.get("hms") + if not isinstance(hms_items, list): + return + decoded: list[dict[str, Any]] = [] + for item in hms_items: + if not isinstance(item, dict) or "attr" not in item or "code" not in item: + continue + decoded.append({**item, "hms_code": format_hms_code(item["attr"], item["code"])}) + if decoded: + message["hms"] = decoded + + +def mqtt_puback_packet(packet_id: int) -> bytes: + return bytes([0x40, 0x02]) + packet_id.to_bytes(2, "big") + + +def read_mqtt_reports_until( + sock: ssl.SSLSocket, + *, + deadline: float, + timeout: float, + expected_puback_packet_id: int | None = None, + stop_after_puback: bool = False, +) -> dict[str, Any]: + messages: list[dict[str, Any]] = [] + puback_received = expected_puback_packet_id is None + while time.monotonic() < deadline: + sock.settimeout(max(0.1, min(timeout, deadline - time.monotonic()))) + try: + header, body = read_mqtt_packet(sock) + except socket.timeout: + break + if header == 0x40: + packet_id = int.from_bytes(body[:2], "big") if len(body) >= 2 else None + if packet_id == expected_puback_packet_id: + puback_received = True + if stop_after_puback: + break + continue + if header & 0xF0 != 0x30: + continue + message_topic, report_payload = parse_mqtt_publish(header, body) + text = report_payload.decode("utf-8", errors="replace") + item: dict[str, Any] = {"topic": message_topic, "payload": text} + try: + item["json"] = json.loads(text) + except json.JSONDecodeError: + pass + annotate_hms_codes(item) + messages.append(item) + qos = (header & 0x06) >> 1 + if qos == 1: + topic_length = int.from_bytes(body[:2], "big") + publish_packet_id_start = 2 + topic_length + packet_id = int.from_bytes(body[publish_packet_id_start : publish_packet_id_start + 2], "big") + sock.sendall(mqtt_puback_packet(packet_id)) + return {"puback_received": puback_received, "messages": messages} + + +def publish_mqtt( + args: argparse.Namespace, + topic: str, + payload: dict[str, Any], + *, + report_topic: str | None = None, +) -> dict[str, Any]: + access_code = access_code_from_args(args) + validate_local_host(args.host, args.allow_nonprivate_host) + context = ssl.create_default_context() + if not args.tls_verify: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + payload_bytes = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + client_id = args.client_id or args.serial or f"codex-bambu-{int(time.time())}" + raw_mqtt_qos = getattr(args, "mqtt_qos", 0) + if not isinstance(raw_mqtt_qos, (int, float, str)): + raw_mqtt_qos = 0 + mqtt_qos = int(raw_mqtt_qos) + publish_packet_id = 2 + raw_wait_after_publish = getattr(args, "wait_after_publish", 0.0) + if not isinstance(raw_wait_after_publish, (int, float, str)): + raw_wait_after_publish = 0.0 + wait_after_publish = float(raw_wait_after_publish or 0.0) + diagnostics: dict[str, Any] = { + "qos": mqtt_qos, + "puback_required": mqtt_qos == 1, + "puback_received": mqtt_qos == 0, + "report_topic": report_topic if wait_after_publish and report_topic else None, + "messages": [], + } + with socket.create_connection((args.host, args.mqtt_port), timeout=args.timeout) as raw_sock: + with context.wrap_socket(raw_sock, server_hostname=args.host) as sock: + sock.sendall(mqtt_connect_packet(client_id, BAMBU_USERNAME, access_code)) + header, body = read_mqtt_packet(sock) + if header != 0x20 or len(body) != 2 or body[1] != 0: + raise BambuPrintError(f"MQTT CONNACK failed: header=0x{header:02x} body={body.hex()}") + if wait_after_publish and report_topic: + sock.sendall(mqtt_subscribe_packet(1, report_topic)) + header, body = read_mqtt_packet(sock) + if header != 0x90 or len(body) < 3 or body[-1] == 0x80: + raise BambuPrintError(f"MQTT SUBACK failed: header=0x{header:02x} body={body.hex()}") + sock.sendall(mqtt_publish_packet(topic, payload_bytes, qos=mqtt_qos, packet_id=publish_packet_id)) + if mqtt_qos == 1 or wait_after_publish: + wait_seconds = wait_after_publish if wait_after_publish else args.timeout + observed = read_mqtt_reports_until( + sock, + deadline=time.monotonic() + wait_seconds, + timeout=args.timeout, + expected_puback_packet_id=publish_packet_id if mqtt_qos == 1 else None, + stop_after_puback=mqtt_qos == 1 and not wait_after_publish, + ) + diagnostics.update(observed) + sock.sendall(bytes([0xE0, 0x00])) + return diagnostics + + +def subscribe_mqtt_reports( + args: argparse.Namespace, + topic: str, + wait_seconds: float, + *, + push_all_topic: str | None = None, + max_messages: int = 1, +) -> list[dict[str, Any]]: + access_code = access_code_from_args(args) + validate_local_host(args.host, args.allow_nonprivate_host) + context = ssl.create_default_context() + if not args.tls_verify: + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + client_id = args.client_id or args.serial or f"codex-bambu-status-{int(time.time())}" + messages: list[dict[str, Any]] = [] + deadline = time.monotonic() + wait_seconds + with socket.create_connection((args.host, args.mqtt_port), timeout=args.timeout) as raw_sock: + with context.wrap_socket(raw_sock, server_hostname=args.host) as sock: + sock.sendall(mqtt_connect_packet(client_id, BAMBU_USERNAME, access_code)) + header, body = read_mqtt_packet(sock) + if header != 0x20 or len(body) != 2 or body[1] != 0: + raise BambuPrintError(f"MQTT CONNACK failed: header=0x{header:02x} body={body.hex()}") + sock.sendall(mqtt_subscribe_packet(1, topic)) + header, body = read_mqtt_packet(sock) + if header != 0x90 or len(body) < 3 or body[-1] == 0x80: + raise BambuPrintError(f"MQTT SUBACK failed: header=0x{header:02x} body={body.hex()}") + if push_all_topic: + payload = { + "pushing": { + "sequence_id": sequence_id(args.sequence_id), + "command": "pushall", + "version": 1, + "push_target": 1, + } + } + sock.sendall(mqtt_publish_packet(push_all_topic, json.dumps(payload, separators=(",", ":")).encode())) + while time.monotonic() < deadline: + sock.settimeout(max(0.1, min(args.timeout, deadline - time.monotonic()))) + try: + header, body = read_mqtt_packet(sock) + except socket.timeout: + break + if header & 0xF0 != 0x30: + continue + message_topic, payload = parse_mqtt_publish(header, body) + text = payload.decode("utf-8", errors="replace") + item: dict[str, Any] = {"topic": message_topic, "payload": text} + try: + item["json"] = json.loads(text) + except json.JSONDecodeError: + pass + annotate_hms_codes(item) + messages.append(item) + if len(messages) >= max_messages: + break + payload_json = item.get("json") + if ( + push_all_topic + and isinstance(payload_json, dict) + and isinstance(payload_json.get("print"), dict) + and payload_json["print"].get("command") == "push_status" + and payload_json["print"].get("msg") == 0 + ): + break + sock.sendall(bytes([0xE0, 0x00])) + return messages + + +def access_code_from_args(args: argparse.Namespace) -> str: + access_code = args.access_code or "" + if not access_code: + raise BambuPrintError("Access code is required for --execute. Configure --printer with config set or pass --access-code.") + if set(access_code) == {"0"}: + raise BambuPrintError("Access code is all zeros; refresh the printer access code before execution.") + return access_code + + +def tail_text(value: str, *, max_lines: int = 80) -> str: + lines = value.splitlines() + if len(lines) <= max_lines: + return value + return "\n".join(lines[-max_lines:]) + + +def package_main(args: argparse.Namespace) -> int: + plan = build_package_plan(args) + if not args.execute: + print(json.dumps(plan, indent=2)) + return 0 + + output_path = Path(args.output).expanduser().resolve() + result = package_with_bambox(args, output_path) + plan["dry_run"] = False + plan["executed"] = ["bambox_pack", "bambox_validate"] + plan["result"] = result + print(json.dumps(plan, indent=2)) + return 0 + + +def config_set_main(args: argparse.Namespace) -> int: + path = config_path_from_args(args) + data = load_config(path) + printers = data.setdefault("printers", {}) + entry = printers.get(args.printer) + if not isinstance(entry, dict): + entry = {} + printers[args.printer] = entry + if args.host: + entry["host"] = args.host + if args.access_code: + entry["access_code"] = args.access_code + if args.model: + entry["model"] = args.model + if args.notes: + entry["notes"] = args.notes + serial = args.serial or "" + if args.fetch_serial: + host = args.host or str(entry.get("host") or "") + if not host: + raise BambuPrintError("Cannot fetch serial without --host or an existing host in the printer config.") + serial = discover_printer_serial( + host=host, + port=args.mqtt_port, + timeout=args.timeout, + tls_verify=args.tls_verify, + allow_nonprivate_host=args.allow_nonprivate_host, + ) + entry["serial_source"] = "printer_tls_certificate_common_name" + if serial: + entry["serial"] = serial + entry["updated_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + save_config(path, data) + print(json.dumps({"ok": True, "config": str(path), "printer": args.printer, "entry": sanitized_printer_entry(entry)}, indent=2)) + return 0 + + +def config_list_main(args: argparse.Namespace) -> int: + path = config_path_from_args(args) + data = load_config(path) + printers = data.get("printers", {}) + payload = { + "config": str(path), + "printers": {printer_id: sanitized_printer_entry(entry) for printer_id, entry in sorted(printers.items())}, + } + print(json.dumps(payload, indent=2)) + return 0 + + +def config_show_main(args: argparse.Namespace) -> int: + path = config_path_from_args(args) + data = load_config(path) + entry = printer_entry(data, args.printer) + print(json.dumps({"config": str(path), "printer": args.printer, "entry": sanitized_printer_entry(entry)}, indent=2)) + return 0 + + +def serial_main(args: argparse.Namespace) -> int: + apply_printer_config(args) + serial = discover_printer_serial( + host=args.host, + port=args.mqtt_port, + timeout=args.timeout, + tls_verify=args.tls_verify, + allow_nonprivate_host=args.allow_nonprivate_host, + ) + args.serial = serial + cache_printer_serial(args, serial) + payload = { + "ok": True, + "printer": args.printer or None, + "host": args.host, + "config": str(config_path_from_args(args)) if args.printer else None, + "port": args.mqtt_port, + "serial": serial, + "source": "printer_tls_certificate_common_name", + } + if args.json: + print(json.dumps(payload, indent=2)) + else: + print(serial) + return 0 + + +def status_main(args: argparse.Namespace) -> int: + apply_printer_config(args) + serial = ensure_printer_serial(args) + if not serial: + raise BambuPrintError("Printer serial is required for status; configure --printer or pass --serial.") + topic = args.topic or f"device/{serial}/report" + push_all_topic = f"device/{serial}/request" if args.push_all else None + messages = subscribe_mqtt_reports( + args, + topic, + args.wait_seconds, + push_all_topic=push_all_topic, + max_messages=args.max_messages, + ) + payload = { + "ok": bool(messages), + "printer": args.printer or None, + "host": args.host, + "topic": topic, + "push_all_requested": bool(args.push_all), + "messages": messages, + } + if not messages: + payload["warning"] = f"No report message received within {args.wait_seconds:g} seconds." + print(json.dumps(payload, indent=2)) + return 0 if messages else 1 + + +def clear_error_main(args: argparse.Namespace) -> int: + apply_printer_config(args) + if args.execute and not ensure_printer_serial(args): + raise BambuPrintError("Printer serial is required to publish clean_print_error.") + plan = build_clear_error_plan(args) + if not args.execute: + print(json.dumps(plan, indent=2)) + return 0 + publish_result = publish_mqtt( + args, + plan["mqtt"]["topic"], + plan["mqtt"]["payload"], + report_topic=f"device/{args.serial}/report" if getattr(args, "serial", "") else None, + ) + plan["dry_run"] = False + plan["executed"] = ["publish_clean_print_error"] + plan["mqtt_publish_result"] = publish_result + print(json.dumps(plan, indent=2)) + return 0 + + +def print_control_main(args: argparse.Namespace) -> int: + apply_printer_config(args) + if args.execute and args.action == "cancel" and not args.confirm_cancel_print: + raise BambuPrintError("Canceling a print requires --confirm-cancel-print.") + if args.execute and not ensure_printer_serial(args): + raise BambuPrintError("Printer serial is required to publish a print-control command.") + plan = build_print_control_plan(args) + if not args.execute: + print(json.dumps(plan, indent=2)) + return 0 + publish_result = publish_mqtt( + args, + plan["mqtt"]["topic"], + plan["mqtt"]["payload"], + report_topic=f"device/{args.serial}/report" if getattr(args, "serial", "") else None, + ) + plan["dry_run"] = False + plan["executed"] = [f"publish_{args.action}_request"] + plan["mqtt_publish_result"] = publish_result + print(json.dumps(plan, indent=2)) + return 0 + + +def send_main(args: argparse.Namespace) -> int: + apply_printer_config(args) + if args.execute and args.action == "plan": + raise BambuPrintError("--execute has no effect with --action plan.") + if args.execute and args.action in {"start", "upload-start"} and not args.confirm_start_print: + raise BambuPrintError("Starting a print requires --confirm-start-print.") + if args.execute and args.action in {"start", "upload-start"} and not ensure_printer_serial(args): + raise BambuPrintError("Printer serial is required to publish a start command.") + plan = build_send_plan(args) + + if args.handoff == "plain": + local_path = Path(args.gcode).expanduser().resolve() + if not args.execute: + print(json.dumps(plan, indent=2)) + return 0 + return execute_send(args, plan, local_path) + + if not args.execute: + print(json.dumps(plan, indent=2)) + return 0 + + if args.handoff == "template-project": + with tempfile.TemporaryDirectory(prefix="bambu-template-project-") as tmp: + project_path = Path(tmp) / default_project_name(Path(args.gcode).expanduser()) + package_result = package_with_template_project(args, project_path) + plan = build_send_plan(args, project_path=project_path) + plan["template_project_execution"] = package_result + return execute_send(args, plan, project_path) + + with tempfile.TemporaryDirectory(prefix="bambu-bambox-") as tmp: + project_path = Path(tmp) / default_project_name(Path(args.gcode).expanduser()) + package_result = package_with_bambox(args, project_path) + plan = build_send_plan(args, project_path=project_path) + plan["bambox_execution"] = package_result + return execute_send(args, plan, project_path) + + +def execute_send(args: argparse.Namespace, plan: dict[str, Any], local_path: Path) -> int: + executed: list[str] = [] + if args.action in {"upload", "upload-start"}: + upload_ftps(args, local_path, plan["ftps"]["remote_path"]) + executed.append("upload") + if args.action in {"start", "upload-start"}: + publish_result = publish_mqtt( + args, + plan["mqtt"]["topic"], + plan["mqtt"]["payload"], + report_topic=f"device/{args.serial}/report" if getattr(args, "serial", "") else None, + ) + if isinstance(publish_result, dict): + plan["mqtt_publish_result"] = publish_result + executed.append("publish_start_request") + plan["dry_run"] = False + plan["local_upload_path"] = str(local_path) + plan["executed"] = executed + if "publish_start_request" in executed: + plan["execution_notes"] = [ + "MQTT start was published as a request; this helper does not confirm printer acceptance or print progress." + ] + print(json.dumps(plan, indent=2)) + return 0 + + +def add_package_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("package", help="Package plain G-code into a supported bambox .gcode.3mf project.") + parser.add_argument("--gcode", required=True, help="Validated plain .gcode input.") + parser.add_argument("--output", required=True, help="Output .gcode.3mf path.") + parser.add_argument("--bambox-profile", required=True, help="Enabled bambox compatibility profile such as p1s-0.4.") + parser.add_argument("--filament", action="append", help="bambox filament spec. Repeat for multi-filament jobs.") + parser.add_argument("--bambox-bin", help="Path to bambox CLI. Defaults to BAMBOX_BIN or bambox on PATH.") + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--dry-run", dest="execute", action="store_false", help="Print the packaging plan without running bambox.") + mode.add_argument("--execute", dest="execute", action="store_true", help="Run bambox pack and bambox validate.") + parser.set_defaults(execute=False, func=package_main) + + +def add_config_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("config", help="Read and write local Bambu printer JSON config.") + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + config_subparsers = parser.add_subparsers(dest="config_command", required=True) + + set_parser = config_subparsers.add_parser("set", help="Create or update a configured printer.") + set_parser.add_argument("--printer", required=True, help="Local printer id, for example a1-mini.") + set_parser.add_argument("--host", help="Printer LAN IP or hostname.") + set_parser.add_argument("--access-code", help="Printer LAN access code.") + set_parser.add_argument("--model", help="Printer model, for example a1-mini.") + set_parser.add_argument("--serial", help="Printer serial. Usually omitted; use --fetch-serial.") + set_parser.add_argument("--notes", help="Optional operator notes.") + set_parser.add_argument("--fetch-serial", action="store_true", help="Fetch and cache serial from the printer TLS certificate.") + set_parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + set_parser.add_argument("--timeout", type=float, default=20.0) + set_parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + set_parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + set_parser.set_defaults(func=config_set_main) + + list_parser = config_subparsers.add_parser("list", help="List configured printers.") + list_parser.set_defaults(func=config_list_main) + + show_parser = config_subparsers.add_parser("show", help="Show one configured printer without printing the access code.") + show_parser.add_argument("--printer", required=True) + show_parser.set_defaults(func=config_show_main) + + +def add_serial_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("serial", help="Fetch the printer serial from its local TLS certificate.") + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + parser.add_argument("--printer", help="Configured printer id.") + parser.add_argument("--host", default="", help="Printer LAN IP or hostname.") + parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + parser.add_argument("--timeout", type=float, default=20.0) + parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + parser.add_argument("--json", action="store_true") + parser.set_defaults(func=serial_main) + + +def add_status_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("status", help="Read one MQTT report message from a configured local printer.") + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + parser.add_argument("--printer", help="Configured printer id.") + parser.add_argument("--host", default="", help="Printer LAN IP or hostname.") + parser.add_argument( + "--serial", + default="", + help="Printer serial number for MQTT topic. Usually loaded from config or fetched automatically.", + ) + parser.add_argument("--access-code", help="Printer access code for one-off use. Prefer --printer config.") + parser.add_argument("--topic", help="MQTT report topic. Defaults to device/{serial}/report.") + parser.add_argument("--push-all", action="store_true", help="Request a full status report after subscribing.") + parser.add_argument("--wait-seconds", type=float, default=10.0) + parser.add_argument("--max-messages", type=int, default=1, help="Maximum report messages to collect before returning.") + parser.add_argument("--sequence-id", help="MQTT sequence id for --push-all. Defaults to current timestamp milliseconds.") + parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + parser.add_argument("--timeout", type=float, default=20.0) + parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + parser.add_argument("--client-id", help="MQTT client id. Defaults to the printer serial.") + parser.set_defaults(func=status_main) + + +def add_clear_error_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("clear-error", help="Publish Bambu clean_print_error; dry-run by default.") + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + parser.add_argument("--printer", help="Configured printer id.") + parser.add_argument("--host", default="", help="Printer LAN IP or hostname.") + parser.add_argument( + "--serial", + default="", + help="Printer serial number for MQTT topic. Usually loaded from config or fetched automatically.", + ) + parser.add_argument("--access-code", help="Printer access code for one-off use. Prefer --printer config.") + parser.add_argument("--execute", action="store_true", help="Publish clean_print_error to the printer.") + parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + parser.add_argument("--timeout", type=float, default=20.0) + parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + parser.add_argument("--client-id", help="MQTT client id. Defaults to the printer serial.") + parser.add_argument("--sequence-id", help="MQTT sequence id. Defaults to current timestamp milliseconds.") + parser.add_argument( + "--mqtt-qos", + type=int, + choices=[0, 1], + default=0, + help="MQTT publish QoS. Default 0; use 1 for protocol diagnostics.", + ) + parser.add_argument( + "--wait-after-publish", + type=float, + default=0.0, + help="After publishing, stay connected for this many seconds and capture report messages.", + ) + parser.set_defaults(func=clear_error_main) + + +def add_print_control_parser(subparsers: argparse._SubParsersAction, action: str) -> None: + help_text = "Pause the active local print; dry-run by default." if action == "pause" else "Cancel the active local print; dry-run by default." + parser = subparsers.add_parser(action, help=help_text) + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + parser.add_argument("--printer", help="Configured printer id.") + parser.add_argument("--host", default="", help="Printer LAN IP or hostname.") + parser.add_argument( + "--serial", + default="", + help="Printer serial number for MQTT topic. Usually loaded from config or fetched automatically.", + ) + parser.add_argument("--access-code", help="Printer access code for one-off use. Prefer --printer config.") + parser.add_argument("--execute", action="store_true", help=f"Publish the {action} request to the printer.") + if action == "cancel": + parser.add_argument( + "--confirm-cancel-print", + action="store_true", + help="Required with --execute because canceling stops the active print job.", + ) + parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + parser.add_argument("--timeout", type=float, default=20.0) + parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + parser.add_argument("--client-id", help="MQTT client id. Defaults to the printer serial.") + parser.add_argument("--sequence-id", help="MQTT sequence id. Defaults to current timestamp milliseconds.") + parser.add_argument( + "--mqtt-qos", + type=int, + choices=[0, 1], + default=0, + help="MQTT publish QoS. Default 0; use 1 for protocol diagnostics.", + ) + parser.add_argument( + "--wait-after-publish", + type=float, + default=0.0, + help="After publishing, stay connected for this many seconds and capture report messages.", + ) + parser.set_defaults(action=action, func=print_control_main) + + +def add_send_parser(subparsers: argparse._SubParsersAction) -> None: + parser = subparsers.add_parser("send", help="Inspect, upload, and optionally start a validated plain G-code job.") + parser.add_argument("--config", default=str(DEFAULT_CONFIG_PATH), help="Printer config JSON path.") + parser.add_argument("--printer", help="Configured printer id. Preferred over repeating host/access-code flags.") + parser.add_argument("--gcode", required=True, help="Validated plain .gcode input.") + parser.add_argument("--handoff", choices=["plain", "bambox-project", "template-project"], default="plain") + parser.add_argument("--host", default="", help="Printer LAN IP or hostname.") + parser.add_argument( + "--serial", + default="", + help="Printer serial number for MQTT topic. Usually omitted; the helper fetches it from the printer TLS certificate.", + ) + parser.add_argument("--access-code", help="Printer access code for one-off use. Prefer --printer config for repeated use.") + parser.add_argument("--action", choices=["plan", "upload", "start", "upload-start"], default="plan") + parser.add_argument("--execute", action="store_true", help="Perform network action selected by --action.") + parser.add_argument("--confirm-start-print", action="store_true", help="Required for --action start or upload-start with --execute.") + parser.add_argument( + "--remote-dir", + default=None, + help="Remote directory. Defaults to cache for plain G-code and printer root for project handoffs.", + ) + parser.add_argument("--remote-name", help="Filename to store on the printer.") + parser.add_argument("--gcode-param", help="Override MQTT gcode_file param. Defaults to the uploaded remote path.") + parser.add_argument("--plate", type=int, help="Plate number for packaged projects. Defaults to the first plate.") + parser.add_argument("--bambox-profile", default="p1s-0.4", help="Enabled bambox profile for --handoff bambox-project.") + parser.add_argument("--filament", action="append", help="bambox filament spec. Repeat for multi-filament jobs.") + parser.add_argument("--bambox-bin", help="Path to bambox CLI. Defaults to BAMBOX_BIN or bambox on PATH.") + parser.add_argument( + "--template-project", + help="Same-printer sliced .gcode.3mf template for --handoff template-project.", + ) + parser.add_argument("--ftps-port", type=int, default=DEFAULT_FTPS_PORT) + parser.add_argument("--mqtt-port", type=int, default=DEFAULT_MQTT_PORT) + parser.add_argument("--timeout", type=float, default=20.0) + parser.add_argument("--tls-verify", action="store_true", help="Verify printer TLS certificates.") + parser.add_argument( + "--allow-nonprivate-host", + action="store_true", + help="Allow printer hosts that resolve outside private/link-local/loopback ranges.", + ) + parser.add_argument("--client-id", help="MQTT client id. Defaults to the printer serial for start commands.") + parser.add_argument("--sequence-id", help="MQTT sequence id. Defaults to current timestamp milliseconds.") + parser.add_argument( + "--mqtt-qos", + type=int, + choices=[0, 1], + default=0, + help="MQTT publish QoS for start commands. Default 0; use 1 for protocol diagnostics.", + ) + parser.add_argument( + "--wait-after-publish", + type=float, + default=0.0, + help="After publishing a start command, stay connected for this many seconds and capture report messages.", + ) + parser.set_defaults(func=send_main) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + add_package_parser(subparsers) + add_config_parser(subparsers) + add_serial_parser(subparsers) + add_status_parser(subparsers) + add_clear_error_parser(subparsers) + add_print_control_parser(subparsers, "pause") + add_print_control_parser(subparsers, "cancel") + add_send_parser(subparsers) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv if argv is not None else sys.argv[1:]) + try: + return args.func(args) + except BambuPrintError as exc: + print(json.dumps({"error": str(exc)}, indent=2), file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/cad/skills/cad b/plugins/cad/skills/cad deleted file mode 120000 index 2081dcdf..00000000 --- a/plugins/cad/skills/cad +++ /dev/null @@ -1 +0,0 @@ -../../../skills/cad \ No newline at end of file diff --git a/plugins/cad/skills/cad-viewer b/plugins/cad/skills/cad-viewer deleted file mode 120000 index ee8c5061..00000000 --- a/plugins/cad/skills/cad-viewer +++ /dev/null @@ -1 +0,0 @@ -../../../skills/cad-viewer \ No newline at end of file diff --git a/plugins/cad/skills/cad-viewer/LICENSE b/plugins/cad/skills/cad-viewer/LICENSE new file mode 100644 index 00000000..eabcff2c --- /dev/null +++ b/plugins/cad/skills/cad-viewer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 earthtojake + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/cad/skills/cad-viewer/SKILL.md b/plugins/cad/skills/cad-viewer/SKILL.md new file mode 100644 index 00000000..8653a2d6 --- /dev/null +++ b/plugins/cad/skills/cad-viewer/SKILL.md @@ -0,0 +1,95 @@ +--- +name: cad-viewer +description: Start or reuse CAD Viewer and return review links for explicit CAD, implicit CAD, robot-description, and G-code files. Use when visually reviewing `.step`, `.stp`, `.implicit.js`, `.implicit.mjs`, `.glb`, `.stl`, `.3mf`, `.gcode`, `.dxf`, `.urdf`, `.srdf`, or `.sdf` files, especially when handed off from CAD, implicit-cad, G-code, URDF, SRDF, or SDF generation skills. +--- + +# CAD Viewer + +Provenance: maintained in [earthtojake/text-to-cad](https://github.com/earthtojake/text-to-cad). +Use the installed local skill files as the runtime source of truth; the +repository link is only for provenance and release review. + +Use this skill to open existing or newly generated CAD, implicit CAD, +robot-description, DXF, or plain FDM G-code files in CAD Viewer and hand back +live review links. The expected input is one or more explicit file paths. + +## Start Viewer + +Start or reuse one local CAD Viewer with `npm run agent:start`, passing the +absolute artifact directory as `--dir`. The `agent:start` launcher owns port +selection, compatible-server reuse, directory activation, and the `?dir=` query +parameter. Dev-mode viewers are reused only for matching git identities; dist +bundle viewers can be reused across git branches when their viewer versions +match. It activates reused servers through the Viewer's lightweight directory +activation API, without requiring agents to probe ports or trigger catalog +scans manually. Use the Viewer URL printed by `agent:start` as-is, then add only +a `file=` query value for the artifact you want to review. + +Choose `--dir` as the absolute directory that contains the model +artifacts and sidecars, commonly `/models` or the consuming project's +equivalent model directory. The `file=` value must be relative to that `--dir`. +Do not manually choose ports, probe servers, rewrite `?dir=`, or start a +separate Viewer just to change directories. + +Run from this skill directory: + +```bash +npm --prefix scripts/viewer run agent:start -- --host 127.0.0.1 --dir +``` + +Use the printed Viewer URL and append `file=`: + +```bash +http://127.0.0.1:/?dir=/absolute/project/models&file=path/to/model.step +``` + +If a non-Viewer process or another worktree's Viewer occupies the candidate +port, the launcher will continue automatically. In sandboxed agent environments, +local binding or probe failures such as `EPERM` or `EACCES` can be expected; +rerun the same command with the needed permission/escalation. + +## Links + +- Before returning any `file=` link, resolve `/` and confirm the + artifact exists. Pass the generated artifact (e.g. `.step`), not its + generator source (e.g. `.py`). If the resolved path is missing, do not + return the link, and instead report the problem and point to the correct + generated artifact path. +- Return one Viewer URL per requested file. +- Start/reuse the Viewer once per absolute directory `--dir`, then append + `file=` for each requested file. The file path must be relative to + `--dir`. +- For directory-only review links, return the URL printed by `agent:start` + without adding `file=`. +- Do not stop an existing Viewer server unless the user asks. +- If Viewer startup fails, report the failure and continue with the owning skill's non-GUI validation or artifacts. + +## Claude Preview + +The viewer port is dynamic — it is chosen at startup and may differ across +worktrees. To integrate with the Claude Preview tool, add `--json` to the +`agent:start` command: + +```bash +npm --prefix scripts/viewer run agent:start -- --host 127.0.0.1 --dir --json +``` + +The launcher writes a JSON result line to stdout after the human-readable lines. +Parse it by taking the last line of stdout that begins with `{`: + +```json +{"url":"http://127.0.0.1:/?dir=","port":,"action":"reuse"} +``` + +`action` is `"reuse"` when an existing server was reused and is immediately +ready, or `"start"` when a new server process was spawned and may still be +initializing. For a `"start"` result, probe `GET /__cad/server` on the base +URL (e.g. `http://127.0.0.1:/__cad/server`) until it returns HTTP 200 +before passing the `url` value to the Claude Preview tool. + +## References + +- Read `references/development.md` when the user asks to modify, debug, or + iterate on CAD Viewer source. +- Read `references/viewer-features.md` when you need supported file types, Viewer controls, or file-specific feature details. +- Read `references/moveit2-server.md` only when the user specifically needs optional SRDF MoveIt2 IK or path-planning controls. diff --git a/plugins/cad/skills/cad-viewer/agents/openai.yaml b/plugins/cad/skills/cad-viewer/agents/openai.yaml new file mode 100644 index 00000000..e754081f --- /dev/null +++ b/plugins/cad/skills/cad-viewer/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "CAD Viewer" + short_description: "Open CAD, G-code, and robot files in Viewer." + default_prompt: "Use $cad-viewer to start CAD Viewer and return review links for explicit CAD, G-code, or robot-description files." diff --git a/plugins/cad/skills/cad-viewer/references/development.md b/plugins/cad/skills/cad-viewer/references/development.md new file mode 100644 index 00000000..e9d5c04a --- /dev/null +++ b/plugins/cad/skills/cad-viewer/references/development.md @@ -0,0 +1,12 @@ +# CAD Viewer Development + +Use this reference only when the user asks to modify, debug, or iterate on CAD +Viewer source. + +CAD Viewer source development happens in the root +source workspace, not inside this generated skill runtime. + +If you are not currently working directly in the editable source workspace, ask +the user for the correct checkout and run the agent from that repository. + +Once inside the repository, read root `AGENTS.md` for guidance on modifying and running the Viewer. diff --git a/plugins/cad/skills/cad-viewer/references/moveit2-server.md b/plugins/cad/skills/cad-viewer/references/moveit2-server.md new file mode 100644 index 00000000..b2208e12 --- /dev/null +++ b/plugins/cad/skills/cad-viewer/references/moveit2-server.md @@ -0,0 +1,29 @@ +# MoveIt2 Server + +Load this only when the user specifically needs CAD Viewer's optional SRDF IK or path-planning controls. Plain SRDF review and normal Viewer links do not require MoveIt2. + +## Start + +Run from the `cad-viewer` skill directory: + +```bash +npm --prefix scripts/viewer run moveit2:setup +npm --prefix scripts/viewer run moveit2:check +npm --prefix scripts/viewer run moveit2:serve +``` + +The server defaults to `ws://127.0.0.1:8765/ws`. CAD Viewer connects to that URL when `VIEWER_MOVEIT2_WS_URL` or the browser `?moveit2Ws=` query points at it. + +Use the configured ROS 2 / MoveIt2 environment. Do not install ROS 2 or MoveIt2 packages into the repository CAD `.venv`. + +## Viewer Controls + +Open an `.srdf` file, expand the right-side `MoveIt2` sheet, and use: + +- Status: linked SRDF and MoveIt2 server status. +- Target: planning group, end effector, target frame, and X/Y/Z target coordinates. +- Solver: IK timeout, attempts, and tolerance. +- Planning: pipeline, planner ID, plan time, velocity scale, and acceleration scale. +- Actions: select pose from the model, reset to the current pose, solve pose, or plan to pose. + +Report whether the environment check passed, whether pose solving/planning worked, and any viewer/server error text. diff --git a/plugins/cad/skills/cad-viewer/references/viewer-features.md b/plugins/cad/skills/cad-viewer/references/viewer-features.md new file mode 100644 index 00000000..70c5c30a --- /dev/null +++ b/plugins/cad/skills/cad-viewer/references/viewer-features.md @@ -0,0 +1,22 @@ +# CAD Viewer Features + +Load this only when a task needs Viewer file-support details or UI control guidance. + +## Supported Files + +- `.step`, `.stp`: STEP/STP review through hidden GLB sidecars; supports assembly trees, part hide/show, inspect/focus, face/edge/vertex/part selection, copied `#...` CAD references, display modes, clip planes, and optional STEP module parameters/animations when a sidecar module exists. +- `.stl`, `.3mf`, `.glb`: mesh viewing with orbit/pan/zoom, screenshots, appearance controls, and solid/wireframe display where available. +- `.dxf`: read-only flat-pattern viewing, plus plate thickness and bend direction/angle controls when bend preview data is available. +- `.gcode`: diagnostic toolpath preview; shows layer-colored extrusion ribbons, optional travel moves, visible-layer and detail controls, feature markers, movement stats, bounds, and parser warnings. It does not reslice, simulate firmware, or replace G-code validation. +- `.urdf`: robot link/mesh viewing with movable joint sliders, reset pose, and copied joint values. +- `.srdf`: linked-URDF viewing with planning groups, group-state presets, joint controls, and optional MoveIt2 IK/planning controls. +- `.sdf`: SDF model/world viewing with metadata, counts, warnings, and joint controls when available. + +## Controls + +- Navigation: left-drag to orbit, right/middle-drag to pan, wheel or pinch to zoom, and Arrow/WASD keys to orbit. Use the view sphere for top/bottom/front/back/left/right views; click its center for the default isometric view. +- File browser: toggle the left CAD Viewer sidebar, search files/ids/paths, expand folders, select entries, or switch files from the breadcrumb menus. +- Floating toolbar: `Select` copies STEP topology references, `Draw` opens annotation tools, `Select Pose` appears for robot target picking when available, `Open orbit preview` starts an auto-rotating preview, and the copy/download buttons capture screenshots. +- Drawing tools: freehand, line, arrow, expand, rectangle, circle, fill, erase, undo, redo, and clear. +- File sheet: open the right sheet for file-specific controls such as STEP tree/parameters, G-code layers/detail/travel, DXF thickness/bends, URDF/SRDF/SDF joints and metadata, plus display and appearance controls. +- Display/appearance: use the file sheet and theme menu for solid/wireframe/x-ray modes, STEP clipping, an independent Explode toggle for animated vertical STEP disassembly, surface colors, edge visibility/detail, highlight styling, backdrop, floor/grid, lighting, theme presets, and color mode. diff --git a/plugins/cad/skills/cad-viewer/requirements.txt b/plugins/cad/skills/cad-viewer/requirements.txt new file mode 100644 index 00000000..271f36ff --- /dev/null +++ b/plugins/cad/skills/cad-viewer/requirements.txt @@ -0,0 +1 @@ +--editable ./scripts/viewer/packages/cadpy diff --git a/plugins/cad/skills/cad-viewer/scripts/viewer/.gitignore b/plugins/cad/skills/cad-viewer/scripts/viewer/.gitignore new file mode 100644 index 00000000..92dad316 --- /dev/null +++ b/plugins/cad/skills/cad-viewer/scripts/viewer/.gitignore @@ -0,0 +1,12 @@ +node_modules +.env +.env.* +!.env.example +!.env.*.example +__pycache__ +*.py[cod] +.pytest_cache +tmp + +!dist +!dist/** diff --git a/plugins/cad/skills/cad-viewer/scripts/viewer/backend/server.mjs b/plugins/cad/skills/cad-viewer/scripts/viewer/backend/server.mjs new file mode 100755 index 00000000..edb0c978 --- /dev/null +++ b/plugins/cad/skills/cad-viewer/scripts/viewer/backend/server.mjs @@ -0,0 +1,28692 @@ +#!/usr/bin/env node +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] +}) : x)(function(x) { + if (typeof require !== "undefined") return require.apply(this, arguments); + throw Error('Dynamic require of "' + x + '" is not supported'); +}); +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __commonJS = (cb, mod2) => function __require2() { + return mod2 || (0, cb[__getOwnPropNames(cb)[0]])((mod2 = { exports: {} }).exports, mod2), mod2.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod2, isNodeMode, target) => (target = mod2 != null ? __create(__getProtoOf(mod2)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod2 || !mod2.__esModule ? __defProp(target, "default", { value: mod2, enumerable: true }) : target, + mod2 +)); + +// viewer/node_modules/is-node-process/lib/index.mjs +function isNodeProcess() { + if (typeof navigator !== "undefined" && navigator.product === "ReactNative") { + return true; + } + if (typeof process !== "undefined") { + const type = process.type; + if (type === "renderer" || type === "worker") { + return false; + } + return !!(process.versions && process.versions.node); + } + return false; +} +var init_lib = __esm({ + "viewer/node_modules/is-node-process/lib/index.mjs"() { + } +}); + +// viewer/node_modules/is-buffer/index.js +var require_is_buffer = __commonJS({ + "viewer/node_modules/is-buffer/index.js"(exports, module) { + module.exports = function isBuffer2(obj) { + return obj != null && obj.constructor != null && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj); + }; + } +}); + +// viewer/node_modules/retry/lib/retry_operation.js +var require_retry_operation = __commonJS({ + "viewer/node_modules/retry/lib/retry_operation.js"(exports, module) { + function RetryOperation(timeouts, options) { + if (typeof options === "boolean") { + options = { forever: options }; + } + this._originalTimeouts = JSON.parse(JSON.stringify(timeouts)); + this._timeouts = timeouts; + this._options = options || {}; + this._maxRetryTime = options && options.maxRetryTime || Infinity; + this._fn = null; + this._errors = []; + this._attempts = 1; + this._operationTimeout = null; + this._operationTimeoutCb = null; + this._timeout = null; + this._operationStart = null; + this._timer = null; + if (this._options.forever) { + this._cachedTimeouts = this._timeouts.slice(0); + } + } + module.exports = RetryOperation; + RetryOperation.prototype.reset = function() { + this._attempts = 1; + this._timeouts = this._originalTimeouts.slice(0); + }; + RetryOperation.prototype.stop = function() { + if (this._timeout) { + clearTimeout(this._timeout); + } + if (this._timer) { + clearTimeout(this._timer); + } + this._timeouts = []; + this._cachedTimeouts = null; + }; + RetryOperation.prototype.retry = function(err) { + if (this._timeout) { + clearTimeout(this._timeout); + } + if (!err) { + return false; + } + var currentTime = (/* @__PURE__ */ new Date()).getTime(); + if (err && currentTime - this._operationStart >= this._maxRetryTime) { + this._errors.push(err); + this._errors.unshift(new Error("RetryOperation timeout occurred")); + return false; + } + this._errors.push(err); + var timeout = this._timeouts.shift(); + if (timeout === void 0) { + if (this._cachedTimeouts) { + this._errors.splice(0, this._errors.length - 1); + timeout = this._cachedTimeouts.slice(-1); + } else { + return false; + } + } + var self = this; + this._timer = setTimeout(function() { + self._attempts++; + if (self._operationTimeoutCb) { + self._timeout = setTimeout(function() { + self._operationTimeoutCb(self._attempts); + }, self._operationTimeout); + if (self._options.unref) { + self._timeout.unref(); + } + } + self._fn(self._attempts); + }, timeout); + if (this._options.unref) { + this._timer.unref(); + } + return true; + }; + RetryOperation.prototype.attempt = function(fn, timeoutOps) { + this._fn = fn; + if (timeoutOps) { + if (timeoutOps.timeout) { + this._operationTimeout = timeoutOps.timeout; + } + if (timeoutOps.cb) { + this._operationTimeoutCb = timeoutOps.cb; + } + } + var self = this; + if (this._operationTimeoutCb) { + this._timeout = setTimeout(function() { + self._operationTimeoutCb(); + }, self._operationTimeout); + } + this._operationStart = (/* @__PURE__ */ new Date()).getTime(); + this._fn(this._attempts); + }; + RetryOperation.prototype.try = function(fn) { + console.log("Using RetryOperation.try() is deprecated"); + this.attempt(fn); + }; + RetryOperation.prototype.start = function(fn) { + console.log("Using RetryOperation.start() is deprecated"); + this.attempt(fn); + }; + RetryOperation.prototype.start = RetryOperation.prototype.try; + RetryOperation.prototype.errors = function() { + return this._errors; + }; + RetryOperation.prototype.attempts = function() { + return this._attempts; + }; + RetryOperation.prototype.mainError = function() { + if (this._errors.length === 0) { + return null; + } + var counts = {}; + var mainError = null; + var mainErrorCount = 0; + for (var i = 0; i < this._errors.length; i++) { + var error = this._errors[i]; + var message = error.message; + var count = (counts[message] || 0) + 1; + counts[message] = count; + if (count >= mainErrorCount) { + mainError = error; + mainErrorCount = count; + } + } + return mainError; + }; + } +}); + +// viewer/node_modules/retry/lib/retry.js +var require_retry = __commonJS({ + "viewer/node_modules/retry/lib/retry.js"(exports) { + var RetryOperation = require_retry_operation(); + exports.operation = function(options) { + var timeouts = exports.timeouts(options); + return new RetryOperation(timeouts, { + forever: options && (options.forever || options.retries === Infinity), + unref: options && options.unref, + maxRetryTime: options && options.maxRetryTime + }); + }; + exports.timeouts = function(options) { + if (options instanceof Array) { + return [].concat(options); + } + var opts = { + retries: 10, + factor: 2, + minTimeout: 1 * 1e3, + maxTimeout: Infinity, + randomize: false + }; + for (var key in options) { + opts[key] = options[key]; + } + if (opts.minTimeout > opts.maxTimeout) { + throw new Error("minTimeout is greater than maxTimeout"); + } + var timeouts = []; + for (var i = 0; i < opts.retries; i++) { + timeouts.push(this.createTimeout(i, opts)); + } + if (options && options.forever && !timeouts.length) { + timeouts.push(this.createTimeout(i, opts)); + } + timeouts.sort(function(a, b) { + return a - b; + }); + return timeouts; + }; + exports.createTimeout = function(attempt, opts) { + var random = opts.randomize ? Math.random() + 1 : 1; + var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt)); + timeout = Math.min(timeout, opts.maxTimeout); + return timeout; + }; + exports.wrap = function(obj, options, methods) { + if (options instanceof Array) { + methods = options; + options = null; + } + if (!methods) { + methods = []; + for (var key in obj) { + if (typeof obj[key] === "function") { + methods.push(key); + } + } + } + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + var original = obj[method]; + obj[method] = function retryWrapper(original2) { + var op = exports.operation(options); + var args = Array.prototype.slice.call(arguments, 1); + var callback = args.pop(); + args.push(function(err) { + if (op.retry(err)) { + return; + } + if (err) { + arguments[0] = op.mainError(); + } + callback.apply(this, arguments); + }); + op.attempt(function() { + original2.apply(obj, args); + }); + }.bind(obj, original); + obj[method].options = options; + } + }; + } +}); + +// viewer/node_modules/retry/index.js +var require_retry2 = __commonJS({ + "viewer/node_modules/retry/index.js"(exports, module) { + module.exports = require_retry(); + } +}); + +// viewer/node_modules/async-retry/lib/index.js +var require_lib = __commonJS({ + "viewer/node_modules/async-retry/lib/index.js"(exports, module) { + var retrier = require_retry2(); + function retry2(fn, opts) { + function run(resolve, reject) { + var options = opts || {}; + var op; + if (!("randomize" in options)) { + options.randomize = true; + } + op = retrier.operation(options); + function bail(err) { + reject(err || new Error("Aborted")); + } + function onError(err, num) { + if (err.bail) { + bail(err); + return; + } + if (!op.retry(err)) { + reject(op.mainError()); + } else if (options.onRetry) { + options.onRetry(err, num); + } + } + function runAttempt(num) { + var val; + try { + val = fn(bail, num); + } catch (err) { + onError(err, num); + return; + } + Promise.resolve(val).then(resolve).catch(function catchIt(err) { + onError(err, num); + }); + } + op.attempt(runAttempt); + } + return new Promise(run); + } + module.exports = retry2; + } +}); + +// viewer/node_modules/undici/lib/core/symbols.js +var require_symbols = __commonJS({ + "viewer/node_modules/undici/lib/core/symbols.js"(exports, module) { + module.exports = { + kClose: /* @__PURE__ */ Symbol("close"), + kDestroy: /* @__PURE__ */ Symbol("destroy"), + kDispatch: /* @__PURE__ */ Symbol("dispatch"), + kUrl: /* @__PURE__ */ Symbol("url"), + kWriting: /* @__PURE__ */ Symbol("writing"), + kResuming: /* @__PURE__ */ Symbol("resuming"), + kQueue: /* @__PURE__ */ Symbol("queue"), + kConnect: /* @__PURE__ */ Symbol("connect"), + kConnecting: /* @__PURE__ */ Symbol("connecting"), + kKeepAliveDefaultTimeout: /* @__PURE__ */ Symbol("default keep alive timeout"), + kKeepAliveMaxTimeout: /* @__PURE__ */ Symbol("max keep alive timeout"), + kKeepAliveTimeoutThreshold: /* @__PURE__ */ Symbol("keep alive timeout threshold"), + kKeepAliveTimeoutValue: /* @__PURE__ */ Symbol("keep alive timeout"), + kKeepAlive: /* @__PURE__ */ Symbol("keep alive"), + kHeadersTimeout: /* @__PURE__ */ Symbol("headers timeout"), + kBodyTimeout: /* @__PURE__ */ Symbol("body timeout"), + kServerName: /* @__PURE__ */ Symbol("server name"), + kLocalAddress: /* @__PURE__ */ Symbol("local address"), + kHost: /* @__PURE__ */ Symbol("host"), + kNoRef: /* @__PURE__ */ Symbol("no ref"), + kBodyUsed: /* @__PURE__ */ Symbol("used"), + kBody: /* @__PURE__ */ Symbol("abstracted request body"), + kRunning: /* @__PURE__ */ Symbol("running"), + kBlocking: /* @__PURE__ */ Symbol("blocking"), + kPending: /* @__PURE__ */ Symbol("pending"), + kSize: /* @__PURE__ */ Symbol("size"), + kBusy: /* @__PURE__ */ Symbol("busy"), + kQueued: /* @__PURE__ */ Symbol("queued"), + kFree: /* @__PURE__ */ Symbol("free"), + kConnected: /* @__PURE__ */ Symbol("connected"), + kClosed: /* @__PURE__ */ Symbol("closed"), + kNeedDrain: /* @__PURE__ */ Symbol("need drain"), + kReset: /* @__PURE__ */ Symbol("reset"), + kDestroyed: /* @__PURE__ */ Symbol.for("nodejs.stream.destroyed"), + kResume: /* @__PURE__ */ Symbol("resume"), + kOnError: /* @__PURE__ */ Symbol("on error"), + kMaxHeadersSize: /* @__PURE__ */ Symbol("max headers size"), + kRunningIdx: /* @__PURE__ */ Symbol("running index"), + kPendingIdx: /* @__PURE__ */ Symbol("pending index"), + kError: /* @__PURE__ */ Symbol("error"), + kClients: /* @__PURE__ */ Symbol("clients"), + kClient: /* @__PURE__ */ Symbol("client"), + kParser: /* @__PURE__ */ Symbol("parser"), + kOnDestroyed: /* @__PURE__ */ Symbol("destroy callbacks"), + kPipelining: /* @__PURE__ */ Symbol("pipelining"), + kSocket: /* @__PURE__ */ Symbol("socket"), + kHostHeader: /* @__PURE__ */ Symbol("host header"), + kConnector: /* @__PURE__ */ Symbol("connector"), + kStrictContentLength: /* @__PURE__ */ Symbol("strict content length"), + kMaxRedirections: /* @__PURE__ */ Symbol("maxRedirections"), + kMaxRequests: /* @__PURE__ */ Symbol("maxRequestsPerClient"), + kProxy: /* @__PURE__ */ Symbol("proxy agent options"), + kCounter: /* @__PURE__ */ Symbol("socket request counter"), + kInterceptors: /* @__PURE__ */ Symbol("dispatch interceptors"), + kMaxResponseSize: /* @__PURE__ */ Symbol("max response size"), + kHTTP2Session: /* @__PURE__ */ Symbol("http2Session"), + kHTTP2SessionState: /* @__PURE__ */ Symbol("http2Session state"), + kRetryHandlerDefaultRetry: /* @__PURE__ */ Symbol("retry agent default retry"), + kConstruct: /* @__PURE__ */ Symbol("constructable"), + kListeners: /* @__PURE__ */ Symbol("listeners"), + kHTTPContext: /* @__PURE__ */ Symbol("http context"), + kMaxConcurrentStreams: /* @__PURE__ */ Symbol("max concurrent streams"), + kNoProxyAgent: /* @__PURE__ */ Symbol("no proxy agent"), + kHttpProxyAgent: /* @__PURE__ */ Symbol("http proxy agent"), + kHttpsProxyAgent: /* @__PURE__ */ Symbol("https proxy agent") + }; + } +}); + +// viewer/node_modules/undici/lib/core/errors.js +var require_errors = __commonJS({ + "viewer/node_modules/undici/lib/core/errors.js"(exports, module) { + "use strict"; + var kUndiciError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR"); + var UndiciError = class extends Error { + constructor(message) { + super(message); + this.name = "UndiciError"; + this.code = "UND_ERR"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kUndiciError] === true; + } + [kUndiciError] = true; + }; + var kConnectTimeoutError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_CONNECT_TIMEOUT"); + var ConnectTimeoutError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "ConnectTimeoutError"; + this.message = message || "Connect Timeout Error"; + this.code = "UND_ERR_CONNECT_TIMEOUT"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kConnectTimeoutError] === true; + } + [kConnectTimeoutError] = true; + }; + var kHeadersTimeoutError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_HEADERS_TIMEOUT"); + var HeadersTimeoutError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "HeadersTimeoutError"; + this.message = message || "Headers Timeout Error"; + this.code = "UND_ERR_HEADERS_TIMEOUT"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kHeadersTimeoutError] === true; + } + [kHeadersTimeoutError] = true; + }; + var kHeadersOverflowError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_HEADERS_OVERFLOW"); + var HeadersOverflowError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "HeadersOverflowError"; + this.message = message || "Headers Overflow Error"; + this.code = "UND_ERR_HEADERS_OVERFLOW"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kHeadersOverflowError] === true; + } + [kHeadersOverflowError] = true; + }; + var kBodyTimeoutError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_BODY_TIMEOUT"); + var BodyTimeoutError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "BodyTimeoutError"; + this.message = message || "Body Timeout Error"; + this.code = "UND_ERR_BODY_TIMEOUT"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kBodyTimeoutError] === true; + } + [kBodyTimeoutError] = true; + }; + var kResponseStatusCodeError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_RESPONSE_STATUS_CODE"); + var ResponseStatusCodeError = class extends UndiciError { + constructor(message, statusCode, headers, body) { + super(message); + this.name = "ResponseStatusCodeError"; + this.message = message || "Response Status Code Error"; + this.code = "UND_ERR_RESPONSE_STATUS_CODE"; + this.body = body; + this.status = statusCode; + this.statusCode = statusCode; + this.headers = headers; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kResponseStatusCodeError] === true; + } + [kResponseStatusCodeError] = true; + }; + var kInvalidArgumentError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_INVALID_ARG"); + var InvalidArgumentError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "InvalidArgumentError"; + this.message = message || "Invalid Argument Error"; + this.code = "UND_ERR_INVALID_ARG"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kInvalidArgumentError] === true; + } + [kInvalidArgumentError] = true; + }; + var kInvalidReturnValueError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_INVALID_RETURN_VALUE"); + var InvalidReturnValueError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "InvalidReturnValueError"; + this.message = message || "Invalid Return Value Error"; + this.code = "UND_ERR_INVALID_RETURN_VALUE"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kInvalidReturnValueError] === true; + } + [kInvalidReturnValueError] = true; + }; + var kAbortError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_ABORT"); + var AbortError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "AbortError"; + this.message = message || "The operation was aborted"; + this.code = "UND_ERR_ABORT"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kAbortError] === true; + } + [kAbortError] = true; + }; + var kRequestAbortedError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_ABORTED"); + var RequestAbortedError = class extends AbortError { + constructor(message) { + super(message); + this.name = "AbortError"; + this.message = message || "Request aborted"; + this.code = "UND_ERR_ABORTED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kRequestAbortedError] === true; + } + [kRequestAbortedError] = true; + }; + var kInformationalError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_INFO"); + var InformationalError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "InformationalError"; + this.message = message || "Request information"; + this.code = "UND_ERR_INFO"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kInformationalError] === true; + } + [kInformationalError] = true; + }; + var kRequestContentLengthMismatchError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_REQ_CONTENT_LENGTH_MISMATCH"); + var RequestContentLengthMismatchError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "RequestContentLengthMismatchError"; + this.message = message || "Request body length does not match content-length header"; + this.code = "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kRequestContentLengthMismatchError] === true; + } + [kRequestContentLengthMismatchError] = true; + }; + var kResponseContentLengthMismatchError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_RES_CONTENT_LENGTH_MISMATCH"); + var ResponseContentLengthMismatchError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "ResponseContentLengthMismatchError"; + this.message = message || "Response body length does not match content-length header"; + this.code = "UND_ERR_RES_CONTENT_LENGTH_MISMATCH"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kResponseContentLengthMismatchError] === true; + } + [kResponseContentLengthMismatchError] = true; + }; + var kClientDestroyedError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_DESTROYED"); + var ClientDestroyedError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "ClientDestroyedError"; + this.message = message || "The client is destroyed"; + this.code = "UND_ERR_DESTROYED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kClientDestroyedError] === true; + } + [kClientDestroyedError] = true; + }; + var kClientClosedError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_CLOSED"); + var ClientClosedError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "ClientClosedError"; + this.message = message || "The client is closed"; + this.code = "UND_ERR_CLOSED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kClientClosedError] === true; + } + [kClientClosedError] = true; + }; + var kSocketError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_SOCKET"); + var SocketError = class extends UndiciError { + constructor(message, socket) { + super(message); + this.name = "SocketError"; + this.message = message || "Socket error"; + this.code = "UND_ERR_SOCKET"; + this.socket = socket; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kSocketError] === true; + } + [kSocketError] = true; + }; + var kNotSupportedError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_NOT_SUPPORTED"); + var NotSupportedError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "NotSupportedError"; + this.message = message || "Not supported error"; + this.code = "UND_ERR_NOT_SUPPORTED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kNotSupportedError] === true; + } + [kNotSupportedError] = true; + }; + var kBalancedPoolMissingUpstreamError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_BPL_MISSING_UPSTREAM"); + var BalancedPoolMissingUpstreamError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "MissingUpstreamError"; + this.message = message || "No upstream has been added to the BalancedPool"; + this.code = "UND_ERR_BPL_MISSING_UPSTREAM"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kBalancedPoolMissingUpstreamError] === true; + } + [kBalancedPoolMissingUpstreamError] = true; + }; + var kHTTPParserError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_HTTP_PARSER"); + var HTTPParserError = class extends Error { + constructor(message, code, data) { + super(message); + this.name = "HTTPParserError"; + this.code = code ? `HPE_${code}` : void 0; + this.data = data ? data.toString() : void 0; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kHTTPParserError] === true; + } + [kHTTPParserError] = true; + }; + var kResponseExceededMaxSizeError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_RES_EXCEEDED_MAX_SIZE"); + var ResponseExceededMaxSizeError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "ResponseExceededMaxSizeError"; + this.message = message || "Response content exceeded max size"; + this.code = "UND_ERR_RES_EXCEEDED_MAX_SIZE"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kResponseExceededMaxSizeError] === true; + } + [kResponseExceededMaxSizeError] = true; + }; + var kRequestRetryError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_REQ_RETRY"); + var RequestRetryError = class extends UndiciError { + constructor(message, code, { headers, data }) { + super(message); + this.name = "RequestRetryError"; + this.message = message || "Request retry error"; + this.code = "UND_ERR_REQ_RETRY"; + this.statusCode = code; + this.data = data; + this.headers = headers; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kRequestRetryError] === true; + } + [kRequestRetryError] = true; + }; + var kResponseError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_RESPONSE"); + var ResponseError = class extends UndiciError { + constructor(message, code, { headers, data }) { + super(message); + this.name = "ResponseError"; + this.message = message || "Response error"; + this.code = "UND_ERR_RESPONSE"; + this.statusCode = code; + this.data = data; + this.headers = headers; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kResponseError] === true; + } + [kResponseError] = true; + }; + var kSecureProxyConnectionError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_PRX_TLS"); + var SecureProxyConnectionError = class extends UndiciError { + constructor(cause, message, options) { + super(message, { cause, ...options ?? {} }); + this.name = "SecureProxyConnectionError"; + this.message = message || "Secure Proxy Connection failed"; + this.code = "UND_ERR_PRX_TLS"; + this.cause = cause; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kSecureProxyConnectionError] === true; + } + [kSecureProxyConnectionError] = true; + }; + var kMessageSizeExceededError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_WS_MESSAGE_SIZE_EXCEEDED"); + var MessageSizeExceededError = class extends UndiciError { + constructor(message) { + super(message); + this.name = "MessageSizeExceededError"; + this.message = message || "Max decompressed message size exceeded"; + this.code = "UND_ERR_WS_MESSAGE_SIZE_EXCEEDED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kMessageSizeExceededError] === true; + } + get [kMessageSizeExceededError]() { + return true; + } + }; + module.exports = { + AbortError, + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError, + ResponseError, + SecureProxyConnectionError, + MessageSizeExceededError + }; + } +}); + +// viewer/node_modules/undici/lib/core/constants.js +var require_constants = __commonJS({ + "viewer/node_modules/undici/lib/core/constants.js"(exports, module) { + "use strict"; + var headerNameLowerCasedRecord = {}; + var wellknownHeaderNames = [ + "Accept", + "Accept-Encoding", + "Accept-Language", + "Accept-Ranges", + "Access-Control-Allow-Credentials", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Origin", + "Access-Control-Expose-Headers", + "Access-Control-Max-Age", + "Access-Control-Request-Headers", + "Access-Control-Request-Method", + "Age", + "Allow", + "Alt-Svc", + "Alt-Used", + "Authorization", + "Cache-Control", + "Clear-Site-Data", + "Connection", + "Content-Disposition", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-Range", + "Content-Security-Policy", + "Content-Security-Policy-Report-Only", + "Content-Type", + "Cookie", + "Cross-Origin-Embedder-Policy", + "Cross-Origin-Opener-Policy", + "Cross-Origin-Resource-Policy", + "Date", + "Device-Memory", + "Downlink", + "ECT", + "ETag", + "Expect", + "Expect-CT", + "Expires", + "Forwarded", + "From", + "Host", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", + "Keep-Alive", + "Last-Modified", + "Link", + "Location", + "Max-Forwards", + "Origin", + "Permissions-Policy", + "Pragma", + "Proxy-Authenticate", + "Proxy-Authorization", + "RTT", + "Range", + "Referer", + "Referrer-Policy", + "Refresh", + "Retry-After", + "Sec-WebSocket-Accept", + "Sec-WebSocket-Extensions", + "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", + "Sec-WebSocket-Version", + "Server", + "Server-Timing", + "Service-Worker-Allowed", + "Service-Worker-Navigation-Preload", + "Set-Cookie", + "SourceMap", + "Strict-Transport-Security", + "Supports-Loading-Mode", + "TE", + "Timing-Allow-Origin", + "Trailer", + "Transfer-Encoding", + "Upgrade", + "Upgrade-Insecure-Requests", + "User-Agent", + "Vary", + "Via", + "WWW-Authenticate", + "X-Content-Type-Options", + "X-DNS-Prefetch-Control", + "X-Frame-Options", + "X-Permitted-Cross-Domain-Policies", + "X-Powered-By", + "X-Requested-With", + "X-XSS-Protection" + ]; + for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = wellknownHeaderNames[i]; + const lowerCasedKey = key.toLowerCase(); + headerNameLowerCasedRecord[key] = headerNameLowerCasedRecord[lowerCasedKey] = lowerCasedKey; + } + Object.setPrototypeOf(headerNameLowerCasedRecord, null); + module.exports = { + wellknownHeaderNames, + headerNameLowerCasedRecord + }; + } +}); + +// viewer/node_modules/undici/lib/core/tree.js +var require_tree = __commonJS({ + "viewer/node_modules/undici/lib/core/tree.js"(exports, module) { + "use strict"; + var { + wellknownHeaderNames, + headerNameLowerCasedRecord + } = require_constants(); + var TstNode = class _TstNode { + /** @type {any} */ + value = null; + /** @type {null | TstNode} */ + left = null; + /** @type {null | TstNode} */ + middle = null; + /** @type {null | TstNode} */ + right = null; + /** @type {number} */ + code; + /** + * @param {string} key + * @param {any} value + * @param {number} index + */ + constructor(key, value, index) { + if (index === void 0 || index >= key.length) { + throw new TypeError("Unreachable"); + } + const code = this.code = key.charCodeAt(index); + if (code > 127) { + throw new TypeError("key must be ascii string"); + } + if (key.length !== ++index) { + this.middle = new _TstNode(key, value, index); + } else { + this.value = value; + } + } + /** + * @param {string} key + * @param {any} value + */ + add(key, value) { + const length2 = key.length; + if (length2 === 0) { + throw new TypeError("Unreachable"); + } + let index = 0; + let node = this; + while (true) { + const code = key.charCodeAt(index); + if (code > 127) { + throw new TypeError("key must be ascii string"); + } + if (node.code === code) { + if (length2 === ++index) { + node.value = value; + break; + } else if (node.middle !== null) { + node = node.middle; + } else { + node.middle = new _TstNode(key, value, index); + break; + } + } else if (node.code < code) { + if (node.left !== null) { + node = node.left; + } else { + node.left = new _TstNode(key, value, index); + break; + } + } else if (node.right !== null) { + node = node.right; + } else { + node.right = new _TstNode(key, value, index); + break; + } + } + } + /** + * @param {Uint8Array} key + * @return {TstNode | null} + */ + search(key) { + const keylength = key.length; + let index = 0; + let node = this; + while (node !== null && index < keylength) { + let code = key[index]; + if (code <= 90 && code >= 65) { + code |= 32; + } + while (node !== null) { + if (code === node.code) { + if (keylength === ++index) { + return node; + } + node = node.middle; + break; + } + node = node.code < code ? node.left : node.right; + } + } + return null; + } + }; + var TernarySearchTree = class { + /** @type {TstNode | null} */ + node = null; + /** + * @param {string} key + * @param {any} value + * */ + insert(key, value) { + if (this.node === null) { + this.node = new TstNode(key, value, 0); + } else { + this.node.add(key, value); + } + } + /** + * @param {Uint8Array} key + * @return {any} + */ + lookup(key) { + return this.node?.search(key)?.value ?? null; + } + }; + var tree = new TernarySearchTree(); + for (let i = 0; i < wellknownHeaderNames.length; ++i) { + const key = headerNameLowerCasedRecord[wellknownHeaderNames[i]]; + tree.insert(key, key); + } + module.exports = { + TernarySearchTree, + tree + }; + } +}); + +// viewer/node_modules/undici/lib/core/util.js +var require_util = __commonJS({ + "viewer/node_modules/undici/lib/core/util.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { kDestroyed, kBodyUsed, kListeners, kBody } = require_symbols(); + var { IncomingMessage } = __require("node:http"); + var stream = __require("node:stream"); + var net = __require("node:net"); + var { Blob: Blob2 } = __require("node:buffer"); + var nodeUtil = __require("node:util"); + var { stringify } = __require("node:querystring"); + var { EventEmitter: EE } = __require("node:events"); + var { InvalidArgumentError } = require_errors(); + var { headerNameLowerCasedRecord } = require_constants(); + var { tree } = require_tree(); + var [nodeMajor, nodeMinor] = process.versions.node.split(".").map((v) => Number(v)); + var BodyAsyncIterable = class { + constructor(body) { + this[kBody] = body; + this[kBodyUsed] = false; + } + async *[Symbol.asyncIterator]() { + assert(!this[kBodyUsed], "disturbed"); + this[kBodyUsed] = true; + yield* this[kBody]; + } + }; + function wrapRequestBody(body) { + if (isStream2(body)) { + if (bodyLength(body) === 0) { + body.on("data", function() { + assert(false); + }); + } + if (typeof body.readableDidRead !== "boolean") { + body[kBodyUsed] = false; + EE.prototype.on.call(body, "data", function() { + this[kBodyUsed] = true; + }); + } + return body; + } else if (body && typeof body.pipeTo === "function") { + return new BodyAsyncIterable(body); + } else if (body && typeof body !== "string" && !ArrayBuffer.isView(body) && isIterable(body)) { + return new BodyAsyncIterable(body); + } else { + return body; + } + } + function nop() { + } + function isStream2(obj) { + return obj && typeof obj === "object" && typeof obj.pipe === "function" && typeof obj.on === "function"; + } + function isBlobLike(object) { + if (object === null) { + return false; + } else if (object instanceof Blob2) { + return true; + } else if (typeof object !== "object") { + return false; + } else { + const sTag = object[Symbol.toStringTag]; + return (sTag === "Blob" || sTag === "File") && ("stream" in object && typeof object.stream === "function" || "arrayBuffer" in object && typeof object.arrayBuffer === "function"); + } + } + function buildURL(url, queryParams) { + if (url.includes("?") || url.includes("#")) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".'); + } + const stringified = stringify(queryParams); + if (stringified) { + url += "?" + stringified; + } + return url; + } + function isValidPort(port2) { + const value = parseInt(port2, 10); + return value === Number(port2) && value >= 0 && value <= 65535; + } + function isHttpOrHttpsPrefixed(value) { + return value != null && value[0] === "h" && value[1] === "t" && value[2] === "t" && value[3] === "p" && (value[4] === ":" || value[4] === "s" && value[5] === ":"); + } + function parseURL(url) { + if (typeof url === "string") { + url = new URL(url); + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError("Invalid URL protocol: the URL must start with `http:` or `https:`."); + } + return url; + } + if (!url || typeof url !== "object") { + throw new InvalidArgumentError("Invalid URL: The URL argument must be a non-null object."); + } + if (!(url instanceof URL)) { + if (url.port != null && url.port !== "" && isValidPort(url.port) === false) { + throw new InvalidArgumentError("Invalid URL: port must be a valid integer or a string representation of an integer."); + } + if (url.path != null && typeof url.path !== "string") { + throw new InvalidArgumentError("Invalid URL path: the path must be a string or null/undefined."); + } + if (url.pathname != null && typeof url.pathname !== "string") { + throw new InvalidArgumentError("Invalid URL pathname: the pathname must be a string or null/undefined."); + } + if (url.hostname != null && typeof url.hostname !== "string") { + throw new InvalidArgumentError("Invalid URL hostname: the hostname must be a string or null/undefined."); + } + if (url.origin != null && typeof url.origin !== "string") { + throw new InvalidArgumentError("Invalid URL origin: the origin must be a string or null/undefined."); + } + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError("Invalid URL protocol: the URL must start with `http:` or `https:`."); + } + const port2 = url.port != null ? url.port : url.protocol === "https:" ? 443 : 80; + let origin = url.origin != null ? url.origin : `${url.protocol || ""}//${url.hostname || ""}:${port2}`; + let path14 = url.path != null ? url.path : `${url.pathname || ""}${url.search || ""}`; + if (origin[origin.length - 1] === "/") { + origin = origin.slice(0, origin.length - 1); + } + if (path14 && path14[0] !== "/") { + path14 = `/${path14}`; + } + return new URL(`${origin}${path14}`); + } + if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { + throw new InvalidArgumentError("Invalid URL protocol: the URL must start with `http:` or `https:`."); + } + return url; + } + function parseOrigin(url) { + url = parseURL(url); + if (url.pathname !== "/" || url.search || url.hash) { + throw new InvalidArgumentError("invalid url"); + } + return url; + } + function getHostname(host2) { + if (host2[0] === "[") { + const idx2 = host2.indexOf("]"); + assert(idx2 !== -1); + return host2.substring(1, idx2); + } + const idx = host2.indexOf(":"); + if (idx === -1) return host2; + return host2.substring(0, idx); + } + function getServerName(host2) { + if (!host2) { + return null; + } + assert(typeof host2 === "string"); + const servername = getHostname(host2); + if (net.isIP(servername)) { + return ""; + } + return servername; + } + function deepClone(obj) { + return JSON.parse(JSON.stringify(obj)); + } + function isAsyncIterable(obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === "function"); + } + function isIterable(obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === "function" || typeof obj[Symbol.asyncIterator] === "function")); + } + function bodyLength(body) { + if (body == null) { + return 0; + } else if (isStream2(body)) { + const state = body._readableState; + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) ? state.length : null; + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null; + } else if (isBuffer2(body)) { + return body.byteLength; + } + return null; + } + function isDestroyed(body) { + return body && !!(body.destroyed || body[kDestroyed] || stream.isDestroyed?.(body)); + } + function destroy(stream2, err) { + if (stream2 == null || !isStream2(stream2) || isDestroyed(stream2)) { + return; + } + if (typeof stream2.destroy === "function") { + if (Object.getPrototypeOf(stream2).constructor === IncomingMessage) { + stream2.socket = null; + } + stream2.destroy(err); + } else if (err) { + queueMicrotask(() => { + stream2.emit("error", err); + }); + } + if (stream2.destroyed !== true) { + stream2[kDestroyed] = true; + } + } + var KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/; + function parseKeepAliveTimeout(val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR); + return m ? parseInt(m[1], 10) * 1e3 : null; + } + function headerNameToString(value) { + return typeof value === "string" ? headerNameLowerCasedRecord[value] ?? value.toLowerCase() : tree.lookup(value) ?? value.toString("latin1").toLowerCase(); + } + function bufferToLowerCasedHeaderName(value) { + return tree.lookup(value) ?? value.toString("latin1").toLowerCase(); + } + function parseHeaders(headers, obj) { + if (obj === void 0) obj = {}; + for (let i = 0; i < headers.length; i += 2) { + const key = headerNameToString(headers[i]); + let val = obj[key]; + if (val) { + if (typeof val === "string") { + val = [val]; + obj[key] = val; + } + val.push(headers[i + 1].toString("utf8")); + } else { + const headersValue = headers[i + 1]; + if (typeof headersValue === "string") { + obj[key] = headersValue; + } else { + obj[key] = Array.isArray(headersValue) ? headersValue.map((x) => x.toString("utf8")) : headersValue.toString("utf8"); + } + } + } + if ("content-length" in obj && "content-disposition" in obj) { + obj["content-disposition"] = Buffer.from(obj["content-disposition"]).toString("latin1"); + } + return obj; + } + function parseRawHeaders(headers) { + const len = headers.length; + const ret = new Array(len); + let hasContentLength = false; + let contentDispositionIdx = -1; + let key; + let val; + let kLen = 0; + for (let n = 0; n < headers.length; n += 2) { + key = headers[n]; + val = headers[n + 1]; + typeof key !== "string" && (key = key.toString()); + typeof val !== "string" && (val = val.toString("utf8")); + kLen = key.length; + if (kLen === 14 && key[7] === "-" && (key === "content-length" || key.toLowerCase() === "content-length")) { + hasContentLength = true; + } else if (kLen === 19 && key[7] === "-" && (key === "content-disposition" || key.toLowerCase() === "content-disposition")) { + contentDispositionIdx = n + 1; + } + ret[n] = key; + ret[n + 1] = val; + } + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString("latin1"); + } + return ret; + } + function isBuffer2(buffer) { + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer); + } + function validateHandler(handler, method, upgrade) { + if (!handler || typeof handler !== "object") { + throw new InvalidArgumentError("handler must be an object"); + } + if (typeof handler.onConnect !== "function") { + throw new InvalidArgumentError("invalid onConnect method"); + } + if (typeof handler.onError !== "function") { + throw new InvalidArgumentError("invalid onError method"); + } + if (typeof handler.onBodySent !== "function" && handler.onBodySent !== void 0) { + throw new InvalidArgumentError("invalid onBodySent method"); + } + if (upgrade || method === "CONNECT") { + if (typeof handler.onUpgrade !== "function") { + throw new InvalidArgumentError("invalid onUpgrade method"); + } + } else { + if (typeof handler.onHeaders !== "function") { + throw new InvalidArgumentError("invalid onHeaders method"); + } + if (typeof handler.onData !== "function") { + throw new InvalidArgumentError("invalid onData method"); + } + if (typeof handler.onComplete !== "function") { + throw new InvalidArgumentError("invalid onComplete method"); + } + } + } + function isDisturbed(body) { + return !!(body && (stream.isDisturbed(body) || body[kBodyUsed])); + } + function isErrored(body) { + return !!(body && stream.isErrored(body)); + } + function isReadable(body) { + return !!(body && stream.isReadable(body)); + } + function getSocketInfo(socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + }; + } + function ReadableStreamFrom(iterable) { + let iterator; + return new ReadableStream( + { + async start() { + iterator = iterable[Symbol.asyncIterator](); + }, + async pull(controller) { + const { done, value } = await iterator.next(); + if (done) { + queueMicrotask(() => { + controller.close(); + controller.byobRequest?.respond(0); + }); + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value); + if (buf.byteLength) { + controller.enqueue(new Uint8Array(buf)); + } + } + return controller.desiredSize > 0; + }, + async cancel(reason) { + await iterator.return(); + }, + type: "bytes" + } + ); + } + function isFormDataLike(object) { + return object && typeof object === "object" && typeof object.append === "function" && typeof object.delete === "function" && typeof object.get === "function" && typeof object.getAll === "function" && typeof object.has === "function" && typeof object.set === "function" && object[Symbol.toStringTag] === "FormData"; + } + function addAbortListener(signal, listener) { + if ("addEventListener" in signal) { + signal.addEventListener("abort", listener, { once: true }); + return () => signal.removeEventListener("abort", listener); + } + signal.addListener("abort", listener); + return () => signal.removeListener("abort", listener); + } + var hasToWellFormed = typeof String.prototype.toWellFormed === "function"; + var hasIsWellFormed = typeof String.prototype.isWellFormed === "function"; + function toUSVString(val) { + return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val); + } + function isUSVString(val) { + return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`; + } + function isTokenCharCode(c) { + switch (c) { + case 34: + case 40: + case 41: + case 44: + case 47: + case 58: + case 59: + case 60: + case 61: + case 62: + case 63: + case 64: + case 91: + case 92: + case 93: + case 123: + case 125: + return false; + default: + return c >= 33 && c <= 126; + } + } + function isValidHTTPToken(characters) { + if (characters.length === 0) { + return false; + } + for (let i = 0; i < characters.length; ++i) { + if (!isTokenCharCode(characters.charCodeAt(i))) { + return false; + } + } + return true; + } + var headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; + function isValidHeaderValue(characters) { + return !headerCharRegex.test(characters); + } + function parseRangeHeader(range) { + if (range == null || range === "") return { start: 0, end: null, size: null }; + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null; + return m ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } : null; + } + function addListener(obj, name, listener) { + const listeners = obj[kListeners] ??= []; + listeners.push([name, listener]); + obj.on(name, listener); + return obj; + } + function removeAllListeners(obj) { + for (const [name, listener] of obj[kListeners] ?? []) { + obj.removeListener(name, listener); + } + obj[kListeners] = null; + } + function errorRequest(client, request, err) { + try { + request.onError(err); + assert(request.aborted); + } catch (err2) { + client.emit("error", err2); + } + } + var kEnumerableProperty = /* @__PURE__ */ Object.create(null); + kEnumerableProperty.enumerable = true; + var normalizedMethodRecordsBase = { + delete: "DELETE", + DELETE: "DELETE", + get: "GET", + GET: "GET", + head: "HEAD", + HEAD: "HEAD", + options: "OPTIONS", + OPTIONS: "OPTIONS", + post: "POST", + POST: "POST", + put: "PUT", + PUT: "PUT" + }; + var normalizedMethodRecords = { + ...normalizedMethodRecordsBase, + patch: "patch", + PATCH: "PATCH" + }; + Object.setPrototypeOf(normalizedMethodRecordsBase, null); + Object.setPrototypeOf(normalizedMethodRecords, null); + module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString, + isUSVString, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream: isStream2, + isIterable, + isAsyncIterable, + isDestroyed, + headerNameToString, + bufferToLowerCasedHeaderName, + addListener, + removeAllListeners, + errorRequest, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer: isBuffer2, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL, + addAbortListener, + isValidHTTPToken, + isValidHeaderValue, + isTokenCharCode, + parseRangeHeader, + normalizedMethodRecordsBase, + normalizedMethodRecords, + isValidPort, + isHttpOrHttpsPrefixed, + nodeMajor, + nodeMinor, + safeHTTPMethods: ["GET", "HEAD", "OPTIONS", "TRACE"], + wrapRequestBody + }; + } +}); + +// viewer/node_modules/undici/lib/core/diagnostics.js +var require_diagnostics = __commonJS({ + "viewer/node_modules/undici/lib/core/diagnostics.js"(exports, module) { + "use strict"; + var diagnosticsChannel = __require("node:diagnostics_channel"); + var util = __require("node:util"); + var undiciDebugLog = util.debuglog("undici"); + var fetchDebuglog = util.debuglog("fetch"); + var websocketDebuglog = util.debuglog("websocket"); + var isClientSet = false; + var channels = { + // Client + beforeConnect: diagnosticsChannel.channel("undici:client:beforeConnect"), + connected: diagnosticsChannel.channel("undici:client:connected"), + connectError: diagnosticsChannel.channel("undici:client:connectError"), + sendHeaders: diagnosticsChannel.channel("undici:client:sendHeaders"), + // Request + create: diagnosticsChannel.channel("undici:request:create"), + bodySent: diagnosticsChannel.channel("undici:request:bodySent"), + headers: diagnosticsChannel.channel("undici:request:headers"), + trailers: diagnosticsChannel.channel("undici:request:trailers"), + error: diagnosticsChannel.channel("undici:request:error"), + // WebSocket + open: diagnosticsChannel.channel("undici:websocket:open"), + close: diagnosticsChannel.channel("undici:websocket:close"), + socketError: diagnosticsChannel.channel("undici:websocket:socket_error"), + ping: diagnosticsChannel.channel("undici:websocket:ping"), + pong: diagnosticsChannel.channel("undici:websocket:pong") + }; + if (undiciDebugLog.enabled || fetchDebuglog.enabled) { + const debuglog = fetchDebuglog.enabled ? fetchDebuglog : undiciDebugLog; + diagnosticsChannel.channel("undici:client:beforeConnect").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 } + } = evt; + debuglog( + "connecting to %s using %s%s", + `${host2}${port2 ? `:${port2}` : ""}`, + protocol, + version + ); + }); + diagnosticsChannel.channel("undici:client:connected").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 } + } = evt; + debuglog( + "connected to %s using %s%s", + `${host2}${port2 ? `:${port2}` : ""}`, + protocol, + version + ); + }); + diagnosticsChannel.channel("undici:client:connectError").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 }, + error + } = evt; + debuglog( + "connection to %s using %s%s errored - %s", + `${host2}${port2 ? `:${port2}` : ""}`, + protocol, + version, + error.message + ); + }); + diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => { + const { + request: { method, path: path14, origin } + } = evt; + debuglog("sending request to %s %s/%s", method, origin, path14); + }); + diagnosticsChannel.channel("undici:request:headers").subscribe((evt) => { + const { + request: { method, path: path14, origin }, + response: { statusCode } + } = evt; + debuglog( + "received response to %s %s/%s - HTTP %d", + method, + origin, + path14, + statusCode + ); + }); + diagnosticsChannel.channel("undici:request:trailers").subscribe((evt) => { + const { + request: { method, path: path14, origin } + } = evt; + debuglog("trailers received from %s %s/%s", method, origin, path14); + }); + diagnosticsChannel.channel("undici:request:error").subscribe((evt) => { + const { + request: { method, path: path14, origin }, + error + } = evt; + debuglog( + "request to %s %s/%s errored - %s", + method, + origin, + path14, + error.message + ); + }); + isClientSet = true; + } + if (websocketDebuglog.enabled) { + if (!isClientSet) { + const debuglog = undiciDebugLog.enabled ? undiciDebugLog : websocketDebuglog; + diagnosticsChannel.channel("undici:client:beforeConnect").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 } + } = evt; + debuglog( + "connecting to %s%s using %s%s", + host2, + port2 ? `:${port2}` : "", + protocol, + version + ); + }); + diagnosticsChannel.channel("undici:client:connected").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 } + } = evt; + debuglog( + "connected to %s%s using %s%s", + host2, + port2 ? `:${port2}` : "", + protocol, + version + ); + }); + diagnosticsChannel.channel("undici:client:connectError").subscribe((evt) => { + const { + connectParams: { version, protocol, port: port2, host: host2 }, + error + } = evt; + debuglog( + "connection to %s%s using %s%s errored - %s", + host2, + port2 ? `:${port2}` : "", + protocol, + version, + error.message + ); + }); + diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => { + const { + request: { method, path: path14, origin } + } = evt; + debuglog("sending request to %s %s/%s", method, origin, path14); + }); + } + diagnosticsChannel.channel("undici:websocket:open").subscribe((evt) => { + const { + address: { address, port: port2 } + } = evt; + websocketDebuglog("connection opened %s%s", address, port2 ? `:${port2}` : ""); + }); + diagnosticsChannel.channel("undici:websocket:close").subscribe((evt) => { + const { websocket, code, reason } = evt; + websocketDebuglog( + "closed connection to %s - %s %s", + websocket.url, + code, + reason + ); + }); + diagnosticsChannel.channel("undici:websocket:socket_error").subscribe((err) => { + websocketDebuglog("connection errored - %s", err.message); + }); + diagnosticsChannel.channel("undici:websocket:ping").subscribe((evt) => { + websocketDebuglog("ping received"); + }); + diagnosticsChannel.channel("undici:websocket:pong").subscribe((evt) => { + websocketDebuglog("pong received"); + }); + } + module.exports = { + channels + }; + } +}); + +// viewer/node_modules/undici/lib/core/request.js +var require_request = __commonJS({ + "viewer/node_modules/undici/lib/core/request.js"(exports, module) { + "use strict"; + var { + InvalidArgumentError, + NotSupportedError + } = require_errors(); + var assert = __require("node:assert"); + var { + isValidHTTPToken, + isValidHeaderValue, + isStream: isStream2, + destroy, + isBuffer: isBuffer2, + isFormDataLike, + isIterable, + isBlobLike, + buildURL, + validateHandler, + getServerName, + normalizedMethodRecords + } = require_util(); + var { channels } = require_diagnostics(); + var { headerNameLowerCasedRecord } = require_constants(); + var invalidPathRegex = /[^\u0021-\u00ff]/; + var kHandler = /* @__PURE__ */ Symbol("handler"); + var Request2 = class { + constructor(origin, { + path: path14, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + throwOnError, + expectContinue, + servername + }, handler) { + if (typeof path14 !== "string") { + throw new InvalidArgumentError("path must be a string"); + } else if (path14[0] !== "/" && !(path14.startsWith("http://") || path14.startsWith("https://")) && method !== "CONNECT") { + throw new InvalidArgumentError("path must be an absolute URL or start with a slash"); + } else if (invalidPathRegex.test(path14)) { + throw new InvalidArgumentError("invalid request path"); + } + if (typeof method !== "string") { + throw new InvalidArgumentError("method must be a string"); + } else if (normalizedMethodRecords[method] === void 0 && !isValidHTTPToken(method)) { + throw new InvalidArgumentError("invalid request method"); + } + if (upgrade && typeof upgrade !== "string") { + throw new InvalidArgumentError("upgrade must be a string"); + } + if (upgrade && !isValidHeaderValue(upgrade)) { + throw new InvalidArgumentError("invalid upgrade header"); + } + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError("invalid headersTimeout"); + } + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError("invalid bodyTimeout"); + } + if (reset != null && typeof reset !== "boolean") { + throw new InvalidArgumentError("invalid reset"); + } + if (expectContinue != null && typeof expectContinue !== "boolean") { + throw new InvalidArgumentError("invalid expectContinue"); + } + this.headersTimeout = headersTimeout; + this.bodyTimeout = bodyTimeout; + this.throwOnError = throwOnError === true; + this.method = method; + this.abort = null; + if (body == null) { + this.body = null; + } else if (isStream2(body)) { + this.body = body; + const rState = this.body._readableState; + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy() { + destroy(this); + }; + this.body.on("end", this.endHandler); + } + this.errorHandler = (err) => { + if (this.abort) { + this.abort(err); + } else { + this.error = err; + } + }; + this.body.on("error", this.errorHandler); + } else if (isBuffer2(body)) { + this.body = body.byteLength ? body : null; + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null; + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null; + } else if (typeof body === "string") { + this.body = body.length ? Buffer.from(body) : null; + } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) { + this.body = body; + } else { + throw new InvalidArgumentError("body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable"); + } + this.completed = false; + this.aborted = false; + this.upgrade = upgrade || null; + this.path = query ? buildURL(path14, query) : path14; + this.origin = origin; + this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" : idempotent; + this.blocking = blocking == null ? false : blocking; + this.reset = reset == null ? null : reset; + this.host = null; + this.contentLength = null; + this.contentType = null; + this.headers = []; + this.expectContinue = expectContinue != null ? expectContinue : false; + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError("headers array must be even"); + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]); + } + } else if (headers && typeof headers === "object") { + if (headers[Symbol.iterator]) { + for (const header of headers) { + if (!Array.isArray(header) || header.length !== 2) { + throw new InvalidArgumentError("headers must be in key-value pair format"); + } + processHeader(this, header[0], header[1]); + } + } else { + const keys = Object.keys(headers); + for (let i = 0; i < keys.length; ++i) { + processHeader(this, keys[i], headers[keys[i]]); + } + } + } else if (headers != null) { + throw new InvalidArgumentError("headers must be an object or an array"); + } + validateHandler(handler, method, upgrade); + this.servername = servername || getServerName(this.host); + this[kHandler] = handler; + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }); + } + } + onBodySent(chunk) { + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk); + } catch (err) { + this.abort(err); + } + } + } + onRequestSent() { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }); + } + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent(); + } catch (err) { + this.abort(err); + } + } + } + onConnect(abort) { + assert(!this.aborted); + assert(!this.completed); + if (this.error) { + abort(this.error); + } else { + this.abort = abort; + return this[kHandler].onConnect(abort); + } + } + onResponseStarted() { + return this[kHandler].onResponseStarted?.(); + } + onHeaders(statusCode, headers, resume, statusText) { + assert(!this.aborted); + assert(!this.completed); + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }); + } + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText); + } catch (err) { + this.abort(err); + } + } + onData(chunk) { + assert(!this.aborted); + assert(!this.completed); + try { + return this[kHandler].onData(chunk); + } catch (err) { + this.abort(err); + return false; + } + } + onUpgrade(statusCode, headers, socket) { + assert(!this.aborted); + assert(!this.completed); + return this[kHandler].onUpgrade(statusCode, headers, socket); + } + onComplete(trailers) { + this.onFinally(); + assert(!this.aborted); + this.completed = true; + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }); + } + try { + return this[kHandler].onComplete(trailers); + } catch (err) { + this.onError(err); + } + } + onError(error) { + this.onFinally(); + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }); + } + if (this.aborted) { + return; + } + this.aborted = true; + return this[kHandler].onError(error); + } + onFinally() { + if (this.errorHandler) { + this.body.off("error", this.errorHandler); + this.errorHandler = null; + } + if (this.endHandler) { + this.body.off("end", this.endHandler); + this.endHandler = null; + } + } + addHeader(key, value) { + processHeader(this, key, value); + return this; + } + }; + function processHeader(request, key, val) { + if (val && (typeof val === "object" && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`); + } else if (val === void 0) { + return; + } + let headerName = headerNameLowerCasedRecord[key]; + if (headerName === void 0) { + headerName = key.toLowerCase(); + if (headerNameLowerCasedRecord[headerName] === void 0 && !isValidHTTPToken(headerName)) { + throw new InvalidArgumentError("invalid header key"); + } + } + if (Array.isArray(val)) { + const arr = []; + for (let i = 0; i < val.length; i++) { + if (typeof val[i] === "string") { + if (!isValidHeaderValue(val[i])) { + throw new InvalidArgumentError(`invalid ${key} header`); + } + arr.push(val[i]); + } else if (val[i] === null) { + arr.push(""); + } else if (typeof val[i] === "object") { + throw new InvalidArgumentError(`invalid ${key} header`); + } else { + arr.push(`${val[i]}`); + } + } + val = arr; + } else if (typeof val === "string") { + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`); + } + } else if (val === null) { + val = ""; + } else { + val = `${val}`; + } + if (headerName === "host") { + if (request.host !== null) { + throw new InvalidArgumentError("duplicate host header"); + } + if (typeof val !== "string") { + throw new InvalidArgumentError("invalid host header"); + } + request.host = val; + } else if (headerName === "content-length") { + if (request.contentLength !== null) { + throw new InvalidArgumentError("duplicate content-length header"); + } + request.contentLength = parseInt(val, 10); + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError("invalid content-length header"); + } + } else if (request.contentType === null && headerName === "content-type") { + request.contentType = val; + request.headers.push(key, val); + } else if (headerName === "transfer-encoding" || headerName === "keep-alive" || headerName === "upgrade") { + throw new InvalidArgumentError(`invalid ${headerName} header`); + } else if (headerName === "connection") { + const value = typeof val === "string" ? val.toLowerCase() : null; + if (value !== "close" && value !== "keep-alive") { + throw new InvalidArgumentError("invalid connection header"); + } + if (value === "close") { + request.reset = true; + } + } else if (headerName === "expect") { + throw new NotSupportedError("expect header not supported"); + } else { + request.headers.push(key, val); + } + } + module.exports = Request2; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/dispatcher.js +var require_dispatcher = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/dispatcher.js"(exports, module) { + "use strict"; + var EventEmitter = __require("node:events"); + var Dispatcher = class extends EventEmitter { + dispatch() { + throw new Error("not implemented"); + } + close() { + throw new Error("not implemented"); + } + destroy() { + throw new Error("not implemented"); + } + compose(...args) { + const interceptors = Array.isArray(args[0]) ? args[0] : args; + let dispatch = this.dispatch.bind(this); + for (const interceptor of interceptors) { + if (interceptor == null) { + continue; + } + if (typeof interceptor !== "function") { + throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`); + } + dispatch = interceptor(dispatch); + if (dispatch == null || typeof dispatch !== "function" || dispatch.length !== 2) { + throw new TypeError("invalid interceptor"); + } + } + return new ComposedDispatcher(this, dispatch); + } + }; + var ComposedDispatcher = class extends Dispatcher { + #dispatcher = null; + #dispatch = null; + constructor(dispatcher, dispatch) { + super(); + this.#dispatcher = dispatcher; + this.#dispatch = dispatch; + } + dispatch(...args) { + this.#dispatch(...args); + } + close(...args) { + return this.#dispatcher.close(...args); + } + destroy(...args) { + return this.#dispatcher.destroy(...args); + } + }; + module.exports = Dispatcher; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/dispatcher-base.js +var require_dispatcher_base = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/dispatcher-base.js"(exports, module) { + "use strict"; + var Dispatcher = require_dispatcher(); + var { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError + } = require_errors(); + var { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = require_symbols(); + var kOnDestroyed = /* @__PURE__ */ Symbol("onDestroyed"); + var kOnClosed = /* @__PURE__ */ Symbol("onClosed"); + var kInterceptedDispatch = /* @__PURE__ */ Symbol("Intercepted Dispatch"); + var kWebSocketOptions = /* @__PURE__ */ Symbol("webSocketOptions"); + var DispatcherBase = class extends Dispatcher { + constructor(opts) { + super(); + this[kDestroyed] = false; + this[kOnDestroyed] = null; + this[kClosed] = false; + this[kOnClosed] = []; + this[kWebSocketOptions] = opts?.webSocket ?? {}; + } + get webSocketOptions() { + return { + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 + }; + } + get destroyed() { + return this[kDestroyed]; + } + get closed() { + return this[kClosed]; + } + get interceptors() { + return this[kInterceptors]; + } + set interceptors(newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i]; + if (typeof interceptor !== "function") { + throw new InvalidArgumentError("interceptor must be an function"); + } + } + } + this[kInterceptors] = newInterceptors; + } + close(callback) { + if (callback === void 0) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data); + }); + }); + } + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)); + return; + } + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback); + } else { + queueMicrotask(() => callback(null, null)); + } + return; + } + this[kClosed] = true; + this[kOnClosed].push(callback); + const onClosed = () => { + const callbacks = this[kOnClosed]; + this[kOnClosed] = null; + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null); + } + }; + this[kClose]().then(() => this.destroy()).then(() => { + queueMicrotask(onClosed); + }); + } + destroy(err, callback) { + if (typeof err === "function") { + callback = err; + err = null; + } + if (callback === void 0) { + return new Promise((resolve, reject) => { + this.destroy(err, (err2, data) => { + return err2 ? ( + /* istanbul ignore next: should never error */ + reject(err2) + ) : resolve(data); + }); + }); + } + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback); + } else { + queueMicrotask(() => callback(null, null)); + } + return; + } + if (!err) { + err = new ClientDestroyedError(); + } + this[kDestroyed] = true; + this[kOnDestroyed] = this[kOnDestroyed] || []; + this[kOnDestroyed].push(callback); + const onDestroyed = () => { + const callbacks = this[kOnDestroyed]; + this[kOnDestroyed] = null; + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null); + } + }; + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed); + }); + } + [kInterceptedDispatch](opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch]; + return this[kDispatch](opts, handler); + } + let dispatch = this[kDispatch].bind(this); + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch); + } + this[kInterceptedDispatch] = dispatch; + return dispatch(opts, handler); + } + dispatch(opts, handler) { + if (!handler || typeof handler !== "object") { + throw new InvalidArgumentError("handler must be an object"); + } + try { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("opts must be an object."); + } + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError(); + } + if (this[kClosed]) { + throw new ClientClosedError(); + } + return this[kInterceptedDispatch](opts, handler); + } catch (err) { + if (typeof handler.onError !== "function") { + throw new InvalidArgumentError("invalid onError method"); + } + handler.onError(err); + return false; + } + } + }; + module.exports = DispatcherBase; + } +}); + +// viewer/node_modules/undici/lib/util/timers.js +var require_timers = __commonJS({ + "viewer/node_modules/undici/lib/util/timers.js"(exports, module) { + "use strict"; + var fastNow = 0; + var RESOLUTION_MS = 1e3; + var TICK_MS = (RESOLUTION_MS >> 1) - 1; + var fastNowTimeout; + var kFastTimer = /* @__PURE__ */ Symbol("kFastTimer"); + var fastTimers = []; + var NOT_IN_LIST = -2; + var TO_BE_CLEARED = -1; + var PENDING = 0; + var ACTIVE = 1; + function onTick() { + fastNow += TICK_MS; + let idx = 0; + let len = fastTimers.length; + while (idx < len) { + const timer = fastTimers[idx]; + if (timer._state === PENDING) { + timer._idleStart = fastNow - TICK_MS; + timer._state = ACTIVE; + } else if (timer._state === ACTIVE && fastNow >= timer._idleStart + timer._idleTimeout) { + timer._state = TO_BE_CLEARED; + timer._idleStart = -1; + timer._onTimeout(timer._timerArg); + } + if (timer._state === TO_BE_CLEARED) { + timer._state = NOT_IN_LIST; + if (--len !== 0) { + fastTimers[idx] = fastTimers[len]; + } + } else { + ++idx; + } + } + fastTimers.length = len; + if (fastTimers.length !== 0) { + refreshTimeout(); + } + } + function refreshTimeout() { + if (fastNowTimeout) { + fastNowTimeout.refresh(); + } else { + clearTimeout(fastNowTimeout); + fastNowTimeout = setTimeout(onTick, TICK_MS); + if (fastNowTimeout.unref) { + fastNowTimeout.unref(); + } + } + } + var FastTimer = class { + [kFastTimer] = true; + /** + * The state of the timer, which can be one of the following: + * - NOT_IN_LIST (-2) + * - TO_BE_CLEARED (-1) + * - PENDING (0) + * - ACTIVE (1) + * + * @type {-2|-1|0|1} + * @private + */ + _state = NOT_IN_LIST; + /** + * The number of milliseconds to wait before calling the callback. + * + * @type {number} + * @private + */ + _idleTimeout = -1; + /** + * The time in milliseconds when the timer was started. This value is used to + * calculate when the timer should expire. + * + * @type {number} + * @default -1 + * @private + */ + _idleStart = -1; + /** + * The function to be executed when the timer expires. + * @type {Function} + * @private + */ + _onTimeout; + /** + * The argument to be passed to the callback when the timer expires. + * + * @type {*} + * @private + */ + _timerArg; + /** + * @constructor + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should wait + * before the specified function or code is executed. + * @param {*} arg + */ + constructor(callback, delay, arg) { + this._onTimeout = callback; + this._idleTimeout = delay; + this._timerArg = arg; + this.refresh(); + } + /** + * Sets the timer's start time to the current time, and reschedules the timer + * to call its callback at the previously specified duration adjusted to the + * current time. + * Using this on a timer that has already called its callback will reactivate + * the timer. + * + * @returns {void} + */ + refresh() { + if (this._state === NOT_IN_LIST) { + fastTimers.push(this); + } + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout(); + } + this._state = PENDING; + } + /** + * The `clear` method cancels the timer, preventing it from executing. + * + * @returns {void} + * @private + */ + clear() { + this._state = TO_BE_CLEARED; + this._idleStart = -1; + } + }; + module.exports = { + /** + * The setTimeout() method sets a timer which executes a function once the + * timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {NodeJS.Timeout|FastTimer} + */ + setTimeout(callback, delay, arg) { + return delay <= RESOLUTION_MS ? setTimeout(callback, delay, arg) : new FastTimer(callback, delay, arg); + }, + /** + * The clearTimeout method cancels an instantiated Timer previously created + * by calling setTimeout. + * + * @param {NodeJS.Timeout|FastTimer} timeout + */ + clearTimeout(timeout) { + if (timeout[kFastTimer]) { + timeout.clear(); + } else { + clearTimeout(timeout); + } + }, + /** + * The setFastTimeout() method sets a fastTimer which executes a function once + * the timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {FastTimer} + */ + setFastTimeout(callback, delay, arg) { + return new FastTimer(callback, delay, arg); + }, + /** + * The clearTimeout method cancels an instantiated FastTimer previously + * created by calling setFastTimeout. + * + * @param {FastTimer} timeout + */ + clearFastTimeout(timeout) { + timeout.clear(); + }, + /** + * The now method returns the value of the internal fast timer clock. + * + * @returns {number} + */ + now() { + return fastNow; + }, + /** + * Trigger the onTick function to process the fastTimers array. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + * @param {number} [delay=0] The delay in milliseconds to add to the now value. + */ + tick(delay = 0) { + fastNow += delay - RESOLUTION_MS + 1; + onTick(); + onTick(); + }, + /** + * Reset FastTimers. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + reset() { + fastNow = 0; + fastTimers.length = 0; + clearTimeout(fastNowTimeout); + fastNowTimeout = null; + }, + /** + * Exporting for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + kFastTimer + }; + } +}); + +// viewer/node_modules/undici/lib/core/connect.js +var require_connect = __commonJS({ + "viewer/node_modules/undici/lib/core/connect.js"(exports, module) { + "use strict"; + var net = __require("node:net"); + var assert = __require("node:assert"); + var util = require_util(); + var { InvalidArgumentError, ConnectTimeoutError } = require_errors(); + var timers = require_timers(); + function noop() { + } + var tls; + var SessionCache; + if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) { + SessionCache = class WeakSessionCache { + constructor(maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions; + this._sessionCache = /* @__PURE__ */ new Map(); + this._sessionRegistry = new global.FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return; + } + const ref = this._sessionCache.get(key); + if (ref !== void 0 && ref.deref() === void 0) { + this._sessionCache.delete(key); + } + }); + } + get(sessionKey) { + const ref = this._sessionCache.get(sessionKey); + return ref ? ref.deref() : null; + } + set(sessionKey, session) { + if (this._maxCachedSessions === 0) { + return; + } + this._sessionCache.set(sessionKey, new WeakRef(session)); + this._sessionRegistry.register(session, sessionKey); + } + }; + } else { + SessionCache = class SimpleSessionCache { + constructor(maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions; + this._sessionCache = /* @__PURE__ */ new Map(); + } + get(sessionKey) { + return this._sessionCache.get(sessionKey); + } + set(sessionKey, session) { + if (this._maxCachedSessions === 0) { + return; + } + if (this._sessionCache.size >= this._maxCachedSessions) { + const { value: oldestKey } = this._sessionCache.keys().next(); + this._sessionCache.delete(oldestKey); + } + this._sessionCache.set(sessionKey, session); + } + }; + } + function buildConnector({ allowH2, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero"); + } + const options = { path: socketPath, ...opts }; + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions); + timeout = timeout == null ? 1e4 : timeout; + allowH2 = allowH2 != null ? allowH2 : false; + return function connect({ hostname, host: host2, protocol, port: port2, servername, localAddress, httpSocket }, callback) { + let socket; + if (protocol === "https:") { + if (!tls) { + tls = __require("node:tls"); + } + servername = servername || options.servername || util.getServerName(host2) || null; + const sessionKey = servername || hostname; + assert(sessionKey); + const session = customSession || sessionCache.get(sessionKey) || null; + port2 = port2 || 443; + socket = tls.connect({ + highWaterMark: 16384, + // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ["http/1.1", "h2"] : ["http/1.1"], + socket: httpSocket, + // upgrade socket connection + port: port2, + host: hostname + }); + socket.on("session", function(session2) { + sessionCache.set(sessionKey, session2); + }); + } else { + assert(!httpSocket, "httpSocket can only be sent on TLS update"); + port2 = port2 || 80; + socket = net.connect({ + highWaterMark: 64 * 1024, + // Same as nodejs fs streams. + ...options, + localAddress, + port: port2, + host: hostname + }); + } + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === void 0 ? 6e4 : options.keepAliveInitialDelay; + socket.setKeepAlive(true, keepAliveInitialDelay); + } + const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port: port2 }); + socket.setNoDelay(true).once(protocol === "https:" ? "secureConnect" : "connect", function() { + queueMicrotask(clearConnectTimeout); + if (callback) { + const cb = callback; + callback = null; + cb(null, this); + } + }).on("error", function(err) { + queueMicrotask(clearConnectTimeout); + if (callback) { + const cb = callback; + callback = null; + cb(err); + } + }); + return socket; + }; + } + var setupConnectTimeout = process.platform === "win32" ? (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop; + } + let s1 = null; + let s2 = null; + const fastTimer = timers.setFastTimeout(() => { + s1 = setImmediate(() => { + s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts)); + }); + }, opts.timeout); + return () => { + timers.clearFastTimeout(fastTimer); + clearImmediate(s1); + clearImmediate(s2); + }; + } : (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop; + } + let s1 = null; + const fastTimer = timers.setFastTimeout(() => { + s1 = setImmediate(() => { + onConnectTimeout(socketWeakRef.deref(), opts); + }); + }, opts.timeout); + return () => { + timers.clearFastTimeout(fastTimer); + clearImmediate(s1); + }; + }; + function onConnectTimeout(socket, opts) { + if (socket == null) { + return; + } + let message = "Connect Timeout Error"; + if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) { + message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(", ")},`; + } else { + message += ` (attempted address: ${opts.hostname}:${opts.port},`; + } + message += ` timeout: ${opts.timeout}ms)`; + util.destroy(socket, new ConnectTimeoutError(message)); + } + module.exports = buildConnector; + } +}); + +// viewer/node_modules/undici/lib/llhttp/utils.js +var require_utils = __commonJS({ + "viewer/node_modules/undici/lib/llhttp/utils.js"(exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.enumToMap = void 0; + function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === "number") { + res[key] = value; + } + }); + return res; + } + exports.enumToMap = enumToMap; + } +}); + +// viewer/node_modules/undici/lib/llhttp/constants.js +var require_constants2 = __commonJS({ + "viewer/node_modules/undici/lib/llhttp/constants.js"(exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; + var utils_1 = require_utils(); + var ERROR; + (function(ERROR2) { + ERROR2[ERROR2["OK"] = 0] = "OK"; + ERROR2[ERROR2["INTERNAL"] = 1] = "INTERNAL"; + ERROR2[ERROR2["STRICT"] = 2] = "STRICT"; + ERROR2[ERROR2["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR2[ERROR2["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR2[ERROR2["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR2[ERROR2["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR2[ERROR2["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR2[ERROR2["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR2[ERROR2["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR2[ERROR2["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR2[ERROR2["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR2[ERROR2["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR2[ERROR2["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR2[ERROR2["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR2[ERROR2["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR2[ERROR2["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR2[ERROR2["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR2[ERROR2["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR2[ERROR2["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR2[ERROR2["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR2[ERROR2["PAUSED"] = 21] = "PAUSED"; + ERROR2[ERROR2["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR2[ERROR2["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR2[ERROR2["USER"] = 24] = "USER"; + })(ERROR = exports.ERROR || (exports.ERROR = {})); + var TYPE; + (function(TYPE2) { + TYPE2[TYPE2["BOTH"] = 0] = "BOTH"; + TYPE2[TYPE2["REQUEST"] = 1] = "REQUEST"; + TYPE2[TYPE2["RESPONSE"] = 2] = "RESPONSE"; + })(TYPE = exports.TYPE || (exports.TYPE = {})); + var FLAGS; + (function(FLAGS2) { + FLAGS2[FLAGS2["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS2[FLAGS2["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS2[FLAGS2["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS2[FLAGS2["CHUNKED"] = 8] = "CHUNKED"; + FLAGS2[FLAGS2["UPGRADE"] = 16] = "UPGRADE"; + FLAGS2[FLAGS2["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS2[FLAGS2["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS2[FLAGS2["TRAILING"] = 128] = "TRAILING"; + FLAGS2[FLAGS2["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; + })(FLAGS = exports.FLAGS || (exports.FLAGS = {})); + var LENIENT_FLAGS; + (function(LENIENT_FLAGS2) { + LENIENT_FLAGS2[LENIENT_FLAGS2["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS2[LENIENT_FLAGS2["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS2[LENIENT_FLAGS2["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; + })(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); + var METHODS; + (function(METHODS2) { + METHODS2[METHODS2["DELETE"] = 0] = "DELETE"; + METHODS2[METHODS2["GET"] = 1] = "GET"; + METHODS2[METHODS2["HEAD"] = 2] = "HEAD"; + METHODS2[METHODS2["POST"] = 3] = "POST"; + METHODS2[METHODS2["PUT"] = 4] = "PUT"; + METHODS2[METHODS2["CONNECT"] = 5] = "CONNECT"; + METHODS2[METHODS2["OPTIONS"] = 6] = "OPTIONS"; + METHODS2[METHODS2["TRACE"] = 7] = "TRACE"; + METHODS2[METHODS2["COPY"] = 8] = "COPY"; + METHODS2[METHODS2["LOCK"] = 9] = "LOCK"; + METHODS2[METHODS2["MKCOL"] = 10] = "MKCOL"; + METHODS2[METHODS2["MOVE"] = 11] = "MOVE"; + METHODS2[METHODS2["PROPFIND"] = 12] = "PROPFIND"; + METHODS2[METHODS2["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS2[METHODS2["SEARCH"] = 14] = "SEARCH"; + METHODS2[METHODS2["UNLOCK"] = 15] = "UNLOCK"; + METHODS2[METHODS2["BIND"] = 16] = "BIND"; + METHODS2[METHODS2["REBIND"] = 17] = "REBIND"; + METHODS2[METHODS2["UNBIND"] = 18] = "UNBIND"; + METHODS2[METHODS2["ACL"] = 19] = "ACL"; + METHODS2[METHODS2["REPORT"] = 20] = "REPORT"; + METHODS2[METHODS2["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS2[METHODS2["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS2[METHODS2["MERGE"] = 23] = "MERGE"; + METHODS2[METHODS2["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS2[METHODS2["NOTIFY"] = 25] = "NOTIFY"; + METHODS2[METHODS2["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS2[METHODS2["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + METHODS2[METHODS2["PATCH"] = 28] = "PATCH"; + METHODS2[METHODS2["PURGE"] = 29] = "PURGE"; + METHODS2[METHODS2["MKCALENDAR"] = 30] = "MKCALENDAR"; + METHODS2[METHODS2["LINK"] = 31] = "LINK"; + METHODS2[METHODS2["UNLINK"] = 32] = "UNLINK"; + METHODS2[METHODS2["SOURCE"] = 33] = "SOURCE"; + METHODS2[METHODS2["PRI"] = 34] = "PRI"; + METHODS2[METHODS2["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS2[METHODS2["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS2[METHODS2["SETUP"] = 37] = "SETUP"; + METHODS2[METHODS2["PLAY"] = 38] = "PLAY"; + METHODS2[METHODS2["PAUSE"] = 39] = "PAUSE"; + METHODS2[METHODS2["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS2[METHODS2["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS2[METHODS2["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS2[METHODS2["REDIRECT"] = 43] = "REDIRECT"; + METHODS2[METHODS2["RECORD"] = 44] = "RECORD"; + METHODS2[METHODS2["FLUSH"] = 45] = "FLUSH"; + })(METHODS = exports.METHODS || (exports.METHODS = {})); + exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS["M-SEARCH"], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE + ]; + exports.METHODS_ICE = [ + METHODS.SOURCE + ]; + exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST + ]; + exports.METHOD_MAP = utils_1.enumToMap(METHODS); + exports.H_METHOD_MAP = {}; + Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } + }); + var FINISH; + (function(FINISH2) { + FINISH2[FINISH2["SAFE"] = 0] = "SAFE"; + FINISH2[FINISH2["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH2[FINISH2["UNSAFE"] = 2] = "UNSAFE"; + })(FINISH = exports.FINISH || (exports.FINISH = {})); + exports.ALPHA = []; + for (let i = "A".charCodeAt(0); i <= "Z".charCodeAt(0); i++) { + exports.ALPHA.push(String.fromCharCode(i)); + exports.ALPHA.push(String.fromCharCode(i + 32)); + } + exports.NUM_MAP = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9 + }; + exports.HEX_MAP = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + A: 10, + B: 11, + C: 12, + D: 13, + E: 14, + F: 15, + a: 10, + b: 11, + c: 12, + d: 13, + e: 14, + f: 15 + }; + exports.NUM = [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9" + ]; + exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); + exports.MARK = ["-", "_", ".", "!", "~", "*", "'", "(", ")"]; + exports.USERINFO_CHARS = exports.ALPHANUM.concat(exports.MARK).concat(["%", ";", ":", "&", "=", "+", "$", ","]); + exports.STRICT_URL_CHAR = [ + "!", + '"', + "$", + "%", + "&", + "'", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + ":", + ";", + "<", + "=", + ">", + "@", + "[", + "\\", + "]", + "^", + "_", + "`", + "{", + "|", + "}", + "~" + ].concat(exports.ALPHANUM); + exports.URL_CHAR = exports.STRICT_URL_CHAR.concat([" ", "\f"]); + for (let i = 128; i <= 255; i++) { + exports.URL_CHAR.push(i); + } + exports.HEX = exports.NUM.concat(["a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"]); + exports.STRICT_TOKEN = [ + "!", + "#", + "$", + "%", + "&", + "'", + "*", + "+", + "-", + ".", + "^", + "_", + "`", + "|", + "~" + ].concat(exports.ALPHANUM); + exports.TOKEN = exports.STRICT_TOKEN.concat([" "]); + exports.HEADER_CHARS = [" "]; + for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } + } + exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); + exports.MAJOR = exports.NUM_MAP; + exports.MINOR = exports.MAJOR; + var HEADER_STATE; + (function(HEADER_STATE2) { + HEADER_STATE2[HEADER_STATE2["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE2[HEADER_STATE2["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE2[HEADER_STATE2["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE2[HEADER_STATE2["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE2[HEADER_STATE2["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE2[HEADER_STATE2["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE2[HEADER_STATE2["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE2[HEADER_STATE2["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE2[HEADER_STATE2["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; + })(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); + exports.SPECIAL_HEADERS = { + "connection": HEADER_STATE.CONNECTION, + "content-length": HEADER_STATE.CONTENT_LENGTH, + "proxy-connection": HEADER_STATE.CONNECTION, + "transfer-encoding": HEADER_STATE.TRANSFER_ENCODING, + "upgrade": HEADER_STATE.UPGRADE + }; + } +}); + +// viewer/node_modules/undici/lib/llhttp/llhttp-wasm.js +var require_llhttp_wasm = __commonJS({ + "viewer/node_modules/undici/lib/llhttp/llhttp-wasm.js"(exports, module) { + "use strict"; + var { Buffer: Buffer2 } = __require("node:buffer"); + module.exports = Buffer2.from("AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAX8AYAJ/fwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAy0sBQYAAAIAAAAAAAACAQIAAgICAAADAAAAAAMDAwMBAQEBAQEBAQEAAAIAAAAEBQFwARISBQMBAAIGCAF/AUGA1AQLB9EFIgZtZW1vcnkCAAtfaW5pdGlhbGl6ZQAIGV9faW5kaXJlY3RfZnVuY3Rpb25fdGFibGUBAAtsbGh0dHBfaW5pdAAJGGxsaHR0cF9zaG91bGRfa2VlcF9hbGl2ZQAvDGxsaHR0cF9hbGxvYwALBm1hbGxvYwAxC2xsaHR0cF9mcmVlAAwEZnJlZQAMD2xsaHR0cF9nZXRfdHlwZQANFWxsaHR0cF9nZXRfaHR0cF9tYWpvcgAOFWxsaHR0cF9nZXRfaHR0cF9taW5vcgAPEWxsaHR0cF9nZXRfbWV0aG9kABAWbGxodHRwX2dldF9zdGF0dXNfY29kZQAREmxsaHR0cF9nZXRfdXBncmFkZQASDGxsaHR0cF9yZXNldAATDmxsaHR0cF9leGVjdXRlABQUbGxodHRwX3NldHRpbmdzX2luaXQAFQ1sbGh0dHBfZmluaXNoABYMbGxodHRwX3BhdXNlABcNbGxodHRwX3Jlc3VtZQAYG2xsaHR0cF9yZXN1bWVfYWZ0ZXJfdXBncmFkZQAZEGxsaHR0cF9nZXRfZXJybm8AGhdsbGh0dHBfZ2V0X2Vycm9yX3JlYXNvbgAbF2xsaHR0cF9zZXRfZXJyb3JfcmVhc29uABwUbGxodHRwX2dldF9lcnJvcl9wb3MAHRFsbGh0dHBfZXJybm9fbmFtZQAeEmxsaHR0cF9tZXRob2RfbmFtZQAfEmxsaHR0cF9zdGF0dXNfbmFtZQAgGmxsaHR0cF9zZXRfbGVuaWVudF9oZWFkZXJzACEhbGxodHRwX3NldF9sZW5pZW50X2NodW5rZWRfbGVuZ3RoACIdbGxodHRwX3NldF9sZW5pZW50X2tlZXBfYWxpdmUAIyRsbGh0dHBfc2V0X2xlbmllbnRfdHJhbnNmZXJfZW5jb2RpbmcAJBhsbGh0dHBfbWVzc2FnZV9uZWVkc19lb2YALgkXAQBBAQsRAQIDBAUKBgcrLSwqKSglJyYK07MCLBYAQYjQACgCAARAAAtBiNAAQQE2AgALFAAgABAwIAAgAjYCOCAAIAE6ACgLFAAgACAALwEyIAAtAC4gABAvEAALHgEBf0HAABAyIgEQMCABQYAINgI4IAEgADoAKCABC48MAQd/AkAgAEUNACAAQQhrIgEgAEEEaygCACIAQXhxIgRqIQUCQCAAQQFxDQAgAEEDcUUNASABIAEoAgAiAGsiAUGc0AAoAgBJDQEgACAEaiEEAkACQEGg0AAoAgAgAUcEQCAAQf8BTQRAIABBA3YhAyABKAIIIgAgASgCDCICRgRAQYzQAEGM0AAoAgBBfiADd3E2AgAMBQsgAiAANgIIIAAgAjYCDAwECyABKAIYIQYgASABKAIMIgBHBEAgACABKAIIIgI2AgggAiAANgIMDAMLIAFBFGoiAygCACICRQRAIAEoAhAiAkUNAiABQRBqIQMLA0AgAyEHIAIiAEEUaiIDKAIAIgINACAAQRBqIQMgACgCECICDQALIAdBADYCAAwCCyAFKAIEIgBBA3FBA0cNAiAFIABBfnE2AgRBlNAAIAQ2AgAgBSAENgIAIAEgBEEBcjYCBAwDC0EAIQALIAZFDQACQCABKAIcIgJBAnRBvNIAaiIDKAIAIAFGBEAgAyAANgIAIAANAUGQ0ABBkNAAKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgAUYbaiAANgIAIABFDQELIAAgBjYCGCABKAIQIgIEQCAAIAI2AhAgAiAANgIYCyABQRRqKAIAIgJFDQAgAEEUaiACNgIAIAIgADYCGAsgASAFTw0AIAUoAgQiAEEBcUUNAAJAAkACQAJAIABBAnFFBEBBpNAAKAIAIAVGBEBBpNAAIAE2AgBBmNAAQZjQACgCACAEaiIANgIAIAEgAEEBcjYCBCABQaDQACgCAEcNBkGU0ABBADYCAEGg0ABBADYCAAwGC0Gg0AAoAgAgBUYEQEGg0AAgATYCAEGU0ABBlNAAKAIAIARqIgA2AgAgASAAQQFyNgIEIAAgAWogADYCAAwGCyAAQXhxIARqIQQgAEH/AU0EQCAAQQN2IQMgBSgCCCIAIAUoAgwiAkYEQEGM0ABBjNAAKAIAQX4gA3dxNgIADAULIAIgADYCCCAAIAI2AgwMBAsgBSgCGCEGIAUgBSgCDCIARwRAQZzQACgCABogACAFKAIIIgI2AgggAiAANgIMDAMLIAVBFGoiAygCACICRQRAIAUoAhAiAkUNAiAFQRBqIQMLA0AgAyEHIAIiAEEUaiIDKAIAIgINACAAQRBqIQMgACgCECICDQALIAdBADYCAAwCCyAFIABBfnE2AgQgASAEaiAENgIAIAEgBEEBcjYCBAwDC0EAIQALIAZFDQACQCAFKAIcIgJBAnRBvNIAaiIDKAIAIAVGBEAgAyAANgIAIAANAUGQ0ABBkNAAKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgBUYbaiAANgIAIABFDQELIAAgBjYCGCAFKAIQIgIEQCAAIAI2AhAgAiAANgIYCyAFQRRqKAIAIgJFDQAgAEEUaiACNgIAIAIgADYCGAsgASAEaiAENgIAIAEgBEEBcjYCBCABQaDQACgCAEcNAEGU0AAgBDYCAAwBCyAEQf8BTQRAIARBeHFBtNAAaiEAAn9BjNAAKAIAIgJBASAEQQN2dCIDcUUEQEGM0AAgAiADcjYCACAADAELIAAoAggLIgIgATYCDCAAIAE2AgggASAANgIMIAEgAjYCCAwBC0EfIQIgBEH///8HTQRAIARBJiAEQQh2ZyIAa3ZBAXEgAEEBdGtBPmohAgsgASACNgIcIAFCADcCECACQQJ0QbzSAGohAAJAQZDQACgCACIDQQEgAnQiB3FFBEAgACABNgIAQZDQACADIAdyNgIAIAEgADYCGCABIAE2AgggASABNgIMDAELIARBGSACQQF2a0EAIAJBH0cbdCECIAAoAgAhAAJAA0AgACIDKAIEQXhxIARGDQEgAkEddiEAIAJBAXQhAiADIABBBHFqQRBqIgcoAgAiAA0ACyAHIAE2AgAgASADNgIYIAEgATYCDCABIAE2AggMAQsgAygCCCIAIAE2AgwgAyABNgIIIAFBADYCGCABIAM2AgwgASAANgIIC0Gs0ABBrNAAKAIAQQFrIgBBfyAAGzYCAAsLBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LQAEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABAwIAAgBDYCOCAAIAM6ACggACACOgAtIAAgATYCGAu74gECB38DfiABIAJqIQQCQCAAIgIoAgwiAA0AIAIoAgQEQCACIAE2AgQLIwBBEGsiCCQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAIoAhwiA0EBaw7dAdoBAdkBAgMEBQYHCAkKCwwNDtgBDxDXARES1gETFBUWFxgZGhvgAd8BHB0e1QEfICEiIyQl1AEmJygpKiss0wHSAS0u0QHQAS8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRtsBR0hJSs8BzgFLzQFMzAFNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBywHKAbgByQG5AcgBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgEA3AELQQAMxgELQQ4MxQELQQ0MxAELQQ8MwwELQRAMwgELQRMMwQELQRQMwAELQRUMvwELQRYMvgELQRgMvQELQRkMvAELQRoMuwELQRsMugELQRwMuQELQR0MuAELQQgMtwELQR4MtgELQSAMtQELQR8MtAELQQcMswELQSEMsgELQSIMsQELQSMMsAELQSQMrwELQRIMrgELQREMrQELQSUMrAELQSYMqwELQScMqgELQSgMqQELQcMBDKgBC0EqDKcBC0ErDKYBC0EsDKUBC0EtDKQBC0EuDKMBC0EvDKIBC0HEAQyhAQtBMAygAQtBNAyfAQtBDAyeAQtBMQydAQtBMgycAQtBMwybAQtBOQyaAQtBNQyZAQtBxQEMmAELQQsMlwELQToMlgELQTYMlQELQQoMlAELQTcMkwELQTgMkgELQTwMkQELQTsMkAELQT0MjwELQQkMjgELQSkMjQELQT4MjAELQT8MiwELQcAADIoBC0HBAAyJAQtBwgAMiAELQcMADIcBC0HEAAyGAQtBxQAMhQELQcYADIQBC0EXDIMBC0HHAAyCAQtByAAMgQELQckADIABC0HKAAx/C0HLAAx+C0HNAAx9C0HMAAx8C0HOAAx7C0HPAAx6C0HQAAx5C0HRAAx4C0HSAAx3C0HTAAx2C0HUAAx1C0HWAAx0C0HVAAxzC0EGDHILQdcADHELQQUMcAtB2AAMbwtBBAxuC0HZAAxtC0HaAAxsC0HbAAxrC0HcAAxqC0EDDGkLQd0ADGgLQd4ADGcLQd8ADGYLQeEADGULQeAADGQLQeIADGMLQeMADGILQQIMYQtB5AAMYAtB5QAMXwtB5gAMXgtB5wAMXQtB6AAMXAtB6QAMWwtB6gAMWgtB6wAMWQtB7AAMWAtB7QAMVwtB7gAMVgtB7wAMVQtB8AAMVAtB8QAMUwtB8gAMUgtB8wAMUQtB9AAMUAtB9QAMTwtB9gAMTgtB9wAMTQtB+AAMTAtB+QAMSwtB+gAMSgtB+wAMSQtB/AAMSAtB/QAMRwtB/gAMRgtB/wAMRQtBgAEMRAtBgQEMQwtBggEMQgtBgwEMQQtBhAEMQAtBhQEMPwtBhgEMPgtBhwEMPQtBiAEMPAtBiQEMOwtBigEMOgtBiwEMOQtBjAEMOAtBjQEMNwtBjgEMNgtBjwEMNQtBkAEMNAtBkQEMMwtBkgEMMgtBkwEMMQtBlAEMMAtBlQEMLwtBlgEMLgtBlwEMLQtBmAEMLAtBmQEMKwtBmgEMKgtBmwEMKQtBnAEMKAtBnQEMJwtBngEMJgtBnwEMJQtBoAEMJAtBoQEMIwtBogEMIgtBowEMIQtBpAEMIAtBpQEMHwtBpgEMHgtBpwEMHQtBqAEMHAtBqQEMGwtBqgEMGgtBqwEMGQtBrAEMGAtBrQEMFwtBrgEMFgtBAQwVC0GvAQwUC0GwAQwTC0GxAQwSC0GzAQwRC0GyAQwQC0G0AQwPC0G1AQwOC0G2AQwNC0G3AQwMC0G4AQwLC0G5AQwKC0G6AQwJC0G7AQwIC0HGAQwHC0G8AQwGC0G9AQwFC0G+AQwEC0G/AQwDC0HAAQwCC0HCAQwBC0HBAQshAwNAAkACQAJAAkACQAJAAkACQAJAIAICfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAgJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADDsYBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHyAhIyUmKCorLC8wMTIzNDU2Nzk6Ozw9lANAQkRFRklLTk9QUVJTVFVWWFpbXF1eX2BhYmNkZWZnaGpsb3Bxc3V2eHl6e3x/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAccByAHJAcsBzAHNAc4BzwGKA4kDiAOHA4QDgwOAA/sC+gL5AvgC9wL0AvMC8gLLAsECsALZAQsgASAERw3wAkHdASEDDLMDCyABIARHDcgBQcMBIQMMsgMLIAEgBEcNe0H3ACEDDLEDCyABIARHDXBB7wAhAwywAwsgASAERw1pQeoAIQMMrwMLIAEgBEcNZUHoACEDDK4DCyABIARHDWJB5gAhAwytAwsgASAERw0aQRghAwysAwsgASAERw0VQRIhAwyrAwsgASAERw1CQcUAIQMMqgMLIAEgBEcNNEE/IQMMqQMLIAEgBEcNMkE8IQMMqAMLIAEgBEcNK0ExIQMMpwMLIAItAC5BAUYNnwMMwQILQQAhAAJAAkACQCACLQAqRQ0AIAItACtFDQAgAi8BMCIDQQJxRQ0BDAILIAIvATAiA0EBcUUNAQtBASEAIAItAChBAUYNACACLwEyIgVB5ABrQeQASQ0AIAVBzAFGDQAgBUGwAkYNACADQcAAcQ0AQQAhACADQYgEcUGABEYNACADQShxQQBHIQALIAJBADsBMCACQQA6AC8gAEUN3wIgAkIANwMgDOACC0EAIQACQCACKAI4IgNFDQAgAygCLCIDRQ0AIAIgAxEAACEACyAARQ3MASAAQRVHDd0CIAJBBDYCHCACIAE2AhQgAkGwGDYCECACQRU2AgxBACEDDKQDCyABIARGBEBBBiEDDKQDCyABQQFqIQFBACEAAkAgAigCOCIDRQ0AIAMoAlQiA0UNACACIAMRAAAhAAsgAA3ZAgwcCyACQgA3AyBBEiEDDIkDCyABIARHDRZBHSEDDKEDCyABIARHBEAgAUEBaiEBQRAhAwyIAwtBByEDDKADCyACIAIpAyAiCiAEIAFrrSILfSIMQgAgCiAMWhs3AyAgCiALWA3UAkEIIQMMnwMLIAEgBEcEQCACQQk2AgggAiABNgIEQRQhAwyGAwtBCSEDDJ4DCyACKQMgQgBSDccBIAIgAi8BMEGAAXI7ATAMQgsgASAERw0/QdAAIQMMnAMLIAEgBEYEQEELIQMMnAMLIAFBAWohAUEAIQACQCACKAI4IgNFDQAgAygCUCIDRQ0AIAIgAxEAACEACyAADc8CDMYBC0EAIQACQCACKAI4IgNFDQAgAygCSCIDRQ0AIAIgAxEAACEACyAARQ3GASAAQRVHDc0CIAJBCzYCHCACIAE2AhQgAkGCGTYCECACQRU2AgxBACEDDJoDC0EAIQACQCACKAI4IgNFDQAgAygCSCIDRQ0AIAIgAxEAACEACyAARQ0MIABBFUcNygIgAkEaNgIcIAIgATYCFCACQYIZNgIQIAJBFTYCDEEAIQMMmQMLQQAhAAJAIAIoAjgiA0UNACADKAJMIgNFDQAgAiADEQAAIQALIABFDcQBIABBFUcNxwIgAkELNgIcIAIgATYCFCACQZEXNgIQIAJBFTYCDEEAIQMMmAMLIAEgBEYEQEEPIQMMmAMLIAEtAAAiAEE7Rg0HIABBDUcNxAIgAUEBaiEBDMMBC0EAIQACQCACKAI4IgNFDQAgAygCTCIDRQ0AIAIgAxEAACEACyAARQ3DASAAQRVHDcICIAJBDzYCHCACIAE2AhQgAkGRFzYCECACQRU2AgxBACEDDJYDCwNAIAEtAABB8DVqLQAAIgBBAUcEQCAAQQJHDcECIAIoAgQhAEEAIQMgAkEANgIEIAIgACABQQFqIgEQLSIADcICDMUBCyAEIAFBAWoiAUcNAAtBEiEDDJUDC0EAIQACQCACKAI4IgNFDQAgAygCTCIDRQ0AIAIgAxEAACEACyAARQ3FASAAQRVHDb0CIAJBGzYCHCACIAE2AhQgAkGRFzYCECACQRU2AgxBACEDDJQDCyABIARGBEBBFiEDDJQDCyACQQo2AgggAiABNgIEQQAhAAJAIAIoAjgiA0UNACADKAJIIgNFDQAgAiADEQAAIQALIABFDcIBIABBFUcNuQIgAkEVNgIcIAIgATYCFCACQYIZNgIQIAJBFTYCDEEAIQMMkwMLIAEgBEcEQANAIAEtAABB8DdqLQAAIgBBAkcEQAJAIABBAWsOBMQCvQIAvgK9AgsgAUEBaiEBQQghAwz8AgsgBCABQQFqIgFHDQALQRUhAwyTAwtBFSEDDJIDCwNAIAEtAABB8DlqLQAAIgBBAkcEQCAAQQFrDgTFArcCwwK4ArcCCyAEIAFBAWoiAUcNAAtBGCEDDJEDCyABIARHBEAgAkELNgIIIAIgATYCBEEHIQMM+AILQRkhAwyQAwsgAUEBaiEBDAILIAEgBEYEQEEaIQMMjwMLAkAgAS0AAEENaw4UtQG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwEAvwELQQAhAyACQQA2AhwgAkGvCzYCECACQQI2AgwgAiABQQFqNgIUDI4DCyABIARGBEBBGyEDDI4DCyABLQAAIgBBO0cEQCAAQQ1HDbECIAFBAWohAQy6AQsgAUEBaiEBC0EiIQMM8wILIAEgBEYEQEEcIQMMjAMLQgAhCgJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAS0AAEEwaw43wQLAAgABAgMEBQYH0AHQAdAB0AHQAdAB0AEICQoLDA3QAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdABDg8QERIT0AELQgIhCgzAAgtCAyEKDL8CC0IEIQoMvgILQgUhCgy9AgtCBiEKDLwCC0IHIQoMuwILQgghCgy6AgtCCSEKDLkCC0IKIQoMuAILQgshCgy3AgtCDCEKDLYCC0INIQoMtQILQg4hCgy0AgtCDyEKDLMCC0IKIQoMsgILQgshCgyxAgtCDCEKDLACC0INIQoMrwILQg4hCgyuAgtCDyEKDK0CC0IAIQoCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBMGsON8ACvwIAAQIDBAUGB74CvgK+Ar4CvgK+Ar4CCAkKCwwNvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ag4PEBESE74CC0ICIQoMvwILQgMhCgy+AgtCBCEKDL0CC0IFIQoMvAILQgYhCgy7AgtCByEKDLoCC0IIIQoMuQILQgkhCgy4AgtCCiEKDLcCC0ILIQoMtgILQgwhCgy1AgtCDSEKDLQCC0IOIQoMswILQg8hCgyyAgtCCiEKDLECC0ILIQoMsAILQgwhCgyvAgtCDSEKDK4CC0IOIQoMrQILQg8hCgysAgsgAiACKQMgIgogBCABa60iC30iDEIAIAogDFobNwMgIAogC1gNpwJBHyEDDIkDCyABIARHBEAgAkEJNgIIIAIgATYCBEElIQMM8AILQSAhAwyIAwtBASEFIAIvATAiA0EIcUUEQCACKQMgQgBSIQULAkAgAi0ALgRAQQEhACACLQApQQVGDQEgA0HAAHFFIAVxRQ0BC0EAIQAgA0HAAHENAEECIQAgA0EIcQ0AIANBgARxBEACQCACLQAoQQFHDQAgAi0ALUEKcQ0AQQUhAAwCC0EEIQAMAQsgA0EgcUUEQAJAIAItAChBAUYNACACLwEyIgBB5ABrQeQASQ0AIABBzAFGDQAgAEGwAkYNAEEEIQAgA0EocUUNAiADQYgEcUGABEYNAgtBACEADAELQQBBAyACKQMgUBshAAsgAEEBaw4FvgIAsAEBpAKhAgtBESEDDO0CCyACQQE6AC8MhAMLIAEgBEcNnQJBJCEDDIQDCyABIARHDRxBxgAhAwyDAwtBACEAAkAgAigCOCIDRQ0AIAMoAkQiA0UNACACIAMRAAAhAAsgAEUNJyAAQRVHDZgCIAJB0AA2AhwgAiABNgIUIAJBkRg2AhAgAkEVNgIMQQAhAwyCAwsgASAERgRAQSghAwyCAwtBACEDIAJBADYCBCACQQw2AgggAiABIAEQKiIARQ2UAiACQSc2AhwgAiABNgIUIAIgADYCDAyBAwsgASAERgRAQSkhAwyBAwsgAS0AACIAQSBGDRMgAEEJRw2VAiABQQFqIQEMFAsgASAERwRAIAFBAWohAQwWC0EqIQMM/wILIAEgBEYEQEErIQMM/wILIAEtAAAiAEEJRyAAQSBHcQ2QAiACLQAsQQhHDd0CIAJBADoALAzdAgsgASAERgRAQSwhAwz+AgsgAS0AAEEKRw2OAiABQQFqIQEMsAELIAEgBEcNigJBLyEDDPwCCwNAIAEtAAAiAEEgRwRAIABBCmsOBIQCiAKIAoQChgILIAQgAUEBaiIBRw0AC0ExIQMM+wILQTIhAyABIARGDfoCIAIoAgAiACAEIAFraiEHIAEgAGtBA2ohBgJAA0AgAEHwO2otAAAgAS0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQEgAEEDRgRAQQYhAQziAgsgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAc2AgAM+wILIAJBADYCAAyGAgtBMyEDIAQgASIARg35AiAEIAFrIAIoAgAiAWohByAAIAFrQQhqIQYCQANAIAFB9DtqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBCEYEQEEFIQEM4QILIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADPoCCyACQQA2AgAgACEBDIUCC0E0IQMgBCABIgBGDfgCIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgJAA0AgAUHQwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBBUYEQEEHIQEM4AILIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADPkCCyACQQA2AgAgACEBDIQCCyABIARHBEADQCABLQAAQYA+ai0AACIAQQFHBEAgAEECRg0JDIECCyAEIAFBAWoiAUcNAAtBMCEDDPgCC0EwIQMM9wILIAEgBEcEQANAIAEtAAAiAEEgRwRAIABBCmsOBP8B/gH+Af8B/gELIAQgAUEBaiIBRw0AC0E4IQMM9wILQTghAwz2AgsDQCABLQAAIgBBIEcgAEEJR3EN9gEgBCABQQFqIgFHDQALQTwhAwz1AgsDQCABLQAAIgBBIEcEQAJAIABBCmsOBPkBBAT5AQALIABBLEYN9QEMAwsgBCABQQFqIgFHDQALQT8hAwz0AgtBwAAhAyABIARGDfMCIAIoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAEGAQGstAAAgAS0AAEEgckcNASAAQQZGDdsCIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPQCCyACQQA2AgALQTYhAwzZAgsgASAERgRAQcEAIQMM8gILIAJBDDYCCCACIAE2AgQgAi0ALEEBaw4E+wHuAewB6wHUAgsgAUEBaiEBDPoBCyABIARHBEADQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxIgBBCUYNACAAQSBGDQACQAJAAkACQCAAQeMAaw4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUExIQMM3AILIAFBAWohAUEyIQMM2wILIAFBAWohAUEzIQMM2gILDP4BCyAEIAFBAWoiAUcNAAtBNSEDDPACC0E1IQMM7wILIAEgBEcEQANAIAEtAABBgDxqLQAAQQFHDfcBIAQgAUEBaiIBRw0AC0E9IQMM7wILQT0hAwzuAgtBACEAAkAgAigCOCIDRQ0AIAMoAkAiA0UNACACIAMRAAAhAAsgAEUNASAAQRVHDeYBIAJBwgA2AhwgAiABNgIUIAJB4xg2AhAgAkEVNgIMQQAhAwztAgsgAUEBaiEBC0E8IQMM0gILIAEgBEYEQEHCACEDDOsCCwJAA0ACQCABLQAAQQlrDhgAAswCzALRAswCzALMAswCzALMAswCzALMAswCzALMAswCzALMAswCzALMAgDMAgsgBCABQQFqIgFHDQALQcIAIQMM6wILIAFBAWohASACLQAtQQFxRQ3+AQtBLCEDDNACCyABIARHDd4BQcQAIQMM6AILA0AgAS0AAEGQwABqLQAAQQFHDZwBIAQgAUEBaiIBRw0AC0HFACEDDOcCCyABLQAAIgBBIEYN/gEgAEE6Rw3AAiACKAIEIQBBACEDIAJBADYCBCACIAAgARApIgAN3gEM3QELQccAIQMgBCABIgBGDeUCIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgNAIAFBkMIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNvwIgAUEFRg3CAiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBzYCAAzlAgtByAAhAyAEIAEiAEYN5AIgBCABayACKAIAIgFqIQcgACABa0EJaiEGA0AgAUGWwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw2+AkECIAFBCUYNwgIaIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADOQCCyABIARGBEBByQAhAwzkAgsCQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxQe4Aaw4HAL8CvwK/Ar8CvwIBvwILIAFBAWohAUE+IQMMywILIAFBAWohAUE/IQMMygILQcoAIQMgBCABIgBGDeICIAQgAWsgAigCACIBaiEGIAAgAWtBAWohBwNAIAFBoMIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNvAIgAUEBRg2+AiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBjYCAAziAgtBywAhAyAEIAEiAEYN4QIgBCABayACKAIAIgFqIQcgACABa0EOaiEGA0AgAUGiwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw27AiABQQ5GDb4CIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADOECC0HMACEDIAQgASIARg3gAiAEIAFrIAIoAgAiAWohByAAIAFrQQ9qIQYDQCABQcDCAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDboCQQMgAUEPRg2+AhogAUEBaiEBIAQgAEEBaiIARw0ACyACIAc2AgAM4AILQc0AIQMgBCABIgBGDd8CIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgNAIAFB0MIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNuQJBBCABQQVGDb0CGiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBzYCAAzfAgsgASAERgRAQc4AIQMM3wILAkACQAJAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXFB4wBrDhMAvAK8ArwCvAK8ArwCvAK8ArwCvAK8ArwCAbwCvAK8AgIDvAILIAFBAWohAUHBACEDDMgCCyABQQFqIQFBwgAhAwzHAgsgAUEBaiEBQcMAIQMMxgILIAFBAWohAUHEACEDDMUCCyABIARHBEAgAkENNgIIIAIgATYCBEHFACEDDMUCC0HPACEDDN0CCwJAAkAgAS0AAEEKaw4EAZABkAEAkAELIAFBAWohAQtBKCEDDMMCCyABIARGBEBB0QAhAwzcAgsgAS0AAEEgRw0AIAFBAWohASACLQAtQQFxRQ3QAQtBFyEDDMECCyABIARHDcsBQdIAIQMM2QILQdMAIQMgASAERg3YAiACKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABB1sIAai0AAEcNxwEgAEEBRg3KASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBjYCAAzYAgsgASAERgRAQdUAIQMM2AILIAEtAABBCkcNwgEgAUEBaiEBDMoBCyABIARGBEBB1gAhAwzXAgsCQAJAIAEtAABBCmsOBADDAcMBAcMBCyABQQFqIQEMygELIAFBAWohAUHKACEDDL0CC0EAIQACQCACKAI4IgNFDQAgAygCPCIDRQ0AIAIgAxEAACEACyAADb8BQc0AIQMMvAILIAItAClBIkYNzwIMiQELIAQgASIFRgRAQdsAIQMM1AILQQAhAEEBIQFBASEGQQAhAwJAAn8CQAJAAkACQAJAAkACQCAFLQAAQTBrDgrFAcQBAAECAwQFBgjDAQtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshA0EAIQFBACEGDL0BC0EJIQNBASEAQQAhAUEAIQYMvAELIAEgBEYEQEHdACEDDNMCCyABLQAAQS5HDbgBIAFBAWohAQyIAQsgASAERw22AUHfACEDDNECCyABIARHBEAgAkEONgIIIAIgATYCBEHQACEDDLgCC0HgACEDDNACC0HhACEDIAEgBEYNzwIgAigCACIAIAQgAWtqIQUgASAAa0EDaiEGA0AgAS0AACAAQeLCAGotAABHDbEBIABBA0YNswEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMzwILQeIAIQMgASAERg3OAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYDQCABLQAAIABB5sIAai0AAEcNsAEgAEECRg2vASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAzOAgtB4wAhAyABIARGDc0CIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgNAIAEtAAAgAEHpwgBqLQAARw2vASAAQQNGDa0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADM0CCyABIARGBEBB5QAhAwzNAgsgAUEBaiEBQQAhAAJAIAIoAjgiA0UNACADKAIwIgNFDQAgAiADEQAAIQALIAANqgFB1gAhAwyzAgsgASAERwRAA0AgAS0AACIAQSBHBEACQAJAAkAgAEHIAGsOCwABswGzAbMBswGzAbMBswGzAQKzAQsgAUEBaiEBQdIAIQMMtwILIAFBAWohAUHTACEDDLYCCyABQQFqIQFB1AAhAwy1AgsgBCABQQFqIgFHDQALQeQAIQMMzAILQeQAIQMMywILA0AgAS0AAEHwwgBqLQAAIgBBAUcEQCAAQQJrDgOnAaYBpQGkAQsgBCABQQFqIgFHDQALQeYAIQMMygILIAFBAWogASAERw0CGkHnACEDDMkCCwNAIAEtAABB8MQAai0AACIAQQFHBEACQCAAQQJrDgSiAaEBoAEAnwELQdcAIQMMsQILIAQgAUEBaiIBRw0AC0HoACEDDMgCCyABIARGBEBB6QAhAwzIAgsCQCABLQAAIgBBCmsOGrcBmwGbAbQBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBpAGbAZsBAJkBCyABQQFqCyEBQQYhAwytAgsDQCABLQAAQfDGAGotAABBAUcNfSAEIAFBAWoiAUcNAAtB6gAhAwzFAgsgAUEBaiABIARHDQIaQesAIQMMxAILIAEgBEYEQEHsACEDDMQCCyABQQFqDAELIAEgBEYEQEHtACEDDMMCCyABQQFqCyEBQQQhAwyoAgsgASAERgRAQe4AIQMMwQILAkACQAJAIAEtAABB8MgAai0AAEEBaw4HkAGPAY4BAHwBAo0BCyABQQFqIQEMCwsgAUEBagyTAQtBACEDIAJBADYCHCACQZsSNgIQIAJBBzYCDCACIAFBAWo2AhQMwAILAkADQCABLQAAQfDIAGotAAAiAEEERwRAAkACQCAAQQFrDgeUAZMBkgGNAQAEAY0BC0HaACEDDKoCCyABQQFqIQFB3AAhAwypAgsgBCABQQFqIgFHDQALQe8AIQMMwAILIAFBAWoMkQELIAQgASIARgRAQfAAIQMMvwILIAAtAABBL0cNASAAQQFqIQEMBwsgBCABIgBGBEBB8QAhAwy+AgsgAC0AACIBQS9GBEAgAEEBaiEBQd0AIQMMpQILIAFBCmsiA0EWSw0AIAAhAUEBIAN0QYmAgAJxDfkBC0EAIQMgAkEANgIcIAIgADYCFCACQYwcNgIQIAJBBzYCDAy8AgsgASAERwRAIAFBAWohAUHeACEDDKMCC0HyACEDDLsCCyABIARGBEBB9AAhAwy7AgsCQCABLQAAQfDMAGotAABBAWsOA/cBcwCCAQtB4QAhAwyhAgsgASAERwRAA0AgAS0AAEHwygBqLQAAIgBBA0cEQAJAIABBAWsOAvkBAIUBC0HfACEDDKMCCyAEIAFBAWoiAUcNAAtB8wAhAwy6AgtB8wAhAwy5AgsgASAERwRAIAJBDzYCCCACIAE2AgRB4AAhAwygAgtB9QAhAwy4AgsgASAERgRAQfYAIQMMuAILIAJBDzYCCCACIAE2AgQLQQMhAwydAgsDQCABLQAAQSBHDY4CIAQgAUEBaiIBRw0AC0H3ACEDDLUCCyABIARGBEBB+AAhAwy1AgsgAS0AAEEgRw16IAFBAWohAQxbC0EAIQACQCACKAI4IgNFDQAgAygCOCIDRQ0AIAIgAxEAACEACyAADXgMgAILIAEgBEYEQEH6ACEDDLMCCyABLQAAQcwARw10IAFBAWohAUETDHYLQfsAIQMgASAERg2xAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYDQCABLQAAIABB8M4Aai0AAEcNcyAAQQVGDXUgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMsQILIAEgBEYEQEH8ACEDDLECCwJAAkAgAS0AAEHDAGsODAB0dHR0dHR0dHR0AXQLIAFBAWohAUHmACEDDJgCCyABQQFqIQFB5wAhAwyXAgtB/QAhAyABIARGDa8CIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQe3PAGotAABHDXIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADLACCyACQQA2AgAgBkEBaiEBQRAMcwtB/gAhAyABIARGDa4CIAIoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQfbOAGotAABHDXEgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADK8CCyACQQA2AgAgBkEBaiEBQRYMcgtB/wAhAyABIARGDa0CIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQfzOAGotAABHDXAgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADK4CCyACQQA2AgAgBkEBaiEBQQUMcQsgASAERgRAQYABIQMMrQILIAEtAABB2QBHDW4gAUEBaiEBQQgMcAsgASAERgRAQYEBIQMMrAILAkACQCABLQAAQc4Aaw4DAG8BbwsgAUEBaiEBQesAIQMMkwILIAFBAWohAUHsACEDDJICCyABIARGBEBBggEhAwyrAgsCQAJAIAEtAABByABrDggAbm5ubm5uAW4LIAFBAWohAUHqACEDDJICCyABQQFqIQFB7QAhAwyRAgtBgwEhAyABIARGDakCIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQYDPAGotAABHDWwgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADKoCCyACQQA2AgAgBkEBaiEBQQAMbQtBhAEhAyABIARGDagCIAIoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQYPPAGotAABHDWsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADKkCCyACQQA2AgAgBkEBaiEBQSMMbAsgASAERgRAQYUBIQMMqAILAkACQCABLQAAQcwAaw4IAGtra2trawFrCyABQQFqIQFB7wAhAwyPAgsgAUEBaiEBQfAAIQMMjgILIAEgBEYEQEGGASEDDKcCCyABLQAAQcUARw1oIAFBAWohAQxgC0GHASEDIAEgBEYNpQIgAigCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBiM8Aai0AAEcNaCAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMpgILIAJBADYCACAGQQFqIQFBLQxpC0GIASEDIAEgBEYNpAIgAigCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABB0M8Aai0AAEcNZyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMpQILIAJBADYCACAGQQFqIQFBKQxoCyABIARGBEBBiQEhAwykAgtBASABLQAAQd8ARw1nGiABQQFqIQEMXgtBigEhAyABIARGDaICIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgNAIAEtAAAgAEGMzwBqLQAARw1kIABBAUYN+gEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMogILQYsBIQMgASAERg2hAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGOzwBqLQAARw1kIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyiAgsgAkEANgIAIAZBAWohAUECDGULQYwBIQMgASAERg2gAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHwzwBqLQAARw1jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyhAgsgAkEANgIAIAZBAWohAUEfDGQLQY0BIQMgASAERg2fAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHyzwBqLQAARw1iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAygAgsgAkEANgIAIAZBAWohAUEJDGMLIAEgBEYEQEGOASEDDJ8CCwJAAkAgAS0AAEHJAGsOBwBiYmJiYgFiCyABQQFqIQFB+AAhAwyGAgsgAUEBaiEBQfkAIQMMhQILQY8BIQMgASAERg2dAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGRzwBqLQAARw1gIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyeAgsgAkEANgIAIAZBAWohAUEYDGELQZABIQMgASAERg2cAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGXzwBqLQAARw1fIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAydAgsgAkEANgIAIAZBAWohAUEXDGALQZEBIQMgASAERg2bAiACKAIAIgAgBCABa2ohBSABIABrQQZqIQYCQANAIAEtAAAgAEGazwBqLQAARw1eIABBBkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAycAgsgAkEANgIAIAZBAWohAUEVDF8LQZIBIQMgASAERg2aAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGhzwBqLQAARw1dIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAybAgsgAkEANgIAIAZBAWohAUEeDF4LIAEgBEYEQEGTASEDDJoCCyABLQAAQcwARw1bIAFBAWohAUEKDF0LIAEgBEYEQEGUASEDDJkCCwJAAkAgAS0AAEHBAGsODwBcXFxcXFxcXFxcXFxcAVwLIAFBAWohAUH+ACEDDIACCyABQQFqIQFB/wAhAwz/AQsgASAERgRAQZUBIQMMmAILAkACQCABLQAAQcEAaw4DAFsBWwsgAUEBaiEBQf0AIQMM/wELIAFBAWohAUGAASEDDP4BC0GWASEDIAEgBEYNlgIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBp88Aai0AAEcNWSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlwILIAJBADYCACAGQQFqIQFBCwxaCyABIARGBEBBlwEhAwyWAgsCQAJAAkACQCABLQAAQS1rDiMAW1tbW1tbW1tbW1tbW1tbW1tbW1tbW1sBW1tbW1sCW1tbA1sLIAFBAWohAUH7ACEDDP8BCyABQQFqIQFB/AAhAwz+AQsgAUEBaiEBQYEBIQMM/QELIAFBAWohAUGCASEDDPwBC0GYASEDIAEgBEYNlAIgAigCACIAIAQgAWtqIQUgASAAa0EEaiEGAkADQCABLQAAIABBqc8Aai0AAEcNVyAAQQRGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlQILIAJBADYCACAGQQFqIQFBGQxYC0GZASEDIAEgBEYNkwIgAigCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBrs8Aai0AAEcNViAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlAILIAJBADYCACAGQQFqIQFBBgxXC0GaASEDIAEgBEYNkgIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBtM8Aai0AAEcNVSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMkwILIAJBADYCACAGQQFqIQFBHAxWC0GbASEDIAEgBEYNkQIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBts8Aai0AAEcNVCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMkgILIAJBADYCACAGQQFqIQFBJwxVCyABIARGBEBBnAEhAwyRAgsCQAJAIAEtAABB1ABrDgIAAVQLIAFBAWohAUGGASEDDPgBCyABQQFqIQFBhwEhAwz3AQtBnQEhAyABIARGDY8CIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjPAGotAABHDVIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADJACCyACQQA2AgAgBkEBaiEBQSYMUwtBngEhAyABIARGDY4CIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbrPAGotAABHDVEgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI8CCyACQQA2AgAgBkEBaiEBQQMMUgtBnwEhAyABIARGDY0CIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQe3PAGotAABHDVAgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI4CCyACQQA2AgAgBkEBaiEBQQwMUQtBoAEhAyABIARGDYwCIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQbzPAGotAABHDU8gAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI0CCyACQQA2AgAgBkEBaiEBQQ0MUAsgASAERgRAQaEBIQMMjAILAkACQCABLQAAQcYAaw4LAE9PT09PT09PTwFPCyABQQFqIQFBiwEhAwzzAQsgAUEBaiEBQYwBIQMM8gELIAEgBEYEQEGiASEDDIsCCyABLQAAQdAARw1MIAFBAWohAQxGCyABIARGBEBBowEhAwyKAgsCQAJAIAEtAABByQBrDgcBTU1NTU0ATQsgAUEBaiEBQY4BIQMM8QELIAFBAWohAUEiDE0LQaQBIQMgASAERg2IAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHAzwBqLQAARw1LIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyJAgsgAkEANgIAIAZBAWohAUEdDEwLIAEgBEYEQEGlASEDDIgCCwJAAkAgAS0AAEHSAGsOAwBLAUsLIAFBAWohAUGQASEDDO8BCyABQQFqIQFBBAxLCyABIARGBEBBpgEhAwyHAgsCQAJAAkACQAJAIAEtAABBwQBrDhUATU1NTU1NTU1NTQFNTQJNTQNNTQRNCyABQQFqIQFBiAEhAwzxAQsgAUEBaiEBQYkBIQMM8AELIAFBAWohAUGKASEDDO8BCyABQQFqIQFBjwEhAwzuAQsgAUEBaiEBQZEBIQMM7QELQacBIQMgASAERg2FAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHtzwBqLQAARw1IIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyGAgsgAkEANgIAIAZBAWohAUERDEkLQagBIQMgASAERg2EAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHCzwBqLQAARw1HIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyFAgsgAkEANgIAIAZBAWohAUEsDEgLQakBIQMgASAERg2DAiACKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHFzwBqLQAARw1GIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyEAgsgAkEANgIAIAZBAWohAUErDEcLQaoBIQMgASAERg2CAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHKzwBqLQAARw1FIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyDAgsgAkEANgIAIAZBAWohAUEUDEYLIAEgBEYEQEGrASEDDIICCwJAAkACQAJAIAEtAABBwgBrDg8AAQJHR0dHR0dHR0dHRwNHCyABQQFqIQFBkwEhAwzrAQsgAUEBaiEBQZQBIQMM6gELIAFBAWohAUGVASEDDOkBCyABQQFqIQFBlgEhAwzoAQsgASAERgRAQawBIQMMgQILIAEtAABBxQBHDUIgAUEBaiEBDD0LQa0BIQMgASAERg3/ASACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHNzwBqLQAARw1CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyAAgsgAkEANgIAIAZBAWohAUEODEMLIAEgBEYEQEGuASEDDP8BCyABLQAAQdAARw1AIAFBAWohAUElDEILQa8BIQMgASAERg39ASACKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEHQzwBqLQAARw1AIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz+AQsgAkEANgIAIAZBAWohAUEqDEELIAEgBEYEQEGwASEDDP0BCwJAAkAgAS0AAEHVAGsOCwBAQEBAQEBAQEABQAsgAUEBaiEBQZoBIQMM5AELIAFBAWohAUGbASEDDOMBCyABIARGBEBBsQEhAwz8AQsCQAJAIAEtAABBwQBrDhQAPz8/Pz8/Pz8/Pz8/Pz8/Pz8/AT8LIAFBAWohAUGZASEDDOMBCyABQQFqIQFBnAEhAwziAQtBsgEhAyABIARGDfoBIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQdnPAGotAABHDT0gAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPsBCyACQQA2AgAgBkEBaiEBQSEMPgtBswEhAyABIARGDfkBIAIoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAS0AACAAQd3PAGotAABHDTwgAEEGRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPoBCyACQQA2AgAgBkEBaiEBQRoMPQsgASAERgRAQbQBIQMM+QELAkACQAJAIAEtAABBxQBrDhEAPT09PT09PT09AT09PT09Aj0LIAFBAWohAUGdASEDDOEBCyABQQFqIQFBngEhAwzgAQsgAUEBaiEBQZ8BIQMM3wELQbUBIQMgASAERg33ASACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHkzwBqLQAARw06IABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz4AQsgAkEANgIAIAZBAWohAUEoDDsLQbYBIQMgASAERg32ASACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHqzwBqLQAARw05IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz3AQsgAkEANgIAIAZBAWohAUEHDDoLIAEgBEYEQEG3ASEDDPYBCwJAAkAgAS0AAEHFAGsODgA5OTk5OTk5OTk5OTkBOQsgAUEBaiEBQaEBIQMM3QELIAFBAWohAUGiASEDDNwBC0G4ASEDIAEgBEYN9AEgAigCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB7c8Aai0AAEcNNyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM9QELIAJBADYCACAGQQFqIQFBEgw4C0G5ASEDIAEgBEYN8wEgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8M8Aai0AAEcNNiAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM9AELIAJBADYCACAGQQFqIQFBIAw3C0G6ASEDIAEgBEYN8gEgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8s8Aai0AAEcNNSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM8wELIAJBADYCACAGQQFqIQFBDww2CyABIARGBEBBuwEhAwzyAQsCQAJAIAEtAABByQBrDgcANTU1NTUBNQsgAUEBaiEBQaUBIQMM2QELIAFBAWohAUGmASEDDNgBC0G8ASEDIAEgBEYN8AEgAigCACIAIAQgAWtqIQUgASAAa0EHaiEGAkADQCABLQAAIABB9M8Aai0AAEcNMyAAQQdGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM8QELIAJBADYCACAGQQFqIQFBGww0CyABIARGBEBBvQEhAwzwAQsCQAJAAkAgAS0AAEHCAGsOEgA0NDQ0NDQ0NDQBNDQ0NDQ0AjQLIAFBAWohAUGkASEDDNgBCyABQQFqIQFBpwEhAwzXAQsgAUEBaiEBQagBIQMM1gELIAEgBEYEQEG+ASEDDO8BCyABLQAAQc4ARw0wIAFBAWohAQwsCyABIARGBEBBvwEhAwzuAQsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCABLQAAQcEAaw4VAAECAz8EBQY/Pz8HCAkKCz8MDQ4PPwsgAUEBaiEBQegAIQMM4wELIAFBAWohAUHpACEDDOIBCyABQQFqIQFB7gAhAwzhAQsgAUEBaiEBQfIAIQMM4AELIAFBAWohAUHzACEDDN8BCyABQQFqIQFB9gAhAwzeAQsgAUEBaiEBQfcAIQMM3QELIAFBAWohAUH6ACEDDNwBCyABQQFqIQFBgwEhAwzbAQsgAUEBaiEBQYQBIQMM2gELIAFBAWohAUGFASEDDNkBCyABQQFqIQFBkgEhAwzYAQsgAUEBaiEBQZgBIQMM1wELIAFBAWohAUGgASEDDNYBCyABQQFqIQFBowEhAwzVAQsgAUEBaiEBQaoBIQMM1AELIAEgBEcEQCACQRA2AgggAiABNgIEQasBIQMM1AELQcABIQMM7AELQQAhAAJAIAIoAjgiA0UNACADKAI0IgNFDQAgAiADEQAAIQALIABFDV4gAEEVRw0HIAJB0QA2AhwgAiABNgIUIAJBsBc2AhAgAkEVNgIMQQAhAwzrAQsgAUEBaiABIARHDQgaQcIBIQMM6gELA0ACQCABLQAAQQprDgQIAAALAAsgBCABQQFqIgFHDQALQcMBIQMM6QELIAEgBEcEQCACQRE2AgggAiABNgIEQQEhAwzQAQtBxAEhAwzoAQsgASAERgRAQcUBIQMM6AELAkACQCABLQAAQQprDgQBKCgAKAsgAUEBagwJCyABQQFqDAULIAEgBEYEQEHGASEDDOcBCwJAAkAgAS0AAEEKaw4XAQsLAQsLCwsLCwsLCwsLCwsLCwsLCwALCyABQQFqIQELQbABIQMMzQELIAEgBEYEQEHIASEDDOYBCyABLQAAQSBHDQkgAkEAOwEyIAFBAWohAUGzASEDDMwBCwNAIAEhAAJAIAEgBEcEQCABLQAAQTBrQf8BcSIDQQpJDQEMJwtBxwEhAwzmAQsCQCACLwEyIgFBmTNLDQAgAiABQQpsIgU7ATIgBUH+/wNxIANB//8Dc0sNACAAQQFqIQEgAiADIAVqIgM7ATIgA0H//wNxQegHSQ0BCwtBACEDIAJBADYCHCACQcEJNgIQIAJBDTYCDCACIABBAWo2AhQM5AELIAJBADYCHCACIAE2AhQgAkHwDDYCECACQRs2AgxBACEDDOMBCyACKAIEIQAgAkEANgIEIAIgACABECYiAA0BIAFBAWoLIQFBrQEhAwzIAQsgAkHBATYCHCACIAA2AgwgAiABQQFqNgIUQQAhAwzgAQsgAigCBCEAIAJBADYCBCACIAAgARAmIgANASABQQFqCyEBQa4BIQMMxQELIAJBwgE2AhwgAiAANgIMIAIgAUEBajYCFEEAIQMM3QELIAJBADYCHCACIAE2AhQgAkGXCzYCECACQQ02AgxBACEDDNwBCyACQQA2AhwgAiABNgIUIAJB4xA2AhAgAkEJNgIMQQAhAwzbAQsgAkECOgAoDKwBC0EAIQMgAkEANgIcIAJBrws2AhAgAkECNgIMIAIgAUEBajYCFAzZAQtBAiEDDL8BC0ENIQMMvgELQSYhAwy9AQtBFSEDDLwBC0EWIQMMuwELQRghAwy6AQtBHCEDDLkBC0EdIQMMuAELQSAhAwy3AQtBISEDDLYBC0EjIQMMtQELQcYAIQMMtAELQS4hAwyzAQtBPSEDDLIBC0HLACEDDLEBC0HOACEDDLABC0HYACEDDK8BC0HZACEDDK4BC0HbACEDDK0BC0HxACEDDKwBC0H0ACEDDKsBC0GNASEDDKoBC0GXASEDDKkBC0GpASEDDKgBC0GvASEDDKcBC0GxASEDDKYBCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJB8Rs2AhAgAkEGNgIMDL0BCyACQQA2AgAgBkEBaiEBQSQLOgApIAIoAgQhACACQQA2AgQgAiAAIAEQJyIARQRAQeUAIQMMowELIAJB+QA2AhwgAiABNgIUIAIgADYCDEEAIQMMuwELIABBFUcEQCACQQA2AhwgAiABNgIUIAJBzA42AhAgAkEgNgIMQQAhAwy7AQsgAkH4ADYCHCACIAE2AhQgAkHKGDYCECACQRU2AgxBACEDDLoBCyACQQA2AhwgAiABNgIUIAJBjhs2AhAgAkEGNgIMQQAhAwy5AQsgAkEANgIcIAIgATYCFCACQf4RNgIQIAJBBzYCDEEAIQMMuAELIAJBADYCHCACIAE2AhQgAkGMHDYCECACQQc2AgxBACEDDLcBCyACQQA2AhwgAiABNgIUIAJBww82AhAgAkEHNgIMQQAhAwy2AQsgAkEANgIcIAIgATYCFCACQcMPNgIQIAJBBzYCDEEAIQMMtQELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0RIAJB5QA2AhwgAiABNgIUIAIgADYCDEEAIQMMtAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0gIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMswELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0iIAJB0gA2AhwgAiABNgIUIAIgADYCDEEAIQMMsgELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0OIAJB5QA2AhwgAiABNgIUIAIgADYCDEEAIQMMsQELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0dIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMsAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0fIAJB0gA2AhwgAiABNgIUIAIgADYCDEEAIQMMrwELIABBP0cNASABQQFqCyEBQQUhAwyUAQtBACEDIAJBADYCHCACIAE2AhQgAkH9EjYCECACQQc2AgwMrAELIAJBADYCHCACIAE2AhQgAkHcCDYCECACQQc2AgxBACEDDKsBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNByACQeUANgIcIAIgATYCFCACIAA2AgxBACEDDKoBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNFiACQdMANgIcIAIgATYCFCACIAA2AgxBACEDDKkBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNGCACQdIANgIcIAIgATYCFCACIAA2AgxBACEDDKgBCyACQQA2AhwgAiABNgIUIAJBxgo2AhAgAkEHNgIMQQAhAwynAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDQMgAkHlADYCHCACIAE2AhQgAiAANgIMQQAhAwymAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDRIgAkHTADYCHCACIAE2AhQgAiAANgIMQQAhAwylAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDRQgAkHSADYCHCACIAE2AhQgAiAANgIMQQAhAwykAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDQAgAkHlADYCHCACIAE2AhQgAiAANgIMQQAhAwyjAQtB1QAhAwyJAQsgAEEVRwRAIAJBADYCHCACIAE2AhQgAkG5DTYCECACQRo2AgxBACEDDKIBCyACQeQANgIcIAIgATYCFCACQeMXNgIQIAJBFTYCDEEAIQMMoQELIAJBADYCACAGQQFqIQEgAi0AKSIAQSNrQQtJDQQCQCAAQQZLDQBBASAAdEHKAHFFDQAMBQtBACEDIAJBADYCHCACIAE2AhQgAkH3CTYCECACQQg2AgwMoAELIAJBADYCACAGQQFqIQEgAi0AKUEhRg0DIAJBADYCHCACIAE2AhQgAkGbCjYCECACQQg2AgxBACEDDJ8BCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJBkDM2AhAgAkEINgIMDJ0BCyACQQA2AgAgBkEBaiEBIAItAClBI0kNACACQQA2AhwgAiABNgIUIAJB0wk2AhAgAkEINgIMQQAhAwycAQtB0QAhAwyCAQsgAS0AAEEwayIAQf8BcUEKSQRAIAIgADoAKiABQQFqIQFBzwAhAwyCAQsgAigCBCEAIAJBADYCBCACIAAgARAoIgBFDYYBIAJB3gA2AhwgAiABNgIUIAIgADYCDEEAIQMMmgELIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ2GASACQdwANgIcIAIgATYCFCACIAA2AgxBACEDDJkBCyACKAIEIQAgAkEANgIEIAIgACAFECgiAEUEQCAFIQEMhwELIAJB2gA2AhwgAiAFNgIUIAIgADYCDAyYAQtBACEBQQEhAwsgAiADOgArIAVBAWohAwJAAkACQCACLQAtQRBxDQACQAJAAkAgAi0AKg4DAQACBAsgBkUNAwwCCyAADQEMAgsgAUUNAQsgAigCBCEAIAJBADYCBCACIAAgAxAoIgBFBEAgAyEBDAILIAJB2AA2AhwgAiADNgIUIAIgADYCDEEAIQMMmAELIAIoAgQhACACQQA2AgQgAiAAIAMQKCIARQRAIAMhAQyHAQsgAkHZADYCHCACIAM2AhQgAiAANgIMQQAhAwyXAQtBzAAhAwx9CyAAQRVHBEAgAkEANgIcIAIgATYCFCACQZQNNgIQIAJBITYCDEEAIQMMlgELIAJB1wA2AhwgAiABNgIUIAJByRc2AhAgAkEVNgIMQQAhAwyVAQtBACEDIAJBADYCHCACIAE2AhQgAkGAETYCECACQQk2AgwMlAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0AIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMkwELQckAIQMMeQsgAkEANgIcIAIgATYCFCACQcEoNgIQIAJBBzYCDCACQQA2AgBBACEDDJEBCyACKAIEIQBBACEDIAJBADYCBCACIAAgARAlIgBFDQAgAkHSADYCHCACIAE2AhQgAiAANgIMDJABC0HIACEDDHYLIAJBADYCACAFIQELIAJBgBI7ASogAUEBaiEBQQAhAAJAIAIoAjgiA0UNACADKAIwIgNFDQAgAiADEQAAIQALIAANAQtBxwAhAwxzCyAAQRVGBEAgAkHRADYCHCACIAE2AhQgAkHjFzYCECACQRU2AgxBACEDDIwBC0EAIQMgAkEANgIcIAIgATYCFCACQbkNNgIQIAJBGjYCDAyLAQtBACEDIAJBADYCHCACIAE2AhQgAkGgGTYCECACQR42AgwMigELIAEtAABBOkYEQCACKAIEIQBBACEDIAJBADYCBCACIAAgARApIgBFDQEgAkHDADYCHCACIAA2AgwgAiABQQFqNgIUDIoBC0EAIQMgAkEANgIcIAIgATYCFCACQbERNgIQIAJBCjYCDAyJAQsgAUEBaiEBQTshAwxvCyACQcMANgIcIAIgADYCDCACIAFBAWo2AhQMhwELQQAhAyACQQA2AhwgAiABNgIUIAJB8A42AhAgAkEcNgIMDIYBCyACIAIvATBBEHI7ATAMZgsCQCACLwEwIgBBCHFFDQAgAi0AKEEBRw0AIAItAC1BCHFFDQMLIAIgAEH3+wNxQYAEcjsBMAwECyABIARHBEACQANAIAEtAABBMGsiAEH/AXFBCk8EQEE1IQMMbgsgAikDICIKQpmz5syZs+bMGVYNASACIApCCn4iCjcDICAKIACtQv8BgyILQn+FVg0BIAIgCiALfDcDICAEIAFBAWoiAUcNAAtBOSEDDIUBCyACKAIEIQBBACEDIAJBADYCBCACIAAgAUEBaiIBECoiAA0MDHcLQTkhAwyDAQsgAi0AMEEgcQ0GQcUBIQMMaQtBACEDIAJBADYCBCACIAEgARAqIgBFDQQgAkE6NgIcIAIgADYCDCACIAFBAWo2AhQMgQELIAItAChBAUcNACACLQAtQQhxRQ0BC0E3IQMMZgsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIABEAgAkE7NgIcIAIgADYCDCACIAFBAWo2AhQMfwsgAUEBaiEBDG4LIAJBCDoALAwECyABQQFqIQEMbQtBACEDIAJBADYCHCACIAE2AhQgAkHkEjYCECACQQQ2AgwMewsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIARQ1sIAJBNzYCHCACIAE2AhQgAiAANgIMDHoLIAIgAi8BMEEgcjsBMAtBMCEDDF8LIAJBNjYCHCACIAE2AhQgAiAANgIMDHcLIABBLEcNASABQQFqIQBBASEBAkACQAJAAkACQCACLQAsQQVrDgQDAQIEAAsgACEBDAQLQQIhAQwBC0EEIQELIAJBAToALCACIAIvATAgAXI7ATAgACEBDAELIAIgAi8BMEEIcjsBMCAAIQELQTkhAwxcCyACQQA6ACwLQTQhAwxaCyABIARGBEBBLSEDDHMLAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0EtIQMMdAsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIARQ0CIAJBLDYCHCACIAE2AhQgAiAANgIMDHMLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABECoiAEUEQCABQQFqIQEMAgsgAkEsNgIcIAIgADYCDCACIAFBAWo2AhQMcgsgAS0AAEENRgRAIAIoAgQhAEEAIQMgAkEANgIEIAIgACABECoiAEUEQCABQQFqIQEMAgsgAkEsNgIcIAIgADYCDCACIAFBAWo2AhQMcgsgAi0ALUEBcQRAQcQBIQMMWQsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIADQEMZQtBLyEDDFcLIAJBLjYCHCACIAE2AhQgAiAANgIMDG8LQQAhAyACQQA2AhwgAiABNgIUIAJB8BQ2AhAgAkEDNgIMDG4LQQEhAwJAAkACQAJAIAItACxBBWsOBAMBAgAECyACIAIvATBBCHI7ATAMAwtBAiEDDAELQQQhAwsgAkEBOgAsIAIgAi8BMCADcjsBMAtBKiEDDFMLQQAhAyACQQA2AhwgAiABNgIUIAJB4Q82AhAgAkEKNgIMDGsLQQEhAwJAAkACQAJAAkACQCACLQAsQQJrDgcFBAQDAQIABAsgAiACLwEwQQhyOwEwDAMLQQIhAwwBC0EEIQMLIAJBAToALCACIAIvATAgA3I7ATALQSshAwxSC0EAIQMgAkEANgIcIAIgATYCFCACQasSNgIQIAJBCzYCDAxqC0EAIQMgAkEANgIcIAIgATYCFCACQf0NNgIQIAJBHTYCDAxpCyABIARHBEADQCABLQAAQSBHDUggBCABQQFqIgFHDQALQSUhAwxpC0ElIQMMaAsgAi0ALUEBcQRAQcMBIQMMTwsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKSIABEAgAkEmNgIcIAIgADYCDCACIAFBAWo2AhQMaAsgAUEBaiEBDFwLIAFBAWohASACLwEwIgBBgAFxBEBBACEAAkAgAigCOCIDRQ0AIAMoAlQiA0UNACACIAMRAAAhAAsgAEUNBiAAQRVHDR8gAkEFNgIcIAIgATYCFCACQfkXNgIQIAJBFTYCDEEAIQMMZwsCQCAAQaAEcUGgBEcNACACLQAtQQJxDQBBACEDIAJBADYCHCACIAE2AhQgAkGWEzYCECACQQQ2AgwMZwsgAgJ/IAIvATBBFHFBFEYEQEEBIAItAChBAUYNARogAi8BMkHlAEYMAQsgAi0AKUEFRgs6AC5BACEAAkAgAigCOCIDRQ0AIAMoAiQiA0UNACACIAMRAAAhAAsCQAJAAkACQAJAIAAOFgIBAAQEBAQEBAQEBAQEBAQEBAQEBAMECyACQQE6AC4LIAIgAi8BMEHAAHI7ATALQSchAwxPCyACQSM2AhwgAiABNgIUIAJBpRY2AhAgAkEVNgIMQQAhAwxnC0EAIQMgAkEANgIcIAIgATYCFCACQdULNgIQIAJBETYCDAxmC0EAIQACQCACKAI4IgNFDQAgAygCLCIDRQ0AIAIgAxEAACEACyAADQELQQ4hAwxLCyAAQRVGBEAgAkECNgIcIAIgATYCFCACQbAYNgIQIAJBFTYCDEEAIQMMZAtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMYwtBACEDIAJBADYCHCACIAE2AhQgAkGqHDYCECACQQ82AgwMYgsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEgCqdqIgEQKyIARQ0AIAJBBTYCHCACIAE2AhQgAiAANgIMDGELQQ8hAwxHC0EAIQMgAkEANgIcIAIgATYCFCACQc0TNgIQIAJBDDYCDAxfC0IBIQoLIAFBAWohAQJAIAIpAyAiC0L//////////w9YBEAgAiALQgSGIAqENwMgDAELQQAhAyACQQA2AhwgAiABNgIUIAJBrQk2AhAgAkEMNgIMDF4LQSQhAwxEC0EAIQMgAkEANgIcIAIgATYCFCACQc0TNgIQIAJBDDYCDAxcCyACKAIEIQBBACEDIAJBADYCBCACIAAgARAsIgBFBEAgAUEBaiEBDFILIAJBFzYCHCACIAA2AgwgAiABQQFqNgIUDFsLIAIoAgQhAEEAIQMgAkEANgIEAkAgAiAAIAEQLCIARQRAIAFBAWohAQwBCyACQRY2AhwgAiAANgIMIAIgAUEBajYCFAxbC0EfIQMMQQtBACEDIAJBADYCHCACIAE2AhQgAkGaDzYCECACQSI2AgwMWQsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQLSIARQRAIAFBAWohAQxQCyACQRQ2AhwgAiAANgIMIAIgAUEBajYCFAxYCyACKAIEIQBBACEDIAJBADYCBAJAIAIgACABEC0iAEUEQCABQQFqIQEMAQsgAkETNgIcIAIgADYCDCACIAFBAWo2AhQMWAtBHiEDDD4LQQAhAyACQQA2AhwgAiABNgIUIAJBxgw2AhAgAkEjNgIMDFYLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABEC0iAEUEQCABQQFqIQEMTgsgAkERNgIcIAIgADYCDCACIAFBAWo2AhQMVQsgAkEQNgIcIAIgATYCFCACIAA2AgwMVAtBACEDIAJBADYCHCACIAE2AhQgAkHGDDYCECACQSM2AgwMUwtBACEDIAJBADYCHCACIAE2AhQgAkHAFTYCECACQQI2AgwMUgsgAigCBCEAQQAhAyACQQA2AgQCQCACIAAgARAtIgBFBEAgAUEBaiEBDAELIAJBDjYCHCACIAA2AgwgAiABQQFqNgIUDFILQRshAww4C0EAIQMgAkEANgIcIAIgATYCFCACQcYMNgIQIAJBIzYCDAxQCyACKAIEIQBBACEDIAJBADYCBAJAIAIgACABECwiAEUEQCABQQFqIQEMAQsgAkENNgIcIAIgADYCDCACIAFBAWo2AhQMUAtBGiEDDDYLQQAhAyACQQA2AhwgAiABNgIUIAJBmg82AhAgAkEiNgIMDE4LIAIoAgQhAEEAIQMgAkEANgIEAkAgAiAAIAEQLCIARQRAIAFBAWohAQwBCyACQQw2AhwgAiAANgIMIAIgAUEBajYCFAxOC0EZIQMMNAtBACEDIAJBADYCHCACIAE2AhQgAkGaDzYCECACQSI2AgwMTAsgAEEVRwRAQQAhAyACQQA2AhwgAiABNgIUIAJBgww2AhAgAkETNgIMDEwLIAJBCjYCHCACIAE2AhQgAkHkFjYCECACQRU2AgxBACEDDEsLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABIAqnaiIBECsiAARAIAJBBzYCHCACIAE2AhQgAiAANgIMDEsLQRMhAwwxCyAAQRVHBEBBACEDIAJBADYCHCACIAE2AhQgAkHaDTYCECACQRQ2AgwMSgsgAkEeNgIcIAIgATYCFCACQfkXNgIQIAJBFTYCDEEAIQMMSQtBACEAAkAgAigCOCIDRQ0AIAMoAiwiA0UNACACIAMRAAAhAAsgAEUNQSAAQRVGBEAgAkEDNgIcIAIgATYCFCACQbAYNgIQIAJBFTYCDEEAIQMMSQtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMSAtBACEDIAJBADYCHCACIAE2AhQgAkHaDTYCECACQRQ2AgwMRwtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMRgsgAkEAOgAvIAItAC1BBHFFDT8LIAJBADoALyACQQE6ADRBACEDDCsLQQAhAyACQQA2AhwgAkHkETYCECACQQc2AgwgAiABQQFqNgIUDEMLAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB3QEhAwxDCwJAAkAgAi0ANEEBRw0AQQAhAAJAIAIoAjgiA0UNACADKAJYIgNFDQAgAiADEQAAIQALIABFDQAgAEEVRw0BIAJB3AE2AhwgAiABNgIUIAJB1RY2AhAgAkEVNgIMQQAhAwxEC0HBASEDDCoLIAJBADYCHCACIAE2AhQgAkHpCzYCECACQR82AgxBACEDDEILAkACQCACLQAoQQFrDgIEAQALQcABIQMMKQtBuQEhAwwoCyACQQI6AC9BACEAAkAgAigCOCIDRQ0AIAMoAgAiA0UNACACIAMRAAAhAAsgAEUEQEHCASEDDCgLIABBFUcEQCACQQA2AhwgAiABNgIUIAJBpAw2AhAgAkEQNgIMQQAhAwxBCyACQdsBNgIcIAIgATYCFCACQfoWNgIQIAJBFTYCDEEAIQMMQAsgASAERgRAQdoBIQMMQAsgAS0AAEHIAEYNASACQQE6ACgLQawBIQMMJQtBvwEhAwwkCyABIARHBEAgAkEQNgIIIAIgATYCBEG+ASEDDCQLQdkBIQMMPAsgASAERgRAQdgBIQMMPAsgAS0AAEHIAEcNBCABQQFqIQFBvQEhAwwiCyABIARGBEBB1wEhAww7CwJAAkAgAS0AAEHFAGsOEAAFBQUFBQUFBQUFBQUFBQEFCyABQQFqIQFBuwEhAwwiCyABQQFqIQFBvAEhAwwhC0HWASEDIAEgBEYNOSACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGD0ABqLQAARw0DIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAw6CyACKAIEIQAgAkIANwMAIAIgACAGQQFqIgEQJyIARQRAQcYBIQMMIQsgAkHVATYCHCACIAE2AhQgAiAANgIMQQAhAww5C0HUASEDIAEgBEYNOCACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGB0ABqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAw5CyACQYEEOwEoIAIoAgQhACACQgA3AwAgAiAAIAZBAWoiARAnIgANAwwCCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJB2Bs2AhAgAkEINgIMDDYLQboBIQMMHAsgAkHTATYCHCACIAE2AhQgAiAANgIMQQAhAww0C0EAIQACQCACKAI4IgNFDQAgAygCOCIDRQ0AIAIgAxEAACEACyAARQ0AIABBFUYNASACQQA2AhwgAiABNgIUIAJBzA42AhAgAkEgNgIMQQAhAwwzC0HkACEDDBkLIAJB+AA2AhwgAiABNgIUIAJByhg2AhAgAkEVNgIMQQAhAwwxC0HSASEDIAQgASIARg0wIAQgAWsgAigCACIBaiEFIAAgAWtBBGohBgJAA0AgAC0AACABQfzPAGotAABHDQEgAUEERg0DIAFBAWohASAEIABBAWoiAEcNAAsgAiAFNgIADDELIAJBADYCHCACIAA2AhQgAkGQMzYCECACQQg2AgwgAkEANgIAQQAhAwwwCyABIARHBEAgAkEONgIIIAIgATYCBEG3ASEDDBcLQdEBIQMMLwsgAkEANgIAIAZBAWohAQtBuAEhAwwUCyABIARGBEBB0AEhAwwtCyABLQAAQTBrIgBB/wFxQQpJBEAgAiAAOgAqIAFBAWohAUG2ASEDDBQLIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ0UIAJBzwE2AhwgAiABNgIUIAIgADYCDEEAIQMMLAsgASAERgRAQc4BIQMMLAsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ0VIAJBzQE2AhwgAiABNgIUIAIgADYCDEEAIQMMLAtBtQEhAwwSCyAEIAEiBUYEQEHMASEDDCsLQQAhAEEBIQFBASEGQQAhAwJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAUtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyEDQQAhAUEAIQYMAgtBCSEDQQEhAEEAIQFBACEGDAELQQAhAUEBIQMLIAIgAzoAKyAFQQFqIQMCQAJAIAItAC1BEHENAAJAAkACQCACLQAqDgMBAAIECyAGRQ0DDAILIAANAQwCCyABRQ0BCyACKAIEIQAgAkEANgIEIAIgACADECgiAEUEQCADIQEMAwsgAkHJATYCHCACIAM2AhQgAiAANgIMQQAhAwwtCyACKAIEIQAgAkEANgIEIAIgACADECgiAEUEQCADIQEMGAsgAkHKATYCHCACIAM2AhQgAiAANgIMQQAhAwwsCyACKAIEIQAgAkEANgIEIAIgACAFECgiAEUEQCAFIQEMFgsgAkHLATYCHCACIAU2AhQgAiAANgIMDCsLQbQBIQMMEQtBACEAAkAgAigCOCIDRQ0AIAMoAjwiA0UNACACIAMRAAAhAAsCQCAABEAgAEEVRg0BIAJBADYCHCACIAE2AhQgAkGUDTYCECACQSE2AgxBACEDDCsLQbIBIQMMEQsgAkHIATYCHCACIAE2AhQgAkHJFzYCECACQRU2AgxBACEDDCkLIAJBADYCACAGQQFqIQFB9QAhAwwPCyACLQApQQVGBEBB4wAhAwwPC0HiACEDDA4LIAAhASACQQA2AgALIAJBADoALEEJIQMMDAsgAkEANgIAIAdBAWohAUHAACEDDAsLQQELOgAsIAJBADYCACAGQQFqIQELQSkhAwwIC0E4IQMMBwsCQCABIARHBEADQCABLQAAQYA+ai0AACIAQQFHBEAgAEECRw0DIAFBAWohAQwFCyAEIAFBAWoiAUcNAAtBPiEDDCELQT4hAwwgCwsgAkEAOgAsDAELQQshAwwEC0E6IQMMAwsgAUEBaiEBQS0hAwwCCyACIAE6ACwgAkEANgIAIAZBAWohAUEMIQMMAQsgAkEANgIAIAZBAWohAUEKIQMMAAsAC0EAIQMgAkEANgIcIAIgATYCFCACQc0QNgIQIAJBCTYCDAwXC0EAIQMgAkEANgIcIAIgATYCFCACQekKNgIQIAJBCTYCDAwWC0EAIQMgAkEANgIcIAIgATYCFCACQbcQNgIQIAJBCTYCDAwVC0EAIQMgAkEANgIcIAIgATYCFCACQZwRNgIQIAJBCTYCDAwUC0EAIQMgAkEANgIcIAIgATYCFCACQc0QNgIQIAJBCTYCDAwTC0EAIQMgAkEANgIcIAIgATYCFCACQekKNgIQIAJBCTYCDAwSC0EAIQMgAkEANgIcIAIgATYCFCACQbcQNgIQIAJBCTYCDAwRC0EAIQMgAkEANgIcIAIgATYCFCACQZwRNgIQIAJBCTYCDAwQC0EAIQMgAkEANgIcIAIgATYCFCACQZcVNgIQIAJBDzYCDAwPC0EAIQMgAkEANgIcIAIgATYCFCACQZcVNgIQIAJBDzYCDAwOC0EAIQMgAkEANgIcIAIgATYCFCACQcASNgIQIAJBCzYCDAwNC0EAIQMgAkEANgIcIAIgATYCFCACQZUJNgIQIAJBCzYCDAwMC0EAIQMgAkEANgIcIAIgATYCFCACQeEPNgIQIAJBCjYCDAwLC0EAIQMgAkEANgIcIAIgATYCFCACQfsPNgIQIAJBCjYCDAwKC0EAIQMgAkEANgIcIAIgATYCFCACQfEZNgIQIAJBAjYCDAwJC0EAIQMgAkEANgIcIAIgATYCFCACQcQUNgIQIAJBAjYCDAwIC0EAIQMgAkEANgIcIAIgATYCFCACQfIVNgIQIAJBAjYCDAwHCyACQQI2AhwgAiABNgIUIAJBnBo2AhAgAkEWNgIMQQAhAwwGC0EBIQMMBQtB1AAhAyABIARGDQQgCEEIaiEJIAIoAgAhBQJAAkAgASAERwRAIAVB2MIAaiEHIAQgBWogAWshACAFQX9zQQpqIgUgAWohBgNAIAEtAAAgBy0AAEcEQEECIQcMAwsgBUUEQEEAIQcgBiEBDAMLIAVBAWshBSAHQQFqIQcgBCABQQFqIgFHDQALIAAhBSAEIQELIAlBATYCACACIAU2AgAMAQsgAkEANgIAIAkgBzYCAAsgCSABNgIEIAgoAgwhACAIKAIIDgMBBAIACwALIAJBADYCHCACQbUaNgIQIAJBFzYCDCACIABBAWo2AhRBACEDDAILIAJBADYCHCACIAA2AhQgAkHKGjYCECACQQk2AgxBACEDDAELIAEgBEYEQEEiIQMMAQsgAkEJNgIIIAIgATYCBEEhIQMLIAhBEGokACADRQRAIAIoAgwhAAwBCyACIAM2AhxBACEAIAIoAgQiAUUNACACIAEgBCACKAIIEQEAIgFFDQAgAiAENgIUIAIgATYCDCABIQALIAALvgIBAn8gAEEAOgAAIABB3ABqIgFBAWtBADoAACAAQQA6AAIgAEEAOgABIAFBA2tBADoAACABQQJrQQA6AAAgAEEAOgADIAFBBGtBADoAAEEAIABrQQNxIgEgAGoiAEEANgIAQdwAIAFrQXxxIgIgAGoiAUEEa0EANgIAAkAgAkEJSQ0AIABBADYCCCAAQQA2AgQgAUEIa0EANgIAIAFBDGtBADYCACACQRlJDQAgAEEANgIYIABBADYCFCAAQQA2AhAgAEEANgIMIAFBEGtBADYCACABQRRrQQA2AgAgAUEYa0EANgIAIAFBHGtBADYCACACIABBBHFBGHIiAmsiAUEgSQ0AIAAgAmohAANAIABCADcDGCAAQgA3AxAgAEIANwMIIABCADcDACAAQSBqIQAgAUEgayIBQR9LDQALCwtWAQF/AkAgACgCDA0AAkACQAJAAkAgAC0ALw4DAQADAgsgACgCOCIBRQ0AIAEoAiwiAUUNACAAIAERAAAiAQ0DC0EADwsACyAAQcMWNgIQQQ4hAQsgAQsaACAAKAIMRQRAIABB0Rs2AhAgAEEVNgIMCwsUACAAKAIMQRVGBEAgAEEANgIMCwsUACAAKAIMQRZGBEAgAEEANgIMCwsHACAAKAIMCwcAIAAoAhALCQAgACABNgIQCwcAIAAoAhQLFwAgAEEkTwRAAAsgAEECdEGgM2ooAgALFwAgAEEuTwRAAAsgAEECdEGwNGooAgALvwkBAX9B6yghAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB5ABrDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0HhJw8LQaQhDwtByywPC0H+MQ8LQcAkDwtBqyQPC0GNKA8LQeImDwtBgDAPC0G5Lw8LQdckDwtB7x8PC0HhHw8LQfofDwtB8iAPC0GoLw8LQa4yDwtBiDAPC0HsJw8LQYIiDwtBjh0PC0HQLg8LQcojDwtBxTIPC0HfHA8LQdIcDwtBxCAPC0HXIA8LQaIfDwtB7S4PC0GrMA8LQdQlDwtBzC4PC0H6Lg8LQfwrDwtB0jAPC0HxHQ8LQbsgDwtB9ysPC0GQMQ8LQdcxDwtBoi0PC0HUJw8LQeArDwtBnywPC0HrMQ8LQdUfDwtByjEPC0HeJQ8LQdQeDwtB9BwPC0GnMg8LQbEdDwtBoB0PC0G5MQ8LQbwwDwtBkiEPC0GzJg8LQeksDwtBrB4PC0HUKw8LQfcmDwtBgCYPC0GwIQ8LQf4eDwtBjSMPC0GJLQ8LQfciDwtBoDEPC0GuHw8LQcYlDwtB6B4PC0GTIg8LQcIvDwtBwx0PC0GLLA8LQeEdDwtBjS8PC0HqIQ8LQbQtDwtB0i8PC0HfMg8LQdIyDwtB8DAPC0GpIg8LQfkjDwtBmR4PC0G1LA8LQZswDwtBkjIPC0G2Kw8LQcIiDwtB+DIPC0GeJQ8LQdAiDwtBuh4PC0GBHg8LAAtB1iEhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCz4BAn8CQCAAKAI4IgNFDQAgAygCBCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBxhE2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCCCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB9go2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCDCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB7Ro2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCECIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBlRA2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCFCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBqhs2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCGCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB7RM2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCKCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB9gg2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCHCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBwhk2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCICIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBlBQ2AhBBGCEECyAEC1kBAn8CQCAALQAoQQFGDQAgAC8BMiIBQeQAa0HkAEkNACABQcwBRg0AIAFBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhAiAAQYgEcUGABEYNACAAQShxRSECCyACC4wBAQJ/AkACQAJAIAAtACpFDQAgAC0AK0UNACAALwEwIgFBAnFFDQEMAgsgAC8BMCIBQQFxRQ0BC0EBIQIgAC0AKEEBRg0AIAAvATIiAEHkAGtB5ABJDQAgAEHMAUYNACAAQbACRg0AIAFBwABxDQBBACECIAFBiARxQYAERg0AIAFBKHFBAEchAgsgAgtXACAAQRhqQgA3AwAgAEIANwMAIABBOGpCADcDACAAQTBqQgA3AwAgAEEoakIANwMAIABBIGpCADcDACAAQRBqQgA3AwAgAEEIakIANwMAIABB3QE2AhwLBgAgABAyC5otAQt/IwBBEGsiCiQAQaTQACgCACIJRQRAQeTTACgCACIFRQRAQfDTAEJ/NwIAQejTAEKAgISAgIDAADcCAEHk0wAgCkEIakFwcUHYqtWqBXMiBTYCAEH40wBBADYCAEHI0wBBADYCAAtBzNMAQYDUBDYCAEGc0ABBgNQENgIAQbDQACAFNgIAQazQAEF/NgIAQdDTAEGArAM2AgADQCABQcjQAGogAUG80ABqIgI2AgAgAiABQbTQAGoiAzYCACABQcDQAGogAzYCACABQdDQAGogAUHE0ABqIgM2AgAgAyACNgIAIAFB2NAAaiABQczQAGoiAjYCACACIAM2AgAgAUHU0ABqIAI2AgAgAUEgaiIBQYACRw0AC0GM1ARBwasDNgIAQajQAEH00wAoAgA2AgBBmNAAQcCrAzYCAEGk0ABBiNQENgIAQcz/B0E4NgIAQYjUBCEJCwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB7AFNBEBBjNAAKAIAIgZBECAAQRNqQXBxIABBC0kbIgRBA3YiAHYiAUEDcQRAAkAgAUEBcSAAckEBcyICQQN0IgBBtNAAaiIBIABBvNAAaigCACIAKAIIIgNGBEBBjNAAIAZBfiACd3E2AgAMAQsgASADNgIIIAMgATYCDAsgAEEIaiEBIAAgAkEDdCICQQNyNgIEIAAgAmoiACAAKAIEQQFyNgIEDBELQZTQACgCACIIIARPDQEgAQRAAkBBAiAAdCICQQAgAmtyIAEgAHRxaCIAQQN0IgJBtNAAaiIBIAJBvNAAaigCACICKAIIIgNGBEBBjNAAIAZBfiAAd3EiBjYCAAwBCyABIAM2AgggAyABNgIMCyACIARBA3I2AgQgAEEDdCIAIARrIQUgACACaiAFNgIAIAIgBGoiBCAFQQFyNgIEIAgEQCAIQXhxQbTQAGohAEGg0AAoAgAhAwJ/QQEgCEEDdnQiASAGcUUEQEGM0AAgASAGcjYCACAADAELIAAoAggLIgEgAzYCDCAAIAM2AgggAyAANgIMIAMgATYCCAsgAkEIaiEBQaDQACAENgIAQZTQACAFNgIADBELQZDQACgCACILRQ0BIAtoQQJ0QbzSAGooAgAiACgCBEF4cSAEayEFIAAhAgNAAkAgAigCECIBRQRAIAJBFGooAgAiAUUNAQsgASgCBEF4cSAEayIDIAVJIQIgAyAFIAIbIQUgASAAIAIbIQAgASECDAELCyAAKAIYIQkgACgCDCIDIABHBEBBnNAAKAIAGiADIAAoAggiATYCCCABIAM2AgwMEAsgAEEUaiICKAIAIgFFBEAgACgCECIBRQ0DIABBEGohAgsDQCACIQcgASIDQRRqIgIoAgAiAQ0AIANBEGohAiADKAIQIgENAAsgB0EANgIADA8LQX8hBCAAQb9/Sw0AIABBE2oiAUFwcSEEQZDQACgCACIIRQ0AQQAgBGshBQJAAkACQAJ/QQAgBEGAAkkNABpBHyAEQf///wdLDQAaIARBJiABQQh2ZyIAa3ZBAXEgAEEBdGtBPmoLIgZBAnRBvNIAaigCACICRQRAQQAhAUEAIQMMAQtBACEBIARBGSAGQQF2a0EAIAZBH0cbdCEAQQAhAwNAAkAgAigCBEF4cSAEayIHIAVPDQAgAiEDIAciBQ0AQQAhBSACIQEMAwsgASACQRRqKAIAIgcgByACIABBHXZBBHFqQRBqKAIAIgJGGyABIAcbIQEgAEEBdCEAIAINAAsLIAEgA3JFBEBBACEDQQIgBnQiAEEAIABrciAIcSIARQ0DIABoQQJ0QbzSAGooAgAhAQsgAUUNAQsDQCABKAIEQXhxIARrIgIgBUkhACACIAUgABshBSABIAMgABshAyABKAIQIgAEfyAABSABQRRqKAIACyIBDQALCyADRQ0AIAVBlNAAKAIAIARrTw0AIAMoAhghByADIAMoAgwiAEcEQEGc0AAoAgAaIAAgAygCCCIBNgIIIAEgADYCDAwOCyADQRRqIgIoAgAiAUUEQCADKAIQIgFFDQMgA0EQaiECCwNAIAIhBiABIgBBFGoiAigCACIBDQAgAEEQaiECIAAoAhAiAQ0ACyAGQQA2AgAMDQtBlNAAKAIAIgMgBE8EQEGg0AAoAgAhAQJAIAMgBGsiAkEQTwRAIAEgBGoiACACQQFyNgIEIAEgA2ogAjYCACABIARBA3I2AgQMAQsgASADQQNyNgIEIAEgA2oiACAAKAIEQQFyNgIEQQAhAEEAIQILQZTQACACNgIAQaDQACAANgIAIAFBCGohAQwPC0GY0AAoAgAiAyAESwRAIAQgCWoiACADIARrIgFBAXI2AgRBpNAAIAA2AgBBmNAAIAE2AgAgCSAEQQNyNgIEIAlBCGohAQwPC0EAIQEgBAJ/QeTTACgCAARAQezTACgCAAwBC0Hw0wBCfzcCAEHo0wBCgICEgICAwAA3AgBB5NMAIApBDGpBcHFB2KrVqgVzNgIAQfjTAEEANgIAQcjTAEEANgIAQYCABAsiACAEQccAaiIFaiIGQQAgAGsiB3EiAk8EQEH80wBBMDYCAAwPCwJAQcTTACgCACIBRQ0AQbzTACgCACIIIAJqIQAgACABTSAAIAhLcQ0AQQAhAUH80wBBMDYCAAwPC0HI0wAtAABBBHENBAJAAkAgCQRAQczTACEBA0AgASgCACIAIAlNBEAgACABKAIEaiAJSw0DCyABKAIIIgENAAsLQQAQMyIAQX9GDQUgAiEGQejTACgCACIBQQFrIgMgAHEEQCACIABrIAAgA2pBACABa3FqIQYLIAQgBk8NBSAGQf7///8HSw0FQcTTACgCACIDBEBBvNMAKAIAIgcgBmohASABIAdNDQYgASADSw0GCyAGEDMiASAARw0BDAcLIAYgA2sgB3EiBkH+////B0sNBCAGEDMhACAAIAEoAgAgASgCBGpGDQMgACEBCwJAIAYgBEHIAGpPDQAgAUF/Rg0AQezTACgCACIAIAUgBmtqQQAgAGtxIgBB/v///wdLBEAgASEADAcLIAAQM0F/RwRAIAAgBmohBiABIQAMBwtBACAGaxAzGgwECyABIgBBf0cNBQwDC0EAIQMMDAtBACEADAoLIABBf0cNAgtByNMAQcjTACgCAEEEcjYCAAsgAkH+////B0sNASACEDMhAEEAEDMhASAAQX9GDQEgAUF/Rg0BIAAgAU8NASABIABrIgYgBEE4ak0NAQtBvNMAQbzTACgCACAGaiIBNgIAQcDTACgCACABSQRAQcDTACABNgIACwJAAkACQEGk0AAoAgAiAgRAQczTACEBA0AgACABKAIAIgMgASgCBCIFakYNAiABKAIIIgENAAsMAgtBnNAAKAIAIgFBAEcgACABT3FFBEBBnNAAIAA2AgALQQAhAUHQ0wAgBjYCAEHM0wAgADYCAEGs0ABBfzYCAEGw0ABB5NMAKAIANgIAQdjTAEEANgIAA0AgAUHI0ABqIAFBvNAAaiICNgIAIAIgAUG00ABqIgM2AgAgAUHA0ABqIAM2AgAgAUHQ0ABqIAFBxNAAaiIDNgIAIAMgAjYCACABQdjQAGogAUHM0ABqIgI2AgAgAiADNgIAIAFB1NAAaiACNgIAIAFBIGoiAUGAAkcNAAtBeCAAa0EPcSIBIABqIgIgBkE4ayIDIAFrIgFBAXI2AgRBqNAAQfTTACgCADYCAEGY0AAgATYCAEGk0AAgAjYCACAAIANqQTg2AgQMAgsgACACTQ0AIAIgA0kNACABKAIMQQhxDQBBeCACa0EPcSIAIAJqIgNBmNAAKAIAIAZqIgcgAGsiAEEBcjYCBCABIAUgBmo2AgRBqNAAQfTTACgCADYCAEGY0AAgADYCAEGk0AAgAzYCACACIAdqQTg2AgQMAQsgAEGc0AAoAgBJBEBBnNAAIAA2AgALIAAgBmohA0HM0wAhAQJAAkACQANAIAMgASgCAEcEQCABKAIIIgENAQwCCwsgAS0ADEEIcUUNAQtBzNMAIQEDQCABKAIAIgMgAk0EQCADIAEoAgRqIgUgAksNAwsgASgCCCEBDAALAAsgASAANgIAIAEgASgCBCAGajYCBCAAQXggAGtBD3FqIgkgBEEDcjYCBCADQXggA2tBD3FqIgYgBCAJaiIEayEBIAIgBkYEQEGk0AAgBDYCAEGY0ABBmNAAKAIAIAFqIgA2AgAgBCAAQQFyNgIEDAgLQaDQACgCACAGRgRAQaDQACAENgIAQZTQAEGU0AAoAgAgAWoiADYCACAEIABBAXI2AgQgACAEaiAANgIADAgLIAYoAgQiBUEDcUEBRw0GIAVBeHEhCCAFQf8BTQRAIAVBA3YhAyAGKAIIIgAgBigCDCICRgRAQYzQAEGM0AAoAgBBfiADd3E2AgAMBwsgAiAANgIIIAAgAjYCDAwGCyAGKAIYIQcgBiAGKAIMIgBHBEAgACAGKAIIIgI2AgggAiAANgIMDAULIAZBFGoiAigCACIFRQRAIAYoAhAiBUUNBCAGQRBqIQILA0AgAiEDIAUiAEEUaiICKAIAIgUNACAAQRBqIQIgACgCECIFDQALIANBADYCAAwEC0F4IABrQQ9xIgEgAGoiByAGQThrIgMgAWsiAUEBcjYCBCAAIANqQTg2AgQgAiAFQTcgBWtBD3FqQT9rIgMgAyACQRBqSRsiA0EjNgIEQajQAEH00wAoAgA2AgBBmNAAIAE2AgBBpNAAIAc2AgAgA0EQakHU0wApAgA3AgAgA0HM0wApAgA3AghB1NMAIANBCGo2AgBB0NMAIAY2AgBBzNMAIAA2AgBB2NMAQQA2AgAgA0EkaiEBA0AgAUEHNgIAIAUgAUEEaiIBSw0ACyACIANGDQAgAyADKAIEQX5xNgIEIAMgAyACayIFNgIAIAIgBUEBcjYCBCAFQf8BTQRAIAVBeHFBtNAAaiEAAn9BjNAAKAIAIgFBASAFQQN2dCIDcUUEQEGM0AAgASADcjYCACAADAELIAAoAggLIgEgAjYCDCAAIAI2AgggAiAANgIMIAIgATYCCAwBC0EfIQEgBUH///8HTQRAIAVBJiAFQQh2ZyIAa3ZBAXEgAEEBdGtBPmohAQsgAiABNgIcIAJCADcCECABQQJ0QbzSAGohAEGQ0AAoAgAiA0EBIAF0IgZxRQRAIAAgAjYCAEGQ0AAgAyAGcjYCACACIAA2AhggAiACNgIIIAIgAjYCDAwBCyAFQRkgAUEBdmtBACABQR9HG3QhASAAKAIAIQMCQANAIAMiACgCBEF4cSAFRg0BIAFBHXYhAyABQQF0IQEgACADQQRxakEQaiIGKAIAIgMNAAsgBiACNgIAIAIgADYCGCACIAI2AgwgAiACNgIIDAELIAAoAggiASACNgIMIAAgAjYCCCACQQA2AhggAiAANgIMIAIgATYCCAtBmNAAKAIAIgEgBE0NAEGk0AAoAgAiACAEaiICIAEgBGsiAUEBcjYCBEGY0AAgATYCAEGk0AAgAjYCACAAIARBA3I2AgQgAEEIaiEBDAgLQQAhAUH80wBBMDYCAAwHC0EAIQALIAdFDQACQCAGKAIcIgJBAnRBvNIAaiIDKAIAIAZGBEAgAyAANgIAIAANAUGQ0ABBkNAAKAIAQX4gAndxNgIADAILIAdBEEEUIAcoAhAgBkYbaiAANgIAIABFDQELIAAgBzYCGCAGKAIQIgIEQCAAIAI2AhAgAiAANgIYCyAGQRRqKAIAIgJFDQAgAEEUaiACNgIAIAIgADYCGAsgASAIaiEBIAYgCGoiBigCBCEFCyAGIAVBfnE2AgQgASAEaiABNgIAIAQgAUEBcjYCBCABQf8BTQRAIAFBeHFBtNAAaiEAAn9BjNAAKAIAIgJBASABQQN2dCIBcUUEQEGM0AAgASACcjYCACAADAELIAAoAggLIgEgBDYCDCAAIAQ2AgggBCAANgIMIAQgATYCCAwBC0EfIQUgAUH///8HTQRAIAFBJiABQQh2ZyIAa3ZBAXEgAEEBdGtBPmohBQsgBCAFNgIcIARCADcCECAFQQJ0QbzSAGohAEGQ0AAoAgAiAkEBIAV0IgNxRQRAIAAgBDYCAEGQ0AAgAiADcjYCACAEIAA2AhggBCAENgIIIAQgBDYCDAwBCyABQRkgBUEBdmtBACAFQR9HG3QhBSAAKAIAIQACQANAIAAiAigCBEF4cSABRg0BIAVBHXYhACAFQQF0IQUgAiAAQQRxakEQaiIDKAIAIgANAAsgAyAENgIAIAQgAjYCGCAEIAQ2AgwgBCAENgIIDAELIAIoAggiACAENgIMIAIgBDYCCCAEQQA2AhggBCACNgIMIAQgADYCCAsgCUEIaiEBDAILAkAgB0UNAAJAIAMoAhwiAUECdEG80gBqIgIoAgAgA0YEQCACIAA2AgAgAA0BQZDQACAIQX4gAXdxIgg2AgAMAgsgB0EQQRQgBygCECADRhtqIAA2AgAgAEUNAQsgACAHNgIYIAMoAhAiAQRAIAAgATYCECABIAA2AhgLIANBFGooAgAiAUUNACAAQRRqIAE2AgAgASAANgIYCwJAIAVBD00EQCADIAQgBWoiAEEDcjYCBCAAIANqIgAgACgCBEEBcjYCBAwBCyADIARqIgIgBUEBcjYCBCADIARBA3I2AgQgAiAFaiAFNgIAIAVB/wFNBEAgBUF4cUG00ABqIQACf0GM0AAoAgAiAUEBIAVBA3Z0IgVxRQRAQYzQACABIAVyNgIAIAAMAQsgACgCCAsiASACNgIMIAAgAjYCCCACIAA2AgwgAiABNgIIDAELQR8hASAFQf///wdNBEAgBUEmIAVBCHZnIgBrdkEBcSAAQQF0a0E+aiEBCyACIAE2AhwgAkIANwIQIAFBAnRBvNIAaiEAQQEgAXQiBCAIcUUEQCAAIAI2AgBBkNAAIAQgCHI2AgAgAiAANgIYIAIgAjYCCCACIAI2AgwMAQsgBUEZIAFBAXZrQQAgAUEfRxt0IQEgACgCACEEAkADQCAEIgAoAgRBeHEgBUYNASABQR12IQQgAUEBdCEBIAAgBEEEcWpBEGoiBigCACIEDQALIAYgAjYCACACIAA2AhggAiACNgIMIAIgAjYCCAwBCyAAKAIIIgEgAjYCDCAAIAI2AgggAkEANgIYIAIgADYCDCACIAE2AggLIANBCGohAQwBCwJAIAlFDQACQCAAKAIcIgFBAnRBvNIAaiICKAIAIABGBEAgAiADNgIAIAMNAUGQ0AAgC0F+IAF3cTYCAAwCCyAJQRBBFCAJKAIQIABGG2ogAzYCACADRQ0BCyADIAk2AhggACgCECIBBEAgAyABNgIQIAEgAzYCGAsgAEEUaigCACIBRQ0AIANBFGogATYCACABIAM2AhgLAkAgBUEPTQRAIAAgBCAFaiIBQQNyNgIEIAAgAWoiASABKAIEQQFyNgIEDAELIAAgBGoiByAFQQFyNgIEIAAgBEEDcjYCBCAFIAdqIAU2AgAgCARAIAhBeHFBtNAAaiEBQaDQACgCACEDAn9BASAIQQN2dCICIAZxRQRAQYzQACACIAZyNgIAIAEMAQsgASgCCAsiAiADNgIMIAEgAzYCCCADIAE2AgwgAyACNgIIC0Gg0AAgBzYCAEGU0AAgBTYCAAsgAEEIaiEBCyAKQRBqJAAgAQtDACAARQRAPwBBEHQPCwJAIABB//8DcQ0AIABBAEgNACAAQRB2QAAiAEF/RgRAQfzTAEEwNgIAQX8PCyAAQRB0DwsACwvcPyIAQYAICwkBAAAAAgAAAAMAQZQICwUEAAAABQBBpAgLCQYAAAAHAAAACABB3AgLii1JbnZhbGlkIGNoYXIgaW4gdXJsIHF1ZXJ5AFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fYm9keQBDb250ZW50LUxlbmd0aCBvdmVyZmxvdwBDaHVuayBzaXplIG92ZXJmbG93AFJlc3BvbnNlIG92ZXJmbG93AEludmFsaWQgbWV0aG9kIGZvciBIVFRQL3gueCByZXF1ZXN0AEludmFsaWQgbWV0aG9kIGZvciBSVFNQL3gueCByZXF1ZXN0AEV4cGVjdGVkIFNPVVJDRSBtZXRob2QgZm9yIElDRS94LnggcmVxdWVzdABJbnZhbGlkIGNoYXIgaW4gdXJsIGZyYWdtZW50IHN0YXJ0AEV4cGVjdGVkIGRvdABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3N0YXR1cwBJbnZhbGlkIHJlc3BvbnNlIHN0YXR1cwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zAFVzZXIgY2FsbGJhY2sgZXJyb3IAYG9uX3Jlc2V0YCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfaGVhZGVyYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9iZWdpbmAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3N0YXR1c19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3ZlcnNpb25fY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl91cmxfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXRob2RfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfZmllbGRfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fbmFtZWAgY2FsbGJhY2sgZXJyb3IAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzZXJ2ZXIASW52YWxpZCBoZWFkZXIgdmFsdWUgY2hhcgBJbnZhbGlkIGhlYWRlciBmaWVsZCBjaGFyAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdmVyc2lvbgBJbnZhbGlkIG1pbm9yIHZlcnNpb24ASW52YWxpZCBtYWpvciB2ZXJzaW9uAEV4cGVjdGVkIHNwYWNlIGFmdGVyIHZlcnNpb24ARXhwZWN0ZWQgQ1JMRiBhZnRlciB2ZXJzaW9uAEludmFsaWQgSFRUUCB2ZXJzaW9uAEludmFsaWQgaGVhZGVyIHRva2VuAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fdXJsAEludmFsaWQgY2hhcmFjdGVycyBpbiB1cmwAVW5leHBlY3RlZCBzdGFydCBjaGFyIGluIHVybABEb3VibGUgQCBpbiB1cmwARW1wdHkgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyYWN0ZXIgaW4gQ29udGVudC1MZW5ndGgARHVwbGljYXRlIENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhciBpbiB1cmwgcGF0aABDb250ZW50LUxlbmd0aCBjYW4ndCBiZSBwcmVzZW50IHdpdGggVHJhbnNmZXItRW5jb2RpbmcASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgc2l6ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl92YWx1ZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHZhbHVlAE1pc3NpbmcgZXhwZWN0ZWQgTEYgYWZ0ZXIgaGVhZGVyIHZhbHVlAEludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYCBoZWFkZXIgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZSB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlZCB2YWx1ZQBQYXVzZWQgYnkgb25faGVhZGVyc19jb21wbGV0ZQBJbnZhbGlkIEVPRiBzdGF0ZQBvbl9yZXNldCBwYXVzZQBvbl9jaHVua19oZWFkZXIgcGF1c2UAb25fbWVzc2FnZV9iZWdpbiBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fdmFsdWUgcGF1c2UAb25fc3RhdHVzX2NvbXBsZXRlIHBhdXNlAG9uX3ZlcnNpb25fY29tcGxldGUgcGF1c2UAb25fdXJsX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl92YWx1ZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXNzYWdlX2NvbXBsZXRlIHBhdXNlAG9uX21ldGhvZF9jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfZmllbGRfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUgcGF1c2UAVW5leHBlY3RlZCBzcGFjZSBhZnRlciBzdGFydCBsaW5lAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX25hbWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBuYW1lAFBhdXNlIG9uIENPTk5FQ1QvVXBncmFkZQBQYXVzZSBvbiBQUkkvVXBncmFkZQBFeHBlY3RlZCBIVFRQLzIgQ29ubmVjdGlvbiBQcmVmYWNlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fbWV0aG9kAEV4cGVjdGVkIHNwYWNlIGFmdGVyIG1ldGhvZABTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2hlYWRlcl9maWVsZABQYXVzZWQASW52YWxpZCB3b3JkIGVuY291bnRlcmVkAEludmFsaWQgbWV0aG9kIGVuY291bnRlcmVkAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2NoZW1hAFJlcXVlc3QgaGFzIGludmFsaWQgYFRyYW5zZmVyLUVuY29kaW5nYABTV0lUQ0hfUFJPWFkAVVNFX1BST1hZAE1LQUNUSVZJVFkAVU5QUk9DRVNTQUJMRV9FTlRJVFkAQ09QWQBNT1ZFRF9QRVJNQU5FTlRMWQBUT09fRUFSTFkATk9USUZZAEZBSUxFRF9ERVBFTkRFTkNZAEJBRF9HQVRFV0FZAFBMQVkAUFVUAENIRUNLT1VUAEdBVEVXQVlfVElNRU9VVABSRVFVRVNUX1RJTUVPVVQATkVUV09SS19DT05ORUNUX1RJTUVPVVQAQ09OTkVDVElPTl9USU1FT1VUAExPR0lOX1RJTUVPVVQATkVUV09SS19SRUFEX1RJTUVPVVQAUE9TVABNSVNESVJFQ1RFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX0xPQURfQkFMQU5DRURfUkVRVUVTVABCQURfUkVRVUVTVABIVFRQX1JFUVVFU1RfU0VOVF9UT19IVFRQU19QT1JUAFJFUE9SVABJTV9BX1RFQVBPVABSRVNFVF9DT05URU5UAE5PX0NPTlRFTlQAUEFSVElBTF9DT05URU5UAEhQRV9JTlZBTElEX0NPTlNUQU5UAEhQRV9DQl9SRVNFVABHRVQASFBFX1NUUklDVABDT05GTElDVABURU1QT1JBUllfUkVESVJFQ1QAUEVSTUFORU5UX1JFRElSRUNUAENPTk5FQ1QATVVMVElfU1RBVFVTAEhQRV9JTlZBTElEX1NUQVRVUwBUT09fTUFOWV9SRVFVRVNUUwBFQVJMWV9ISU5UUwBVTkFWQUlMQUJMRV9GT1JfTEVHQUxfUkVBU09OUwBPUFRJT05TAFNXSVRDSElOR19QUk9UT0NPTFMAVkFSSUFOVF9BTFNPX05FR09USUFURVMATVVMVElQTEVfQ0hPSUNFUwBJTlRFUk5BTF9TRVJWRVJfRVJST1IAV0VCX1NFUlZFUl9VTktOT1dOX0VSUk9SAFJBSUxHVU5fRVJST1IASURFTlRJVFlfUFJPVklERVJfQVVUSEVOVElDQVRJT05fRVJST1IAU1NMX0NFUlRJRklDQVRFX0VSUk9SAElOVkFMSURfWF9GT1JXQVJERURfRk9SAFNFVF9QQVJBTUVURVIAR0VUX1BBUkFNRVRFUgBIUEVfVVNFUgBTRUVfT1RIRVIASFBFX0NCX0NIVU5LX0hFQURFUgBNS0NBTEVOREFSAFNFVFVQAFdFQl9TRVJWRVJfSVNfRE9XTgBURUFSRE9XTgBIUEVfQ0xPU0VEX0NPTk5FQ1RJT04ASEVVUklTVElDX0VYUElSQVRJT04ARElTQ09OTkVDVEVEX09QRVJBVElPTgBOT05fQVVUSE9SSVRBVElWRV9JTkZPUk1BVElPTgBIUEVfSU5WQUxJRF9WRVJTSU9OAEhQRV9DQl9NRVNTQUdFX0JFR0lOAFNJVEVfSVNfRlJPWkVOAEhQRV9JTlZBTElEX0hFQURFUl9UT0tFTgBJTlZBTElEX1RPS0VOAEZPUkJJRERFTgBFTkhBTkNFX1lPVVJfQ0FMTQBIUEVfSU5WQUxJRF9VUkwAQkxPQ0tFRF9CWV9QQVJFTlRBTF9DT05UUk9MAE1LQ09MAEFDTABIUEVfSU5URVJOQUwAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRV9VTk9GRklDSUFMAEhQRV9PSwBVTkxJTksAVU5MT0NLAFBSSQBSRVRSWV9XSVRIAEhQRV9JTlZBTElEX0NPTlRFTlRfTEVOR1RIAEhQRV9VTkVYUEVDVEVEX0NPTlRFTlRfTEVOR1RIAEZMVVNIAFBST1BQQVRDSABNLVNFQVJDSABVUklfVE9PX0xPTkcAUFJPQ0VTU0lORwBNSVNDRUxMQU5FT1VTX1BFUlNJU1RFTlRfV0FSTklORwBNSVNDRUxMQU5FT1VTX1dBUk5JTkcASFBFX0lOVkFMSURfVFJBTlNGRVJfRU5DT0RJTkcARXhwZWN0ZWQgQ1JMRgBIUEVfSU5WQUxJRF9DSFVOS19TSVpFAE1PVkUAQ09OVElOVUUASFBFX0NCX1NUQVRVU19DT01QTEVURQBIUEVfQ0JfSEVBREVSU19DT01QTEVURQBIUEVfQ0JfVkVSU0lPTl9DT01QTEVURQBIUEVfQ0JfVVJMX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19DT01QTEVURQBIUEVfQ0JfSEVBREVSX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9OQU1FX0NPTVBMRVRFAEhQRV9DQl9NRVNTQUdFX0NPTVBMRVRFAEhQRV9DQl9NRVRIT0RfQ09NUExFVEUASFBFX0NCX0hFQURFUl9GSUVMRF9DT01QTEVURQBERUxFVEUASFBFX0lOVkFMSURfRU9GX1NUQVRFAElOVkFMSURfU1NMX0NFUlRJRklDQVRFAFBBVVNFAE5PX1JFU1BPTlNFAFVOU1VQUE9SVEVEX01FRElBX1RZUEUAR09ORQBOT1RfQUNDRVBUQUJMRQBTRVJWSUNFX1VOQVZBSUxBQkxFAFJBTkdFX05PVF9TQVRJU0ZJQUJMRQBPUklHSU5fSVNfVU5SRUFDSEFCTEUAUkVTUE9OU0VfSVNfU1RBTEUAUFVSR0UATUVSR0UAUkVRVUVTVF9IRUFERVJfRklFTERTX1RPT19MQVJHRQBSRVFVRVNUX0hFQURFUl9UT09fTEFSR0UAUEFZTE9BRF9UT09fTEFSR0UASU5TVUZGSUNJRU5UX1NUT1JBR0UASFBFX1BBVVNFRF9VUEdSQURFAEhQRV9QQVVTRURfSDJfVVBHUkFERQBTT1VSQ0UAQU5OT1VOQ0UAVFJBQ0UASFBFX1VORVhQRUNURURfU1BBQ0UAREVTQ1JJQkUAVU5TVUJTQ1JJQkUAUkVDT1JEAEhQRV9JTlZBTElEX01FVEhPRABOT1RfRk9VTkQAUFJPUEZJTkQAVU5CSU5EAFJFQklORABVTkFVVEhPUklaRUQATUVUSE9EX05PVF9BTExPV0VEAEhUVFBfVkVSU0lPTl9OT1RfU1VQUE9SVEVEAEFMUkVBRFlfUkVQT1JURUQAQUNDRVBURUQATk9UX0lNUExFTUVOVEVEAExPT1BfREVURUNURUQASFBFX0NSX0VYUEVDVEVEAEhQRV9MRl9FWFBFQ1RFRABDUkVBVEVEAElNX1VTRUQASFBFX1BBVVNFRABUSU1FT1VUX09DQ1VSRUQAUEFZTUVOVF9SRVFVSVJFRABQUkVDT05ESVRJT05fUkVRVUlSRUQAUFJPWFlfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATkVUV09SS19BVVRIRU5USUNBVElPTl9SRVFVSVJFRABMRU5HVEhfUkVRVUlSRUQAU1NMX0NFUlRJRklDQVRFX1JFUVVJUkVEAFVQR1JBREVfUkVRVUlSRUQAUEFHRV9FWFBJUkVEAFBSRUNPTkRJVElPTl9GQUlMRUQARVhQRUNUQVRJT05fRkFJTEVEAFJFVkFMSURBVElPTl9GQUlMRUQAU1NMX0hBTkRTSEFLRV9GQUlMRUQATE9DS0VEAFRSQU5TRk9STUFUSU9OX0FQUExJRUQATk9UX01PRElGSUVEAE5PVF9FWFRFTkRFRABCQU5EV0lEVEhfTElNSVRfRVhDRUVERUQAU0lURV9JU19PVkVSTE9BREVEAEhFQUQARXhwZWN0ZWQgSFRUUC8AAF4TAAAmEwAAMBAAAPAXAACdEwAAFRIAADkXAADwEgAAChAAAHUSAACtEgAAghMAAE8UAAB/EAAAoBUAACMUAACJEgAAixQAAE0VAADUEQAAzxQAABAYAADJFgAA3BYAAMERAADgFwAAuxQAAHQUAAB8FQAA5RQAAAgXAAAfEAAAZRUAAKMUAAAoFQAAAhUAAJkVAAAsEAAAixkAAE8PAADUDgAAahAAAM4QAAACFwAAiQ4AAG4TAAAcEwAAZhQAAFYXAADBEwAAzRMAAGwTAABoFwAAZhcAAF8XAAAiEwAAzg8AAGkOAADYDgAAYxYAAMsTAACqDgAAKBcAACYXAADFEwAAXRYAAOgRAABnEwAAZRMAAPIWAABzEwAAHRcAAPkWAADzEQAAzw4AAM4VAAAMEgAAsxEAAKURAABhEAAAMhcAALsTAEH5NQsBAQBBkDYL4AEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBB/TcLAQEAQZE4C14CAwICAgICAAACAgACAgACAgICAgICAgICAAQAAAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAEH9OQsBAQBBkToLXgIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAQfA7Cw1sb3NlZWVwLWFsaXZlAEGJPAsBAQBBoDwL4AEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBBiT4LAQEAQaA+C+cBAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAEGwwAALXwEBAAEBAQEBAAABAQABAQABAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAEGQwgALIWVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgBBwMIACy1yYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AQfnCAAsFAQIAAQMAQZDDAAvgAQQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAEH5xAALBQECAAEDAEGQxQAL4AEEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBB+cYACwQBAAABAEGRxwAL3wEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAEH6yAALBAEAAAIAQZDJAAtfAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAQfrKAAsEAQAAAQBBkMsACwEBAEGqywALQQIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEH6zAALBAEAAAEAQZDNAAsBAQBBms0ACwYCAAAAAAIAQbHNAAs6AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBB8M4AC5YBTk9VTkNFRUNLT1VUTkVDVEVURUNSSUJFTFVTSEVURUFEU0VBUkNIUkdFQ1RJVklUWUxFTkRBUlZFT1RJRllQVElPTlNDSFNFQVlTVEFUQ0hHRU9SRElSRUNUT1JUUkNIUEFSQU1FVEVSVVJDRUJTQ1JJQkVBUkRPV05BQ0VJTkROS0NLVUJTQ1JJQkVIVFRQL0FEVFAv", "base64"); + } +}); + +// viewer/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +var require_llhttp_simd_wasm = __commonJS({ + "viewer/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js"(exports, module) { + "use strict"; + var { Buffer: Buffer2 } = __require("node:buffer"); + module.exports = Buffer2.from("AGFzbQEAAAABJwdgAX8Bf2ADf39/AX9gAX8AYAJ/fwBgBH9/f38Bf2AAAGADf39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQAEA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAAy0sBQYAAAIAAAAAAAACAQIAAgICAAADAAAAAAMDAwMBAQEBAQEBAQEAAAIAAAAEBQFwARISBQMBAAIGCAF/AUGA1AQLB9EFIgZtZW1vcnkCAAtfaW5pdGlhbGl6ZQAIGV9faW5kaXJlY3RfZnVuY3Rpb25fdGFibGUBAAtsbGh0dHBfaW5pdAAJGGxsaHR0cF9zaG91bGRfa2VlcF9hbGl2ZQAvDGxsaHR0cF9hbGxvYwALBm1hbGxvYwAxC2xsaHR0cF9mcmVlAAwEZnJlZQAMD2xsaHR0cF9nZXRfdHlwZQANFWxsaHR0cF9nZXRfaHR0cF9tYWpvcgAOFWxsaHR0cF9nZXRfaHR0cF9taW5vcgAPEWxsaHR0cF9nZXRfbWV0aG9kABAWbGxodHRwX2dldF9zdGF0dXNfY29kZQAREmxsaHR0cF9nZXRfdXBncmFkZQASDGxsaHR0cF9yZXNldAATDmxsaHR0cF9leGVjdXRlABQUbGxodHRwX3NldHRpbmdzX2luaXQAFQ1sbGh0dHBfZmluaXNoABYMbGxodHRwX3BhdXNlABcNbGxodHRwX3Jlc3VtZQAYG2xsaHR0cF9yZXN1bWVfYWZ0ZXJfdXBncmFkZQAZEGxsaHR0cF9nZXRfZXJybm8AGhdsbGh0dHBfZ2V0X2Vycm9yX3JlYXNvbgAbF2xsaHR0cF9zZXRfZXJyb3JfcmVhc29uABwUbGxodHRwX2dldF9lcnJvcl9wb3MAHRFsbGh0dHBfZXJybm9fbmFtZQAeEmxsaHR0cF9tZXRob2RfbmFtZQAfEmxsaHR0cF9zdGF0dXNfbmFtZQAgGmxsaHR0cF9zZXRfbGVuaWVudF9oZWFkZXJzACEhbGxodHRwX3NldF9sZW5pZW50X2NodW5rZWRfbGVuZ3RoACIdbGxodHRwX3NldF9sZW5pZW50X2tlZXBfYWxpdmUAIyRsbGh0dHBfc2V0X2xlbmllbnRfdHJhbnNmZXJfZW5jb2RpbmcAJBhsbGh0dHBfbWVzc2FnZV9uZWVkc19lb2YALgkXAQBBAQsRAQIDBAUKBgcrLSwqKSglJyYK77MCLBYAQYjQACgCAARAAAtBiNAAQQE2AgALFAAgABAwIAAgAjYCOCAAIAE6ACgLFAAgACAALwEyIAAtAC4gABAvEAALHgEBf0HAABAyIgEQMCABQYAINgI4IAEgADoAKCABC48MAQd/AkAgAEUNACAAQQhrIgEgAEEEaygCACIAQXhxIgRqIQUCQCAAQQFxDQAgAEEDcUUNASABIAEoAgAiAGsiAUGc0AAoAgBJDQEgACAEaiEEAkACQEGg0AAoAgAgAUcEQCAAQf8BTQRAIABBA3YhAyABKAIIIgAgASgCDCICRgRAQYzQAEGM0AAoAgBBfiADd3E2AgAMBQsgAiAANgIIIAAgAjYCDAwECyABKAIYIQYgASABKAIMIgBHBEAgACABKAIIIgI2AgggAiAANgIMDAMLIAFBFGoiAygCACICRQRAIAEoAhAiAkUNAiABQRBqIQMLA0AgAyEHIAIiAEEUaiIDKAIAIgINACAAQRBqIQMgACgCECICDQALIAdBADYCAAwCCyAFKAIEIgBBA3FBA0cNAiAFIABBfnE2AgRBlNAAIAQ2AgAgBSAENgIAIAEgBEEBcjYCBAwDC0EAIQALIAZFDQACQCABKAIcIgJBAnRBvNIAaiIDKAIAIAFGBEAgAyAANgIAIAANAUGQ0ABBkNAAKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgAUYbaiAANgIAIABFDQELIAAgBjYCGCABKAIQIgIEQCAAIAI2AhAgAiAANgIYCyABQRRqKAIAIgJFDQAgAEEUaiACNgIAIAIgADYCGAsgASAFTw0AIAUoAgQiAEEBcUUNAAJAAkACQAJAIABBAnFFBEBBpNAAKAIAIAVGBEBBpNAAIAE2AgBBmNAAQZjQACgCACAEaiIANgIAIAEgAEEBcjYCBCABQaDQACgCAEcNBkGU0ABBADYCAEGg0ABBADYCAAwGC0Gg0AAoAgAgBUYEQEGg0AAgATYCAEGU0ABBlNAAKAIAIARqIgA2AgAgASAAQQFyNgIEIAAgAWogADYCAAwGCyAAQXhxIARqIQQgAEH/AU0EQCAAQQN2IQMgBSgCCCIAIAUoAgwiAkYEQEGM0ABBjNAAKAIAQX4gA3dxNgIADAULIAIgADYCCCAAIAI2AgwMBAsgBSgCGCEGIAUgBSgCDCIARwRAQZzQACgCABogACAFKAIIIgI2AgggAiAANgIMDAMLIAVBFGoiAygCACICRQRAIAUoAhAiAkUNAiAFQRBqIQMLA0AgAyEHIAIiAEEUaiIDKAIAIgINACAAQRBqIQMgACgCECICDQALIAdBADYCAAwCCyAFIABBfnE2AgQgASAEaiAENgIAIAEgBEEBcjYCBAwDC0EAIQALIAZFDQACQCAFKAIcIgJBAnRBvNIAaiIDKAIAIAVGBEAgAyAANgIAIAANAUGQ0ABBkNAAKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgBUYbaiAANgIAIABFDQELIAAgBjYCGCAFKAIQIgIEQCAAIAI2AhAgAiAANgIYCyAFQRRqKAIAIgJFDQAgAEEUaiACNgIAIAIgADYCGAsgASAEaiAENgIAIAEgBEEBcjYCBCABQaDQACgCAEcNAEGU0AAgBDYCAAwBCyAEQf8BTQRAIARBeHFBtNAAaiEAAn9BjNAAKAIAIgJBASAEQQN2dCIDcUUEQEGM0AAgAiADcjYCACAADAELIAAoAggLIgIgATYCDCAAIAE2AgggASAANgIMIAEgAjYCCAwBC0EfIQIgBEH///8HTQRAIARBJiAEQQh2ZyIAa3ZBAXEgAEEBdGtBPmohAgsgASACNgIcIAFCADcCECACQQJ0QbzSAGohAAJAQZDQACgCACIDQQEgAnQiB3FFBEAgACABNgIAQZDQACADIAdyNgIAIAEgADYCGCABIAE2AgggASABNgIMDAELIARBGSACQQF2a0EAIAJBH0cbdCECIAAoAgAhAAJAA0AgACIDKAIEQXhxIARGDQEgAkEddiEAIAJBAXQhAiADIABBBHFqQRBqIgcoAgAiAA0ACyAHIAE2AgAgASADNgIYIAEgATYCDCABIAE2AggMAQsgAygCCCIAIAE2AgwgAyABNgIIIAFBADYCGCABIAM2AgwgASAANgIIC0Gs0ABBrNAAKAIAQQFrIgBBfyAAGzYCAAsLBwAgAC0AKAsHACAALQAqCwcAIAAtACsLBwAgAC0AKQsHACAALwEyCwcAIAAtAC4LQAEEfyAAKAIYIQEgAC0ALSECIAAtACghAyAAKAI4IQQgABAwIAAgBDYCOCAAIAM6ACggACACOgAtIAAgATYCGAu74gECB38DfiABIAJqIQQCQCAAIgIoAgwiAA0AIAIoAgQEQCACIAE2AgQLIwBBEGsiCCQAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAIoAhwiA0EBaw7dAdoBAdkBAgMEBQYHCAkKCwwNDtgBDxDXARES1gETFBUWFxgZGhvgAd8BHB0e1QEfICEiIyQl1AEmJygpKiss0wHSAS0u0QHQAS8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRtsBR0hJSs8BzgFLzQFMzAFNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AAYEBggGDAYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYAZkBmgGbAZwBnQGeAZ8BoAGhAaIBowGkAaUBpgGnAagBqQGqAasBrAGtAa4BrwGwAbEBsgGzAbQBtQG2AbcBywHKAbgByQG5AcgBugG7AbwBvQG+Ab8BwAHBAcIBwwHEAcUBxgEA3AELQQAMxgELQQ4MxQELQQ0MxAELQQ8MwwELQRAMwgELQRMMwQELQRQMwAELQRUMvwELQRYMvgELQRgMvQELQRkMvAELQRoMuwELQRsMugELQRwMuQELQR0MuAELQQgMtwELQR4MtgELQSAMtQELQR8MtAELQQcMswELQSEMsgELQSIMsQELQSMMsAELQSQMrwELQRIMrgELQREMrQELQSUMrAELQSYMqwELQScMqgELQSgMqQELQcMBDKgBC0EqDKcBC0ErDKYBC0EsDKUBC0EtDKQBC0EuDKMBC0EvDKIBC0HEAQyhAQtBMAygAQtBNAyfAQtBDAyeAQtBMQydAQtBMgycAQtBMwybAQtBOQyaAQtBNQyZAQtBxQEMmAELQQsMlwELQToMlgELQTYMlQELQQoMlAELQTcMkwELQTgMkgELQTwMkQELQTsMkAELQT0MjwELQQkMjgELQSkMjQELQT4MjAELQT8MiwELQcAADIoBC0HBAAyJAQtBwgAMiAELQcMADIcBC0HEAAyGAQtBxQAMhQELQcYADIQBC0EXDIMBC0HHAAyCAQtByAAMgQELQckADIABC0HKAAx/C0HLAAx+C0HNAAx9C0HMAAx8C0HOAAx7C0HPAAx6C0HQAAx5C0HRAAx4C0HSAAx3C0HTAAx2C0HUAAx1C0HWAAx0C0HVAAxzC0EGDHILQdcADHELQQUMcAtB2AAMbwtBBAxuC0HZAAxtC0HaAAxsC0HbAAxrC0HcAAxqC0EDDGkLQd0ADGgLQd4ADGcLQd8ADGYLQeEADGULQeAADGQLQeIADGMLQeMADGILQQIMYQtB5AAMYAtB5QAMXwtB5gAMXgtB5wAMXQtB6AAMXAtB6QAMWwtB6gAMWgtB6wAMWQtB7AAMWAtB7QAMVwtB7gAMVgtB7wAMVQtB8AAMVAtB8QAMUwtB8gAMUgtB8wAMUQtB9AAMUAtB9QAMTwtB9gAMTgtB9wAMTQtB+AAMTAtB+QAMSwtB+gAMSgtB+wAMSQtB/AAMSAtB/QAMRwtB/gAMRgtB/wAMRQtBgAEMRAtBgQEMQwtBggEMQgtBgwEMQQtBhAEMQAtBhQEMPwtBhgEMPgtBhwEMPQtBiAEMPAtBiQEMOwtBigEMOgtBiwEMOQtBjAEMOAtBjQEMNwtBjgEMNgtBjwEMNQtBkAEMNAtBkQEMMwtBkgEMMgtBkwEMMQtBlAEMMAtBlQEMLwtBlgEMLgtBlwEMLQtBmAEMLAtBmQEMKwtBmgEMKgtBmwEMKQtBnAEMKAtBnQEMJwtBngEMJgtBnwEMJQtBoAEMJAtBoQEMIwtBogEMIgtBowEMIQtBpAEMIAtBpQEMHwtBpgEMHgtBpwEMHQtBqAEMHAtBqQEMGwtBqgEMGgtBqwEMGQtBrAEMGAtBrQEMFwtBrgEMFgtBAQwVC0GvAQwUC0GwAQwTC0GxAQwSC0GzAQwRC0GyAQwQC0G0AQwPC0G1AQwOC0G2AQwNC0G3AQwMC0G4AQwLC0G5AQwKC0G6AQwJC0G7AQwIC0HGAQwHC0G8AQwGC0G9AQwFC0G+AQwEC0G/AQwDC0HAAQwCC0HCAQwBC0HBAQshAwNAAkACQAJAAkACQAJAAkACQAJAIAICfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAgJ/AkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACfwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACfwJAAkACQAJAAn8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCADDsYBAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHyAhIyUmKCorLC8wMTIzNDU2Nzk6Ozw9lANAQkRFRklLTk9QUVJTVFVWWFpbXF1eX2BhYmNkZWZnaGpsb3Bxc3V2eHl6e3x/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQGWAZcBmAGZAZoBmwGcAZ0BngGfAaABoQGiAaMBpAGlAaYBpwGoAakBqgGrAawBrQGuAa8BsAGxAbIBswG0AbUBtgG3AbgBuQG6AbsBvAG9Ab4BvwHAAcEBwgHDAcQBxQHGAccByAHJAcsBzAHNAc4BzwGKA4kDiAOHA4QDgwOAA/sC+gL5AvgC9wL0AvMC8gLLAsECsALZAQsgASAERw3wAkHdASEDDLMDCyABIARHDcgBQcMBIQMMsgMLIAEgBEcNe0H3ACEDDLEDCyABIARHDXBB7wAhAwywAwsgASAERw1pQeoAIQMMrwMLIAEgBEcNZUHoACEDDK4DCyABIARHDWJB5gAhAwytAwsgASAERw0aQRghAwysAwsgASAERw0VQRIhAwyrAwsgASAERw1CQcUAIQMMqgMLIAEgBEcNNEE/IQMMqQMLIAEgBEcNMkE8IQMMqAMLIAEgBEcNK0ExIQMMpwMLIAItAC5BAUYNnwMMwQILQQAhAAJAAkACQCACLQAqRQ0AIAItACtFDQAgAi8BMCIDQQJxRQ0BDAILIAIvATAiA0EBcUUNAQtBASEAIAItAChBAUYNACACLwEyIgVB5ABrQeQASQ0AIAVBzAFGDQAgBUGwAkYNACADQcAAcQ0AQQAhACADQYgEcUGABEYNACADQShxQQBHIQALIAJBADsBMCACQQA6AC8gAEUN3wIgAkIANwMgDOACC0EAIQACQCACKAI4IgNFDQAgAygCLCIDRQ0AIAIgAxEAACEACyAARQ3MASAAQRVHDd0CIAJBBDYCHCACIAE2AhQgAkGwGDYCECACQRU2AgxBACEDDKQDCyABIARGBEBBBiEDDKQDCyABQQFqIQFBACEAAkAgAigCOCIDRQ0AIAMoAlQiA0UNACACIAMRAAAhAAsgAA3ZAgwcCyACQgA3AyBBEiEDDIkDCyABIARHDRZBHSEDDKEDCyABIARHBEAgAUEBaiEBQRAhAwyIAwtBByEDDKADCyACIAIpAyAiCiAEIAFrrSILfSIMQgAgCiAMWhs3AyAgCiALWA3UAkEIIQMMnwMLIAEgBEcEQCACQQk2AgggAiABNgIEQRQhAwyGAwtBCSEDDJ4DCyACKQMgQgBSDccBIAIgAi8BMEGAAXI7ATAMQgsgASAERw0/QdAAIQMMnAMLIAEgBEYEQEELIQMMnAMLIAFBAWohAUEAIQACQCACKAI4IgNFDQAgAygCUCIDRQ0AIAIgAxEAACEACyAADc8CDMYBC0EAIQACQCACKAI4IgNFDQAgAygCSCIDRQ0AIAIgAxEAACEACyAARQ3GASAAQRVHDc0CIAJBCzYCHCACIAE2AhQgAkGCGTYCECACQRU2AgxBACEDDJoDC0EAIQACQCACKAI4IgNFDQAgAygCSCIDRQ0AIAIgAxEAACEACyAARQ0MIABBFUcNygIgAkEaNgIcIAIgATYCFCACQYIZNgIQIAJBFTYCDEEAIQMMmQMLQQAhAAJAIAIoAjgiA0UNACADKAJMIgNFDQAgAiADEQAAIQALIABFDcQBIABBFUcNxwIgAkELNgIcIAIgATYCFCACQZEXNgIQIAJBFTYCDEEAIQMMmAMLIAEgBEYEQEEPIQMMmAMLIAEtAAAiAEE7Rg0HIABBDUcNxAIgAUEBaiEBDMMBC0EAIQACQCACKAI4IgNFDQAgAygCTCIDRQ0AIAIgAxEAACEACyAARQ3DASAAQRVHDcICIAJBDzYCHCACIAE2AhQgAkGRFzYCECACQRU2AgxBACEDDJYDCwNAIAEtAABB8DVqLQAAIgBBAUcEQCAAQQJHDcECIAIoAgQhAEEAIQMgAkEANgIEIAIgACABQQFqIgEQLSIADcICDMUBCyAEIAFBAWoiAUcNAAtBEiEDDJUDC0EAIQACQCACKAI4IgNFDQAgAygCTCIDRQ0AIAIgAxEAACEACyAARQ3FASAAQRVHDb0CIAJBGzYCHCACIAE2AhQgAkGRFzYCECACQRU2AgxBACEDDJQDCyABIARGBEBBFiEDDJQDCyACQQo2AgggAiABNgIEQQAhAAJAIAIoAjgiA0UNACADKAJIIgNFDQAgAiADEQAAIQALIABFDcIBIABBFUcNuQIgAkEVNgIcIAIgATYCFCACQYIZNgIQIAJBFTYCDEEAIQMMkwMLIAEgBEcEQANAIAEtAABB8DdqLQAAIgBBAkcEQAJAIABBAWsOBMQCvQIAvgK9AgsgAUEBaiEBQQghAwz8AgsgBCABQQFqIgFHDQALQRUhAwyTAwtBFSEDDJIDCwNAIAEtAABB8DlqLQAAIgBBAkcEQCAAQQFrDgTFArcCwwK4ArcCCyAEIAFBAWoiAUcNAAtBGCEDDJEDCyABIARHBEAgAkELNgIIIAIgATYCBEEHIQMM+AILQRkhAwyQAwsgAUEBaiEBDAILIAEgBEYEQEEaIQMMjwMLAkAgAS0AAEENaw4UtQG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwG/Ab8BvwEAvwELQQAhAyACQQA2AhwgAkGvCzYCECACQQI2AgwgAiABQQFqNgIUDI4DCyABIARGBEBBGyEDDI4DCyABLQAAIgBBO0cEQCAAQQ1HDbECIAFBAWohAQy6AQsgAUEBaiEBC0EiIQMM8wILIAEgBEYEQEEcIQMMjAMLQgAhCgJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgAS0AAEEwaw43wQLAAgABAgMEBQYH0AHQAdAB0AHQAdAB0AEICQoLDA3QAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdABDg8QERIT0AELQgIhCgzAAgtCAyEKDL8CC0IEIQoMvgILQgUhCgy9AgtCBiEKDLwCC0IHIQoMuwILQgghCgy6AgtCCSEKDLkCC0IKIQoMuAILQgshCgy3AgtCDCEKDLYCC0INIQoMtQILQg4hCgy0AgtCDyEKDLMCC0IKIQoMsgILQgshCgyxAgtCDCEKDLACC0INIQoMrwILQg4hCgyuAgtCDyEKDK0CC0IAIQoCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIAEtAABBMGsON8ACvwIAAQIDBAUGB74CvgK+Ar4CvgK+Ar4CCAkKCwwNvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ar4CvgK+Ag4PEBESE74CC0ICIQoMvwILQgMhCgy+AgtCBCEKDL0CC0IFIQoMvAILQgYhCgy7AgtCByEKDLoCC0IIIQoMuQILQgkhCgy4AgtCCiEKDLcCC0ILIQoMtgILQgwhCgy1AgtCDSEKDLQCC0IOIQoMswILQg8hCgyyAgtCCiEKDLECC0ILIQoMsAILQgwhCgyvAgtCDSEKDK4CC0IOIQoMrQILQg8hCgysAgsgAiACKQMgIgogBCABa60iC30iDEIAIAogDFobNwMgIAogC1gNpwJBHyEDDIkDCyABIARHBEAgAkEJNgIIIAIgATYCBEElIQMM8AILQSAhAwyIAwtBASEFIAIvATAiA0EIcUUEQCACKQMgQgBSIQULAkAgAi0ALgRAQQEhACACLQApQQVGDQEgA0HAAHFFIAVxRQ0BC0EAIQAgA0HAAHENAEECIQAgA0EIcQ0AIANBgARxBEACQCACLQAoQQFHDQAgAi0ALUEKcQ0AQQUhAAwCC0EEIQAMAQsgA0EgcUUEQAJAIAItAChBAUYNACACLwEyIgBB5ABrQeQASQ0AIABBzAFGDQAgAEGwAkYNAEEEIQAgA0EocUUNAiADQYgEcUGABEYNAgtBACEADAELQQBBAyACKQMgUBshAAsgAEEBaw4FvgIAsAEBpAKhAgtBESEDDO0CCyACQQE6AC8MhAMLIAEgBEcNnQJBJCEDDIQDCyABIARHDRxBxgAhAwyDAwtBACEAAkAgAigCOCIDRQ0AIAMoAkQiA0UNACACIAMRAAAhAAsgAEUNJyAAQRVHDZgCIAJB0AA2AhwgAiABNgIUIAJBkRg2AhAgAkEVNgIMQQAhAwyCAwsgASAERgRAQSghAwyCAwtBACEDIAJBADYCBCACQQw2AgggAiABIAEQKiIARQ2UAiACQSc2AhwgAiABNgIUIAIgADYCDAyBAwsgASAERgRAQSkhAwyBAwsgAS0AACIAQSBGDRMgAEEJRw2VAiABQQFqIQEMFAsgASAERwRAIAFBAWohAQwWC0EqIQMM/wILIAEgBEYEQEErIQMM/wILIAEtAAAiAEEJRyAAQSBHcQ2QAiACLQAsQQhHDd0CIAJBADoALAzdAgsgASAERgRAQSwhAwz+AgsgAS0AAEEKRw2OAiABQQFqIQEMsAELIAEgBEcNigJBLyEDDPwCCwNAIAEtAAAiAEEgRwRAIABBCmsOBIQCiAKIAoQChgILIAQgAUEBaiIBRw0AC0ExIQMM+wILQTIhAyABIARGDfoCIAIoAgAiACAEIAFraiEHIAEgAGtBA2ohBgJAA0AgAEHwO2otAAAgAS0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDQEgAEEDRgRAQQYhAQziAgsgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAc2AgAM+wILIAJBADYCAAyGAgtBMyEDIAQgASIARg35AiAEIAFrIAIoAgAiAWohByAAIAFrQQhqIQYCQANAIAFB9DtqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBCEYEQEEFIQEM4QILIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADPoCCyACQQA2AgAgACEBDIUCC0E0IQMgBCABIgBGDfgCIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgJAA0AgAUHQwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw0BIAFBBUYEQEEHIQEM4AILIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADPkCCyACQQA2AgAgACEBDIQCCyABIARHBEADQCABLQAAQYA+ai0AACIAQQFHBEAgAEECRg0JDIECCyAEIAFBAWoiAUcNAAtBMCEDDPgCC0EwIQMM9wILIAEgBEcEQANAIAEtAAAiAEEgRwRAIABBCmsOBP8B/gH+Af8B/gELIAQgAUEBaiIBRw0AC0E4IQMM9wILQTghAwz2AgsDQCABLQAAIgBBIEcgAEEJR3EN9gEgBCABQQFqIgFHDQALQTwhAwz1AgsDQCABLQAAIgBBIEcEQAJAIABBCmsOBPkBBAT5AQALIABBLEYN9QEMAwsgBCABQQFqIgFHDQALQT8hAwz0AgtBwAAhAyABIARGDfMCIAIoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAEGAQGstAAAgAS0AAEEgckcNASAAQQZGDdsCIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPQCCyACQQA2AgALQTYhAwzZAgsgASAERgRAQcEAIQMM8gILIAJBDDYCCCACIAE2AgQgAi0ALEEBaw4E+wHuAewB6wHUAgsgAUEBaiEBDPoBCyABIARHBEADQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxIgBBCUYNACAAQSBGDQACQAJAAkACQCAAQeMAaw4TAAMDAwMDAwMBAwMDAwMDAwMDAgMLIAFBAWohAUExIQMM3AILIAFBAWohAUEyIQMM2wILIAFBAWohAUEzIQMM2gILDP4BCyAEIAFBAWoiAUcNAAtBNSEDDPACC0E1IQMM7wILIAEgBEcEQANAIAEtAABBgDxqLQAAQQFHDfcBIAQgAUEBaiIBRw0AC0E9IQMM7wILQT0hAwzuAgtBACEAAkAgAigCOCIDRQ0AIAMoAkAiA0UNACACIAMRAAAhAAsgAEUNASAAQRVHDeYBIAJBwgA2AhwgAiABNgIUIAJB4xg2AhAgAkEVNgIMQQAhAwztAgsgAUEBaiEBC0E8IQMM0gILIAEgBEYEQEHCACEDDOsCCwJAA0ACQCABLQAAQQlrDhgAAswCzALRAswCzALMAswCzALMAswCzALMAswCzALMAswCzALMAswCzALMAgDMAgsgBCABQQFqIgFHDQALQcIAIQMM6wILIAFBAWohASACLQAtQQFxRQ3+AQtBLCEDDNACCyABIARHDd4BQcQAIQMM6AILA0AgAS0AAEGQwABqLQAAQQFHDZwBIAQgAUEBaiIBRw0AC0HFACEDDOcCCyABLQAAIgBBIEYN/gEgAEE6Rw3AAiACKAIEIQBBACEDIAJBADYCBCACIAAgARApIgAN3gEM3QELQccAIQMgBCABIgBGDeUCIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgNAIAFBkMIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNvwIgAUEFRg3CAiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBzYCAAzlAgtByAAhAyAEIAEiAEYN5AIgBCABayACKAIAIgFqIQcgACABa0EJaiEGA0AgAUGWwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw2+AkECIAFBCUYNwgIaIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADOQCCyABIARGBEBByQAhAwzkAgsCQAJAIAEtAAAiAEEgciAAIABBwQBrQf8BcUEaSRtB/wFxQe4Aaw4HAL8CvwK/Ar8CvwIBvwILIAFBAWohAUE+IQMMywILIAFBAWohAUE/IQMMygILQcoAIQMgBCABIgBGDeICIAQgAWsgAigCACIBaiEGIAAgAWtBAWohBwNAIAFBoMIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNvAIgAUEBRg2+AiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBjYCAAziAgtBywAhAyAEIAEiAEYN4QIgBCABayACKAIAIgFqIQcgACABa0EOaiEGA0AgAUGiwgBqLQAAIAAtAAAiBUEgciAFIAVBwQBrQf8BcUEaSRtB/wFxRw27AiABQQ5GDb4CIAFBAWohASAEIABBAWoiAEcNAAsgAiAHNgIADOECC0HMACEDIAQgASIARg3gAiAEIAFrIAIoAgAiAWohByAAIAFrQQ9qIQYDQCABQcDCAGotAAAgAC0AACIFQSByIAUgBUHBAGtB/wFxQRpJG0H/AXFHDboCQQMgAUEPRg2+AhogAUEBaiEBIAQgAEEBaiIARw0ACyACIAc2AgAM4AILQc0AIQMgBCABIgBGDd8CIAQgAWsgAigCACIBaiEHIAAgAWtBBWohBgNAIAFB0MIAai0AACAALQAAIgVBIHIgBSAFQcEAa0H/AXFBGkkbQf8BcUcNuQJBBCABQQVGDb0CGiABQQFqIQEgBCAAQQFqIgBHDQALIAIgBzYCAAzfAgsgASAERgRAQc4AIQMM3wILAkACQAJAAkAgAS0AACIAQSByIAAgAEHBAGtB/wFxQRpJG0H/AXFB4wBrDhMAvAK8ArwCvAK8ArwCvAK8ArwCvAK8ArwCAbwCvAK8AgIDvAILIAFBAWohAUHBACEDDMgCCyABQQFqIQFBwgAhAwzHAgsgAUEBaiEBQcMAIQMMxgILIAFBAWohAUHEACEDDMUCCyABIARHBEAgAkENNgIIIAIgATYCBEHFACEDDMUCC0HPACEDDN0CCwJAAkAgAS0AAEEKaw4EAZABkAEAkAELIAFBAWohAQtBKCEDDMMCCyABIARGBEBB0QAhAwzcAgsgAS0AAEEgRw0AIAFBAWohASACLQAtQQFxRQ3QAQtBFyEDDMECCyABIARHDcsBQdIAIQMM2QILQdMAIQMgASAERg3YAiACKAIAIgAgBCABa2ohBiABIABrQQFqIQUDQCABLQAAIABB1sIAai0AAEcNxwEgAEEBRg3KASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBjYCAAzYAgsgASAERgRAQdUAIQMM2AILIAEtAABBCkcNwgEgAUEBaiEBDMoBCyABIARGBEBB1gAhAwzXAgsCQAJAIAEtAABBCmsOBADDAcMBAcMBCyABQQFqIQEMygELIAFBAWohAUHKACEDDL0CC0EAIQACQCACKAI4IgNFDQAgAygCPCIDRQ0AIAIgAxEAACEACyAADb8BQc0AIQMMvAILIAItAClBIkYNzwIMiQELIAQgASIFRgRAQdsAIQMM1AILQQAhAEEBIQFBASEGQQAhAwJAAn8CQAJAAkACQAJAAkACQCAFLQAAQTBrDgrFAcQBAAECAwQFBgjDAQtBAgwGC0EDDAULQQQMBAtBBQwDC0EGDAILQQcMAQtBCAshA0EAIQFBACEGDL0BC0EJIQNBASEAQQAhAUEAIQYMvAELIAEgBEYEQEHdACEDDNMCCyABLQAAQS5HDbgBIAFBAWohAQyIAQsgASAERw22AUHfACEDDNECCyABIARHBEAgAkEONgIIIAIgATYCBEHQACEDDLgCC0HgACEDDNACC0HhACEDIAEgBEYNzwIgAigCACIAIAQgAWtqIQUgASAAa0EDaiEGA0AgAS0AACAAQeLCAGotAABHDbEBIABBA0YNswEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMzwILQeIAIQMgASAERg3OAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYDQCABLQAAIABB5sIAai0AAEcNsAEgAEECRg2vASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAzOAgtB4wAhAyABIARGDc0CIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgNAIAEtAAAgAEHpwgBqLQAARw2vASAAQQNGDa0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADM0CCyABIARGBEBB5QAhAwzNAgsgAUEBaiEBQQAhAAJAIAIoAjgiA0UNACADKAIwIgNFDQAgAiADEQAAIQALIAANqgFB1gAhAwyzAgsgASAERwRAA0AgAS0AACIAQSBHBEACQAJAAkAgAEHIAGsOCwABswGzAbMBswGzAbMBswGzAQKzAQsgAUEBaiEBQdIAIQMMtwILIAFBAWohAUHTACEDDLYCCyABQQFqIQFB1AAhAwy1AgsgBCABQQFqIgFHDQALQeQAIQMMzAILQeQAIQMMywILA0AgAS0AAEHwwgBqLQAAIgBBAUcEQCAAQQJrDgOnAaYBpQGkAQsgBCABQQFqIgFHDQALQeYAIQMMygILIAFBAWogASAERw0CGkHnACEDDMkCCwNAIAEtAABB8MQAai0AACIAQQFHBEACQCAAQQJrDgSiAaEBoAEAnwELQdcAIQMMsQILIAQgAUEBaiIBRw0AC0HoACEDDMgCCyABIARGBEBB6QAhAwzIAgsCQCABLQAAIgBBCmsOGrcBmwGbAbQBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBmwGbAZsBpAGbAZsBAJkBCyABQQFqCyEBQQYhAwytAgsDQCABLQAAQfDGAGotAABBAUcNfSAEIAFBAWoiAUcNAAtB6gAhAwzFAgsgAUEBaiABIARHDQIaQesAIQMMxAILIAEgBEYEQEHsACEDDMQCCyABQQFqDAELIAEgBEYEQEHtACEDDMMCCyABQQFqCyEBQQQhAwyoAgsgASAERgRAQe4AIQMMwQILAkACQAJAIAEtAABB8MgAai0AAEEBaw4HkAGPAY4BAHwBAo0BCyABQQFqIQEMCwsgAUEBagyTAQtBACEDIAJBADYCHCACQZsSNgIQIAJBBzYCDCACIAFBAWo2AhQMwAILAkADQCABLQAAQfDIAGotAAAiAEEERwRAAkACQCAAQQFrDgeUAZMBkgGNAQAEAY0BC0HaACEDDKoCCyABQQFqIQFB3AAhAwypAgsgBCABQQFqIgFHDQALQe8AIQMMwAILIAFBAWoMkQELIAQgASIARgRAQfAAIQMMvwILIAAtAABBL0cNASAAQQFqIQEMBwsgBCABIgBGBEBB8QAhAwy+AgsgAC0AACIBQS9GBEAgAEEBaiEBQd0AIQMMpQILIAFBCmsiA0EWSw0AIAAhAUEBIAN0QYmAgAJxDfkBC0EAIQMgAkEANgIcIAIgADYCFCACQYwcNgIQIAJBBzYCDAy8AgsgASAERwRAIAFBAWohAUHeACEDDKMCC0HyACEDDLsCCyABIARGBEBB9AAhAwy7AgsCQCABLQAAQfDMAGotAABBAWsOA/cBcwCCAQtB4QAhAwyhAgsgASAERwRAA0AgAS0AAEHwygBqLQAAIgBBA0cEQAJAIABBAWsOAvkBAIUBC0HfACEDDKMCCyAEIAFBAWoiAUcNAAtB8wAhAwy6AgtB8wAhAwy5AgsgASAERwRAIAJBDzYCCCACIAE2AgRB4AAhAwygAgtB9QAhAwy4AgsgASAERgRAQfYAIQMMuAILIAJBDzYCCCACIAE2AgQLQQMhAwydAgsDQCABLQAAQSBHDY4CIAQgAUEBaiIBRw0AC0H3ACEDDLUCCyABIARGBEBB+AAhAwy1AgsgAS0AAEEgRw16IAFBAWohAQxbC0EAIQACQCACKAI4IgNFDQAgAygCOCIDRQ0AIAIgAxEAACEACyAADXgMgAILIAEgBEYEQEH6ACEDDLMCCyABLQAAQcwARw10IAFBAWohAUETDHYLQfsAIQMgASAERg2xAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYDQCABLQAAIABB8M4Aai0AAEcNcyAAQQVGDXUgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMsQILIAEgBEYEQEH8ACEDDLECCwJAAkAgAS0AAEHDAGsODAB0dHR0dHR0dHR0AXQLIAFBAWohAUHmACEDDJgCCyABQQFqIQFB5wAhAwyXAgtB/QAhAyABIARGDa8CIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQe3PAGotAABHDXIgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADLACCyACQQA2AgAgBkEBaiEBQRAMcwtB/gAhAyABIARGDa4CIAIoAgAiACAEIAFraiEFIAEgAGtBBWohBgJAA0AgAS0AACAAQfbOAGotAABHDXEgAEEFRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADK8CCyACQQA2AgAgBkEBaiEBQRYMcgtB/wAhAyABIARGDa0CIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQfzOAGotAABHDXAgAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADK4CCyACQQA2AgAgBkEBaiEBQQUMcQsgASAERgRAQYABIQMMrQILIAEtAABB2QBHDW4gAUEBaiEBQQgMcAsgASAERgRAQYEBIQMMrAILAkACQCABLQAAQc4Aaw4DAG8BbwsgAUEBaiEBQesAIQMMkwILIAFBAWohAUHsACEDDJICCyABIARGBEBBggEhAwyrAgsCQAJAIAEtAABByABrDggAbm5ubm5uAW4LIAFBAWohAUHqACEDDJICCyABQQFqIQFB7QAhAwyRAgtBgwEhAyABIARGDakCIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQYDPAGotAABHDWwgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADKoCCyACQQA2AgAgBkEBaiEBQQAMbQtBhAEhAyABIARGDagCIAIoAgAiACAEIAFraiEFIAEgAGtBBGohBgJAA0AgAS0AACAAQYPPAGotAABHDWsgAEEERg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADKkCCyACQQA2AgAgBkEBaiEBQSMMbAsgASAERgRAQYUBIQMMqAILAkACQCABLQAAQcwAaw4IAGtra2trawFrCyABQQFqIQFB7wAhAwyPAgsgAUEBaiEBQfAAIQMMjgILIAEgBEYEQEGGASEDDKcCCyABLQAAQcUARw1oIAFBAWohAQxgC0GHASEDIAEgBEYNpQIgAigCACIAIAQgAWtqIQUgASAAa0EDaiEGAkADQCABLQAAIABBiM8Aai0AAEcNaCAAQQNGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMpgILIAJBADYCACAGQQFqIQFBLQxpC0GIASEDIAEgBEYNpAIgAigCACIAIAQgAWtqIQUgASAAa0EIaiEGAkADQCABLQAAIABB0M8Aai0AAEcNZyAAQQhGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMpQILIAJBADYCACAGQQFqIQFBKQxoCyABIARGBEBBiQEhAwykAgtBASABLQAAQd8ARw1nGiABQQFqIQEMXgtBigEhAyABIARGDaICIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgNAIAEtAAAgAEGMzwBqLQAARw1kIABBAUYN+gEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMogILQYsBIQMgASAERg2hAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGOzwBqLQAARw1kIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyiAgsgAkEANgIAIAZBAWohAUECDGULQYwBIQMgASAERg2gAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHwzwBqLQAARw1jIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyhAgsgAkEANgIAIAZBAWohAUEfDGQLQY0BIQMgASAERg2fAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHyzwBqLQAARw1iIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAygAgsgAkEANgIAIAZBAWohAUEJDGMLIAEgBEYEQEGOASEDDJ8CCwJAAkAgAS0AAEHJAGsOBwBiYmJiYgFiCyABQQFqIQFB+AAhAwyGAgsgAUEBaiEBQfkAIQMMhQILQY8BIQMgASAERg2dAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGRzwBqLQAARw1gIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyeAgsgAkEANgIAIAZBAWohAUEYDGELQZABIQMgASAERg2cAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGXzwBqLQAARw1fIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAydAgsgAkEANgIAIAZBAWohAUEXDGALQZEBIQMgASAERg2bAiACKAIAIgAgBCABa2ohBSABIABrQQZqIQYCQANAIAEtAAAgAEGazwBqLQAARw1eIABBBkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAycAgsgAkEANgIAIAZBAWohAUEVDF8LQZIBIQMgASAERg2aAiACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEGhzwBqLQAARw1dIABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAybAgsgAkEANgIAIAZBAWohAUEeDF4LIAEgBEYEQEGTASEDDJoCCyABLQAAQcwARw1bIAFBAWohAUEKDF0LIAEgBEYEQEGUASEDDJkCCwJAAkAgAS0AAEHBAGsODwBcXFxcXFxcXFxcXFxcAVwLIAFBAWohAUH+ACEDDIACCyABQQFqIQFB/wAhAwz/AQsgASAERgRAQZUBIQMMmAILAkACQCABLQAAQcEAaw4DAFsBWwsgAUEBaiEBQf0AIQMM/wELIAFBAWohAUGAASEDDP4BC0GWASEDIAEgBEYNlgIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBp88Aai0AAEcNWSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlwILIAJBADYCACAGQQFqIQFBCwxaCyABIARGBEBBlwEhAwyWAgsCQAJAAkACQCABLQAAQS1rDiMAW1tbW1tbW1tbW1tbW1tbW1tbW1tbW1sBW1tbW1sCW1tbA1sLIAFBAWohAUH7ACEDDP8BCyABQQFqIQFB/AAhAwz+AQsgAUEBaiEBQYEBIQMM/QELIAFBAWohAUGCASEDDPwBC0GYASEDIAEgBEYNlAIgAigCACIAIAQgAWtqIQUgASAAa0EEaiEGAkADQCABLQAAIABBqc8Aai0AAEcNVyAAQQRGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlQILIAJBADYCACAGQQFqIQFBGQxYC0GZASEDIAEgBEYNkwIgAigCACIAIAQgAWtqIQUgASAAa0EFaiEGAkADQCABLQAAIABBrs8Aai0AAEcNViAAQQVGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMlAILIAJBADYCACAGQQFqIQFBBgxXC0GaASEDIAEgBEYNkgIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBtM8Aai0AAEcNVSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMkwILIAJBADYCACAGQQFqIQFBHAxWC0GbASEDIAEgBEYNkQIgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABBts8Aai0AAEcNVCAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAMkgILIAJBADYCACAGQQFqIQFBJwxVCyABIARGBEBBnAEhAwyRAgsCQAJAIAEtAABB1ABrDgIAAVQLIAFBAWohAUGGASEDDPgBCyABQQFqIQFBhwEhAwz3AQtBnQEhAyABIARGDY8CIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbjPAGotAABHDVIgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADJACCyACQQA2AgAgBkEBaiEBQSYMUwtBngEhAyABIARGDY4CIAIoAgAiACAEIAFraiEFIAEgAGtBAWohBgJAA0AgAS0AACAAQbrPAGotAABHDVEgAEEBRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI8CCyACQQA2AgAgBkEBaiEBQQMMUgtBnwEhAyABIARGDY0CIAIoAgAiACAEIAFraiEFIAEgAGtBAmohBgJAA0AgAS0AACAAQe3PAGotAABHDVAgAEECRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI4CCyACQQA2AgAgBkEBaiEBQQwMUQtBoAEhAyABIARGDYwCIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQbzPAGotAABHDU8gAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADI0CCyACQQA2AgAgBkEBaiEBQQ0MUAsgASAERgRAQaEBIQMMjAILAkACQCABLQAAQcYAaw4LAE9PT09PT09PTwFPCyABQQFqIQFBiwEhAwzzAQsgAUEBaiEBQYwBIQMM8gELIAEgBEYEQEGiASEDDIsCCyABLQAAQdAARw1MIAFBAWohAQxGCyABIARGBEBBowEhAwyKAgsCQAJAIAEtAABByQBrDgcBTU1NTU0ATQsgAUEBaiEBQY4BIQMM8QELIAFBAWohAUEiDE0LQaQBIQMgASAERg2IAiACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEHAzwBqLQAARw1LIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyJAgsgAkEANgIAIAZBAWohAUEdDEwLIAEgBEYEQEGlASEDDIgCCwJAAkAgAS0AAEHSAGsOAwBLAUsLIAFBAWohAUGQASEDDO8BCyABQQFqIQFBBAxLCyABIARGBEBBpgEhAwyHAgsCQAJAAkACQAJAIAEtAABBwQBrDhUATU1NTU1NTU1NTQFNTQJNTQNNTQRNCyABQQFqIQFBiAEhAwzxAQsgAUEBaiEBQYkBIQMM8AELIAFBAWohAUGKASEDDO8BCyABQQFqIQFBjwEhAwzuAQsgAUEBaiEBQZEBIQMM7QELQacBIQMgASAERg2FAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHtzwBqLQAARw1IIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyGAgsgAkEANgIAIAZBAWohAUERDEkLQagBIQMgASAERg2EAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHCzwBqLQAARw1HIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyFAgsgAkEANgIAIAZBAWohAUEsDEgLQakBIQMgASAERg2DAiACKAIAIgAgBCABa2ohBSABIABrQQRqIQYCQANAIAEtAAAgAEHFzwBqLQAARw1GIABBBEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyEAgsgAkEANgIAIAZBAWohAUErDEcLQaoBIQMgASAERg2CAiACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHKzwBqLQAARw1FIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyDAgsgAkEANgIAIAZBAWohAUEUDEYLIAEgBEYEQEGrASEDDIICCwJAAkACQAJAIAEtAABBwgBrDg8AAQJHR0dHR0dHR0dHRwNHCyABQQFqIQFBkwEhAwzrAQsgAUEBaiEBQZQBIQMM6gELIAFBAWohAUGVASEDDOkBCyABQQFqIQFBlgEhAwzoAQsgASAERgRAQawBIQMMgQILIAEtAABBxQBHDUIgAUEBaiEBDD0LQa0BIQMgASAERg3/ASACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHNzwBqLQAARw1CIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAyAAgsgAkEANgIAIAZBAWohAUEODEMLIAEgBEYEQEGuASEDDP8BCyABLQAAQdAARw1AIAFBAWohAUElDEILQa8BIQMgASAERg39ASACKAIAIgAgBCABa2ohBSABIABrQQhqIQYCQANAIAEtAAAgAEHQzwBqLQAARw1AIABBCEYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz+AQsgAkEANgIAIAZBAWohAUEqDEELIAEgBEYEQEGwASEDDP0BCwJAAkAgAS0AAEHVAGsOCwBAQEBAQEBAQEABQAsgAUEBaiEBQZoBIQMM5AELIAFBAWohAUGbASEDDOMBCyABIARGBEBBsQEhAwz8AQsCQAJAIAEtAABBwQBrDhQAPz8/Pz8/Pz8/Pz8/Pz8/Pz8/AT8LIAFBAWohAUGZASEDDOMBCyABQQFqIQFBnAEhAwziAQtBsgEhAyABIARGDfoBIAIoAgAiACAEIAFraiEFIAEgAGtBA2ohBgJAA0AgAS0AACAAQdnPAGotAABHDT0gAEEDRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPsBCyACQQA2AgAgBkEBaiEBQSEMPgtBswEhAyABIARGDfkBIAIoAgAiACAEIAFraiEFIAEgAGtBBmohBgJAA0AgAS0AACAAQd3PAGotAABHDTwgAEEGRg0BIABBAWohACAEIAFBAWoiAUcNAAsgAiAFNgIADPoBCyACQQA2AgAgBkEBaiEBQRoMPQsgASAERgRAQbQBIQMM+QELAkACQAJAIAEtAABBxQBrDhEAPT09PT09PT09AT09PT09Aj0LIAFBAWohAUGdASEDDOEBCyABQQFqIQFBngEhAwzgAQsgAUEBaiEBQZ8BIQMM3wELQbUBIQMgASAERg33ASACKAIAIgAgBCABa2ohBSABIABrQQVqIQYCQANAIAEtAAAgAEHkzwBqLQAARw06IABBBUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz4AQsgAkEANgIAIAZBAWohAUEoDDsLQbYBIQMgASAERg32ASACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEHqzwBqLQAARw05IABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAz3AQsgAkEANgIAIAZBAWohAUEHDDoLIAEgBEYEQEG3ASEDDPYBCwJAAkAgAS0AAEHFAGsODgA5OTk5OTk5OTk5OTkBOQsgAUEBaiEBQaEBIQMM3QELIAFBAWohAUGiASEDDNwBC0G4ASEDIAEgBEYN9AEgAigCACIAIAQgAWtqIQUgASAAa0ECaiEGAkADQCABLQAAIABB7c8Aai0AAEcNNyAAQQJGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM9QELIAJBADYCACAGQQFqIQFBEgw4C0G5ASEDIAEgBEYN8wEgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8M8Aai0AAEcNNiAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM9AELIAJBADYCACAGQQFqIQFBIAw3C0G6ASEDIAEgBEYN8gEgAigCACIAIAQgAWtqIQUgASAAa0EBaiEGAkADQCABLQAAIABB8s8Aai0AAEcNNSAAQQFGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM8wELIAJBADYCACAGQQFqIQFBDww2CyABIARGBEBBuwEhAwzyAQsCQAJAIAEtAABByQBrDgcANTU1NTUBNQsgAUEBaiEBQaUBIQMM2QELIAFBAWohAUGmASEDDNgBC0G8ASEDIAEgBEYN8AEgAigCACIAIAQgAWtqIQUgASAAa0EHaiEGAkADQCABLQAAIABB9M8Aai0AAEcNMyAAQQdGDQEgAEEBaiEAIAQgAUEBaiIBRw0ACyACIAU2AgAM8QELIAJBADYCACAGQQFqIQFBGww0CyABIARGBEBBvQEhAwzwAQsCQAJAAkAgAS0AAEHCAGsOEgA0NDQ0NDQ0NDQBNDQ0NDQ0AjQLIAFBAWohAUGkASEDDNgBCyABQQFqIQFBpwEhAwzXAQsgAUEBaiEBQagBIQMM1gELIAEgBEYEQEG+ASEDDO8BCyABLQAAQc4ARw0wIAFBAWohAQwsCyABIARGBEBBvwEhAwzuAQsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCABLQAAQcEAaw4VAAECAz8EBQY/Pz8HCAkKCz8MDQ4PPwsgAUEBaiEBQegAIQMM4wELIAFBAWohAUHpACEDDOIBCyABQQFqIQFB7gAhAwzhAQsgAUEBaiEBQfIAIQMM4AELIAFBAWohAUHzACEDDN8BCyABQQFqIQFB9gAhAwzeAQsgAUEBaiEBQfcAIQMM3QELIAFBAWohAUH6ACEDDNwBCyABQQFqIQFBgwEhAwzbAQsgAUEBaiEBQYQBIQMM2gELIAFBAWohAUGFASEDDNkBCyABQQFqIQFBkgEhAwzYAQsgAUEBaiEBQZgBIQMM1wELIAFBAWohAUGgASEDDNYBCyABQQFqIQFBowEhAwzVAQsgAUEBaiEBQaoBIQMM1AELIAEgBEcEQCACQRA2AgggAiABNgIEQasBIQMM1AELQcABIQMM7AELQQAhAAJAIAIoAjgiA0UNACADKAI0IgNFDQAgAiADEQAAIQALIABFDV4gAEEVRw0HIAJB0QA2AhwgAiABNgIUIAJBsBc2AhAgAkEVNgIMQQAhAwzrAQsgAUEBaiABIARHDQgaQcIBIQMM6gELA0ACQCABLQAAQQprDgQIAAALAAsgBCABQQFqIgFHDQALQcMBIQMM6QELIAEgBEcEQCACQRE2AgggAiABNgIEQQEhAwzQAQtBxAEhAwzoAQsgASAERgRAQcUBIQMM6AELAkACQCABLQAAQQprDgQBKCgAKAsgAUEBagwJCyABQQFqDAULIAEgBEYEQEHGASEDDOcBCwJAAkAgAS0AAEEKaw4XAQsLAQsLCwsLCwsLCwsLCwsLCwsLCwALCyABQQFqIQELQbABIQMMzQELIAEgBEYEQEHIASEDDOYBCyABLQAAQSBHDQkgAkEAOwEyIAFBAWohAUGzASEDDMwBCwNAIAEhAAJAIAEgBEcEQCABLQAAQTBrQf8BcSIDQQpJDQEMJwtBxwEhAwzmAQsCQCACLwEyIgFBmTNLDQAgAiABQQpsIgU7ATIgBUH+/wNxIANB//8Dc0sNACAAQQFqIQEgAiADIAVqIgM7ATIgA0H//wNxQegHSQ0BCwtBACEDIAJBADYCHCACQcEJNgIQIAJBDTYCDCACIABBAWo2AhQM5AELIAJBADYCHCACIAE2AhQgAkHwDDYCECACQRs2AgxBACEDDOMBCyACKAIEIQAgAkEANgIEIAIgACABECYiAA0BIAFBAWoLIQFBrQEhAwzIAQsgAkHBATYCHCACIAA2AgwgAiABQQFqNgIUQQAhAwzgAQsgAigCBCEAIAJBADYCBCACIAAgARAmIgANASABQQFqCyEBQa4BIQMMxQELIAJBwgE2AhwgAiAANgIMIAIgAUEBajYCFEEAIQMM3QELIAJBADYCHCACIAE2AhQgAkGXCzYCECACQQ02AgxBACEDDNwBCyACQQA2AhwgAiABNgIUIAJB4xA2AhAgAkEJNgIMQQAhAwzbAQsgAkECOgAoDKwBC0EAIQMgAkEANgIcIAJBrws2AhAgAkECNgIMIAIgAUEBajYCFAzZAQtBAiEDDL8BC0ENIQMMvgELQSYhAwy9AQtBFSEDDLwBC0EWIQMMuwELQRghAwy6AQtBHCEDDLkBC0EdIQMMuAELQSAhAwy3AQtBISEDDLYBC0EjIQMMtQELQcYAIQMMtAELQS4hAwyzAQtBPSEDDLIBC0HLACEDDLEBC0HOACEDDLABC0HYACEDDK8BC0HZACEDDK4BC0HbACEDDK0BC0HxACEDDKwBC0H0ACEDDKsBC0GNASEDDKoBC0GXASEDDKkBC0GpASEDDKgBC0GvASEDDKcBC0GxASEDDKYBCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJB8Rs2AhAgAkEGNgIMDL0BCyACQQA2AgAgBkEBaiEBQSQLOgApIAIoAgQhACACQQA2AgQgAiAAIAEQJyIARQRAQeUAIQMMowELIAJB+QA2AhwgAiABNgIUIAIgADYCDEEAIQMMuwELIABBFUcEQCACQQA2AhwgAiABNgIUIAJBzA42AhAgAkEgNgIMQQAhAwy7AQsgAkH4ADYCHCACIAE2AhQgAkHKGDYCECACQRU2AgxBACEDDLoBCyACQQA2AhwgAiABNgIUIAJBjhs2AhAgAkEGNgIMQQAhAwy5AQsgAkEANgIcIAIgATYCFCACQf4RNgIQIAJBBzYCDEEAIQMMuAELIAJBADYCHCACIAE2AhQgAkGMHDYCECACQQc2AgxBACEDDLcBCyACQQA2AhwgAiABNgIUIAJBww82AhAgAkEHNgIMQQAhAwy2AQsgAkEANgIcIAIgATYCFCACQcMPNgIQIAJBBzYCDEEAIQMMtQELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0RIAJB5QA2AhwgAiABNgIUIAIgADYCDEEAIQMMtAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0gIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMswELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0iIAJB0gA2AhwgAiABNgIUIAIgADYCDEEAIQMMsgELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0OIAJB5QA2AhwgAiABNgIUIAIgADYCDEEAIQMMsQELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0dIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMsAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0fIAJB0gA2AhwgAiABNgIUIAIgADYCDEEAIQMMrwELIABBP0cNASABQQFqCyEBQQUhAwyUAQtBACEDIAJBADYCHCACIAE2AhQgAkH9EjYCECACQQc2AgwMrAELIAJBADYCHCACIAE2AhQgAkHcCDYCECACQQc2AgxBACEDDKsBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNByACQeUANgIcIAIgATYCFCACIAA2AgxBACEDDKoBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNFiACQdMANgIcIAIgATYCFCACIAA2AgxBACEDDKkBCyACKAIEIQAgAkEANgIEIAIgACABECUiAEUNGCACQdIANgIcIAIgATYCFCACIAA2AgxBACEDDKgBCyACQQA2AhwgAiABNgIUIAJBxgo2AhAgAkEHNgIMQQAhAwynAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDQMgAkHlADYCHCACIAE2AhQgAiAANgIMQQAhAwymAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDRIgAkHTADYCHCACIAE2AhQgAiAANgIMQQAhAwylAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDRQgAkHSADYCHCACIAE2AhQgAiAANgIMQQAhAwykAQsgAigCBCEAIAJBADYCBCACIAAgARAlIgBFDQAgAkHlADYCHCACIAE2AhQgAiAANgIMQQAhAwyjAQtB1QAhAwyJAQsgAEEVRwRAIAJBADYCHCACIAE2AhQgAkG5DTYCECACQRo2AgxBACEDDKIBCyACQeQANgIcIAIgATYCFCACQeMXNgIQIAJBFTYCDEEAIQMMoQELIAJBADYCACAGQQFqIQEgAi0AKSIAQSNrQQtJDQQCQCAAQQZLDQBBASAAdEHKAHFFDQAMBQtBACEDIAJBADYCHCACIAE2AhQgAkH3CTYCECACQQg2AgwMoAELIAJBADYCACAGQQFqIQEgAi0AKUEhRg0DIAJBADYCHCACIAE2AhQgAkGbCjYCECACQQg2AgxBACEDDJ8BCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJBkDM2AhAgAkEINgIMDJ0BCyACQQA2AgAgBkEBaiEBIAItAClBI0kNACACQQA2AhwgAiABNgIUIAJB0wk2AhAgAkEINgIMQQAhAwycAQtB0QAhAwyCAQsgAS0AAEEwayIAQf8BcUEKSQRAIAIgADoAKiABQQFqIQFBzwAhAwyCAQsgAigCBCEAIAJBADYCBCACIAAgARAoIgBFDYYBIAJB3gA2AhwgAiABNgIUIAIgADYCDEEAIQMMmgELIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ2GASACQdwANgIcIAIgATYCFCACIAA2AgxBACEDDJkBCyACKAIEIQAgAkEANgIEIAIgACAFECgiAEUEQCAFIQEMhwELIAJB2gA2AhwgAiAFNgIUIAIgADYCDAyYAQtBACEBQQEhAwsgAiADOgArIAVBAWohAwJAAkACQCACLQAtQRBxDQACQAJAAkAgAi0AKg4DAQACBAsgBkUNAwwCCyAADQEMAgsgAUUNAQsgAigCBCEAIAJBADYCBCACIAAgAxAoIgBFBEAgAyEBDAILIAJB2AA2AhwgAiADNgIUIAIgADYCDEEAIQMMmAELIAIoAgQhACACQQA2AgQgAiAAIAMQKCIARQRAIAMhAQyHAQsgAkHZADYCHCACIAM2AhQgAiAANgIMQQAhAwyXAQtBzAAhAwx9CyAAQRVHBEAgAkEANgIcIAIgATYCFCACQZQNNgIQIAJBITYCDEEAIQMMlgELIAJB1wA2AhwgAiABNgIUIAJByRc2AhAgAkEVNgIMQQAhAwyVAQtBACEDIAJBADYCHCACIAE2AhQgAkGAETYCECACQQk2AgwMlAELIAIoAgQhACACQQA2AgQgAiAAIAEQJSIARQ0AIAJB0wA2AhwgAiABNgIUIAIgADYCDEEAIQMMkwELQckAIQMMeQsgAkEANgIcIAIgATYCFCACQcEoNgIQIAJBBzYCDCACQQA2AgBBACEDDJEBCyACKAIEIQBBACEDIAJBADYCBCACIAAgARAlIgBFDQAgAkHSADYCHCACIAE2AhQgAiAANgIMDJABC0HIACEDDHYLIAJBADYCACAFIQELIAJBgBI7ASogAUEBaiEBQQAhAAJAIAIoAjgiA0UNACADKAIwIgNFDQAgAiADEQAAIQALIAANAQtBxwAhAwxzCyAAQRVGBEAgAkHRADYCHCACIAE2AhQgAkHjFzYCECACQRU2AgxBACEDDIwBC0EAIQMgAkEANgIcIAIgATYCFCACQbkNNgIQIAJBGjYCDAyLAQtBACEDIAJBADYCHCACIAE2AhQgAkGgGTYCECACQR42AgwMigELIAEtAABBOkYEQCACKAIEIQBBACEDIAJBADYCBCACIAAgARApIgBFDQEgAkHDADYCHCACIAA2AgwgAiABQQFqNgIUDIoBC0EAIQMgAkEANgIcIAIgATYCFCACQbERNgIQIAJBCjYCDAyJAQsgAUEBaiEBQTshAwxvCyACQcMANgIcIAIgADYCDCACIAFBAWo2AhQMhwELQQAhAyACQQA2AhwgAiABNgIUIAJB8A42AhAgAkEcNgIMDIYBCyACIAIvATBBEHI7ATAMZgsCQCACLwEwIgBBCHFFDQAgAi0AKEEBRw0AIAItAC1BCHFFDQMLIAIgAEH3+wNxQYAEcjsBMAwECyABIARHBEACQANAIAEtAABBMGsiAEH/AXFBCk8EQEE1IQMMbgsgAikDICIKQpmz5syZs+bMGVYNASACIApCCn4iCjcDICAKIACtQv8BgyILQn+FVg0BIAIgCiALfDcDICAEIAFBAWoiAUcNAAtBOSEDDIUBCyACKAIEIQBBACEDIAJBADYCBCACIAAgAUEBaiIBECoiAA0MDHcLQTkhAwyDAQsgAi0AMEEgcQ0GQcUBIQMMaQtBACEDIAJBADYCBCACIAEgARAqIgBFDQQgAkE6NgIcIAIgADYCDCACIAFBAWo2AhQMgQELIAItAChBAUcNACACLQAtQQhxRQ0BC0E3IQMMZgsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIABEAgAkE7NgIcIAIgADYCDCACIAFBAWo2AhQMfwsgAUEBaiEBDG4LIAJBCDoALAwECyABQQFqIQEMbQtBACEDIAJBADYCHCACIAE2AhQgAkHkEjYCECACQQQ2AgwMewsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIARQ1sIAJBNzYCHCACIAE2AhQgAiAANgIMDHoLIAIgAi8BMEEgcjsBMAtBMCEDDF8LIAJBNjYCHCACIAE2AhQgAiAANgIMDHcLIABBLEcNASABQQFqIQBBASEBAkACQAJAAkACQCACLQAsQQVrDgQDAQIEAAsgACEBDAQLQQIhAQwBC0EEIQELIAJBAToALCACIAIvATAgAXI7ATAgACEBDAELIAIgAi8BMEEIcjsBMCAAIQELQTkhAwxcCyACQQA6ACwLQTQhAwxaCyABIARGBEBBLSEDDHMLAkACQANAAkAgAS0AAEEKaw4EAgAAAwALIAQgAUEBaiIBRw0AC0EtIQMMdAsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIARQ0CIAJBLDYCHCACIAE2AhQgAiAANgIMDHMLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABECoiAEUEQCABQQFqIQEMAgsgAkEsNgIcIAIgADYCDCACIAFBAWo2AhQMcgsgAS0AAEENRgRAIAIoAgQhAEEAIQMgAkEANgIEIAIgACABECoiAEUEQCABQQFqIQEMAgsgAkEsNgIcIAIgADYCDCACIAFBAWo2AhQMcgsgAi0ALUEBcQRAQcQBIQMMWQsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKiIADQEMZQtBLyEDDFcLIAJBLjYCHCACIAE2AhQgAiAANgIMDG8LQQAhAyACQQA2AhwgAiABNgIUIAJB8BQ2AhAgAkEDNgIMDG4LQQEhAwJAAkACQAJAIAItACxBBWsOBAMBAgAECyACIAIvATBBCHI7ATAMAwtBAiEDDAELQQQhAwsgAkEBOgAsIAIgAi8BMCADcjsBMAtBKiEDDFMLQQAhAyACQQA2AhwgAiABNgIUIAJB4Q82AhAgAkEKNgIMDGsLQQEhAwJAAkACQAJAAkACQCACLQAsQQJrDgcFBAQDAQIABAsgAiACLwEwQQhyOwEwDAMLQQIhAwwBC0EEIQMLIAJBAToALCACIAIvATAgA3I7ATALQSshAwxSC0EAIQMgAkEANgIcIAIgATYCFCACQasSNgIQIAJBCzYCDAxqC0EAIQMgAkEANgIcIAIgATYCFCACQf0NNgIQIAJBHTYCDAxpCyABIARHBEADQCABLQAAQSBHDUggBCABQQFqIgFHDQALQSUhAwxpC0ElIQMMaAsgAi0ALUEBcQRAQcMBIQMMTwsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQKSIABEAgAkEmNgIcIAIgADYCDCACIAFBAWo2AhQMaAsgAUEBaiEBDFwLIAFBAWohASACLwEwIgBBgAFxBEBBACEAAkAgAigCOCIDRQ0AIAMoAlQiA0UNACACIAMRAAAhAAsgAEUNBiAAQRVHDR8gAkEFNgIcIAIgATYCFCACQfkXNgIQIAJBFTYCDEEAIQMMZwsCQCAAQaAEcUGgBEcNACACLQAtQQJxDQBBACEDIAJBADYCHCACIAE2AhQgAkGWEzYCECACQQQ2AgwMZwsgAgJ/IAIvATBBFHFBFEYEQEEBIAItAChBAUYNARogAi8BMkHlAEYMAQsgAi0AKUEFRgs6AC5BACEAAkAgAigCOCIDRQ0AIAMoAiQiA0UNACACIAMRAAAhAAsCQAJAAkACQAJAIAAOFgIBAAQEBAQEBAQEBAQEBAQEBAQEBAMECyACQQE6AC4LIAIgAi8BMEHAAHI7ATALQSchAwxPCyACQSM2AhwgAiABNgIUIAJBpRY2AhAgAkEVNgIMQQAhAwxnC0EAIQMgAkEANgIcIAIgATYCFCACQdULNgIQIAJBETYCDAxmC0EAIQACQCACKAI4IgNFDQAgAygCLCIDRQ0AIAIgAxEAACEACyAADQELQQ4hAwxLCyAAQRVGBEAgAkECNgIcIAIgATYCFCACQbAYNgIQIAJBFTYCDEEAIQMMZAtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMYwtBACEDIAJBADYCHCACIAE2AhQgAkGqHDYCECACQQ82AgwMYgsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEgCqdqIgEQKyIARQ0AIAJBBTYCHCACIAE2AhQgAiAANgIMDGELQQ8hAwxHC0EAIQMgAkEANgIcIAIgATYCFCACQc0TNgIQIAJBDDYCDAxfC0IBIQoLIAFBAWohAQJAIAIpAyAiC0L//////////w9YBEAgAiALQgSGIAqENwMgDAELQQAhAyACQQA2AhwgAiABNgIUIAJBrQk2AhAgAkEMNgIMDF4LQSQhAwxEC0EAIQMgAkEANgIcIAIgATYCFCACQc0TNgIQIAJBDDYCDAxcCyACKAIEIQBBACEDIAJBADYCBCACIAAgARAsIgBFBEAgAUEBaiEBDFILIAJBFzYCHCACIAA2AgwgAiABQQFqNgIUDFsLIAIoAgQhAEEAIQMgAkEANgIEAkAgAiAAIAEQLCIARQRAIAFBAWohAQwBCyACQRY2AhwgAiAANgIMIAIgAUEBajYCFAxbC0EfIQMMQQtBACEDIAJBADYCHCACIAE2AhQgAkGaDzYCECACQSI2AgwMWQsgAigCBCEAQQAhAyACQQA2AgQgAiAAIAEQLSIARQRAIAFBAWohAQxQCyACQRQ2AhwgAiAANgIMIAIgAUEBajYCFAxYCyACKAIEIQBBACEDIAJBADYCBAJAIAIgACABEC0iAEUEQCABQQFqIQEMAQsgAkETNgIcIAIgADYCDCACIAFBAWo2AhQMWAtBHiEDDD4LQQAhAyACQQA2AhwgAiABNgIUIAJBxgw2AhAgAkEjNgIMDFYLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABEC0iAEUEQCABQQFqIQEMTgsgAkERNgIcIAIgADYCDCACIAFBAWo2AhQMVQsgAkEQNgIcIAIgATYCFCACIAA2AgwMVAtBACEDIAJBADYCHCACIAE2AhQgAkHGDDYCECACQSM2AgwMUwtBACEDIAJBADYCHCACIAE2AhQgAkHAFTYCECACQQI2AgwMUgsgAigCBCEAQQAhAyACQQA2AgQCQCACIAAgARAtIgBFBEAgAUEBaiEBDAELIAJBDjYCHCACIAA2AgwgAiABQQFqNgIUDFILQRshAww4C0EAIQMgAkEANgIcIAIgATYCFCACQcYMNgIQIAJBIzYCDAxQCyACKAIEIQBBACEDIAJBADYCBAJAIAIgACABECwiAEUEQCABQQFqIQEMAQsgAkENNgIcIAIgADYCDCACIAFBAWo2AhQMUAtBGiEDDDYLQQAhAyACQQA2AhwgAiABNgIUIAJBmg82AhAgAkEiNgIMDE4LIAIoAgQhAEEAIQMgAkEANgIEAkAgAiAAIAEQLCIARQRAIAFBAWohAQwBCyACQQw2AhwgAiAANgIMIAIgAUEBajYCFAxOC0EZIQMMNAtBACEDIAJBADYCHCACIAE2AhQgAkGaDzYCECACQSI2AgwMTAsgAEEVRwRAQQAhAyACQQA2AhwgAiABNgIUIAJBgww2AhAgAkETNgIMDEwLIAJBCjYCHCACIAE2AhQgAkHkFjYCECACQRU2AgxBACEDDEsLIAIoAgQhAEEAIQMgAkEANgIEIAIgACABIAqnaiIBECsiAARAIAJBBzYCHCACIAE2AhQgAiAANgIMDEsLQRMhAwwxCyAAQRVHBEBBACEDIAJBADYCHCACIAE2AhQgAkHaDTYCECACQRQ2AgwMSgsgAkEeNgIcIAIgATYCFCACQfkXNgIQIAJBFTYCDEEAIQMMSQtBACEAAkAgAigCOCIDRQ0AIAMoAiwiA0UNACACIAMRAAAhAAsgAEUNQSAAQRVGBEAgAkEDNgIcIAIgATYCFCACQbAYNgIQIAJBFTYCDEEAIQMMSQtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMSAtBACEDIAJBADYCHCACIAE2AhQgAkHaDTYCECACQRQ2AgwMRwtBACEDIAJBADYCHCACIAE2AhQgAkGnDjYCECACQRI2AgwMRgsgAkEAOgAvIAItAC1BBHFFDT8LIAJBADoALyACQQE6ADRBACEDDCsLQQAhAyACQQA2AhwgAkHkETYCECACQQc2AgwgAiABQQFqNgIUDEMLAkADQAJAIAEtAABBCmsOBAACAgACCyAEIAFBAWoiAUcNAAtB3QEhAwxDCwJAAkAgAi0ANEEBRw0AQQAhAAJAIAIoAjgiA0UNACADKAJYIgNFDQAgAiADEQAAIQALIABFDQAgAEEVRw0BIAJB3AE2AhwgAiABNgIUIAJB1RY2AhAgAkEVNgIMQQAhAwxEC0HBASEDDCoLIAJBADYCHCACIAE2AhQgAkHpCzYCECACQR82AgxBACEDDEILAkACQCACLQAoQQFrDgIEAQALQcABIQMMKQtBuQEhAwwoCyACQQI6AC9BACEAAkAgAigCOCIDRQ0AIAMoAgAiA0UNACACIAMRAAAhAAsgAEUEQEHCASEDDCgLIABBFUcEQCACQQA2AhwgAiABNgIUIAJBpAw2AhAgAkEQNgIMQQAhAwxBCyACQdsBNgIcIAIgATYCFCACQfoWNgIQIAJBFTYCDEEAIQMMQAsgASAERgRAQdoBIQMMQAsgAS0AAEHIAEYNASACQQE6ACgLQawBIQMMJQtBvwEhAwwkCyABIARHBEAgAkEQNgIIIAIgATYCBEG+ASEDDCQLQdkBIQMMPAsgASAERgRAQdgBIQMMPAsgAS0AAEHIAEcNBCABQQFqIQFBvQEhAwwiCyABIARGBEBB1wEhAww7CwJAAkAgAS0AAEHFAGsOEAAFBQUFBQUFBQUFBQUFBQEFCyABQQFqIQFBuwEhAwwiCyABQQFqIQFBvAEhAwwhC0HWASEDIAEgBEYNOSACKAIAIgAgBCABa2ohBSABIABrQQJqIQYCQANAIAEtAAAgAEGD0ABqLQAARw0DIABBAkYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAw6CyACKAIEIQAgAkIANwMAIAIgACAGQQFqIgEQJyIARQRAQcYBIQMMIQsgAkHVATYCHCACIAE2AhQgAiAANgIMQQAhAww5C0HUASEDIAEgBEYNOCACKAIAIgAgBCABa2ohBSABIABrQQFqIQYCQANAIAEtAAAgAEGB0ABqLQAARw0CIABBAUYNASAAQQFqIQAgBCABQQFqIgFHDQALIAIgBTYCAAw5CyACQYEEOwEoIAIoAgQhACACQgA3AwAgAiAAIAZBAWoiARAnIgANAwwCCyACQQA2AgALQQAhAyACQQA2AhwgAiABNgIUIAJB2Bs2AhAgAkEINgIMDDYLQboBIQMMHAsgAkHTATYCHCACIAE2AhQgAiAANgIMQQAhAww0C0EAIQACQCACKAI4IgNFDQAgAygCOCIDRQ0AIAIgAxEAACEACyAARQ0AIABBFUYNASACQQA2AhwgAiABNgIUIAJBzA42AhAgAkEgNgIMQQAhAwwzC0HkACEDDBkLIAJB+AA2AhwgAiABNgIUIAJByhg2AhAgAkEVNgIMQQAhAwwxC0HSASEDIAQgASIARg0wIAQgAWsgAigCACIBaiEFIAAgAWtBBGohBgJAA0AgAC0AACABQfzPAGotAABHDQEgAUEERg0DIAFBAWohASAEIABBAWoiAEcNAAsgAiAFNgIADDELIAJBADYCHCACIAA2AhQgAkGQMzYCECACQQg2AgwgAkEANgIAQQAhAwwwCyABIARHBEAgAkEONgIIIAIgATYCBEG3ASEDDBcLQdEBIQMMLwsgAkEANgIAIAZBAWohAQtBuAEhAwwUCyABIARGBEBB0AEhAwwtCyABLQAAQTBrIgBB/wFxQQpJBEAgAiAAOgAqIAFBAWohAUG2ASEDDBQLIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ0UIAJBzwE2AhwgAiABNgIUIAIgADYCDEEAIQMMLAsgASAERgRAQc4BIQMMLAsCQCABLQAAQS5GBEAgAUEBaiEBDAELIAIoAgQhACACQQA2AgQgAiAAIAEQKCIARQ0VIAJBzQE2AhwgAiABNgIUIAIgADYCDEEAIQMMLAtBtQEhAwwSCyAEIAEiBUYEQEHMASEDDCsLQQAhAEEBIQFBASEGQQAhAwJAAkACQAJAAkACfwJAAkACQAJAAkACQAJAIAUtAABBMGsOCgoJAAECAwQFBggLC0ECDAYLQQMMBQtBBAwEC0EFDAMLQQYMAgtBBwwBC0EICyEDQQAhAUEAIQYMAgtBCSEDQQEhAEEAIQFBACEGDAELQQAhAUEBIQMLIAIgAzoAKyAFQQFqIQMCQAJAIAItAC1BEHENAAJAAkACQCACLQAqDgMBAAIECyAGRQ0DDAILIAANAQwCCyABRQ0BCyACKAIEIQAgAkEANgIEIAIgACADECgiAEUEQCADIQEMAwsgAkHJATYCHCACIAM2AhQgAiAANgIMQQAhAwwtCyACKAIEIQAgAkEANgIEIAIgACADECgiAEUEQCADIQEMGAsgAkHKATYCHCACIAM2AhQgAiAANgIMQQAhAwwsCyACKAIEIQAgAkEANgIEIAIgACAFECgiAEUEQCAFIQEMFgsgAkHLATYCHCACIAU2AhQgAiAANgIMDCsLQbQBIQMMEQtBACEAAkAgAigCOCIDRQ0AIAMoAjwiA0UNACACIAMRAAAhAAsCQCAABEAgAEEVRg0BIAJBADYCHCACIAE2AhQgAkGUDTYCECACQSE2AgxBACEDDCsLQbIBIQMMEQsgAkHIATYCHCACIAE2AhQgAkHJFzYCECACQRU2AgxBACEDDCkLIAJBADYCACAGQQFqIQFB9QAhAwwPCyACLQApQQVGBEBB4wAhAwwPC0HiACEDDA4LIAAhASACQQA2AgALIAJBADoALEEJIQMMDAsgAkEANgIAIAdBAWohAUHAACEDDAsLQQELOgAsIAJBADYCACAGQQFqIQELQSkhAwwIC0E4IQMMBwsCQCABIARHBEADQCABLQAAQYA+ai0AACIAQQFHBEAgAEECRw0DIAFBAWohAQwFCyAEIAFBAWoiAUcNAAtBPiEDDCELQT4hAwwgCwsgAkEAOgAsDAELQQshAwwEC0E6IQMMAwsgAUEBaiEBQS0hAwwCCyACIAE6ACwgAkEANgIAIAZBAWohAUEMIQMMAQsgAkEANgIAIAZBAWohAUEKIQMMAAsAC0EAIQMgAkEANgIcIAIgATYCFCACQc0QNgIQIAJBCTYCDAwXC0EAIQMgAkEANgIcIAIgATYCFCACQekKNgIQIAJBCTYCDAwWC0EAIQMgAkEANgIcIAIgATYCFCACQbcQNgIQIAJBCTYCDAwVC0EAIQMgAkEANgIcIAIgATYCFCACQZwRNgIQIAJBCTYCDAwUC0EAIQMgAkEANgIcIAIgATYCFCACQc0QNgIQIAJBCTYCDAwTC0EAIQMgAkEANgIcIAIgATYCFCACQekKNgIQIAJBCTYCDAwSC0EAIQMgAkEANgIcIAIgATYCFCACQbcQNgIQIAJBCTYCDAwRC0EAIQMgAkEANgIcIAIgATYCFCACQZwRNgIQIAJBCTYCDAwQC0EAIQMgAkEANgIcIAIgATYCFCACQZcVNgIQIAJBDzYCDAwPC0EAIQMgAkEANgIcIAIgATYCFCACQZcVNgIQIAJBDzYCDAwOC0EAIQMgAkEANgIcIAIgATYCFCACQcASNgIQIAJBCzYCDAwNC0EAIQMgAkEANgIcIAIgATYCFCACQZUJNgIQIAJBCzYCDAwMC0EAIQMgAkEANgIcIAIgATYCFCACQeEPNgIQIAJBCjYCDAwLC0EAIQMgAkEANgIcIAIgATYCFCACQfsPNgIQIAJBCjYCDAwKC0EAIQMgAkEANgIcIAIgATYCFCACQfEZNgIQIAJBAjYCDAwJC0EAIQMgAkEANgIcIAIgATYCFCACQcQUNgIQIAJBAjYCDAwIC0EAIQMgAkEANgIcIAIgATYCFCACQfIVNgIQIAJBAjYCDAwHCyACQQI2AhwgAiABNgIUIAJBnBo2AhAgAkEWNgIMQQAhAwwGC0EBIQMMBQtB1AAhAyABIARGDQQgCEEIaiEJIAIoAgAhBQJAAkAgASAERwRAIAVB2MIAaiEHIAQgBWogAWshACAFQX9zQQpqIgUgAWohBgNAIAEtAAAgBy0AAEcEQEECIQcMAwsgBUUEQEEAIQcgBiEBDAMLIAVBAWshBSAHQQFqIQcgBCABQQFqIgFHDQALIAAhBSAEIQELIAlBATYCACACIAU2AgAMAQsgAkEANgIAIAkgBzYCAAsgCSABNgIEIAgoAgwhACAIKAIIDgMBBAIACwALIAJBADYCHCACQbUaNgIQIAJBFzYCDCACIABBAWo2AhRBACEDDAILIAJBADYCHCACIAA2AhQgAkHKGjYCECACQQk2AgxBACEDDAELIAEgBEYEQEEiIQMMAQsgAkEJNgIIIAIgATYCBEEhIQMLIAhBEGokACADRQRAIAIoAgwhAAwBCyACIAM2AhxBACEAIAIoAgQiAUUNACACIAEgBCACKAIIEQEAIgFFDQAgAiAENgIUIAIgATYCDCABIQALIAALvgIBAn8gAEEAOgAAIABB3ABqIgFBAWtBADoAACAAQQA6AAIgAEEAOgABIAFBA2tBADoAACABQQJrQQA6AAAgAEEAOgADIAFBBGtBADoAAEEAIABrQQNxIgEgAGoiAEEANgIAQdwAIAFrQXxxIgIgAGoiAUEEa0EANgIAAkAgAkEJSQ0AIABBADYCCCAAQQA2AgQgAUEIa0EANgIAIAFBDGtBADYCACACQRlJDQAgAEEANgIYIABBADYCFCAAQQA2AhAgAEEANgIMIAFBEGtBADYCACABQRRrQQA2AgAgAUEYa0EANgIAIAFBHGtBADYCACACIABBBHFBGHIiAmsiAUEgSQ0AIAAgAmohAANAIABCADcDGCAAQgA3AxAgAEIANwMIIABCADcDACAAQSBqIQAgAUEgayIBQR9LDQALCwtWAQF/AkAgACgCDA0AAkACQAJAAkAgAC0ALw4DAQADAgsgACgCOCIBRQ0AIAEoAiwiAUUNACAAIAERAAAiAQ0DC0EADwsACyAAQcMWNgIQQQ4hAQsgAQsaACAAKAIMRQRAIABB0Rs2AhAgAEEVNgIMCwsUACAAKAIMQRVGBEAgAEEANgIMCwsUACAAKAIMQRZGBEAgAEEANgIMCwsHACAAKAIMCwcAIAAoAhALCQAgACABNgIQCwcAIAAoAhQLFwAgAEEkTwRAAAsgAEECdEGgM2ooAgALFwAgAEEuTwRAAAsgAEECdEGwNGooAgALvwkBAX9B6yghAQJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIABB5ABrDvQDY2IAAWFhYWFhYQIDBAVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhBgcICQoLDA0OD2FhYWFhEGFhYWFhYWFhYWFhEWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYRITFBUWFxgZGhthYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhHB0eHyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2YTc4OTphYWFhYWFhYTthYWE8YWFhYT0+P2FhYWFhYWFhQGFhQWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYUJDREVGR0hJSktMTU5PUFFSU2FhYWFhYWFhVFVWV1hZWlthXF1hYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFeYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhX2BhC0HhJw8LQaQhDwtByywPC0H+MQ8LQcAkDwtBqyQPC0GNKA8LQeImDwtBgDAPC0G5Lw8LQdckDwtB7x8PC0HhHw8LQfofDwtB8iAPC0GoLw8LQa4yDwtBiDAPC0HsJw8LQYIiDwtBjh0PC0HQLg8LQcojDwtBxTIPC0HfHA8LQdIcDwtBxCAPC0HXIA8LQaIfDwtB7S4PC0GrMA8LQdQlDwtBzC4PC0H6Lg8LQfwrDwtB0jAPC0HxHQ8LQbsgDwtB9ysPC0GQMQ8LQdcxDwtBoi0PC0HUJw8LQeArDwtBnywPC0HrMQ8LQdUfDwtByjEPC0HeJQ8LQdQeDwtB9BwPC0GnMg8LQbEdDwtBoB0PC0G5MQ8LQbwwDwtBkiEPC0GzJg8LQeksDwtBrB4PC0HUKw8LQfcmDwtBgCYPC0GwIQ8LQf4eDwtBjSMPC0GJLQ8LQfciDwtBoDEPC0GuHw8LQcYlDwtB6B4PC0GTIg8LQcIvDwtBwx0PC0GLLA8LQeEdDwtBjS8PC0HqIQ8LQbQtDwtB0i8PC0HfMg8LQdIyDwtB8DAPC0GpIg8LQfkjDwtBmR4PC0G1LA8LQZswDwtBkjIPC0G2Kw8LQcIiDwtB+DIPC0GeJQ8LQdAiDwtBuh4PC0GBHg8LAAtB1iEhAQsgAQsWACAAIAAtAC1B/gFxIAFBAEdyOgAtCxkAIAAgAC0ALUH9AXEgAUEAR0EBdHI6AC0LGQAgACAALQAtQfsBcSABQQBHQQJ0cjoALQsZACAAIAAtAC1B9wFxIAFBAEdBA3RyOgAtCz4BAn8CQCAAKAI4IgNFDQAgAygCBCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBxhE2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCCCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB9go2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCDCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB7Ro2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCECIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBlRA2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCFCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBqhs2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCGCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB7RM2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCKCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABB9gg2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCHCIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBwhk2AhBBGCEECyAECz4BAn8CQCAAKAI4IgNFDQAgAygCICIDRQ0AIAAgASACIAFrIAMRAQAiBEF/Rw0AIABBlBQ2AhBBGCEECyAEC1kBAn8CQCAALQAoQQFGDQAgAC8BMiIBQeQAa0HkAEkNACABQcwBRg0AIAFBsAJGDQAgAC8BMCIAQcAAcQ0AQQEhAiAAQYgEcUGABEYNACAAQShxRSECCyACC4wBAQJ/AkACQAJAIAAtACpFDQAgAC0AK0UNACAALwEwIgFBAnFFDQEMAgsgAC8BMCIBQQFxRQ0BC0EBIQIgAC0AKEEBRg0AIAAvATIiAEHkAGtB5ABJDQAgAEHMAUYNACAAQbACRg0AIAFBwABxDQBBACECIAFBiARxQYAERg0AIAFBKHFBAEchAgsgAgtzACAAQRBq/QwAAAAAAAAAAAAAAAAAAAAA/QsDACAA/QwAAAAAAAAAAAAAAAAAAAAA/QsDACAAQTBq/QwAAAAAAAAAAAAAAAAAAAAA/QsDACAAQSBq/QwAAAAAAAAAAAAAAAAAAAAA/QsDACAAQd0BNgIcCwYAIAAQMguaLQELfyMAQRBrIgokAEGk0AAoAgAiCUUEQEHk0wAoAgAiBUUEQEHw0wBCfzcCAEHo0wBCgICEgICAwAA3AgBB5NMAIApBCGpBcHFB2KrVqgVzIgU2AgBB+NMAQQA2AgBByNMAQQA2AgALQczTAEGA1AQ2AgBBnNAAQYDUBDYCAEGw0AAgBTYCAEGs0ABBfzYCAEHQ0wBBgKwDNgIAA0AgAUHI0ABqIAFBvNAAaiICNgIAIAIgAUG00ABqIgM2AgAgAUHA0ABqIAM2AgAgAUHQ0ABqIAFBxNAAaiIDNgIAIAMgAjYCACABQdjQAGogAUHM0ABqIgI2AgAgAiADNgIAIAFB1NAAaiACNgIAIAFBIGoiAUGAAkcNAAtBjNQEQcGrAzYCAEGo0ABB9NMAKAIANgIAQZjQAEHAqwM2AgBBpNAAQYjUBDYCAEHM/wdBODYCAEGI1AQhCQsCQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAQewBTQRAQYzQACgCACIGQRAgAEETakFwcSAAQQtJGyIEQQN2IgB2IgFBA3EEQAJAIAFBAXEgAHJBAXMiAkEDdCIAQbTQAGoiASAAQbzQAGooAgAiACgCCCIDRgRAQYzQACAGQX4gAndxNgIADAELIAEgAzYCCCADIAE2AgwLIABBCGohASAAIAJBA3QiAkEDcjYCBCAAIAJqIgAgACgCBEEBcjYCBAwRC0GU0AAoAgAiCCAETw0BIAEEQAJAQQIgAHQiAkEAIAJrciABIAB0cWgiAEEDdCICQbTQAGoiASACQbzQAGooAgAiAigCCCIDRgRAQYzQACAGQX4gAHdxIgY2AgAMAQsgASADNgIIIAMgATYCDAsgAiAEQQNyNgIEIABBA3QiACAEayEFIAAgAmogBTYCACACIARqIgQgBUEBcjYCBCAIBEAgCEF4cUG00ABqIQBBoNAAKAIAIQMCf0EBIAhBA3Z0IgEgBnFFBEBBjNAAIAEgBnI2AgAgAAwBCyAAKAIICyIBIAM2AgwgACADNgIIIAMgADYCDCADIAE2AggLIAJBCGohAUGg0AAgBDYCAEGU0AAgBTYCAAwRC0GQ0AAoAgAiC0UNASALaEECdEG80gBqKAIAIgAoAgRBeHEgBGshBSAAIQIDQAJAIAIoAhAiAUUEQCACQRRqKAIAIgFFDQELIAEoAgRBeHEgBGsiAyAFSSECIAMgBSACGyEFIAEgACACGyEAIAEhAgwBCwsgACgCGCEJIAAoAgwiAyAARwRAQZzQACgCABogAyAAKAIIIgE2AgggASADNgIMDBALIABBFGoiAigCACIBRQRAIAAoAhAiAUUNAyAAQRBqIQILA0AgAiEHIAEiA0EUaiICKAIAIgENACADQRBqIQIgAygCECIBDQALIAdBADYCAAwPC0F/IQQgAEG/f0sNACAAQRNqIgFBcHEhBEGQ0AAoAgAiCEUNAEEAIARrIQUCQAJAAkACf0EAIARBgAJJDQAaQR8gBEH///8HSw0AGiAEQSYgAUEIdmciAGt2QQFxIABBAXRrQT5qCyIGQQJ0QbzSAGooAgAiAkUEQEEAIQFBACEDDAELQQAhASAEQRkgBkEBdmtBACAGQR9HG3QhAEEAIQMDQAJAIAIoAgRBeHEgBGsiByAFTw0AIAIhAyAHIgUNAEEAIQUgAiEBDAMLIAEgAkEUaigCACIHIAcgAiAAQR12QQRxakEQaigCACICRhsgASAHGyEBIABBAXQhACACDQALCyABIANyRQRAQQAhA0ECIAZ0IgBBACAAa3IgCHEiAEUNAyAAaEECdEG80gBqKAIAIQELIAFFDQELA0AgASgCBEF4cSAEayICIAVJIQAgAiAFIAAbIQUgASADIAAbIQMgASgCECIABH8gAAUgAUEUaigCAAsiAQ0ACwsgA0UNACAFQZTQACgCACAEa08NACADKAIYIQcgAyADKAIMIgBHBEBBnNAAKAIAGiAAIAMoAggiATYCCCABIAA2AgwMDgsgA0EUaiICKAIAIgFFBEAgAygCECIBRQ0DIANBEGohAgsDQCACIQYgASIAQRRqIgIoAgAiAQ0AIABBEGohAiAAKAIQIgENAAsgBkEANgIADA0LQZTQACgCACIDIARPBEBBoNAAKAIAIQECQCADIARrIgJBEE8EQCABIARqIgAgAkEBcjYCBCABIANqIAI2AgAgASAEQQNyNgIEDAELIAEgA0EDcjYCBCABIANqIgAgACgCBEEBcjYCBEEAIQBBACECC0GU0AAgAjYCAEGg0AAgADYCACABQQhqIQEMDwtBmNAAKAIAIgMgBEsEQCAEIAlqIgAgAyAEayIBQQFyNgIEQaTQACAANgIAQZjQACABNgIAIAkgBEEDcjYCBCAJQQhqIQEMDwtBACEBIAQCf0Hk0wAoAgAEQEHs0wAoAgAMAQtB8NMAQn83AgBB6NMAQoCAhICAgMAANwIAQeTTACAKQQxqQXBxQdiq1aoFczYCAEH40wBBADYCAEHI0wBBADYCAEGAgAQLIgAgBEHHAGoiBWoiBkEAIABrIgdxIgJPBEBB/NMAQTA2AgAMDwsCQEHE0wAoAgAiAUUNAEG80wAoAgAiCCACaiEAIAAgAU0gACAIS3ENAEEAIQFB/NMAQTA2AgAMDwtByNMALQAAQQRxDQQCQAJAIAkEQEHM0wAhAQNAIAEoAgAiACAJTQRAIAAgASgCBGogCUsNAwsgASgCCCIBDQALC0EAEDMiAEF/Rg0FIAIhBkHo0wAoAgAiAUEBayIDIABxBEAgAiAAayAAIANqQQAgAWtxaiEGCyAEIAZPDQUgBkH+////B0sNBUHE0wAoAgAiAwRAQbzTACgCACIHIAZqIQEgASAHTQ0GIAEgA0sNBgsgBhAzIgEgAEcNAQwHCyAGIANrIAdxIgZB/v///wdLDQQgBhAzIQAgACABKAIAIAEoAgRqRg0DIAAhAQsCQCAGIARByABqTw0AIAFBf0YNAEHs0wAoAgAiACAFIAZrakEAIABrcSIAQf7///8HSwRAIAEhAAwHCyAAEDNBf0cEQCAAIAZqIQYgASEADAcLQQAgBmsQMxoMBAsgASIAQX9HDQUMAwtBACEDDAwLQQAhAAwKCyAAQX9HDQILQcjTAEHI0wAoAgBBBHI2AgALIAJB/v///wdLDQEgAhAzIQBBABAzIQEgAEF/Rg0BIAFBf0YNASAAIAFPDQEgASAAayIGIARBOGpNDQELQbzTAEG80wAoAgAgBmoiATYCAEHA0wAoAgAgAUkEQEHA0wAgATYCAAsCQAJAAkBBpNAAKAIAIgIEQEHM0wAhAQNAIAAgASgCACIDIAEoAgQiBWpGDQIgASgCCCIBDQALDAILQZzQACgCACIBQQBHIAAgAU9xRQRAQZzQACAANgIAC0EAIQFB0NMAIAY2AgBBzNMAIAA2AgBBrNAAQX82AgBBsNAAQeTTACgCADYCAEHY0wBBADYCAANAIAFByNAAaiABQbzQAGoiAjYCACACIAFBtNAAaiIDNgIAIAFBwNAAaiADNgIAIAFB0NAAaiABQcTQAGoiAzYCACADIAI2AgAgAUHY0ABqIAFBzNAAaiICNgIAIAIgAzYCACABQdTQAGogAjYCACABQSBqIgFBgAJHDQALQXggAGtBD3EiASAAaiICIAZBOGsiAyABayIBQQFyNgIEQajQAEH00wAoAgA2AgBBmNAAIAE2AgBBpNAAIAI2AgAgACADakE4NgIEDAILIAAgAk0NACACIANJDQAgASgCDEEIcQ0AQXggAmtBD3EiACACaiIDQZjQACgCACAGaiIHIABrIgBBAXI2AgQgASAFIAZqNgIEQajQAEH00wAoAgA2AgBBmNAAIAA2AgBBpNAAIAM2AgAgAiAHakE4NgIEDAELIABBnNAAKAIASQRAQZzQACAANgIACyAAIAZqIQNBzNMAIQECQAJAAkADQCADIAEoAgBHBEAgASgCCCIBDQEMAgsLIAEtAAxBCHFFDQELQczTACEBA0AgASgCACIDIAJNBEAgAyABKAIEaiIFIAJLDQMLIAEoAgghAQwACwALIAEgADYCACABIAEoAgQgBmo2AgQgAEF4IABrQQ9xaiIJIARBA3I2AgQgA0F4IANrQQ9xaiIGIAQgCWoiBGshASACIAZGBEBBpNAAIAQ2AgBBmNAAQZjQACgCACABaiIANgIAIAQgAEEBcjYCBAwIC0Gg0AAoAgAgBkYEQEGg0AAgBDYCAEGU0ABBlNAAKAIAIAFqIgA2AgAgBCAAQQFyNgIEIAAgBGogADYCAAwICyAGKAIEIgVBA3FBAUcNBiAFQXhxIQggBUH/AU0EQCAFQQN2IQMgBigCCCIAIAYoAgwiAkYEQEGM0ABBjNAAKAIAQX4gA3dxNgIADAcLIAIgADYCCCAAIAI2AgwMBgsgBigCGCEHIAYgBigCDCIARwRAIAAgBigCCCICNgIIIAIgADYCDAwFCyAGQRRqIgIoAgAiBUUEQCAGKAIQIgVFDQQgBkEQaiECCwNAIAIhAyAFIgBBFGoiAigCACIFDQAgAEEQaiECIAAoAhAiBQ0ACyADQQA2AgAMBAtBeCAAa0EPcSIBIABqIgcgBkE4ayIDIAFrIgFBAXI2AgQgACADakE4NgIEIAIgBUE3IAVrQQ9xakE/ayIDIAMgAkEQakkbIgNBIzYCBEGo0ABB9NMAKAIANgIAQZjQACABNgIAQaTQACAHNgIAIANBEGpB1NMAKQIANwIAIANBzNMAKQIANwIIQdTTACADQQhqNgIAQdDTACAGNgIAQczTACAANgIAQdjTAEEANgIAIANBJGohAQNAIAFBBzYCACAFIAFBBGoiAUsNAAsgAiADRg0AIAMgAygCBEF+cTYCBCADIAMgAmsiBTYCACACIAVBAXI2AgQgBUH/AU0EQCAFQXhxQbTQAGohAAJ/QYzQACgCACIBQQEgBUEDdnQiA3FFBEBBjNAAIAEgA3I2AgAgAAwBCyAAKAIICyIBIAI2AgwgACACNgIIIAIgADYCDCACIAE2AggMAQtBHyEBIAVB////B00EQCAFQSYgBUEIdmciAGt2QQFxIABBAXRrQT5qIQELIAIgATYCHCACQgA3AhAgAUECdEG80gBqIQBBkNAAKAIAIgNBASABdCIGcUUEQCAAIAI2AgBBkNAAIAMgBnI2AgAgAiAANgIYIAIgAjYCCCACIAI2AgwMAQsgBUEZIAFBAXZrQQAgAUEfRxt0IQEgACgCACEDAkADQCADIgAoAgRBeHEgBUYNASABQR12IQMgAUEBdCEBIAAgA0EEcWpBEGoiBigCACIDDQALIAYgAjYCACACIAA2AhggAiACNgIMIAIgAjYCCAwBCyAAKAIIIgEgAjYCDCAAIAI2AgggAkEANgIYIAIgADYCDCACIAE2AggLQZjQACgCACIBIARNDQBBpNAAKAIAIgAgBGoiAiABIARrIgFBAXI2AgRBmNAAIAE2AgBBpNAAIAI2AgAgACAEQQNyNgIEIABBCGohAQwIC0EAIQFB/NMAQTA2AgAMBwtBACEACyAHRQ0AAkAgBigCHCICQQJ0QbzSAGoiAygCACAGRgRAIAMgADYCACAADQFBkNAAQZDQACgCAEF+IAJ3cTYCAAwCCyAHQRBBFCAHKAIQIAZGG2ogADYCACAARQ0BCyAAIAc2AhggBigCECICBEAgACACNgIQIAIgADYCGAsgBkEUaigCACICRQ0AIABBFGogAjYCACACIAA2AhgLIAEgCGohASAGIAhqIgYoAgQhBQsgBiAFQX5xNgIEIAEgBGogATYCACAEIAFBAXI2AgQgAUH/AU0EQCABQXhxQbTQAGohAAJ/QYzQACgCACICQQEgAUEDdnQiAXFFBEBBjNAAIAEgAnI2AgAgAAwBCyAAKAIICyIBIAQ2AgwgACAENgIIIAQgADYCDCAEIAE2AggMAQtBHyEFIAFB////B00EQCABQSYgAUEIdmciAGt2QQFxIABBAXRrQT5qIQULIAQgBTYCHCAEQgA3AhAgBUECdEG80gBqIQBBkNAAKAIAIgJBASAFdCIDcUUEQCAAIAQ2AgBBkNAAIAIgA3I2AgAgBCAANgIYIAQgBDYCCCAEIAQ2AgwMAQsgAUEZIAVBAXZrQQAgBUEfRxt0IQUgACgCACEAAkADQCAAIgIoAgRBeHEgAUYNASAFQR12IQAgBUEBdCEFIAIgAEEEcWpBEGoiAygCACIADQALIAMgBDYCACAEIAI2AhggBCAENgIMIAQgBDYCCAwBCyACKAIIIgAgBDYCDCACIAQ2AgggBEEANgIYIAQgAjYCDCAEIAA2AggLIAlBCGohAQwCCwJAIAdFDQACQCADKAIcIgFBAnRBvNIAaiICKAIAIANGBEAgAiAANgIAIAANAUGQ0AAgCEF+IAF3cSIINgIADAILIAdBEEEUIAcoAhAgA0YbaiAANgIAIABFDQELIAAgBzYCGCADKAIQIgEEQCAAIAE2AhAgASAANgIYCyADQRRqKAIAIgFFDQAgAEEUaiABNgIAIAEgADYCGAsCQCAFQQ9NBEAgAyAEIAVqIgBBA3I2AgQgACADaiIAIAAoAgRBAXI2AgQMAQsgAyAEaiICIAVBAXI2AgQgAyAEQQNyNgIEIAIgBWogBTYCACAFQf8BTQRAIAVBeHFBtNAAaiEAAn9BjNAAKAIAIgFBASAFQQN2dCIFcUUEQEGM0AAgASAFcjYCACAADAELIAAoAggLIgEgAjYCDCAAIAI2AgggAiAANgIMIAIgATYCCAwBC0EfIQEgBUH///8HTQRAIAVBJiAFQQh2ZyIAa3ZBAXEgAEEBdGtBPmohAQsgAiABNgIcIAJCADcCECABQQJ0QbzSAGohAEEBIAF0IgQgCHFFBEAgACACNgIAQZDQACAEIAhyNgIAIAIgADYCGCACIAI2AgggAiACNgIMDAELIAVBGSABQQF2a0EAIAFBH0cbdCEBIAAoAgAhBAJAA0AgBCIAKAIEQXhxIAVGDQEgAUEddiEEIAFBAXQhASAAIARBBHFqQRBqIgYoAgAiBA0ACyAGIAI2AgAgAiAANgIYIAIgAjYCDCACIAI2AggMAQsgACgCCCIBIAI2AgwgACACNgIIIAJBADYCGCACIAA2AgwgAiABNgIICyADQQhqIQEMAQsCQCAJRQ0AAkAgACgCHCIBQQJ0QbzSAGoiAigCACAARgRAIAIgAzYCACADDQFBkNAAIAtBfiABd3E2AgAMAgsgCUEQQRQgCSgCECAARhtqIAM2AgAgA0UNAQsgAyAJNgIYIAAoAhAiAQRAIAMgATYCECABIAM2AhgLIABBFGooAgAiAUUNACADQRRqIAE2AgAgASADNgIYCwJAIAVBD00EQCAAIAQgBWoiAUEDcjYCBCAAIAFqIgEgASgCBEEBcjYCBAwBCyAAIARqIgcgBUEBcjYCBCAAIARBA3I2AgQgBSAHaiAFNgIAIAgEQCAIQXhxQbTQAGohAUGg0AAoAgAhAwJ/QQEgCEEDdnQiAiAGcUUEQEGM0AAgAiAGcjYCACABDAELIAEoAggLIgIgAzYCDCABIAM2AgggAyABNgIMIAMgAjYCCAtBoNAAIAc2AgBBlNAAIAU2AgALIABBCGohAQsgCkEQaiQAIAELQwAgAEUEQD8AQRB0DwsCQCAAQf//A3ENACAAQQBIDQAgAEEQdkAAIgBBf0YEQEH80wBBMDYCAEF/DwsgAEEQdA8LAAsL3D8iAEGACAsJAQAAAAIAAAADAEGUCAsFBAAAAAUAQaQICwkGAAAABwAAAAgAQdwIC4otSW52YWxpZCBjaGFyIGluIHVybCBxdWVyeQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2JvZHkAQ29udGVudC1MZW5ndGggb3ZlcmZsb3cAQ2h1bmsgc2l6ZSBvdmVyZmxvdwBSZXNwb25zZSBvdmVyZmxvdwBJbnZhbGlkIG1ldGhvZCBmb3IgSFRUUC94LnggcmVxdWVzdABJbnZhbGlkIG1ldGhvZCBmb3IgUlRTUC94LnggcmVxdWVzdABFeHBlY3RlZCBTT1VSQ0UgbWV0aG9kIGZvciBJQ0UveC54IHJlcXVlc3QASW52YWxpZCBjaGFyIGluIHVybCBmcmFnbWVudCBzdGFydABFeHBlY3RlZCBkb3QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9zdGF0dXMASW52YWxpZCByZXNwb25zZSBzdGF0dXMASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucwBVc2VyIGNhbGxiYWNrIGVycm9yAGBvbl9yZXNldGAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2hlYWRlcmAgY2FsbGJhY2sgZXJyb3IAYG9uX21lc3NhZ2VfYmVnaW5gIGNhbGxiYWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwBB+TULAQEAQZA2C+ABAQECAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQf03CwEBAEGROAteAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgBB/TkLAQEAQZE6C14CAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAEHwOwsNbG9zZWVlcC1hbGl2ZQBBiTwLAQEAQaA8C+ABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQYk+CwEBAEGgPgvnAQEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZABBsMAAC18BAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQBBkMIACyFlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AQcDCAAstcmFuc2Zlci1lbmNvZGluZ3BncmFkZQ0KDQoNClNNDQoNClRUUC9DRS9UU1AvAEH5wgALBQECAAEDAEGQwwAL4AEEAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBB+cQACwUBAgABAwBBkMUAC+ABBAEBBQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAQfnGAAsEAQAAAQBBkccAC98BAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQBB+sgACwQBAAACAEGQyQALXwMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAEH6ygALBAEAAAEAQZDLAAsBAQBBqssAC0ECAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBB+swACwQBAAABAEGQzQALAQEAQZrNAAsGAgAAAAACAEGxzQALOgMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAQfDOAAuWAU5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==", "base64"); + } +}); + +// viewer/node_modules/undici/lib/web/fetch/constants.js +var require_constants3 = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/constants.js"(exports, module) { + "use strict"; + var corsSafeListedMethods = ( + /** @type {const} */ + ["GET", "HEAD", "POST"] + ); + var corsSafeListedMethodsSet = new Set(corsSafeListedMethods); + var nullBodyStatus = ( + /** @type {const} */ + [101, 204, 205, 304] + ); + var redirectStatus = ( + /** @type {const} */ + [301, 302, 303, 307, 308] + ); + var redirectStatusSet = new Set(redirectStatus); + var badPorts = ( + /** @type {const} */ + [ + "1", + "7", + "9", + "11", + "13", + "15", + "17", + "19", + "20", + "21", + "22", + "23", + "25", + "37", + "42", + "43", + "53", + "69", + "77", + "79", + "87", + "95", + "101", + "102", + "103", + "104", + "109", + "110", + "111", + "113", + "115", + "117", + "119", + "123", + "135", + "137", + "139", + "143", + "161", + "179", + "389", + "427", + "465", + "512", + "513", + "514", + "515", + "526", + "530", + "531", + "532", + "540", + "548", + "554", + "556", + "563", + "587", + "601", + "636", + "989", + "990", + "993", + "995", + "1719", + "1720", + "1723", + "2049", + "3659", + "4045", + "4190", + "5060", + "5061", + "6000", + "6566", + "6665", + "6666", + "6667", + "6668", + "6669", + "6679", + "6697", + "10080" + ] + ); + var badPortsSet = new Set(badPorts); + var referrerPolicy = ( + /** @type {const} */ + [ + "", + "no-referrer", + "no-referrer-when-downgrade", + "same-origin", + "origin", + "strict-origin", + "origin-when-cross-origin", + "strict-origin-when-cross-origin", + "unsafe-url" + ] + ); + var referrerPolicySet = new Set(referrerPolicy); + var requestRedirect = ( + /** @type {const} */ + ["follow", "manual", "error"] + ); + var safeMethods = ( + /** @type {const} */ + ["GET", "HEAD", "OPTIONS", "TRACE"] + ); + var safeMethodsSet = new Set(safeMethods); + var requestMode = ( + /** @type {const} */ + ["navigate", "same-origin", "no-cors", "cors"] + ); + var requestCredentials = ( + /** @type {const} */ + ["omit", "same-origin", "include"] + ); + var requestCache = ( + /** @type {const} */ + [ + "default", + "no-store", + "reload", + "no-cache", + "force-cache", + "only-if-cached" + ] + ); + var requestBodyHeader = ( + /** @type {const} */ + [ + "content-encoding", + "content-language", + "content-location", + "content-type", + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + "content-length" + ] + ); + var requestDuplex = ( + /** @type {const} */ + [ + "half" + ] + ); + var forbiddenMethods = ( + /** @type {const} */ + ["CONNECT", "TRACE", "TRACK"] + ); + var forbiddenMethodsSet = new Set(forbiddenMethods); + var subresource = ( + /** @type {const} */ + [ + "audio", + "audioworklet", + "font", + "image", + "manifest", + "paintworklet", + "script", + "style", + "track", + "video", + "xslt", + "" + ] + ); + var subresourceSet = new Set(subresource); + module.exports = { + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods, + badPorts, + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/global.js +var require_global = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/global.js"(exports, module) { + "use strict"; + var globalOrigin = /* @__PURE__ */ Symbol.for("undici.globalOrigin.1"); + function getGlobalOrigin() { + return globalThis[globalOrigin]; + } + function setGlobalOrigin(newOrigin) { + if (newOrigin === void 0) { + Object.defineProperty(globalThis, globalOrigin, { + value: void 0, + writable: true, + enumerable: false, + configurable: false + }); + return; + } + const parsedURL = new URL(newOrigin); + if (parsedURL.protocol !== "http:" && parsedURL.protocol !== "https:") { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`); + } + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }); + } + module.exports = { + getGlobalOrigin, + setGlobalOrigin + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/data-url.js +var require_data_url = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/data-url.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var encoder = new TextEncoder(); + var HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+\-.^_|~A-Za-z0-9]+$/; + var HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/; + var ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g; + var HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/; + function dataURLProcessor(dataURL) { + assert(dataURL.protocol === "data:"); + let input = URLSerializer(dataURL, true); + input = input.slice(5); + const position = { position: 0 }; + let mimeType = collectASequenceOfCodePointsFast( + ",", + input, + position + ); + const mimeTypeLength = mimeType.length; + mimeType = removeASCIIWhitespace(mimeType, true, true); + if (position.position >= input.length) { + return "failure"; + } + position.position++; + const encodedBody = input.slice(mimeTypeLength + 1); + let body = stringPercentDecode(encodedBody); + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + const stringBody = isomorphicDecode(body); + body = forgivingBase64(stringBody); + if (body === "failure") { + return "failure"; + } + mimeType = mimeType.slice(0, -6); + mimeType = mimeType.replace(/(\u0020)+$/, ""); + mimeType = mimeType.slice(0, -1); + } + if (mimeType.startsWith(";")) { + mimeType = "text/plain" + mimeType; + } + let mimeTypeRecord = parseMIMEType(mimeType); + if (mimeTypeRecord === "failure") { + mimeTypeRecord = parseMIMEType("text/plain;charset=US-ASCII"); + } + return { mimeType: mimeTypeRecord, body }; + } + function URLSerializer(url, excludeFragment = false) { + if (!excludeFragment) { + return url.href; + } + const href = url.href; + const hashLength = url.hash.length; + const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength); + if (!hashLength && href.endsWith("#")) { + return serialized.slice(0, -1); + } + return serialized; + } + function collectASequenceOfCodePoints(condition, input, position) { + let result = ""; + while (position.position < input.length && condition(input[position.position])) { + result += input[position.position]; + position.position++; + } + return result; + } + function collectASequenceOfCodePointsFast(char, input, position) { + const idx = input.indexOf(char, position.position); + const start = position.position; + if (idx === -1) { + position.position = input.length; + return input.slice(start); + } + position.position = idx; + return input.slice(start, position.position); + } + function stringPercentDecode(input) { + const bytes2 = encoder.encode(input); + return percentDecode(bytes2); + } + function isHexCharByte(byte) { + return byte >= 48 && byte <= 57 || byte >= 65 && byte <= 70 || byte >= 97 && byte <= 102; + } + function hexByteToNumber(byte) { + return ( + // 0-9 + byte >= 48 && byte <= 57 ? byte - 48 : (byte & 223) - 55 + ); + } + function percentDecode(input) { + const length2 = input.length; + const output = new Uint8Array(length2); + let j = 0; + for (let i = 0; i < length2; ++i) { + const byte = input[i]; + if (byte !== 37) { + output[j++] = byte; + } else if (byte === 37 && !(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))) { + output[j++] = 37; + } else { + output[j++] = hexByteToNumber(input[i + 1]) << 4 | hexByteToNumber(input[i + 2]); + i += 2; + } + } + return length2 === j ? output : output.subarray(0, j); + } + function parseMIMEType(input) { + input = removeHTTPWhitespace(input, true, true); + const position = { position: 0 }; + const type = collectASequenceOfCodePointsFast( + "/", + input, + position + ); + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { + return "failure"; + } + if (position.position > input.length) { + return "failure"; + } + position.position++; + let subtype = collectASequenceOfCodePointsFast( + ";", + input, + position + ); + subtype = removeHTTPWhitespace(subtype, false, true); + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { + return "failure"; + } + const typeLowercase = type.toLowerCase(); + const subtypeLowercase = subtype.toLowerCase(); + const mimeType = { + type: typeLowercase, + subtype: subtypeLowercase, + /** @type {Map} */ + parameters: /* @__PURE__ */ new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + essence: `${typeLowercase}/${subtypeLowercase}` + }; + while (position.position < input.length) { + position.position++; + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + (char) => HTTP_WHITESPACE_REGEX.test(char), + input, + position + ); + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ";" && char !== "=", + input, + position + ); + parameterName = parameterName.toLowerCase(); + if (position.position < input.length) { + if (input[position.position] === ";") { + continue; + } + position.position++; + } + if (position.position > input.length) { + break; + } + let parameterValue = null; + if (input[position.position] === '"') { + parameterValue = collectAnHTTPQuotedString(input, position, true); + collectASequenceOfCodePointsFast( + ";", + input, + position + ); + } else { + parameterValue = collectASequenceOfCodePointsFast( + ";", + input, + position + ); + parameterValue = removeHTTPWhitespace(parameterValue, false, true); + if (parameterValue.length === 0) { + continue; + } + } + if (parameterName.length !== 0 && HTTP_TOKEN_CODEPOINTS.test(parameterName) && (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && !mimeType.parameters.has(parameterName)) { + mimeType.parameters.set(parameterName, parameterValue); + } + } + return mimeType; + } + function forgivingBase64(data) { + data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, ""); + let dataLength = data.length; + if (dataLength % 4 === 0) { + if (data.charCodeAt(dataLength - 1) === 61) { + --dataLength; + if (data.charCodeAt(dataLength - 1) === 61) { + --dataLength; + } + } + } + if (dataLength % 4 === 1) { + return "failure"; + } + if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) { + return "failure"; + } + const buffer = Buffer.from(data, "base64"); + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); + } + function collectAnHTTPQuotedString(input, position, extractValue) { + const positionStart = position.position; + let value = ""; + assert(input[position.position] === '"'); + position.position++; + while (true) { + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== "\\", + input, + position + ); + if (position.position >= input.length) { + break; + } + const quoteOrBackslash = input[position.position]; + position.position++; + if (quoteOrBackslash === "\\") { + if (position.position >= input.length) { + value += "\\"; + break; + } + value += input[position.position]; + position.position++; + } else { + assert(quoteOrBackslash === '"'); + break; + } + } + if (extractValue) { + return value; + } + return input.slice(positionStart, position.position); + } + function serializeAMimeType(mimeType) { + assert(mimeType !== "failure"); + const { parameters, essence } = mimeType; + let serialization = essence; + for (let [name, value] of parameters.entries()) { + serialization += ";"; + serialization += name; + serialization += "="; + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { + value = value.replace(/(\\|")/g, "\\$1"); + value = '"' + value; + value += '"'; + } + serialization += value; + } + return serialization; + } + function isHTTPWhiteSpace(char) { + return char === 13 || char === 10 || char === 9 || char === 32; + } + function removeHTTPWhitespace(str, leading = true, trailing = true) { + return removeChars(str, leading, trailing, isHTTPWhiteSpace); + } + function isASCIIWhitespace(char) { + return char === 13 || char === 10 || char === 9 || char === 12 || char === 32; + } + function removeASCIIWhitespace(str, leading = true, trailing = true) { + return removeChars(str, leading, trailing, isASCIIWhitespace); + } + function removeChars(str, leading, trailing, predicate) { + let lead = 0; + let trail = str.length - 1; + if (leading) { + while (lead < str.length && predicate(str.charCodeAt(lead))) lead++; + } + if (trailing) { + while (trail > 0 && predicate(str.charCodeAt(trail))) trail--; + } + return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1); + } + function isomorphicDecode(input) { + const length2 = input.length; + if ((2 << 15) - 1 > length2) { + return String.fromCharCode.apply(null, input); + } + let result = ""; + let i = 0; + let addition = (2 << 15) - 1; + while (i < length2) { + if (i + addition > length2) { + addition = length2 - i; + } + result += String.fromCharCode.apply(null, input.subarray(i, i += addition)); + } + return result; + } + function minimizeSupportedMimeType(mimeType) { + switch (mimeType.essence) { + case "application/ecmascript": + case "application/javascript": + case "application/x-ecmascript": + case "application/x-javascript": + case "text/ecmascript": + case "text/javascript": + case "text/javascript1.0": + case "text/javascript1.1": + case "text/javascript1.2": + case "text/javascript1.3": + case "text/javascript1.4": + case "text/javascript1.5": + case "text/jscript": + case "text/livescript": + case "text/x-ecmascript": + case "text/x-javascript": + return "text/javascript"; + case "application/json": + case "text/json": + return "application/json"; + case "image/svg+xml": + return "image/svg+xml"; + case "text/xml": + case "application/xml": + return "application/xml"; + } + if (mimeType.subtype.endsWith("+json")) { + return "application/json"; + } + if (mimeType.subtype.endsWith("+xml")) { + return "application/xml"; + } + return ""; + } + module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType, + removeChars, + removeHTTPWhitespace, + minimizeSupportedMimeType, + HTTP_TOKEN_CODEPOINTS, + isomorphicDecode + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/webidl.js +var require_webidl = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/webidl.js"(exports, module) { + "use strict"; + var { types, inspect } = __require("node:util"); + var { markAsUncloneable } = __require("node:worker_threads"); + var { toUSVString } = require_util(); + var webidl = {}; + webidl.converters = {}; + webidl.util = {}; + webidl.errors = {}; + webidl.errors.exception = function(message) { + return new TypeError(`${message.header}: ${message.message}`); + }; + webidl.errors.conversionFailed = function(context) { + const plural = context.types.length === 1 ? "" : " one of"; + const message = `${context.argument} could not be converted to${plural}: ${context.types.join(", ")}.`; + return webidl.errors.exception({ + header: context.prefix, + message + }); + }; + webidl.errors.invalidArgument = function(context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }); + }; + webidl.brandCheck = function(V, I, opts) { + if (opts?.strict !== false) { + if (!(V instanceof I)) { + const err = new TypeError("Illegal invocation"); + err.code = "ERR_INVALID_THIS"; + throw err; + } + } else { + if (V?.[Symbol.toStringTag] !== I.prototype[Symbol.toStringTag]) { + const err = new TypeError("Illegal invocation"); + err.code = "ERR_INVALID_THIS"; + throw err; + } + } + }; + webidl.argumentLengthCheck = function({ length: length2 }, min, ctx) { + if (length2 < min) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? "s" : ""} required, but${length2 ? " only" : ""} ${length2} found.`, + header: ctx + }); + } + }; + webidl.illegalConstructor = function() { + throw webidl.errors.exception({ + header: "TypeError", + message: "Illegal constructor" + }); + }; + webidl.util.Type = function(V) { + switch (typeof V) { + case "undefined": + return "Undefined"; + case "boolean": + return "Boolean"; + case "string": + return "String"; + case "symbol": + return "Symbol"; + case "number": + return "Number"; + case "bigint": + return "BigInt"; + case "function": + case "object": { + if (V === null) { + return "Null"; + } + return "Object"; + } + } + }; + webidl.util.markAsUncloneable = markAsUncloneable || (() => { + }); + webidl.util.ConvertToInt = function(V, bitLength, signedness, opts) { + let upperBound; + let lowerBound; + if (bitLength === 64) { + upperBound = Math.pow(2, 53) - 1; + if (signedness === "unsigned") { + lowerBound = 0; + } else { + lowerBound = Math.pow(-2, 53) + 1; + } + } else if (signedness === "unsigned") { + lowerBound = 0; + upperBound = Math.pow(2, bitLength) - 1; + } else { + lowerBound = Math.pow(-2, bitLength) - 1; + upperBound = Math.pow(2, bitLength - 1) - 1; + } + let x = Number(V); + if (x === 0) { + x = 0; + } + if (opts?.enforceRange === true) { + if (Number.isNaN(x) || x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY) { + throw webidl.errors.exception({ + header: "Integer conversion", + message: `Could not convert ${webidl.util.Stringify(V)} to an integer.` + }); + } + x = webidl.util.IntegerPart(x); + if (x < lowerBound || x > upperBound) { + throw webidl.errors.exception({ + header: "Integer conversion", + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }); + } + return x; + } + if (!Number.isNaN(x) && opts?.clamp === true) { + x = Math.min(Math.max(x, lowerBound), upperBound); + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x); + } else { + x = Math.ceil(x); + } + return x; + } + if (Number.isNaN(x) || x === 0 && Object.is(0, x) || x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY) { + return 0; + } + x = webidl.util.IntegerPart(x); + x = x % Math.pow(2, bitLength); + if (signedness === "signed" && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength); + } + return x; + }; + webidl.util.IntegerPart = function(n) { + const r = Math.floor(Math.abs(n)); + if (n < 0) { + return -1 * r; + } + return r; + }; + webidl.util.Stringify = function(V) { + const type = webidl.util.Type(V); + switch (type) { + case "Symbol": + return `Symbol(${V.description})`; + case "Object": + return inspect(V); + case "String": + return `"${V}"`; + default: + return `${V}`; + } + }; + webidl.sequenceConverter = function(converter) { + return (V, prefix, argument, Iterable) => { + if (webidl.util.Type(V) !== "Object") { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} (${webidl.util.Stringify(V)}) is not iterable.` + }); + } + const method = typeof Iterable === "function" ? Iterable() : V?.[Symbol.iterator]?.(); + const seq = []; + let index = 0; + if (method === void 0 || typeof method.next !== "function") { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} is not iterable.` + }); + } + while (true) { + const { done, value } = method.next(); + if (done) { + break; + } + seq.push(converter(value, prefix, `${argument}[${index++}]`)); + } + return seq; + }; + }; + webidl.recordConverter = function(keyConverter, valueConverter) { + return (O, prefix, argument) => { + if (webidl.util.Type(O) !== "Object") { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} ("${webidl.util.Type(O)}") is not an Object.` + }); + } + const result = {}; + if (!types.isProxy(O)) { + const keys2 = [...Object.getOwnPropertyNames(O), ...Object.getOwnPropertySymbols(O)]; + for (const key of keys2) { + const typedKey = keyConverter(key, prefix, argument); + const typedValue = valueConverter(O[key], prefix, argument); + result[typedKey] = typedValue; + } + return result; + } + const keys = Reflect.ownKeys(O); + for (const key of keys) { + const desc = Reflect.getOwnPropertyDescriptor(O, key); + if (desc?.enumerable) { + const typedKey = keyConverter(key, prefix, argument); + const typedValue = valueConverter(O[key], prefix, argument); + result[typedKey] = typedValue; + } + } + return result; + }; + }; + webidl.interfaceConverter = function(i) { + return (V, prefix, argument, opts) => { + if (opts?.strict !== false && !(V instanceof i)) { + throw webidl.errors.exception({ + header: prefix, + message: `Expected ${argument} ("${webidl.util.Stringify(V)}") to be an instance of ${i.name}.` + }); + } + return V; + }; + }; + webidl.dictionaryConverter = function(converters) { + return (dictionary, prefix, argument) => { + const type = webidl.util.Type(dictionary); + const dict = {}; + if (type === "Null" || type === "Undefined") { + return dict; + } else if (type !== "Object") { + throw webidl.errors.exception({ + header: prefix, + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }); + } + for (const options of converters) { + const { key, defaultValue, required, converter } = options; + if (required === true) { + if (!Object.hasOwn(dictionary, key)) { + throw webidl.errors.exception({ + header: prefix, + message: `Missing required key "${key}".` + }); + } + } + let value = dictionary[key]; + const hasDefault = Object.hasOwn(options, "defaultValue"); + if (hasDefault && value !== null) { + value ??= defaultValue(); + } + if (required || hasDefault || value !== void 0) { + value = converter(value, prefix, `${argument}.${key}`); + if (options.allowedValues && !options.allowedValues.includes(value)) { + throw webidl.errors.exception({ + header: prefix, + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(", ")}.` + }); + } + dict[key] = value; + } + } + return dict; + }; + }; + webidl.nullableConverter = function(converter) { + return (V, prefix, argument) => { + if (V === null) { + return V; + } + return converter(V, prefix, argument); + }; + }; + webidl.converters.DOMString = function(V, prefix, argument, opts) { + if (V === null && opts?.legacyNullToEmptyString) { + return ""; + } + if (typeof V === "symbol") { + throw webidl.errors.exception({ + header: prefix, + message: `${argument} is a symbol, which cannot be converted to a DOMString.` + }); + } + return String(V); + }; + webidl.converters.ByteString = function(V, prefix, argument) { + const x = webidl.converters.DOMString(V, prefix, argument); + for (let index = 0; index < x.length; index++) { + if (x.charCodeAt(index) > 255) { + throw new TypeError( + `Cannot convert argument to a ByteString because the character at index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` + ); + } + } + return x; + }; + webidl.converters.USVString = toUSVString; + webidl.converters.boolean = function(V) { + const x = Boolean(V); + return x; + }; + webidl.converters.any = function(V) { + return V; + }; + webidl.converters["long long"] = function(V, prefix, argument) { + const x = webidl.util.ConvertToInt(V, 64, "signed", void 0, prefix, argument); + return x; + }; + webidl.converters["unsigned long long"] = function(V, prefix, argument) { + const x = webidl.util.ConvertToInt(V, 64, "unsigned", void 0, prefix, argument); + return x; + }; + webidl.converters["unsigned long"] = function(V, prefix, argument) { + const x = webidl.util.ConvertToInt(V, 32, "unsigned", void 0, prefix, argument); + return x; + }; + webidl.converters["unsigned short"] = function(V, prefix, argument, opts) { + const x = webidl.util.ConvertToInt(V, 16, "unsigned", opts, prefix, argument); + return x; + }; + webidl.converters.ArrayBuffer = function(V, prefix, argument, opts) { + if (webidl.util.Type(V) !== "Object" || !types.isAnyArrayBuffer(V)) { + throw webidl.errors.conversionFailed({ + prefix, + argument: `${argument} ("${webidl.util.Stringify(V)}")`, + types: ["ArrayBuffer"] + }); + } + if (opts?.allowShared === false && types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + if (V.resizable || V.growable) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "Received a resizable ArrayBuffer." + }); + } + return V; + }; + webidl.converters.TypedArray = function(V, T, prefix, name, opts) { + if (webidl.util.Type(V) !== "Object" || !types.isTypedArray(V) || V.constructor.name !== T.name) { + throw webidl.errors.conversionFailed({ + prefix, + argument: `${name} ("${webidl.util.Stringify(V)}")`, + types: [T.name] + }); + } + if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + if (V.buffer.resizable || V.buffer.growable) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "Received a resizable ArrayBuffer." + }); + } + return V; + }; + webidl.converters.DataView = function(V, prefix, name, opts) { + if (webidl.util.Type(V) !== "Object" || !types.isDataView(V)) { + throw webidl.errors.exception({ + header: prefix, + message: `${name} is not a DataView.` + }); + } + if (opts?.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + if (V.buffer.resizable || V.buffer.growable) { + throw webidl.errors.exception({ + header: "ArrayBuffer", + message: "Received a resizable ArrayBuffer." + }); + } + return V; + }; + webidl.converters.BufferSource = function(V, prefix, name, opts) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, prefix, name, { ...opts, allowShared: false }); + } + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor, prefix, name, { ...opts, allowShared: false }); + } + if (types.isDataView(V)) { + return webidl.converters.DataView(V, prefix, name, { ...opts, allowShared: false }); + } + throw webidl.errors.conversionFailed({ + prefix, + argument: `${name} ("${webidl.util.Stringify(V)}")`, + types: ["BufferSource"] + }); + }; + webidl.converters["sequence"] = webidl.sequenceConverter( + webidl.converters.ByteString + ); + webidl.converters["sequence>"] = webidl.sequenceConverter( + webidl.converters["sequence"] + ); + webidl.converters["record"] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString + ); + module.exports = { + webidl + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/util.js +var require_util2 = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/util.js"(exports, module) { + "use strict"; + var { Transform } = __require("node:stream"); + var zlib = __require("node:zlib"); + var { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = require_constants3(); + var { getGlobalOrigin } = require_global(); + var { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require_data_url(); + var { performance: performance2 } = __require("node:perf_hooks"); + var { isBlobLike, ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require_util(); + var assert = __require("node:assert"); + var { isUint8Array } = __require("node:util/types"); + var { webidl } = require_webidl(); + var supportedHashes = []; + var crypto2; + try { + crypto2 = __require("node:crypto"); + const possibleRelevantHashes = ["sha256", "sha384", "sha512"]; + supportedHashes = crypto2.getHashes().filter((hash) => possibleRelevantHashes.includes(hash)); + } catch { + } + function responseURL(response) { + const urlList = response.urlList; + const length2 = urlList.length; + return length2 === 0 ? null : urlList[length2 - 1].toString(); + } + function responseLocationURL(response, requestFragment) { + if (!redirectStatusSet.has(response.status)) { + return null; + } + let location = response.headersList.get("location", true); + if (location !== null && isValidHeaderValue(location)) { + if (!isValidEncodedURL(location)) { + location = normalizeBinaryStringToUtf8(location); + } + location = new URL(location, responseURL(response)); + } + if (location && !location.hash) { + location.hash = requestFragment; + } + return location; + } + function isValidEncodedURL(url) { + for (let i = 0; i < url.length; ++i) { + const code = url.charCodeAt(i); + if (code > 126 || // Non-US-ASCII + DEL + code < 32) { + return false; + } + } + return true; + } + function normalizeBinaryStringToUtf8(value) { + return Buffer.from(value, "binary").toString("utf8"); + } + function requestCurrentURL(request) { + return request.urlList[request.urlList.length - 1]; + } + function requestBadPort(request) { + const url = requestCurrentURL(request); + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { + return "blocked"; + } + return "allowed"; + } + function isErrorLike(object) { + return object instanceof Error || (object?.constructor?.name === "Error" || object?.constructor?.name === "DOMException"); + } + function isValidReasonPhrase(statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i); + if (!(c === 9 || // HTAB + c >= 32 && c <= 126 || // SP / VCHAR + c >= 128 && c <= 255)) { + return false; + } + } + return true; + } + var isValidHeaderName = isValidHTTPToken; + function isValidHeaderValue(potentialValue) { + return (potentialValue[0] === " " || potentialValue[0] === " " || potentialValue[potentialValue.length - 1] === " " || potentialValue[potentialValue.length - 1] === " " || potentialValue.includes("\n") || potentialValue.includes("\r") || potentialValue.includes("\0")) === false; + } + function setRequestReferrerPolicyOnRedirect(request, actualResponse) { + const { headersList } = actualResponse; + const policyHeader = (headersList.get("referrer-policy", true) ?? "").split(","); + let policy = ""; + if (policyHeader.length > 0) { + for (let i = policyHeader.length; i !== 0; i--) { + const token = policyHeader[i - 1].trim(); + if (referrerPolicyTokens.has(token)) { + policy = token; + break; + } + } + } + if (policy !== "") { + request.referrerPolicy = policy; + } + } + function crossOriginResourcePolicyCheck() { + return "allowed"; + } + function corsCheck() { + return "success"; + } + function TAOCheck() { + return "success"; + } + function appendFetchMetadata(httpRequest) { + let header = null; + header = httpRequest.mode; + httpRequest.headersList.set("sec-fetch-mode", header, true); + } + function appendRequestOriginHeader(request) { + let serializedOrigin = request.origin; + if (serializedOrigin === "client" || serializedOrigin === void 0) { + return; + } + if (request.responseTainting === "cors" || request.mode === "websocket") { + request.headersList.append("origin", serializedOrigin, true); + } else if (request.method !== "GET" && request.method !== "HEAD") { + switch (request.referrerPolicy) { + case "no-referrer": + serializedOrigin = null; + break; + case "no-referrer-when-downgrade": + case "strict-origin": + case "strict-origin-when-cross-origin": + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { + serializedOrigin = null; + } + break; + case "same-origin": + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null; + } + break; + default: + } + request.headersList.append("origin", serializedOrigin, true); + } + } + function coarsenTime(timestamp, crossOriginIsolatedCapability) { + return timestamp; + } + function clampAndCoarsenConnectionTimingInfo(connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) { + if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) { + return { + domainLookupStartTime: defaultStartTime, + domainLookupEndTime: defaultStartTime, + connectionStartTime: defaultStartTime, + connectionEndTime: defaultStartTime, + secureConnectionStartTime: defaultStartTime, + ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol + }; + } + return { + domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability), + domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability), + connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability), + connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability), + secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability), + ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol + }; + } + function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) { + return coarsenTime(performance2.now(), crossOriginIsolatedCapability); + } + function createOpaqueTimingInfo(timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + }; + } + function makePolicyContainer() { + return { + referrerPolicy: "strict-origin-when-cross-origin" + }; + } + function clonePolicyContainer(policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + }; + } + function determineRequestsReferrer(request) { + const policy = request.referrerPolicy; + assert(policy); + let referrerSource = null; + if (request.referrer === "client") { + const globalOrigin = getGlobalOrigin(); + if (!globalOrigin || globalOrigin.origin === "null") { + return "no-referrer"; + } + referrerSource = new URL(globalOrigin); + } else if (request.referrer instanceof URL) { + referrerSource = request.referrer; + } + let referrerURL = stripURLForReferrer(referrerSource); + const referrerOrigin = stripURLForReferrer(referrerSource, true); + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin; + } + const areSameOrigin = sameOrigin(request, referrerURL); + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(request.url); + switch (policy) { + case "origin": + return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true); + case "unsafe-url": + return referrerURL; + case "same-origin": + return areSameOrigin ? referrerOrigin : "no-referrer"; + case "origin-when-cross-origin": + return areSameOrigin ? referrerURL : referrerOrigin; + case "strict-origin-when-cross-origin": { + const currentURL = requestCurrentURL(request); + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL; + } + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return "no-referrer"; + } + return referrerOrigin; + } + case "strict-origin": + // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case "no-referrer-when-downgrade": + // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + default: + return isNonPotentiallyTrustWorthy ? "no-referrer" : referrerOrigin; + } + } + function stripURLForReferrer(url, originOnly) { + assert(url instanceof URL); + url = new URL(url); + if (url.protocol === "file:" || url.protocol === "about:" || url.protocol === "blank:") { + return "no-referrer"; + } + url.username = ""; + url.password = ""; + url.hash = ""; + if (originOnly) { + url.pathname = ""; + url.search = ""; + } + return url; + } + function isURLPotentiallyTrustworthy(url) { + if (!(url instanceof URL)) { + return false; + } + if (url.href === "about:blank" || url.href === "about:srcdoc") { + return true; + } + if (url.protocol === "data:") return true; + if (url.protocol === "file:") return true; + return isOriginPotentiallyTrustworthy(url.origin); + function isOriginPotentiallyTrustworthy(origin) { + if (origin == null || origin === "null") return false; + const originAsURL = new URL(origin); + if (originAsURL.protocol === "https:" || originAsURL.protocol === "wss:") { + return true; + } + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || (originAsURL.hostname === "localhost" || originAsURL.hostname.includes("localhost.")) || originAsURL.hostname.endsWith(".localhost")) { + return true; + } + return false; + } + } + function bytesMatch(bytes2, metadataList) { + if (crypto2 === void 0) { + return true; + } + const parsedMetadata = parseMetadata(metadataList); + if (parsedMetadata === "no metadata") { + return true; + } + if (parsedMetadata.length === 0) { + return true; + } + const strongest = getStrongestMetadata(parsedMetadata); + const metadata = filterMetadataListByAlgorithm(parsedMetadata, strongest); + for (const item of metadata) { + const algorithm = item.algo; + const expectedValue = item.hash; + let actualValue = crypto2.createHash(algorithm).update(bytes2).digest("base64"); + if (actualValue[actualValue.length - 1] === "=") { + if (actualValue[actualValue.length - 2] === "=") { + actualValue = actualValue.slice(0, -2); + } else { + actualValue = actualValue.slice(0, -1); + } + } + if (compareBase64Mixed(actualValue, expectedValue)) { + return true; + } + } + return false; + } + var parseHashWithOptions = /(?sha256|sha384|sha512)-((?[A-Za-z0-9+/]+|[A-Za-z0-9_-]+)={0,2}(?:\s|$)( +[!-~]*)?)?/i; + function parseMetadata(metadata) { + const result = []; + let empty = true; + for (const token of metadata.split(" ")) { + empty = false; + const parsedToken = parseHashWithOptions.exec(token); + if (parsedToken === null || parsedToken.groups === void 0 || parsedToken.groups.algo === void 0) { + continue; + } + const algorithm = parsedToken.groups.algo.toLowerCase(); + if (supportedHashes.includes(algorithm)) { + result.push(parsedToken.groups); + } + } + if (empty === true) { + return "no metadata"; + } + return result; + } + function getStrongestMetadata(metadataList) { + let algorithm = metadataList[0].algo; + if (algorithm[3] === "5") { + return algorithm; + } + for (let i = 1; i < metadataList.length; ++i) { + const metadata = metadataList[i]; + if (metadata.algo[3] === "5") { + algorithm = "sha512"; + break; + } else if (algorithm[3] === "3") { + continue; + } else if (metadata.algo[3] === "3") { + algorithm = "sha384"; + } + } + return algorithm; + } + function filterMetadataListByAlgorithm(metadataList, algorithm) { + if (metadataList.length === 1) { + return metadataList; + } + let pos = 0; + for (let i = 0; i < metadataList.length; ++i) { + if (metadataList[i].algo === algorithm) { + metadataList[pos++] = metadataList[i]; + } + } + metadataList.length = pos; + return metadataList; + } + function compareBase64Mixed(actualValue, expectedValue) { + if (actualValue.length !== expectedValue.length) { + return false; + } + for (let i = 0; i < actualValue.length; ++i) { + if (actualValue[i] !== expectedValue[i]) { + if (actualValue[i] === "+" && expectedValue[i] === "-" || actualValue[i] === "/" && expectedValue[i] === "_") { + continue; + } + return false; + } + } + return true; + } + function tryUpgradeRequestToAPotentiallyTrustworthyURL(request) { + } + function sameOrigin(A, B) { + if (A.origin === B.origin && A.origin === "null") { + return true; + } + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true; + } + return false; + } + function createDeferredPromise() { + let res; + let rej; + const promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return { promise, resolve: res, reject: rej }; + } + function isAborted(fetchParams) { + return fetchParams.controller.state === "aborted"; + } + function isCancelled(fetchParams) { + return fetchParams.controller.state === "aborted" || fetchParams.controller.state === "terminated"; + } + function normalizeMethod(method) { + return normalizedMethodRecordsBase[method.toLowerCase()] ?? method; + } + function serializeJavascriptValueToJSONString(value) { + const result = JSON.stringify(value); + if (result === void 0) { + throw new TypeError("Value is not JSON serializable"); + } + assert(typeof result === "string"); + return result; + } + var esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())); + function createIterator(name, kInternalIterator, keyIndex = 0, valueIndex = 1) { + class FastIterableIterator { + /** @type {any} */ + #target; + /** @type {'key' | 'value' | 'key+value'} */ + #kind; + /** @type {number} */ + #index; + /** + * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object + * @param {unknown} target + * @param {'key' | 'value' | 'key+value'} kind + */ + constructor(target, kind) { + this.#target = target; + this.#kind = kind; + this.#index = 0; + } + next() { + if (typeof this !== "object" || this === null || !(#target in this)) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ); + } + const index = this.#index; + const values = this.#target[kInternalIterator]; + const len = values.length; + if (index >= len) { + return { + value: void 0, + done: true + }; + } + const { [keyIndex]: key, [valueIndex]: value } = values[index]; + this.#index = index + 1; + let result; + switch (this.#kind) { + case "key": + result = key; + break; + case "value": + result = value; + break; + case "key+value": + result = [key, value]; + break; + } + return { + value: result, + done: false + }; + } + } + delete FastIterableIterator.prototype.constructor; + Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype); + Object.defineProperties(FastIterableIterator.prototype, { + [Symbol.toStringTag]: { + writable: false, + enumerable: false, + configurable: true, + value: `${name} Iterator` + }, + next: { writable: true, enumerable: true, configurable: true } + }); + return function(target, kind) { + return new FastIterableIterator(target, kind); + }; + } + function iteratorMixin(name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) { + const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex); + const properties = { + keys: { + writable: true, + enumerable: true, + configurable: true, + value: function keys() { + webidl.brandCheck(this, object); + return makeIterator(this, "key"); + } + }, + values: { + writable: true, + enumerable: true, + configurable: true, + value: function values() { + webidl.brandCheck(this, object); + return makeIterator(this, "value"); + } + }, + entries: { + writable: true, + enumerable: true, + configurable: true, + value: function entries() { + webidl.brandCheck(this, object); + return makeIterator(this, "key+value"); + } + }, + forEach: { + writable: true, + enumerable: true, + configurable: true, + value: function forEach(callbackfn, thisArg = globalThis) { + webidl.brandCheck(this, object); + webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`); + if (typeof callbackfn !== "function") { + throw new TypeError( + `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.` + ); + } + for (const { 0: key, 1: value } of makeIterator(this, "key+value")) { + callbackfn.call(thisArg, value, key, this); + } + } + } + }; + return Object.defineProperties(object.prototype, { + ...properties, + [Symbol.iterator]: { + writable: true, + enumerable: false, + configurable: true, + value: properties.entries.value + } + }); + } + async function fullyReadBody(body, processBody, processBodyError) { + const successSteps = processBody; + const errorSteps = processBodyError; + let reader; + try { + reader = body.stream.getReader(); + } catch (e) { + errorSteps(e); + return; + } + try { + successSteps(await readAllBytes(reader)); + } catch (e) { + errorSteps(e); + } + } + function isReadableStreamLike(stream) { + return stream instanceof ReadableStream || stream[Symbol.toStringTag] === "ReadableStream" && typeof stream.tee === "function"; + } + function readableStreamClose(controller) { + try { + controller.close(); + controller.byobRequest?.respond(0); + } catch (err) { + if (!err.message.includes("Controller is already closed") && !err.message.includes("ReadableStream is already closed")) { + throw err; + } + } + } + var invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/; + function isomorphicEncode(input) { + assert(!invalidIsomorphicEncodeValueRegex.test(input)); + return input; + } + async function readAllBytes(reader) { + const bytes2 = []; + let byteLength = 0; + while (true) { + const { done, value: chunk } = await reader.read(); + if (done) { + return Buffer.concat(bytes2, byteLength); + } + if (!isUint8Array(chunk)) { + throw new TypeError("Received non-Uint8Array chunk"); + } + bytes2.push(chunk); + byteLength += chunk.length; + } + } + function urlIsLocal(url) { + assert("protocol" in url); + const protocol = url.protocol; + return protocol === "about:" || protocol === "blob:" || protocol === "data:"; + } + function urlHasHttpsScheme(url) { + return typeof url === "string" && url[5] === ":" && url[0] === "h" && url[1] === "t" && url[2] === "t" && url[3] === "p" && url[4] === "s" || url.protocol === "https:"; + } + function urlIsHttpHttpsScheme(url) { + assert("protocol" in url); + const protocol = url.protocol; + return protocol === "http:" || protocol === "https:"; + } + function simpleRangeHeaderValue(value, allowWhitespace) { + const data = value; + if (!data.startsWith("bytes")) { + return "failure"; + } + const position = { position: 5 }; + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === " " || char === " ", + data, + position + ); + } + if (data.charCodeAt(position.position) !== 61) { + return "failure"; + } + position.position++; + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === " " || char === " ", + data, + position + ); + } + const rangeStart = collectASequenceOfCodePoints( + (char) => { + const code = char.charCodeAt(0); + return code >= 48 && code <= 57; + }, + data, + position + ); + const rangeStartValue = rangeStart.length ? Number(rangeStart) : null; + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === " " || char === " ", + data, + position + ); + } + if (data.charCodeAt(position.position) !== 45) { + return "failure"; + } + position.position++; + if (allowWhitespace) { + collectASequenceOfCodePoints( + (char) => char === " " || char === " ", + data, + position + ); + } + const rangeEnd = collectASequenceOfCodePoints( + (char) => { + const code = char.charCodeAt(0); + return code >= 48 && code <= 57; + }, + data, + position + ); + const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null; + if (position.position < data.length) { + return "failure"; + } + if (rangeEndValue === null && rangeStartValue === null) { + return "failure"; + } + if (rangeStartValue > rangeEndValue) { + return "failure"; + } + return { rangeStartValue, rangeEndValue }; + } + function buildContentRange(rangeStart, rangeEnd, fullLength) { + let contentRange = "bytes "; + contentRange += isomorphicEncode(`${rangeStart}`); + contentRange += "-"; + contentRange += isomorphicEncode(`${rangeEnd}`); + contentRange += "/"; + contentRange += isomorphicEncode(`${fullLength}`); + return contentRange; + } + var InflateStream = class extends Transform { + #zlibOptions; + /** @param {zlib.ZlibOptions} [zlibOptions] */ + constructor(zlibOptions) { + super(); + this.#zlibOptions = zlibOptions; + } + _transform(chunk, encoding, callback) { + if (!this._inflateStream) { + if (chunk.length === 0) { + callback(); + return; + } + this._inflateStream = (chunk[0] & 15) === 8 ? zlib.createInflate(this.#zlibOptions) : zlib.createInflateRaw(this.#zlibOptions); + this._inflateStream.on("data", this.push.bind(this)); + this._inflateStream.on("end", () => this.push(null)); + this._inflateStream.on("error", (err) => this.destroy(err)); + } + this._inflateStream.write(chunk, encoding, callback); + } + _final(callback) { + if (this._inflateStream) { + this._inflateStream.end(); + this._inflateStream = null; + } + callback(); + } + }; + function createInflate(zlibOptions) { + return new InflateStream(zlibOptions); + } + function extractMimeType(headers) { + let charset = null; + let essence = null; + let mimeType = null; + const values = getDecodeSplit("content-type", headers); + if (values === null) { + return "failure"; + } + for (const value of values) { + const temporaryMimeType = parseMIMEType(value); + if (temporaryMimeType === "failure" || temporaryMimeType.essence === "*/*") { + continue; + } + mimeType = temporaryMimeType; + if (mimeType.essence !== essence) { + charset = null; + if (mimeType.parameters.has("charset")) { + charset = mimeType.parameters.get("charset"); + } + essence = mimeType.essence; + } else if (!mimeType.parameters.has("charset") && charset !== null) { + mimeType.parameters.set("charset", charset); + } + } + if (mimeType == null) { + return "failure"; + } + return mimeType; + } + function gettingDecodingSplitting(value) { + const input = value; + const position = { position: 0 }; + const values = []; + let temporaryValue = ""; + while (position.position < input.length) { + temporaryValue += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== ",", + input, + position + ); + if (position.position < input.length) { + if (input.charCodeAt(position.position) === 34) { + temporaryValue += collectAnHTTPQuotedString( + input, + position + ); + if (position.position < input.length) { + continue; + } + } else { + assert(input.charCodeAt(position.position) === 44); + position.position++; + } + } + temporaryValue = removeChars(temporaryValue, true, true, (char) => char === 9 || char === 32); + values.push(temporaryValue); + temporaryValue = ""; + } + return values; + } + function getDecodeSplit(name, list2) { + const value = list2.get(name, true); + if (value === null) { + return null; + } + return gettingDecodingSplitting(value); + } + var textDecoder = new TextDecoder(); + function utf8DecodeBytes(buffer) { + if (buffer.length === 0) { + return ""; + } + if (buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191) { + buffer = buffer.subarray(3); + } + const output = textDecoder.decode(buffer); + return output; + } + var EnvironmentSettingsObjectBase = class { + get baseUrl() { + return getGlobalOrigin(); + } + get origin() { + return this.baseUrl?.origin; + } + policyContainer = makePolicyContainer(); + }; + var EnvironmentSettingsObject = class { + settingsObject = new EnvironmentSettingsObjectBase(); + }; + var environmentSettingsObject = new EnvironmentSettingsObject(); + module.exports = { + isAborted, + isCancelled, + isValidEncodedURL, + createDeferredPromise, + ReadableStreamFrom, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + clampAndCoarsenConnectionTimingInfo, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + iteratorMixin, + createIterator, + isValidHeaderName, + isValidHeaderValue, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme, + readAllBytes, + simpleRangeHeaderValue, + buildContentRange, + parseMetadata, + createInflate, + extractMimeType, + getDecodeSplit, + utf8DecodeBytes, + environmentSettingsObject + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/symbols.js +var require_symbols2 = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/symbols.js"(exports, module) { + "use strict"; + module.exports = { + kUrl: /* @__PURE__ */ Symbol("url"), + kHeaders: /* @__PURE__ */ Symbol("headers"), + kSignal: /* @__PURE__ */ Symbol("signal"), + kState: /* @__PURE__ */ Symbol("state"), + kDispatcher: /* @__PURE__ */ Symbol("dispatcher") + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/file.js +var require_file = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/file.js"(exports, module) { + "use strict"; + var { Blob: Blob2, File } = __require("node:buffer"); + var { kState } = require_symbols2(); + var { webidl } = require_webidl(); + var FileLike = class _FileLike { + constructor(blobLike, fileName, options = {}) { + const n = fileName; + const t = options.type; + const d = options.lastModified ?? Date.now(); + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + }; + } + stream(...args) { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.stream(...args); + } + arrayBuffer(...args) { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.arrayBuffer(...args); + } + slice(...args) { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.slice(...args); + } + text(...args) { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.text(...args); + } + get size() { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.size; + } + get type() { + webidl.brandCheck(this, _FileLike); + return this[kState].blobLike.type; + } + get name() { + webidl.brandCheck(this, _FileLike); + return this[kState].name; + } + get lastModified() { + webidl.brandCheck(this, _FileLike); + return this[kState].lastModified; + } + get [Symbol.toStringTag]() { + return "File"; + } + }; + webidl.converters.Blob = webidl.interfaceConverter(Blob2); + function isFileLike(object) { + return object instanceof File || object && (typeof object.stream === "function" || typeof object.arrayBuffer === "function") && object[Symbol.toStringTag] === "File"; + } + module.exports = { FileLike, isFileLike }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/formdata.js +var require_formdata = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/formdata.js"(exports, module) { + "use strict"; + var { isBlobLike, iteratorMixin } = require_util2(); + var { kState } = require_symbols2(); + var { kEnumerableProperty } = require_util(); + var { FileLike, isFileLike } = require_file(); + var { webidl } = require_webidl(); + var { File: NativeFile } = __require("node:buffer"); + var nodeUtil = __require("node:util"); + var File = globalThis.File ?? NativeFile; + var FormData = class _FormData { + constructor(form) { + webidl.util.markAsUncloneable(this); + if (form !== void 0) { + throw webidl.errors.conversionFailed({ + prefix: "FormData constructor", + argument: "Argument 1", + types: ["undefined"] + }); + } + this[kState] = []; + } + append(name, value, filename = void 0) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.append"; + webidl.argumentLengthCheck(arguments, 2, prefix); + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ); + } + name = webidl.converters.USVString(name, prefix, "name"); + value = isBlobLike(value) ? webidl.converters.Blob(value, prefix, "value", { strict: false }) : webidl.converters.USVString(value, prefix, "value"); + filename = arguments.length === 3 ? webidl.converters.USVString(filename, prefix, "filename") : void 0; + const entry = makeEntry(name, value, filename); + this[kState].push(entry); + } + delete(name) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.delete"; + webidl.argumentLengthCheck(arguments, 1, prefix); + name = webidl.converters.USVString(name, prefix, "name"); + this[kState] = this[kState].filter((entry) => entry.name !== name); + } + get(name) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.get"; + webidl.argumentLengthCheck(arguments, 1, prefix); + name = webidl.converters.USVString(name, prefix, "name"); + const idx = this[kState].findIndex((entry) => entry.name === name); + if (idx === -1) { + return null; + } + return this[kState][idx].value; + } + getAll(name) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.getAll"; + webidl.argumentLengthCheck(arguments, 1, prefix); + name = webidl.converters.USVString(name, prefix, "name"); + return this[kState].filter((entry) => entry.name === name).map((entry) => entry.value); + } + has(name) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.has"; + webidl.argumentLengthCheck(arguments, 1, prefix); + name = webidl.converters.USVString(name, prefix, "name"); + return this[kState].findIndex((entry) => entry.name === name) !== -1; + } + set(name, value, filename = void 0) { + webidl.brandCheck(this, _FormData); + const prefix = "FormData.set"; + webidl.argumentLengthCheck(arguments, 2, prefix); + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ); + } + name = webidl.converters.USVString(name, prefix, "name"); + value = isBlobLike(value) ? webidl.converters.Blob(value, prefix, "name", { strict: false }) : webidl.converters.USVString(value, prefix, "name"); + filename = arguments.length === 3 ? webidl.converters.USVString(filename, prefix, "name") : void 0; + const entry = makeEntry(name, value, filename); + const idx = this[kState].findIndex((entry2) => entry2.name === name); + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry2) => entry2.name !== name) + ]; + } else { + this[kState].push(entry); + } + } + [nodeUtil.inspect.custom](depth, options) { + const state = this[kState].reduce((a, b) => { + if (a[b.name]) { + if (Array.isArray(a[b.name])) { + a[b.name].push(b.value); + } else { + a[b.name] = [a[b.name], b.value]; + } + } else { + a[b.name] = b.value; + } + return a; + }, { __proto__: null }); + options.depth ??= depth; + options.colors ??= true; + const output = nodeUtil.formatWithOptions(options, state); + return `FormData ${output.slice(output.indexOf("]") + 2)}`; + } + }; + iteratorMixin("FormData", FormData, kState, "name", "value"); + Object.defineProperties(FormData.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + getAll: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "FormData", + configurable: true + } + }); + function makeEntry(name, value, filename) { + if (typeof value === "string") { + } else { + if (!isFileLike(value)) { + value = value instanceof Blob ? new File([value], "blob", { type: value.type }) : new FileLike(value, "blob", { type: value.type }); + } + if (filename !== void 0) { + const options = { + type: value.type, + lastModified: value.lastModified + }; + value = value instanceof NativeFile ? new File([value], filename, options) : new FileLike(value, filename, options); + } + } + return { name, value }; + } + module.exports = { FormData, makeEntry }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/formdata-parser.js +var require_formdata_parser = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/formdata-parser.js"(exports, module) { + "use strict"; + var { isUSVString, bufferToLowerCasedHeaderName } = require_util(); + var { utf8DecodeBytes } = require_util2(); + var { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require_data_url(); + var { isFileLike } = require_file(); + var { makeEntry } = require_formdata(); + var assert = __require("node:assert"); + var { File: NodeFile } = __require("node:buffer"); + var File = globalThis.File ?? NodeFile; + var formDataNameBuffer = Buffer.from('form-data; name="'); + var filenameBuffer = Buffer.from("; filename"); + var dd = Buffer.from("--"); + var ddcrlf = Buffer.from("--\r\n"); + function isAsciiString(chars) { + for (let i = 0; i < chars.length; ++i) { + if ((chars.charCodeAt(i) & ~127) !== 0) { + return false; + } + } + return true; + } + function validateBoundary(boundary) { + const length2 = boundary.length; + if (length2 < 27 || length2 > 70) { + return false; + } + for (let i = 0; i < length2; ++i) { + const cp = boundary.charCodeAt(i); + if (!(cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 39 || cp === 45 || cp === 95)) { + return false; + } + } + return true; + } + function multipartFormDataParser(input, mimeType) { + assert(mimeType !== "failure" && mimeType.essence === "multipart/form-data"); + const boundaryString = mimeType.parameters.get("boundary"); + if (boundaryString === void 0) { + return "failure"; + } + const boundary = Buffer.from(`--${boundaryString}`, "utf8"); + const entryList = []; + const position = { position: 0 }; + while (input[position.position] === 13 && input[position.position + 1] === 10) { + position.position += 2; + } + let trailing = input.length; + while (input[trailing - 1] === 10 && input[trailing - 2] === 13) { + trailing -= 2; + } + if (trailing !== input.length) { + input = input.subarray(0, trailing); + } + while (true) { + if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) { + position.position += boundary.length; + } else { + return "failure"; + } + if (position.position === input.length - 2 && bufferStartsWith(input, dd, position) || position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position)) { + return entryList; + } + if (input[position.position] !== 13 || input[position.position + 1] !== 10) { + return "failure"; + } + position.position += 2; + const result = parseMultipartFormDataHeaders(input, position); + if (result === "failure") { + return "failure"; + } + let { name, filename, contentType, encoding } = result; + position.position += 2; + let body; + { + const boundaryIndex = input.indexOf(boundary.subarray(2), position.position); + if (boundaryIndex === -1) { + return "failure"; + } + body = input.subarray(position.position, boundaryIndex - 4); + position.position += body.length; + if (encoding === "base64") { + body = Buffer.from(body.toString(), "base64"); + } + } + if (input[position.position] !== 13 || input[position.position + 1] !== 10) { + return "failure"; + } else { + position.position += 2; + } + let value; + if (filename !== null) { + contentType ??= "text/plain"; + if (!isAsciiString(contentType)) { + contentType = ""; + } + value = new File([body], filename, { type: contentType }); + } else { + value = utf8DecodeBytes(Buffer.from(body)); + } + assert(isUSVString(name)); + assert(typeof value === "string" && isUSVString(value) || isFileLike(value)); + entryList.push(makeEntry(name, value, filename)); + } + } + function parseMultipartFormDataHeaders(input, position) { + let name = null; + let filename = null; + let contentType = null; + let encoding = null; + while (true) { + if (input[position.position] === 13 && input[position.position + 1] === 10) { + if (name === null) { + return "failure"; + } + return { name, filename, contentType, encoding }; + } + let headerName = collectASequenceOfBytes( + (char) => char !== 10 && char !== 13 && char !== 58, + input, + position + ); + headerName = removeChars(headerName, true, true, (char) => char === 9 || char === 32); + if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) { + return "failure"; + } + if (input[position.position] !== 58) { + return "failure"; + } + position.position++; + collectASequenceOfBytes( + (char) => char === 32 || char === 9, + input, + position + ); + switch (bufferToLowerCasedHeaderName(headerName)) { + case "content-disposition": { + name = filename = null; + if (!bufferStartsWith(input, formDataNameBuffer, position)) { + return "failure"; + } + position.position += 17; + name = parseMultipartFormDataName(input, position); + if (name === null) { + return "failure"; + } + if (bufferStartsWith(input, filenameBuffer, position)) { + let check = position.position + filenameBuffer.length; + if (input[check] === 42) { + position.position += 1; + check += 1; + } + if (input[check] !== 61 || input[check + 1] !== 34) { + return "failure"; + } + position.position += 12; + filename = parseMultipartFormDataName(input, position); + if (filename === null) { + return "failure"; + } + } + break; + } + case "content-type": { + let headerValue = collectASequenceOfBytes( + (char) => char !== 10 && char !== 13, + input, + position + ); + headerValue = removeChars(headerValue, false, true, (char) => char === 9 || char === 32); + contentType = isomorphicDecode(headerValue); + break; + } + case "content-transfer-encoding": { + let headerValue = collectASequenceOfBytes( + (char) => char !== 10 && char !== 13, + input, + position + ); + headerValue = removeChars(headerValue, false, true, (char) => char === 9 || char === 32); + encoding = isomorphicDecode(headerValue); + break; + } + default: { + collectASequenceOfBytes( + (char) => char !== 10 && char !== 13, + input, + position + ); + } + } + if (input[position.position] !== 13 && input[position.position + 1] !== 10) { + return "failure"; + } else { + position.position += 2; + } + } + } + function parseMultipartFormDataName(input, position) { + assert(input[position.position - 1] === 34); + let name = collectASequenceOfBytes( + (char) => char !== 10 && char !== 13 && char !== 34, + input, + position + ); + if (input[position.position] !== 34) { + return null; + } else { + position.position++; + } + name = new TextDecoder().decode(name).replace(/%0A/ig, "\n").replace(/%0D/ig, "\r").replace(/%22/g, '"'); + return name; + } + function collectASequenceOfBytes(condition, input, position) { + let start = position.position; + while (start < input.length && condition(input[start])) { + ++start; + } + return input.subarray(position.position, position.position = start); + } + function removeChars(buf, leading, trailing, predicate) { + let lead = 0; + let trail = buf.length - 1; + if (leading) { + while (lead < buf.length && predicate(buf[lead])) lead++; + } + if (trailing) { + while (trail > 0 && predicate(buf[trail])) trail--; + } + return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1); + } + function bufferStartsWith(buffer, start, position) { + if (buffer.length < start.length) { + return false; + } + for (let i = 0; i < start.length; i++) { + if (start[i] !== buffer[position.position + i]) { + return false; + } + } + return true; + } + module.exports = { + multipartFormDataParser, + validateBoundary + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/body.js +var require_body = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/body.js"(exports, module) { + "use strict"; + var util = require_util(); + var { + ReadableStreamFrom, + isBlobLike, + isReadableStreamLike, + readableStreamClose, + createDeferredPromise, + fullyReadBody, + extractMimeType, + utf8DecodeBytes + } = require_util2(); + var { FormData } = require_formdata(); + var { kState } = require_symbols2(); + var { webidl } = require_webidl(); + var { Blob: Blob2 } = __require("node:buffer"); + var assert = __require("node:assert"); + var { isErrored, isDisturbed } = __require("node:stream"); + var { isArrayBuffer } = __require("node:util/types"); + var { serializeAMimeType } = require_data_url(); + var { multipartFormDataParser } = require_formdata_parser(); + var random; + try { + const crypto2 = __require("node:crypto"); + random = (max) => crypto2.randomInt(0, max); + } catch { + random = (max) => Math.floor(Math.random(max)); + } + var textEncoder2 = new TextEncoder(); + function noop() { + } + var hasFinalizationRegistry = globalThis.FinalizationRegistry && process.version.indexOf("v18") !== 0; + var streamRegistry; + if (hasFinalizationRegistry) { + streamRegistry = new FinalizationRegistry((weakRef) => { + const stream = weakRef.deref(); + if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) { + stream.cancel("Response object has been garbage collected").catch(noop); + } + }); + } + function extractBody(object, keepalive = false) { + let stream = null; + if (object instanceof ReadableStream) { + stream = object; + } else if (isBlobLike(object)) { + stream = object.stream(); + } else { + stream = new ReadableStream({ + async pull(controller) { + const buffer = typeof source === "string" ? textEncoder2.encode(source) : source; + if (buffer.byteLength) { + controller.enqueue(buffer); + } + queueMicrotask(() => readableStreamClose(controller)); + }, + start() { + }, + type: "bytes" + }); + } + assert(isReadableStreamLike(stream)); + let action = null; + let source = null; + let length2 = null; + let type = null; + if (typeof object === "string") { + source = object; + type = "text/plain;charset=UTF-8"; + } else if (object instanceof URLSearchParams) { + source = object.toString(); + type = "application/x-www-form-urlencoded;charset=UTF-8"; + } else if (isArrayBuffer(object)) { + source = new Uint8Array(object.slice()); + } else if (ArrayBuffer.isView(object)) { + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)); + } else if (util.isFormDataLike(object)) { + const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, "0")}`; + const prefix = `--${boundary}\r +Content-Disposition: form-data`; + const escape = (str) => str.replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/"/g, "%22"); + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, "\r\n"); + const blobParts = []; + const rn = new Uint8Array([13, 10]); + length2 = 0; + let hasUnknownSizeValue = false; + for (const [name, value] of object) { + if (typeof value === "string") { + const chunk2 = textEncoder2.encode(prefix + `; name="${escape(normalizeLinefeeds(name))}"\r +\r +${normalizeLinefeeds(value)}\r +`); + blobParts.push(chunk2); + length2 += chunk2.byteLength; + } else { + const chunk2 = textEncoder2.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : "") + `\r +Content-Type: ${value.type || "application/octet-stream"}\r +\r +`); + blobParts.push(chunk2, value, rn); + if (typeof value.size === "number") { + length2 += chunk2.byteLength + value.size + rn.byteLength; + } else { + hasUnknownSizeValue = true; + } + } + } + const chunk = textEncoder2.encode(`--${boundary}--\r +`); + blobParts.push(chunk); + length2 += chunk.byteLength; + if (hasUnknownSizeValue) { + length2 = null; + } + source = object; + action = async function* () { + for (const part of blobParts) { + if (part.stream) { + yield* part.stream(); + } else { + yield part; + } + } + }; + type = `multipart/form-data; boundary=${boundary}`; + } else if (isBlobLike(object)) { + source = object; + length2 = object.size; + if (object.type) { + type = object.type; + } + } else if (typeof object[Symbol.asyncIterator] === "function") { + if (keepalive) { + throw new TypeError("keepalive"); + } + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + "Response body object should not be disturbed or locked" + ); + } + stream = object instanceof ReadableStream ? object : ReadableStreamFrom(object); + } + if (typeof source === "string" || util.isBuffer(source)) { + length2 = Buffer.byteLength(source); + } + if (action != null) { + let iterator; + stream = new ReadableStream({ + async start() { + iterator = action(object)[Symbol.asyncIterator](); + }, + async pull(controller) { + const { value, done } = await iterator.next(); + if (done) { + queueMicrotask(() => { + controller.close(); + controller.byobRequest?.respond(0); + }); + } else { + if (!isErrored(stream)) { + const buffer = new Uint8Array(value); + if (buffer.byteLength) { + controller.enqueue(buffer); + } + } + } + return controller.desiredSize > 0; + }, + async cancel(reason) { + await iterator.return(); + }, + type: "bytes" + }); + } + const body = { stream, source, length: length2 }; + return [body, type]; + } + function safelyExtractBody(object, keepalive = false) { + if (object instanceof ReadableStream) { + assert(!util.isDisturbed(object), "The body has already been consumed."); + assert(!object.locked, "The stream is locked."); + } + return extractBody(object, keepalive); + } + function cloneBody(instance, body) { + const [out1, out2] = body.stream.tee(); + body.stream = out1; + return { + stream: out2, + length: body.length, + source: body.source + }; + } + function throwIfAborted(state) { + if (state.aborted) { + throw new DOMException("The operation was aborted.", "AbortError"); + } + } + function bodyMixinMethods(instance) { + const methods = { + blob() { + return consumeBody(this, (bytes2) => { + let mimeType = bodyMimeType(this); + if (mimeType === null) { + mimeType = ""; + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType); + } + return new Blob2([bytes2], { type: mimeType }); + }, instance); + }, + arrayBuffer() { + return consumeBody(this, (bytes2) => { + return new Uint8Array(bytes2).buffer; + }, instance); + }, + text() { + return consumeBody(this, utf8DecodeBytes, instance); + }, + json() { + return consumeBody(this, parseJSONFromBytes, instance); + }, + formData() { + return consumeBody(this, (value) => { + const mimeType = bodyMimeType(this); + if (mimeType !== null) { + switch (mimeType.essence) { + case "multipart/form-data": { + const parsed = multipartFormDataParser(value, mimeType); + if (parsed === "failure") { + throw new TypeError("Failed to parse body as FormData."); + } + const fd = new FormData(); + fd[kState] = parsed; + return fd; + } + case "application/x-www-form-urlencoded": { + const entries = new URLSearchParams(value.toString()); + const fd = new FormData(); + for (const [name, value2] of entries) { + fd.append(name, value2); + } + return fd; + } + } + } + throw new TypeError( + 'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".' + ); + }, instance); + }, + bytes() { + return consumeBody(this, (bytes2) => { + return new Uint8Array(bytes2); + }, instance); + } + }; + return methods; + } + function mixinBody(prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)); + } + async function consumeBody(object, convertBytesToJSValue, instance) { + webidl.brandCheck(object, instance); + if (bodyUnusable(object)) { + throw new TypeError("Body is unusable: Body has already been read"); + } + throwIfAborted(object[kState]); + const promise = createDeferredPromise(); + const errorSteps = (error) => promise.reject(error); + const successSteps = (data) => { + try { + promise.resolve(convertBytesToJSValue(data)); + } catch (e) { + errorSteps(e); + } + }; + if (object[kState].body == null) { + successSteps(Buffer.allocUnsafe(0)); + return promise.promise; + } + await fullyReadBody(object[kState].body, successSteps, errorSteps); + return promise.promise; + } + function bodyUnusable(object) { + const body = object[kState].body; + return body != null && (body.stream.locked || util.isDisturbed(body.stream)); + } + function parseJSONFromBytes(bytes2) { + return JSON.parse(utf8DecodeBytes(bytes2)); + } + function bodyMimeType(requestOrResponse) { + const headers = requestOrResponse[kState].headersList; + const mimeType = extractMimeType(headers); + if (mimeType === "failure") { + return null; + } + return mimeType; + } + module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody, + streamRegistry, + hasFinalizationRegistry, + bodyUnusable + }; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/client-h1.js +var require_client_h1 = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/client-h1.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var util = require_util(); + var { channels } = require_diagnostics(); + var timers = require_timers(); + var { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError + } = require_errors(); + var { + kUrl, + kReset, + kClient, + kParser, + kBlocking, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kMaxRequests, + kCounter, + kMaxResponseSize, + kOnError, + kResume, + kHTTPContext + } = require_symbols(); + var constants = require_constants2(); + var EMPTY_BUF = Buffer.alloc(0); + var FastBuffer = Buffer[Symbol.species]; + var addListener = util.addListener; + var removeAllListeners = util.removeAllListeners; + var extractBody; + async function lazyllhttp() { + const llhttpWasmData = process.env.JEST_WORKER_ID ? require_llhttp_wasm() : void 0; + let mod2; + try { + mod2 = await WebAssembly.compile(require_llhttp_simd_wasm()); + } catch (e) { + mod2 = await WebAssembly.compile(llhttpWasmData || require_llhttp_wasm()); + } + return await WebAssembly.instantiate(mod2, { + env: { + /* eslint-disable camelcase */ + wasm_on_url: (p, at, len) => { + return 0; + }, + wasm_on_status: (p, at, len) => { + assert(currentParser.ptr === p); + const start = at - currentBufferPtr + currentBufferRef.byteOffset; + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0; + }, + wasm_on_message_begin: (p) => { + assert(currentParser.ptr === p); + return currentParser.onMessageBegin() || 0; + }, + wasm_on_header_field: (p, at, len) => { + assert(currentParser.ptr === p); + const start = at - currentBufferPtr + currentBufferRef.byteOffset; + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0; + }, + wasm_on_header_value: (p, at, len) => { + assert(currentParser.ptr === p); + const start = at - currentBufferPtr + currentBufferRef.byteOffset; + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0; + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert(currentParser.ptr === p); + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0; + }, + wasm_on_body: (p, at, len) => { + assert(currentParser.ptr === p); + const start = at - currentBufferPtr + currentBufferRef.byteOffset; + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0; + }, + wasm_on_message_complete: (p) => { + assert(currentParser.ptr === p); + return currentParser.onMessageComplete() || 0; + } + /* eslint-enable camelcase */ + } + }); + } + var llhttpInstance = null; + var llhttpPromise = lazyllhttp(); + llhttpPromise.catch(); + var currentParser = null; + var currentBufferRef = null; + var currentBufferSize = 0; + var currentBufferPtr = null; + var USE_NATIVE_TIMER = 0; + var USE_FAST_TIMER = 1; + var TIMEOUT_HEADERS = 2 | USE_FAST_TIMER; + var TIMEOUT_BODY = 4 | USE_FAST_TIMER; + var TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER; + var Parser2 = class { + constructor(client, socket, { exports: exports2 }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0); + this.llhttp = exports2; + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE); + this.client = client; + this.socket = socket; + this.timeout = null; + this.timeoutValue = null; + this.timeoutType = null; + this.statusCode = null; + this.statusText = ""; + this.upgrade = false; + this.headers = []; + this.headersSize = 0; + this.headersMaxSize = client[kMaxHeadersSize]; + this.shouldKeepAlive = false; + this.paused = false; + this.resume = this.resume.bind(this); + this.bytesRead = 0; + this.keepAlive = ""; + this.contentLength = ""; + this.connection = ""; + this.maxResponseSize = client[kMaxResponseSize]; + } + setTimeout(delay, type) { + if (delay !== this.timeoutValue || type & USE_FAST_TIMER ^ this.timeoutType & USE_FAST_TIMER) { + if (this.timeout) { + timers.clearTimeout(this.timeout); + this.timeout = null; + } + if (delay) { + if (type & USE_FAST_TIMER) { + this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this)); + } else { + this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this)); + this.timeout.unref(); + } + } + this.timeoutValue = delay; + } else if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh(); + } + } + this.timeoutType = type; + } + resume() { + if (this.socket.destroyed || !this.paused) { + return; + } + assert(this.ptr != null); + assert(currentParser == null); + this.llhttp.llhttp_resume(this.ptr); + assert(this.timeoutType === TIMEOUT_BODY); + if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh(); + } + } + this.paused = false; + this.execute(this.socket.read() || EMPTY_BUF); + this.readMore(); + } + readMore() { + while (!this.paused && this.ptr) { + const chunk = this.socket.read(); + if (chunk === null) { + break; + } + this.execute(chunk); + } + } + execute(data) { + assert(this.ptr != null); + assert(currentParser == null); + assert(!this.paused); + const { socket, llhttp } = this; + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr); + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096; + currentBufferPtr = llhttp.malloc(currentBufferSize); + } + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data); + try { + let ret; + try { + currentBufferRef = data; + currentParser = this; + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length); + } catch (err) { + throw err; + } finally { + currentParser = null; + currentBufferRef = null; + } + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr; + if (ret !== constants.ERROR.OK) { + const body = data.subarray(offset); + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(body); + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true; + socket.unshift(body); + } else { + throw this.createError(ret, body); + } + } + } catch (err) { + util.destroy(socket, err); + } + } + finish() { + assert(currentParser === null); + assert(this.ptr != null); + assert(!this.paused); + const { llhttp } = this; + let ret; + try { + currentParser = this; + ret = llhttp.llhttp_finish(this.ptr); + } finally { + currentParser = null; + } + if (ret === constants.ERROR.OK) { + return null; + } + if (ret === constants.ERROR.PAUSED || ret === constants.ERROR.PAUSED_UPGRADE) { + this.paused = true; + return null; + } + return this.createError(ret, EMPTY_BUF); + } + createError(ret, data) { + const { llhttp, contentLength, bytesRead } = this; + if (contentLength && bytesRead !== parseInt(contentLength, 10)) { + return new ResponseContentLengthMismatchError(); + } + const ptr = llhttp.llhttp_get_error_reason(this.ptr); + let message = ""; + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0); + message = "Response does not match the HTTP/1.1 protocol (" + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + ")"; + } + return new HTTPParserError(message, constants.ERROR[ret], data); + } + destroy() { + assert(this.ptr != null); + assert(currentParser == null); + this.llhttp.llhttp_free(this.ptr); + this.ptr = null; + this.timeout && timers.clearTimeout(this.timeout); + this.timeout = null; + this.timeoutValue = null; + this.timeoutType = null; + this.paused = false; + } + onStatus(buf) { + this.statusText = buf.toString(); + } + onMessageBegin() { + const { socket, client } = this; + if (socket.destroyed) { + return -1; + } + const request = client[kQueue][client[kRunningIdx]]; + if (!request) { + return -1; + } + request.onResponseStarted(); + } + onHeaderField(buf) { + const len = this.headers.length; + if ((len & 1) === 0) { + this.headers.push(buf); + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]); + } + this.trackHeader(buf.length); + } + onHeaderValue(buf) { + let len = this.headers.length; + if ((len & 1) === 1) { + this.headers.push(buf); + len += 1; + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]); + } + const key = this.headers[len - 2]; + if (key.length === 10) { + const headerName = util.bufferToLowerCasedHeaderName(key); + if (headerName === "keep-alive") { + this.keepAlive += buf.toString(); + } else if (headerName === "connection") { + this.connection += buf.toString(); + } + } else if (key.length === 14 && util.bufferToLowerCasedHeaderName(key) === "content-length") { + this.contentLength += buf.toString(); + } + this.trackHeader(buf.length); + } + trackHeader(len) { + this.headersSize += len; + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()); + } + } + onUpgrade(head2) { + const { upgrade, client, socket, headers, statusCode } = this; + assert(upgrade); + assert(client[kSocket] === socket); + assert(!socket.destroyed); + assert(!this.paused); + assert((headers.length & 1) === 0); + const request = client[kQueue][client[kRunningIdx]]; + assert(request); + assert(request.upgrade || request.method === "CONNECT"); + this.statusCode = null; + this.statusText = ""; + this.shouldKeepAlive = null; + this.headers = []; + this.headersSize = 0; + socket.unshift(head2); + socket[kParser].destroy(); + socket[kParser] = null; + socket[kClient] = null; + socket[kError] = null; + removeAllListeners(socket); + client[kSocket] = null; + client[kHTTPContext] = null; + client[kQueue][client[kRunningIdx]++] = null; + client.emit("disconnect", client[kUrl], [client], new InformationalError("upgrade")); + try { + request.onUpgrade(statusCode, headers, socket); + } catch (err) { + util.destroy(socket, err); + } + client[kResume](); + } + onHeadersComplete(statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this; + if (socket.destroyed) { + return -1; + } + const request = client[kQueue][client[kRunningIdx]]; + if (!request) { + return -1; + } + assert(!this.upgrade); + assert(this.statusCode < 200); + if (statusCode === 100) { + util.destroy(socket, new SocketError("bad response", util.getSocketInfo(socket))); + return -1; + } + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError("bad upgrade", util.getSocketInfo(socket))); + return -1; + } + assert(this.timeoutType === TIMEOUT_HEADERS); + this.statusCode = statusCode; + this.shouldKeepAlive = shouldKeepAlive || // Override llhttp value which does not allow keepAlive for HEAD. + request.method === "HEAD" && !socket[kReset] && this.connection.toLowerCase() === "keep-alive"; + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null ? request.bodyTimeout : client[kBodyTimeout]; + this.setTimeout(bodyTimeout, TIMEOUT_BODY); + } else if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh(); + } + } + if (request.method === "CONNECT") { + assert(client[kRunning] === 1); + this.upgrade = true; + return 2; + } + if (upgrade) { + assert(client[kRunning] === 1); + this.upgrade = true; + return 2; + } + assert((this.headers.length & 1) === 0); + this.headers = []; + this.headersSize = 0; + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null; + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ); + if (timeout <= 0) { + socket[kReset] = true; + } else { + client[kKeepAliveTimeoutValue] = timeout; + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout]; + } + } else { + socket[kReset] = true; + } + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false; + if (request.aborted) { + return -1; + } + if (request.method === "HEAD") { + return 1; + } + if (statusCode < 200) { + return 1; + } + if (socket[kBlocking]) { + socket[kBlocking] = false; + client[kResume](); + } + return pause ? constants.ERROR.PAUSED : 0; + } + onBody(buf) { + const { client, socket, statusCode, maxResponseSize } = this; + if (socket.destroyed) { + return -1; + } + const request = client[kQueue][client[kRunningIdx]]; + assert(request); + assert(this.timeoutType === TIMEOUT_BODY); + if (this.timeout) { + if (this.timeout.refresh) { + this.timeout.refresh(); + } + } + assert(statusCode >= 200); + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()); + return -1; + } + this.bytesRead += buf.length; + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED; + } + } + onMessageComplete() { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this; + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1; + } + if (upgrade) { + return; + } + assert(statusCode >= 100); + assert((this.headers.length & 1) === 0); + const request = client[kQueue][client[kRunningIdx]]; + assert(request); + this.statusCode = null; + this.statusText = ""; + this.bytesRead = 0; + this.contentLength = ""; + this.keepAlive = ""; + this.connection = ""; + this.headers = []; + this.headersSize = 0; + if (statusCode < 200) { + return; + } + if (request.method !== "HEAD" && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()); + return -1; + } + request.onComplete(headers); + client[kQueue][client[kRunningIdx]++] = null; + if (socket[kWriting]) { + assert(client[kRunning] === 0); + util.destroy(socket, new InformationalError("reset")); + return constants.ERROR.PAUSED; + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError("reset")); + return constants.ERROR.PAUSED; + } else if (socket[kReset] && client[kRunning] === 0) { + util.destroy(socket, new InformationalError("reset")); + return constants.ERROR.PAUSED; + } else if (client[kPipelining] == null || client[kPipelining] === 1) { + setImmediate(() => client[kResume]()); + } else { + client[kResume](); + } + } + }; + function onParserTimeout(parser) { + const { socket, timeoutType, client, paused } = parser.deref(); + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!paused, "cannot be paused while waiting for headers"); + util.destroy(socket, new HeadersTimeoutError()); + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!paused) { + util.destroy(socket, new BodyTimeoutError()); + } + } else if (timeoutType === TIMEOUT_KEEP_ALIVE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]); + util.destroy(socket, new InformationalError("socket idle timeout")); + } + } + async function connectH1(client, socket) { + client[kSocket] = socket; + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise; + llhttpPromise = null; + } + socket[kNoRef] = false; + socket[kWriting] = false; + socket[kReset] = false; + socket[kBlocking] = false; + socket[kParser] = new Parser2(client, socket, llhttpInstance); + addListener(socket, "error", function(err) { + assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); + const parser = this[kParser]; + if (err.code === "ECONNRESET" && parser.statusCode && !parser.shouldKeepAlive) { + const parserErr = parser.finish(); + if (parserErr) { + this[kError] = parserErr; + this[kClient][kOnError](parserErr); + } + return; + } + this[kError] = err; + this[kClient][kOnError](err); + }); + addListener(socket, "readable", function() { + const parser = this[kParser]; + if (parser) { + parser.readMore(); + } + }); + addListener(socket, "end", function() { + const parser = this[kParser]; + if (parser.statusCode && !parser.shouldKeepAlive) { + const parserErr = parser.finish(); + if (parserErr) { + util.destroy(this, parserErr); + } + return; + } + util.destroy(this, new SocketError("other side closed", util.getSocketInfo(this))); + }); + addListener(socket, "close", function() { + const client2 = this[kClient]; + const parser = this[kParser]; + if (parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + this[kError] = parser.finish() || this[kError]; + } + this[kParser].destroy(); + this[kParser] = null; + } + const err = this[kError] || new SocketError("closed", util.getSocketInfo(this)); + client2[kSocket] = null; + client2[kHTTPContext] = null; + if (client2.destroyed) { + assert(client2[kPending] === 0); + const requests = client2[kQueue].splice(client2[kRunningIdx]); + for (let i = 0; i < requests.length; i++) { + const request = requests[i]; + util.errorRequest(client2, request, err); + } + } else if (client2[kRunning] > 0 && err.code !== "UND_ERR_INFO") { + const request = client2[kQueue][client2[kRunningIdx]]; + client2[kQueue][client2[kRunningIdx]++] = null; + util.errorRequest(client2, request, err); + } + client2[kPendingIdx] = client2[kRunningIdx]; + assert(client2[kRunning] === 0); + client2.emit("disconnect", client2[kUrl], [client2], err); + client2[kResume](); + }); + let closed = false; + socket.on("close", () => { + closed = true; + }); + return { + version: "h1", + defaultPipelining: 1, + write(...args) { + return writeH1(client, ...args); + }, + resume() { + resumeH1(client); + }, + destroy(err, callback) { + if (closed) { + queueMicrotask(callback); + } else { + socket.destroy(err).on("close", callback); + } + }, + get destroyed() { + return socket.destroyed; + }, + busy(request) { + if (socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return true; + } + if (request) { + if (client[kRunning] > 0 && !request.idempotent) { + return true; + } + if (client[kRunning] > 0 && (request.upgrade || request.method === "CONNECT")) { + return true; + } + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) { + return true; + } + } + return false; + } + }; + } + function resumeH1(client) { + const socket = client[kSocket]; + if (socket && !socket.destroyed) { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref(); + socket[kNoRef] = true; + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref(); + socket[kNoRef] = false; + } + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE); + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]]; + const headersTimeout = request.headersTimeout != null ? request.headersTimeout : client[kHeadersTimeout]; + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS); + } + } + } + } + function shouldSendContentLength(method) { + return method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE" && method !== "CONNECT"; + } + function writeH1(client, request) { + const { method, path: path14, host: host2, upgrade, blocking, reset } = request; + let { body, headers, contentLength } = request; + const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH" || method === "QUERY" || method === "PROPFIND" || method === "PROPPATCH"; + if (util.isFormDataLike(body)) { + if (!extractBody) { + extractBody = require_body().extractBody; + } + const [bodyStream, contentType] = extractBody(body); + if (request.contentType == null) { + headers.push("content-type", contentType); + } + body = bodyStream.stream; + contentLength = bodyStream.length; + } else if (util.isBlobLike(body) && request.contentType == null && body.type) { + headers.push("content-type", body.type); + } + if (body && typeof body.read === "function") { + body.read(0); + } + const bodyLength = util.bodyLength(body); + contentLength = bodyLength ?? contentLength; + if (contentLength === null) { + contentLength = request.contentLength; + } + if (contentLength === 0 && !expectsPayload) { + contentLength = null; + } + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + util.errorRequest(client, request, new RequestContentLengthMismatchError()); + return false; + } + process.emitWarning(new RequestContentLengthMismatchError()); + } + const socket = client[kSocket]; + const abort = (err) => { + if (request.aborted || request.completed) { + return; + } + util.errorRequest(client, request, err || new RequestAbortedError()); + util.destroy(body); + util.destroy(socket, new InformationalError("aborted")); + }; + try { + request.onConnect(abort); + } catch (err) { + util.errorRequest(client, request, err); + } + if (request.aborted) { + return false; + } + if (method === "HEAD") { + socket[kReset] = true; + } + if (upgrade || method === "CONNECT") { + socket[kReset] = true; + } + if (reset != null) { + socket[kReset] = reset; + } + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true; + } + if (blocking) { + socket[kBlocking] = true; + } + let header = `${method} ${path14} HTTP/1.1\r +`; + if (typeof host2 === "string") { + header += `host: ${host2}\r +`; + } else { + header += client[kHostHeader]; + } + if (upgrade) { + header += `connection: upgrade\r +upgrade: ${upgrade}\r +`; + } else if (client[kPipelining] && !socket[kReset]) { + header += "connection: keep-alive\r\n"; + } else { + header += "connection: close\r\n"; + } + if (Array.isArray(headers)) { + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0]; + const val = headers[n + 1]; + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + header += `${key}: ${val[i]}\r +`; + } + } else { + header += `${key}: ${val}\r +`; + } + } + } + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }); + } + if (!body || bodyLength === 0) { + writeBuffer(abort, null, client, request, socket, contentLength, header, expectsPayload); + } else if (util.isBuffer(body)) { + writeBuffer(abort, body, client, request, socket, contentLength, header, expectsPayload); + } else if (util.isBlobLike(body)) { + if (typeof body.stream === "function") { + writeIterable(abort, body.stream(), client, request, socket, contentLength, header, expectsPayload); + } else { + writeBlob(abort, body, client, request, socket, contentLength, header, expectsPayload); + } + } else if (util.isStream(body)) { + writeStream(abort, body, client, request, socket, contentLength, header, expectsPayload); + } else if (util.isIterable(body)) { + writeIterable(abort, body, client, request, socket, contentLength, header, expectsPayload); + } else { + assert(false); + } + return true; + } + function writeStream(abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, "stream body cannot be pipelined"); + let finished = false; + const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }); + const onData = function(chunk) { + if (finished) { + return; + } + try { + if (!writer.write(chunk) && this.pause) { + this.pause(); + } + } catch (err) { + util.destroy(this, err); + } + }; + const onDrain = function() { + if (finished) { + return; + } + if (body.resume) { + body.resume(); + } + }; + const onClose = function() { + queueMicrotask(() => { + body.removeListener("error", onFinished); + }); + if (!finished) { + const err = new RequestAbortedError(); + queueMicrotask(() => onFinished(err)); + } + }; + const onFinished = function(err) { + if (finished) { + return; + } + finished = true; + assert(socket.destroyed || socket[kWriting] && client[kRunning] <= 1); + socket.off("drain", onDrain).off("error", onFinished); + body.removeListener("data", onData).removeListener("end", onFinished).removeListener("close", onClose); + if (!err) { + try { + writer.end(); + } catch (er) { + err = er; + } + } + writer.destroy(err); + if (err && (err.code !== "UND_ERR_INFO" || err.message !== "reset")) { + util.destroy(body, err); + } else { + util.destroy(body); + } + }; + body.on("data", onData).on("end", onFinished).on("error", onFinished).on("close", onClose); + if (body.resume) { + body.resume(); + } + socket.on("drain", onDrain).on("error", onFinished); + if (body.errorEmitted ?? body.errored) { + setImmediate(() => onFinished(body.errored)); + } else if (body.endEmitted ?? body.readableEnded) { + setImmediate(() => onFinished(null)); + } + if (body.closeEmitted ?? body.closed) { + setImmediate(onClose); + } + } + function writeBuffer(abort, body, client, request, socket, contentLength, header, expectsPayload) { + try { + if (!body) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r +\r +`, "latin1"); + } else { + assert(contentLength === null, "no body must not have content length"); + socket.write(`${header}\r +`, "latin1"); + } + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, "buffer body must have content length"); + socket.cork(); + socket.write(`${header}content-length: ${contentLength}\r +\r +`, "latin1"); + socket.write(body); + socket.uncork(); + request.onBodySent(body); + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true; + } + } + request.onRequestSent(); + client[kResume](); + } catch (err) { + abort(err); + } + } + async function writeBlob(abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength === body.size, "blob body must have content length"); + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError(); + } + const buffer = Buffer.from(await body.arrayBuffer()); + socket.cork(); + socket.write(`${header}content-length: ${contentLength}\r +\r +`, "latin1"); + socket.write(buffer); + socket.uncork(); + request.onBodySent(buffer); + request.onRequestSent(); + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true; + } + client[kResume](); + } catch (err) { + abort(err); + } + } + async function writeIterable(abort, body, client, request, socket, contentLength, header, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, "iterator body cannot be pipelined"); + let callback = null; + function onDrain() { + if (callback) { + const cb = callback; + callback = null; + cb(); + } + } + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null); + if (socket[kError]) { + reject(socket[kError]); + } else { + callback = resolve; + } + }); + socket.on("close", onDrain).on("drain", onDrain); + const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }); + try { + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError]; + } + if (!writer.write(chunk)) { + await waitForDrain(); + } + } + writer.end(); + } catch (err) { + writer.destroy(err); + } finally { + socket.off("close", onDrain).off("drain", onDrain); + } + } + var AsyncWriter = class { + constructor({ abort, socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket; + this.request = request; + this.contentLength = contentLength; + this.client = client; + this.bytesWritten = 0; + this.expectsPayload = expectsPayload; + this.header = header; + this.abort = abort; + socket[kWriting] = true; + } + write(chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this; + if (socket[kError]) { + throw socket[kError]; + } + if (socket.destroyed) { + return false; + } + const len = Buffer.byteLength(chunk); + if (!len) { + return true; + } + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError(); + } + process.emitWarning(new RequestContentLengthMismatchError()); + } + socket.cork(); + if (bytesWritten === 0) { + if (!expectsPayload && request.reset !== false) { + socket[kReset] = true; + } + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r +`, "latin1"); + } else { + socket.write(`${header}content-length: ${contentLength}\r +\r +`, "latin1"); + } + } + if (contentLength === null) { + socket.write(`\r +${len.toString(16)}\r +`, "latin1"); + } + this.bytesWritten += len; + const ret = socket.write(chunk); + socket.uncork(); + request.onBodySent(chunk); + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh(); + } + } + } + return ret; + } + end() { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this; + request.onRequestSent(); + socket[kWriting] = false; + if (socket[kError]) { + throw socket[kError]; + } + if (socket.destroyed) { + return; + } + if (bytesWritten === 0) { + if (expectsPayload) { + socket.write(`${header}content-length: 0\r +\r +`, "latin1"); + } else { + socket.write(`${header}\r +`, "latin1"); + } + } else if (contentLength === null) { + socket.write("\r\n0\r\n\r\n", "latin1"); + } + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError(); + } else { + process.emitWarning(new RequestContentLengthMismatchError()); + } + } + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh(); + } + } + client[kResume](); + } + destroy(err) { + const { socket, client, abort } = this; + socket[kWriting] = false; + if (err) { + assert(client[kRunning] <= 1, "pipeline should only contain this request"); + abort(err); + } + } + }; + module.exports = connectH1; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/client-h2.js +var require_client_h2 = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/client-h2.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { pipeline } = __require("node:stream"); + var util = require_util(); + var { + RequestContentLengthMismatchError, + RequestAbortedError, + SocketError, + InformationalError + } = require_errors(); + var { + kUrl, + kReset, + kClient, + kRunning, + kPending, + kQueue, + kPendingIdx, + kRunningIdx, + kError, + kSocket, + kStrictContentLength, + kOnError, + kMaxConcurrentStreams, + kHTTP2Session, + kResume, + kSize, + kHTTPContext + } = require_symbols(); + var kOpenStreams = /* @__PURE__ */ Symbol("open streams"); + var extractBody; + var h2ExperimentalWarned = false; + var http2; + try { + http2 = __require("node:http2"); + } catch { + http2 = { constants: {} }; + } + var { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } + } = http2; + function parseH2Headers(headers) { + const result = []; + for (const [name, value] of Object.entries(headers)) { + if (Array.isArray(value)) { + for (const subvalue of value) { + result.push(Buffer.from(name), Buffer.from(subvalue)); + } + } else { + result.push(Buffer.from(name), Buffer.from(value)); + } + } + return result; + } + async function connectH2(client, socket) { + client[kSocket] = socket; + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true; + process.emitWarning("H2 support is experimental, expect them to change at any time.", { + code: "UNDICI-H2" + }); + } + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kMaxConcurrentStreams] + }); + session[kOpenStreams] = 0; + session[kClient] = client; + session[kSocket] = socket; + util.addListener(session, "error", onHttp2SessionError); + util.addListener(session, "frameError", onHttp2FrameError); + util.addListener(session, "end", onHttp2SessionEnd); + util.addListener(session, "goaway", onHTTP2GoAway); + util.addListener(session, "close", function() { + const { [kClient]: client2 } = this; + const { [kSocket]: socket2 } = client2; + const err = this[kSocket][kError] || this[kError] || new SocketError("closed", util.getSocketInfo(socket2)); + client2[kHTTP2Session] = null; + if (client2.destroyed) { + assert(client2[kPending] === 0); + const requests = client2[kQueue].splice(client2[kRunningIdx]); + for (let i = 0; i < requests.length; i++) { + const request = requests[i]; + util.errorRequest(client2, request, err); + } + } + }); + session.unref(); + client[kHTTP2Session] = session; + socket[kHTTP2Session] = session; + util.addListener(socket, "error", function(err) { + assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); + this[kError] = err; + this[kClient][kOnError](err); + }); + util.addListener(socket, "end", function() { + util.destroy(this, new SocketError("other side closed", util.getSocketInfo(this))); + }); + util.addListener(socket, "close", function() { + const err = this[kError] || new SocketError("closed", util.getSocketInfo(this)); + client[kSocket] = null; + if (this[kHTTP2Session] != null) { + this[kHTTP2Session].destroy(err); + } + client[kPendingIdx] = client[kRunningIdx]; + assert(client[kRunning] === 0); + client.emit("disconnect", client[kUrl], [client], err); + client[kResume](); + }); + let closed = false; + socket.on("close", () => { + closed = true; + }); + return { + version: "h2", + defaultPipelining: Infinity, + write(...args) { + return writeH2(client, ...args); + }, + resume() { + resumeH2(client); + }, + destroy(err, callback) { + if (closed) { + queueMicrotask(callback); + } else { + socket.destroy(err).on("close", callback); + } + }, + get destroyed() { + return socket.destroyed; + }, + busy() { + return false; + } + }; + } + function resumeH2(client) { + const socket = client[kSocket]; + if (socket?.destroyed === false) { + if (client[kSize] === 0 && client[kMaxConcurrentStreams] === 0) { + socket.unref(); + client[kHTTP2Session].unref(); + } else { + socket.ref(); + client[kHTTP2Session].ref(); + } + } + } + function onHttp2SessionError(err) { + assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID"); + this[kSocket][kError] = err; + this[kClient][kOnError](err); + } + function onHttp2FrameError(type, code, id) { + if (id === 0) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`); + this[kSocket][kError] = err; + this[kClient][kOnError](err); + } + } + function onHttp2SessionEnd() { + const err = new SocketError("other side closed", util.getSocketInfo(this[kSocket])); + this.destroy(err); + util.destroy(this[kSocket], err); + } + function onHTTP2GoAway(code) { + const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${code}`, util.getSocketInfo(this)); + const client = this[kClient]; + client[kSocket] = null; + client[kHTTPContext] = null; + if (this[kHTTP2Session] != null) { + this[kHTTP2Session].destroy(err); + this[kHTTP2Session] = null; + } + util.destroy(this[kSocket], err); + if (client[kRunningIdx] < client[kQueue].length) { + const request = client[kQueue][client[kRunningIdx]]; + client[kQueue][client[kRunningIdx]++] = null; + util.errorRequest(client, request, err); + client[kPendingIdx] = client[kRunningIdx]; + } + assert(client[kRunning] === 0); + client.emit("disconnect", client[kUrl], [client], err); + client[kResume](); + } + function shouldSendContentLength(method) { + return method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE" && method !== "CONNECT"; + } + function writeH2(client, request) { + const session = client[kHTTP2Session]; + const { method, path: path14, host: host2, upgrade, expectContinue, signal, headers: reqHeaders } = request; + let { body } = request; + if (upgrade) { + util.errorRequest(client, request, new Error("Upgrade not supported for H2")); + return false; + } + const headers = {}; + for (let n = 0; n < reqHeaders.length; n += 2) { + const key = reqHeaders[n + 0]; + const val = reqHeaders[n + 1]; + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (headers[key]) { + headers[key] += `,${val[i]}`; + } else { + headers[key] = val[i]; + } + } + } else { + headers[key] = val; + } + } + let stream; + const { hostname, port: port2 } = client[kUrl]; + headers[HTTP2_HEADER_AUTHORITY] = host2 || `${hostname}${port2 ? `:${port2}` : ""}`; + headers[HTTP2_HEADER_METHOD] = method; + const abort = (err) => { + if (request.aborted || request.completed) { + return; + } + err = err || new RequestAbortedError(); + util.errorRequest(client, request, err); + if (stream != null) { + util.destroy(stream, err); + } + util.destroy(body, err); + client[kQueue][client[kRunningIdx]++] = null; + client[kResume](); + }; + try { + request.onConnect(abort); + } catch (err) { + util.errorRequest(client, request, err); + } + if (request.aborted) { + return false; + } + if (method === "CONNECT") { + session.ref(); + stream = session.request(headers, { endStream: false, signal }); + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream); + ++session[kOpenStreams]; + client[kQueue][client[kRunningIdx]++] = null; + } else { + stream.once("ready", () => { + request.onUpgrade(null, null, stream); + ++session[kOpenStreams]; + client[kQueue][client[kRunningIdx]++] = null; + }); + } + stream.once("close", () => { + session[kOpenStreams] -= 1; + if (session[kOpenStreams] === 0) session.unref(); + }); + return true; + } + headers[HTTP2_HEADER_PATH] = path14; + headers[HTTP2_HEADER_SCHEME] = "https"; + const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH"; + if (body && typeof body.read === "function") { + body.read(0); + } + let contentLength = util.bodyLength(body); + if (util.isFormDataLike(body)) { + extractBody ??= require_body().extractBody; + const [bodyStream, contentType] = extractBody(body); + headers["content-type"] = contentType; + body = bodyStream.stream; + contentLength = bodyStream.length; + } + if (contentLength == null) { + contentLength = request.contentLength; + } + if (contentLength === 0 || !expectsPayload) { + contentLength = null; + } + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + util.errorRequest(client, request, new RequestContentLengthMismatchError()); + return false; + } + process.emitWarning(new RequestContentLengthMismatchError()); + } + if (contentLength != null) { + assert(body, "no body must not have content length"); + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`; + } + session.ref(); + const shouldEndStream = method === "GET" || method === "HEAD" || body === null; + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = "100-continue"; + stream = session.request(headers, { endStream: shouldEndStream, signal }); + stream.once("continue", writeBodyH2); + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }); + writeBodyH2(); + } + ++session[kOpenStreams]; + stream.once("response", (headers2) => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers2; + request.onResponseStarted(); + if (request.aborted) { + const err = new RequestAbortedError(); + util.errorRequest(client, request, err); + util.destroy(stream, err); + return; + } + if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), "") === false) { + stream.pause(); + } + stream.on("data", (chunk) => { + if (request.onData(chunk) === false) { + stream.pause(); + } + }); + }); + stream.once("end", () => { + if (stream.state?.state == null || stream.state.state < 6) { + request.onComplete([]); + } + if (session[kOpenStreams] === 0) { + session.unref(); + } + abort(new InformationalError("HTTP/2: stream half-closed (remote)")); + client[kQueue][client[kRunningIdx]++] = null; + client[kPendingIdx] = client[kRunningIdx]; + client[kResume](); + }); + stream.once("close", () => { + session[kOpenStreams] -= 1; + if (session[kOpenStreams] === 0) { + session.unref(); + } + }); + stream.once("error", function(err) { + abort(err); + }); + stream.once("frameError", (type, code) => { + abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)); + }); + return true; + function writeBodyH2() { + if (!body || contentLength === 0) { + writeBuffer( + abort, + stream, + null, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ); + } else if (util.isBuffer(body)) { + writeBuffer( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ); + } else if (util.isBlobLike(body)) { + if (typeof body.stream === "function") { + writeIterable( + abort, + stream, + body.stream(), + client, + request, + client[kSocket], + contentLength, + expectsPayload + ); + } else { + writeBlob( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ); + } + } else if (util.isStream(body)) { + writeStream( + abort, + client[kSocket], + expectsPayload, + stream, + body, + client, + request, + contentLength + ); + } else if (util.isIterable(body)) { + writeIterable( + abort, + stream, + body, + client, + request, + client[kSocket], + contentLength, + expectsPayload + ); + } else { + assert(false); + } + } + } + function writeBuffer(abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + try { + if (body != null && util.isBuffer(body)) { + assert(contentLength === body.byteLength, "buffer body must have content length"); + h2stream.cork(); + h2stream.write(body); + h2stream.uncork(); + h2stream.end(); + request.onBodySent(body); + } + if (!expectsPayload) { + socket[kReset] = true; + } + request.onRequestSent(); + client[kResume](); + } catch (error) { + abort(error); + } + } + function writeStream(abort, socket, expectsPayload, h2stream, body, client, request, contentLength) { + assert(contentLength !== 0 || client[kRunning] === 0, "stream body cannot be pipelined"); + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(pipe, err); + abort(err); + } else { + util.removeAllListeners(pipe); + request.onRequestSent(); + if (!expectsPayload) { + socket[kReset] = true; + } + client[kResume](); + } + } + ); + util.addListener(pipe, "data", onPipeData); + function onPipeData(chunk) { + request.onBodySent(chunk); + } + } + async function writeBlob(abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + assert(contentLength === body.size, "blob body must have content length"); + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError(); + } + const buffer = Buffer.from(await body.arrayBuffer()); + h2stream.cork(); + h2stream.write(buffer); + h2stream.uncork(); + h2stream.end(); + request.onBodySent(buffer); + request.onRequestSent(); + if (!expectsPayload) { + socket[kReset] = true; + } + client[kResume](); + } catch (err) { + abort(err); + } + } + async function writeIterable(abort, h2stream, body, client, request, socket, contentLength, expectsPayload) { + assert(contentLength !== 0 || client[kRunning] === 0, "iterator body cannot be pipelined"); + let callback = null; + function onDrain() { + if (callback) { + const cb = callback; + callback = null; + cb(); + } + } + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null); + if (socket[kError]) { + reject(socket[kError]); + } else { + callback = resolve; + } + }); + h2stream.on("close", onDrain).on("drain", onDrain); + try { + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError]; + } + const res = h2stream.write(chunk); + request.onBodySent(chunk); + if (!res) { + await waitForDrain(); + } + } + h2stream.end(); + request.onRequestSent(); + if (!expectsPayload) { + socket[kReset] = true; + } + client[kResume](); + } catch (err) { + abort(err); + } finally { + h2stream.off("close", onDrain).off("drain", onDrain); + } + } + module.exports = connectH2; + } +}); + +// viewer/node_modules/undici/lib/handler/redirect-handler.js +var require_redirect_handler = __commonJS({ + "viewer/node_modules/undici/lib/handler/redirect-handler.js"(exports, module) { + "use strict"; + var util = require_util(); + var { kBodyUsed } = require_symbols(); + var assert = __require("node:assert"); + var { InvalidArgumentError } = require_errors(); + var EE = __require("node:events"); + var redirectableStatusCodes = [300, 301, 302, 303, 307, 308]; + var kBody = /* @__PURE__ */ Symbol("body"); + var BodyAsyncIterable = class { + constructor(body) { + this[kBody] = body; + this[kBodyUsed] = false; + } + async *[Symbol.asyncIterator]() { + assert(!this[kBodyUsed], "disturbed"); + this[kBodyUsed] = true; + yield* this[kBody]; + } + }; + var RedirectHandler = class { + constructor(dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError("maxRedirections must be a positive number"); + } + util.validateHandler(handler, opts.method, opts.upgrade); + this.dispatch = dispatch; + this.location = null; + this.abort = null; + this.opts = { ...opts, maxRedirections: 0 }; + this.maxRedirections = maxRedirections; + this.handler = handler; + this.history = []; + this.redirectionLimitReached = false; + if (util.isStream(this.opts.body)) { + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body.on("data", function() { + assert(false); + }); + } + if (typeof this.opts.body.readableDidRead !== "boolean") { + this.opts.body[kBodyUsed] = false; + EE.prototype.on.call(this.opts.body, "data", function() { + this[kBodyUsed] = true; + }); + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === "function") { + this.opts.body = new BodyAsyncIterable(this.opts.body); + } else if (this.opts.body && typeof this.opts.body !== "string" && !ArrayBuffer.isView(this.opts.body) && util.isIterable(this.opts.body)) { + this.opts.body = new BodyAsyncIterable(this.opts.body); + } + } + onConnect(abort) { + this.abort = abort; + this.handler.onConnect(abort, { history: this.history }); + } + onUpgrade(statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket); + } + onError(error) { + this.handler.onError(error); + } + onHeaders(statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) ? null : parseLocation(statusCode, headers); + if (this.opts.throwOnMaxRedirect && this.history.length >= this.maxRedirections) { + if (this.request) { + this.request.abort(new Error("max redirects")); + } + this.redirectionLimitReached = true; + this.abort(new Error("max redirects")); + return; + } + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)); + } + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText); + } + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))); + const path14 = search ? `${pathname}${search}` : pathname; + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin); + this.opts.path = path14; + this.opts.origin = origin; + this.opts.maxRedirections = 0; + this.opts.query = null; + if (statusCode === 303 && this.opts.method !== "HEAD") { + this.opts.method = "GET"; + this.opts.body = null; + } + } + onData(chunk) { + if (this.location) { + } else { + return this.handler.onData(chunk); + } + } + onComplete(trailers) { + if (this.location) { + this.location = null; + this.abort = null; + this.dispatch(this.opts, this); + } else { + this.handler.onComplete(trailers); + } + } + onBodySent(chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk); + } + } + }; + function parseLocation(statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null; + } + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].length === 8 && util.headerNameToString(headers[i]) === "location") { + return headers[i + 1]; + } + } + } + function shouldRemoveHeader(header, removeContent, unknownOrigin) { + if (header.length === 4) { + return util.headerNameToString(header) === "host"; + } + if (removeContent && util.headerNameToString(header).startsWith("content-")) { + return true; + } + if (unknownOrigin && (header.length === 13 || header.length === 6 || header.length === 19)) { + const name = util.headerNameToString(header); + return name === "authorization" || name === "cookie" || name === "proxy-authorization"; + } + return false; + } + function cleanRequestHeaders(headers, removeContent, unknownOrigin) { + const ret = []; + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]); + } + } + } else if (headers && typeof headers === "object") { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]); + } + } + } else { + assert(headers == null, "headers must be an object or an array"); + } + return ret; + } + module.exports = RedirectHandler; + } +}); + +// viewer/node_modules/undici/lib/interceptor/redirect-interceptor.js +var require_redirect_interceptor = __commonJS({ + "viewer/node_modules/undici/lib/interceptor/redirect-interceptor.js"(exports, module) { + "use strict"; + var RedirectHandler = require_redirect_handler(); + function createRedirectInterceptor({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept(opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts; + if (!maxRedirections) { + return dispatch(opts, handler); + } + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler); + opts = { ...opts, maxRedirections: 0 }; + return dispatch(opts, redirectHandler); + }; + }; + } + module.exports = createRedirectInterceptor; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/client.js +var require_client = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/client.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var net = __require("node:net"); + var http2 = __require("node:http"); + var util = require_util(); + var { channels } = require_diagnostics(); + var Request2 = require_request(); + var DispatcherBase = require_dispatcher_base(); + var { + InvalidArgumentError, + InformationalError, + ClientDestroyedError + } = require_errors(); + var buildConnector = require_connect(); + var { + kUrl, + kServerName, + kClient, + kBusy, + kConnect, + kResuming, + kRunning, + kPending, + kSize, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize, + kOnError, + kHTTPContext, + kMaxConcurrentStreams, + kResume + } = require_symbols(); + var connectH1 = require_client_h1(); + var connectH2 = require_client_h2(); + var deprecatedInterceptorWarned = false; + var kClosedResolve = /* @__PURE__ */ Symbol("kClosedResolve"); + var noop = () => { + }; + function getPipelining(client) { + return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1; + } + var Client = class extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../../types/client.js').Client.Options} options + */ + constructor(url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect: connect2, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + maxConcurrentStreams, + allowH2, + webSocket + } = {}) { + super({ webSocket }); + if (keepAlive !== void 0) { + throw new InvalidArgumentError("unsupported keepAlive, use pipelining=0 instead"); + } + if (socketTimeout !== void 0) { + throw new InvalidArgumentError("unsupported socketTimeout, use headersTimeout & bodyTimeout instead"); + } + if (requestTimeout !== void 0) { + throw new InvalidArgumentError("unsupported requestTimeout, use headersTimeout & bodyTimeout instead"); + } + if (idleTimeout !== void 0) { + throw new InvalidArgumentError("unsupported idleTimeout, use keepAliveTimeout instead"); + } + if (maxKeepAliveTimeout !== void 0) { + throw new InvalidArgumentError("unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead"); + } + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError("invalid maxHeaderSize"); + } + if (socketPath != null && typeof socketPath !== "string") { + throw new InvalidArgumentError("invalid socketPath"); + } + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError("invalid connectTimeout"); + } + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError("invalid keepAliveTimeout"); + } + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError("invalid keepAliveMaxTimeout"); + } + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError("invalid keepAliveTimeoutThreshold"); + } + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError("headersTimeout must be a positive integer or zero"); + } + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError("bodyTimeout must be a positive integer or zero"); + } + if (connect2 != null && typeof connect2 !== "function" && typeof connect2 !== "object") { + throw new InvalidArgumentError("connect must be a function or an object"); + } + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError("maxRedirections must be a positive number"); + } + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError("maxRequestsPerClient must be a positive number"); + } + if (localAddress != null && (typeof localAddress !== "string" || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError("localAddress must be valid string IP address"); + } + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError("maxResponseSize must be a positive number"); + } + if (autoSelectFamilyAttemptTimeout != null && (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1)) { + throw new InvalidArgumentError("autoSelectFamilyAttemptTimeout must be a positive number"); + } + if (allowH2 != null && typeof allowH2 !== "boolean") { + throw new InvalidArgumentError("allowH2 must be a valid boolean value"); + } + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== "number" || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError("maxConcurrentStreams must be a positive integer, greater than 0"); + } + if (typeof connect2 !== "function") { + connect2 = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : void 0, + ...connect2 + }); + } + if (interceptors?.Client && Array.isArray(interceptors.Client)) { + this[kInterceptors] = interceptors.Client; + if (!deprecatedInterceptorWarned) { + deprecatedInterceptorWarned = true; + process.emitWarning("Client.Options#interceptor is deprecated. Use Dispatcher#compose instead.", { + code: "UNDICI-CLIENT-INTERCEPTOR-DEPRECATED" + }); + } + } else { + this[kInterceptors] = [createRedirectInterceptor({ maxRedirections })]; + } + this[kUrl] = util.parseOrigin(url); + this[kConnector] = connect2; + this[kPipelining] = pipelining != null ? pipelining : 1; + this[kMaxHeadersSize] = maxHeaderSize || http2.maxHeaderSize; + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout; + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 6e5 : keepAliveMaxTimeout; + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold; + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout]; + this[kServerName] = null; + this[kLocalAddress] = localAddress != null ? localAddress : null; + this[kResuming] = 0; + this[kNeedDrain] = 0; + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ""}\r +`; + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 3e5; + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 3e5; + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength; + this[kMaxRedirections] = maxRedirections; + this[kMaxRequests] = maxRequestsPerClient; + this[kClosedResolve] = null; + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1; + this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100; + this[kHTTPContext] = null; + this[kQueue] = []; + this[kRunningIdx] = 0; + this[kPendingIdx] = 0; + this[kResume] = (sync) => resume(this, sync); + this[kOnError] = (err) => onError(this, err); + } + get pipelining() { + return this[kPipelining]; + } + set pipelining(value) { + this[kPipelining] = value; + this[kResume](true); + } + get [kPending]() { + return this[kQueue].length - this[kPendingIdx]; + } + get [kRunning]() { + return this[kPendingIdx] - this[kRunningIdx]; + } + get [kSize]() { + return this[kQueue].length - this[kRunningIdx]; + } + get [kConnected]() { + return !!this[kHTTPContext] && !this[kConnecting] && !this[kHTTPContext].destroyed; + } + get [kBusy]() { + return Boolean( + this[kHTTPContext]?.busy(null) || this[kSize] >= (getPipelining(this) || 1) || this[kPending] > 0 + ); + } + /* istanbul ignore: only used for test */ + [kConnect](cb) { + connect(this); + this.once("connect", cb); + } + [kDispatch](opts, handler) { + const origin = opts.origin || this[kUrl].origin; + const request = new Request2(origin, opts, handler); + this[kQueue].push(request); + if (this[kResuming]) { + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + this[kResuming] = 1; + queueMicrotask(() => resume(this)); + } else { + this[kResume](true); + } + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2; + } + return this[kNeedDrain] < 2; + } + async [kClose]() { + return new Promise((resolve) => { + if (this[kSize]) { + this[kClosedResolve] = resolve; + } else { + resolve(null); + } + }); + } + async [kDestroy](err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]); + for (let i = 0; i < requests.length; i++) { + const request = requests[i]; + util.errorRequest(this, request, err); + } + const callback = () => { + if (this[kClosedResolve]) { + this[kClosedResolve](); + this[kClosedResolve] = null; + } + resolve(null); + }; + if (this[kHTTPContext]) { + this[kHTTPContext].destroy(err, callback); + this[kHTTPContext] = null; + } else { + queueMicrotask(callback); + } + this[kResume](); + }); + } + }; + var createRedirectInterceptor = require_redirect_interceptor(); + function onError(client, err) { + if (client[kRunning] === 0 && err.code !== "UND_ERR_INFO" && err.code !== "UND_ERR_SOCKET") { + assert(client[kPendingIdx] === client[kRunningIdx]); + const requests = client[kQueue].splice(client[kRunningIdx]); + for (let i = 0; i < requests.length; i++) { + const request = requests[i]; + util.errorRequest(client, request, err); + } + assert(client[kSize] === 0); + } + } + async function connect(client) { + assert(!client[kConnecting]); + assert(!client[kHTTPContext]); + let { host: host2, hostname, protocol, port: port2 } = client[kUrl]; + if (hostname[0] === "[") { + const idx = hostname.indexOf("]"); + assert(idx !== -1); + const ip = hostname.substring(1, idx); + assert(net.isIP(ip)); + hostname = ip; + } + client[kConnecting] = true; + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host: host2, + hostname, + protocol, + port: port2, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }); + } + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host: host2, + hostname, + protocol, + port: port2, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket2) => { + if (err) { + reject(err); + } else { + resolve(socket2); + } + }); + }); + if (client.destroyed) { + util.destroy(socket.on("error", noop), new ClientDestroyedError()); + return; + } + assert(socket); + try { + client[kHTTPContext] = socket.alpnProtocol === "h2" ? await connectH2(client, socket) : await connectH1(client, socket); + } catch (err) { + socket.destroy().on("error", noop); + throw err; + } + client[kConnecting] = false; + socket[kCounter] = 0; + socket[kMaxRequests] = client[kMaxRequests]; + socket[kClient] = client; + socket[kError] = null; + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host: host2, + hostname, + protocol, + port: port2, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }); + } + client.emit("connect", client[kUrl], [client]); + } catch (err) { + if (client.destroyed) { + return; + } + client[kConnecting] = false; + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host: host2, + hostname, + protocol, + port: port2, + version: client[kHTTPContext]?.version, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }); + } + if (err.code === "ERR_TLS_CERT_ALTNAME_INVALID") { + assert(client[kRunning] === 0); + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++]; + util.errorRequest(client, request, err); + } + } else { + onError(client, err); + } + client.emit("connectionError", client[kUrl], [client], err); + } + client[kResume](); + } + function emitDrain(client) { + client[kNeedDrain] = 0; + client.emit("drain", client[kUrl], [client]); + } + function resume(client, sync) { + if (client[kResuming] === 2) { + return; + } + client[kResuming] = 2; + _resume(client, sync); + client[kResuming] = 0; + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]); + client[kPendingIdx] -= client[kRunningIdx]; + client[kRunningIdx] = 0; + } + } + function _resume(client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0); + return; + } + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve](); + client[kClosedResolve] = null; + return; + } + if (client[kHTTPContext]) { + client[kHTTPContext].resume(); + } + if (client[kBusy]) { + client[kNeedDrain] = 2; + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1; + queueMicrotask(() => emitDrain(client)); + } else { + emitDrain(client); + } + continue; + } + if (client[kPending] === 0) { + return; + } + if (client[kRunning] >= (getPipelining(client) || 1)) { + return; + } + const request = client[kQueue][client[kPendingIdx]]; + if (client[kUrl].protocol === "https:" && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return; + } + client[kServerName] = request.servername; + client[kHTTPContext]?.destroy(new InformationalError("servername changed"), () => { + client[kHTTPContext] = null; + resume(client); + }); + } + if (client[kConnecting]) { + return; + } + if (!client[kHTTPContext]) { + connect(client); + return; + } + if (client[kHTTPContext].destroyed) { + return; + } + if (client[kHTTPContext].busy(request)) { + return; + } + if (!request.aborted && client[kHTTPContext].write(request)) { + client[kPendingIdx]++; + } else { + client[kQueue].splice(client[kPendingIdx], 1); + } + } + } + module.exports = Client; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/fixed-queue.js +var require_fixed_queue = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/fixed-queue.js"(exports, module) { + "use strict"; + var kSize = 2048; + var kMask = kSize - 1; + var FixedCircularBuffer = class { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + isEmpty() { + return this.top === this.bottom; + } + isFull() { + return (this.top + 1 & kMask) === this.bottom; + } + push(data) { + this.list[this.top] = data; + this.top = this.top + 1 & kMask; + } + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === void 0) + return null; + this.list[this.bottom] = void 0; + this.bottom = this.bottom + 1 & kMask; + return nextItem; + } + }; + module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + isEmpty() { + return this.head.isEmpty(); + } + push(data) { + if (this.head.isFull()) { + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + this.tail = tail.next; + } + return next; + } + }; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/pool-stats.js +var require_pool_stats = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/pool-stats.js"(exports, module) { + var { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require_symbols(); + var kPool = /* @__PURE__ */ Symbol("pool"); + var PoolStats = class { + constructor(pool) { + this[kPool] = pool; + } + get connected() { + return this[kPool][kConnected]; + } + get free() { + return this[kPool][kFree]; + } + get pending() { + return this[kPool][kPending]; + } + get queued() { + return this[kPool][kQueued]; + } + get running() { + return this[kPool][kRunning]; + } + get size() { + return this[kPool][kSize]; + } + }; + module.exports = PoolStats; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/pool-base.js +var require_pool_base = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/pool-base.js"(exports, module) { + "use strict"; + var DispatcherBase = require_dispatcher_base(); + var FixedQueue = require_fixed_queue(); + var { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = require_symbols(); + var PoolStats = require_pool_stats(); + var kClients = /* @__PURE__ */ Symbol("clients"); + var kNeedDrain = /* @__PURE__ */ Symbol("needDrain"); + var kQueue = /* @__PURE__ */ Symbol("queue"); + var kClosedResolve = /* @__PURE__ */ Symbol("closed resolve"); + var kOnDrain = /* @__PURE__ */ Symbol("onDrain"); + var kOnConnect = /* @__PURE__ */ Symbol("onConnect"); + var kOnDisconnect = /* @__PURE__ */ Symbol("onDisconnect"); + var kOnConnectionError = /* @__PURE__ */ Symbol("onConnectionError"); + var kGetDispatcher = /* @__PURE__ */ Symbol("get dispatcher"); + var kAddClient = /* @__PURE__ */ Symbol("add client"); + var kRemoveClient = /* @__PURE__ */ Symbol("remove client"); + var kStats = /* @__PURE__ */ Symbol("stats"); + var PoolBase = class extends DispatcherBase { + constructor(opts) { + super(opts); + this[kQueue] = new FixedQueue(); + this[kClients] = []; + this[kQueued] = 0; + const pool = this; + this[kOnDrain] = function onDrain(origin, targets) { + const queue = pool[kQueue]; + let needDrain = false; + while (!needDrain) { + const item = queue.shift(); + if (!item) { + break; + } + pool[kQueued]--; + needDrain = !this.dispatch(item.opts, item.handler); + } + this[kNeedDrain] = needDrain; + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false; + pool.emit("drain", origin, [pool, ...targets]); + } + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise.all(pool[kClients].map((c) => c.close())).then(pool[kClosedResolve]); + } + }; + this[kOnConnect] = (origin, targets) => { + pool.emit("connect", origin, [pool, ...targets]); + }; + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit("disconnect", origin, [pool, ...targets], err); + }; + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit("connectionError", origin, [pool, ...targets], err); + }; + this[kStats] = new PoolStats(this); + } + get [kBusy]() { + return this[kNeedDrain]; + } + get [kConnected]() { + return this[kClients].filter((client) => client[kConnected]).length; + } + get [kFree]() { + return this[kClients].filter((client) => client[kConnected] && !client[kNeedDrain]).length; + } + get [kPending]() { + let ret = this[kQueued]; + for (const { [kPending]: pending } of this[kClients]) { + ret += pending; + } + return ret; + } + get [kRunning]() { + let ret = 0; + for (const { [kRunning]: running } of this[kClients]) { + ret += running; + } + return ret; + } + get [kSize]() { + let ret = this[kQueued]; + for (const { [kSize]: size } of this[kClients]) { + ret += size; + } + return ret; + } + get stats() { + return this[kStats]; + } + async [kClose]() { + if (this[kQueue].isEmpty()) { + await Promise.all(this[kClients].map((c) => c.close())); + } else { + await new Promise((resolve) => { + this[kClosedResolve] = resolve; + }); + } + } + async [kDestroy](err) { + while (true) { + const item = this[kQueue].shift(); + if (!item) { + break; + } + item.handler.onError(err); + } + await Promise.all(this[kClients].map((c) => c.destroy(err))); + } + [kDispatch](opts, handler) { + const dispatcher = this[kGetDispatcher](); + if (!dispatcher) { + this[kNeedDrain] = true; + this[kQueue].push({ opts, handler }); + this[kQueued]++; + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true; + this[kNeedDrain] = !this[kGetDispatcher](); + } + return !this[kNeedDrain]; + } + [kAddClient](client) { + client.on("drain", this[kOnDrain]).on("connect", this[kOnConnect]).on("disconnect", this[kOnDisconnect]).on("connectionError", this[kOnConnectionError]); + this[kClients].push(client); + if (this[kNeedDrain]) { + queueMicrotask(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]); + } + }); + } + return this; + } + [kRemoveClient](client) { + client.close(() => { + const idx = this[kClients].indexOf(client); + if (idx !== -1) { + this[kClients].splice(idx, 1); + } + }); + this[kNeedDrain] = this[kClients].some((dispatcher) => !dispatcher[kNeedDrain] && dispatcher.closed !== true && dispatcher.destroyed !== true); + } + }; + module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher + }; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/pool.js +var require_pool = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/pool.js"(exports, module) { + "use strict"; + var { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher + } = require_pool_base(); + var Client = require_client(); + var { + InvalidArgumentError + } = require_errors(); + var util = require_util(); + var { kUrl, kInterceptors } = require_symbols(); + var buildConnector = require_connect(); + var kOptions = /* @__PURE__ */ Symbol("options"); + var kConnections = /* @__PURE__ */ Symbol("connections"); + var kFactory = /* @__PURE__ */ Symbol("factory"); + function defaultFactory(origin, opts) { + return new Client(origin, opts); + } + var Pool = class extends PoolBase { + constructor(origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + ...options + } = {}) { + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError("invalid connections"); + } + if (typeof factory !== "function") { + throw new InvalidArgumentError("factory must be a function."); + } + if (connect != null && typeof connect !== "function" && typeof connect !== "object") { + throw new InvalidArgumentError("connect must be a function or an object"); + } + if (typeof connect !== "function") { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : void 0, + ...connect + }); + } + super(options); + this[kInterceptors] = options.interceptors?.Pool && Array.isArray(options.interceptors.Pool) ? options.interceptors.Pool : []; + this[kConnections] = connections || null; + this[kUrl] = util.parseOrigin(origin); + this[kOptions] = { ...util.deepClone(options), connect, allowH2 }; + this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : void 0; + this[kFactory] = factory; + this.on("connectionError", (origin2, targets, error) => { + for (const target of targets) { + const idx = this[kClients].indexOf(target); + if (idx !== -1) { + this[kClients].splice(idx, 1); + } + } + }); + } + [kGetDispatcher]() { + for (const client of this[kClients]) { + if (!client[kNeedDrain]) { + return client; + } + } + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + const dispatcher = this[kFactory](this[kUrl], this[kOptions]); + this[kAddClient](dispatcher); + return dispatcher; + } + } + }; + module.exports = Pool; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/balanced-pool.js +var require_balanced_pool = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/balanced-pool.js"(exports, module) { + "use strict"; + var { + BalancedPoolMissingUpstreamError, + InvalidArgumentError + } = require_errors(); + var { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher + } = require_pool_base(); + var Pool = require_pool(); + var { kUrl, kInterceptors } = require_symbols(); + var { parseOrigin } = require_util(); + var kFactory = /* @__PURE__ */ Symbol("factory"); + var kOptions = /* @__PURE__ */ Symbol("options"); + var kGreatestCommonDivisor = /* @__PURE__ */ Symbol("kGreatestCommonDivisor"); + var kCurrentWeight = /* @__PURE__ */ Symbol("kCurrentWeight"); + var kIndex = /* @__PURE__ */ Symbol("kIndex"); + var kWeight = /* @__PURE__ */ Symbol("kWeight"); + var kMaxWeightPerServer = /* @__PURE__ */ Symbol("kMaxWeightPerServer"); + var kErrorPenalty = /* @__PURE__ */ Symbol("kErrorPenalty"); + function getGreatestCommonDivisor(a, b) { + if (a === 0) return b; + while (b !== 0) { + const t = b; + b = a % b; + a = t; + } + return a; + } + function defaultFactory(origin, opts) { + return new Pool(origin, opts); + } + var BalancedPool = class extends PoolBase { + constructor(upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super(); + this[kOptions] = opts; + this[kIndex] = -1; + this[kCurrentWeight] = 0; + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100; + this[kErrorPenalty] = this[kOptions].errorPenalty || 15; + if (!Array.isArray(upstreams)) { + upstreams = [upstreams]; + } + if (typeof factory !== "function") { + throw new InvalidArgumentError("factory must be a function."); + } + this[kInterceptors] = opts.interceptors?.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) ? opts.interceptors.BalancedPool : []; + this[kFactory] = factory; + for (const upstream of upstreams) { + this.addUpstream(upstream); + } + this._updateBalancedPoolStats(); + } + addUpstream(upstream) { + const upstreamOrigin = parseOrigin(upstream).origin; + if (this[kClients].find((pool2) => pool2[kUrl].origin === upstreamOrigin && pool2.closed !== true && pool2.destroyed !== true)) { + return this; + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])); + this[kAddClient](pool); + pool.on("connect", () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]); + }); + pool.on("connectionError", () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]); + this._updateBalancedPoolStats(); + }); + pool.on("disconnect", (...args) => { + const err = args[2]; + if (err && err.code === "UND_ERR_SOCKET") { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]); + this._updateBalancedPoolStats(); + } + }); + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer]; + } + this._updateBalancedPoolStats(); + return this; + } + _updateBalancedPoolStats() { + let result = 0; + for (let i = 0; i < this[kClients].length; i++) { + result = getGreatestCommonDivisor(this[kClients][i][kWeight], result); + } + this[kGreatestCommonDivisor] = result; + } + removeUpstream(upstream) { + const upstreamOrigin = parseOrigin(upstream).origin; + const pool = this[kClients].find((pool2) => pool2[kUrl].origin === upstreamOrigin && pool2.closed !== true && pool2.destroyed !== true); + if (pool) { + this[kRemoveClient](pool); + } + return this; + } + get upstreams() { + return this[kClients].filter((dispatcher) => dispatcher.closed !== true && dispatcher.destroyed !== true).map((p) => p[kUrl].origin); + } + [kGetDispatcher]() { + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError(); + } + const dispatcher = this[kClients].find((dispatcher2) => !dispatcher2[kNeedDrain] && dispatcher2.closed !== true && dispatcher2.destroyed !== true); + if (!dispatcher) { + return; + } + const allClientsBusy = this[kClients].map((pool) => pool[kNeedDrain]).reduce((a, b) => a && b, true); + if (allClientsBusy) { + return; + } + let counter = 0; + let maxWeightIndex = this[kClients].findIndex((pool) => !pool[kNeedDrain]); + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length; + const pool = this[kClients][this[kIndex]]; + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex]; + } + if (this[kIndex] === 0) { + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor]; + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer]; + } + } + if (pool[kWeight] >= this[kCurrentWeight] && !pool[kNeedDrain]) { + return pool; + } + } + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight]; + this[kIndex] = maxWeightIndex; + return this[kClients][maxWeightIndex]; + } + }; + module.exports = BalancedPool; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/agent.js +var require_agent = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/agent.js"(exports, module) { + "use strict"; + var { InvalidArgumentError } = require_errors(); + var { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require_symbols(); + var DispatcherBase = require_dispatcher_base(); + var Pool = require_pool(); + var Client = require_client(); + var util = require_util(); + var createRedirectInterceptor = require_redirect_interceptor(); + var kOnConnect = /* @__PURE__ */ Symbol("onConnect"); + var kOnDisconnect = /* @__PURE__ */ Symbol("onDisconnect"); + var kOnConnectionError = /* @__PURE__ */ Symbol("onConnectionError"); + var kMaxRedirections = /* @__PURE__ */ Symbol("maxRedirections"); + var kOnDrain = /* @__PURE__ */ Symbol("onDrain"); + var kFactory = /* @__PURE__ */ Symbol("factory"); + var kOptions = /* @__PURE__ */ Symbol("options"); + function defaultFactory(origin, opts) { + return opts && opts.connections === 1 ? new Client(origin, opts) : new Pool(origin, opts); + } + var Agent = class extends DispatcherBase { + constructor({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + if (typeof factory !== "function") { + throw new InvalidArgumentError("factory must be a function."); + } + if (connect != null && typeof connect !== "function" && typeof connect !== "object") { + throw new InvalidArgumentError("connect must be a function or an object"); + } + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError("maxRedirections must be a positive number"); + } + super(options); + if (connect && typeof connect !== "function") { + connect = { ...connect }; + } + this[kInterceptors] = options.interceptors?.Agent && Array.isArray(options.interceptors.Agent) ? options.interceptors.Agent : [createRedirectInterceptor({ maxRedirections })]; + this[kOptions] = { ...util.deepClone(options), connect }; + this[kOptions].interceptors = options.interceptors ? { ...options.interceptors } : void 0; + this[kMaxRedirections] = maxRedirections; + this[kFactory] = factory; + this[kClients] = /* @__PURE__ */ new Map(); + this[kOnDrain] = (origin, targets) => { + this.emit("drain", origin, [this, ...targets]); + }; + this[kOnConnect] = (origin, targets) => { + this.emit("connect", origin, [this, ...targets]); + }; + this[kOnDisconnect] = (origin, targets, err) => { + this.emit("disconnect", origin, [this, ...targets], err); + }; + this[kOnConnectionError] = (origin, targets, err) => { + this.emit("connectionError", origin, [this, ...targets], err); + }; + } + get [kRunning]() { + let ret = 0; + for (const client of this[kClients].values()) { + ret += client[kRunning]; + } + return ret; + } + [kDispatch](opts, handler) { + let key; + if (opts.origin && (typeof opts.origin === "string" || opts.origin instanceof URL)) { + key = String(opts.origin); + } else { + throw new InvalidArgumentError("opts.origin must be a non-empty string or URL."); + } + let dispatcher = this[kClients].get(key); + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]).on("drain", this[kOnDrain]).on("connect", this[kOnConnect]).on("disconnect", this[kOnDisconnect]).on("connectionError", this[kOnConnectionError]); + this[kClients].set(key, dispatcher); + } + return dispatcher.dispatch(opts, handler); + } + async [kClose]() { + const closePromises = []; + for (const client of this[kClients].values()) { + closePromises.push(client.close()); + } + this[kClients].clear(); + await Promise.all(closePromises); + } + async [kDestroy](err) { + const destroyPromises = []; + for (const client of this[kClients].values()) { + destroyPromises.push(client.destroy(err)); + } + this[kClients].clear(); + await Promise.all(destroyPromises); + } + }; + module.exports = Agent; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/proxy-agent.js +var require_proxy_agent = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/proxy-agent.js"(exports, module) { + "use strict"; + var { kProxy, kClose, kDestroy, kDispatch, kInterceptors } = require_symbols(); + var { URL: URL2 } = __require("node:url"); + var Agent = require_agent(); + var Pool = require_pool(); + var DispatcherBase = require_dispatcher_base(); + var { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require_errors(); + var buildConnector = require_connect(); + var Client = require_client(); + var kAgent = /* @__PURE__ */ Symbol("proxy agent"); + var kClient = /* @__PURE__ */ Symbol("proxy client"); + var kProxyHeaders = /* @__PURE__ */ Symbol("proxy headers"); + var kRequestTls = /* @__PURE__ */ Symbol("request tls settings"); + var kProxyTls = /* @__PURE__ */ Symbol("proxy tls settings"); + var kConnectEndpoint = /* @__PURE__ */ Symbol("connect endpoint function"); + var kTunnelProxy = /* @__PURE__ */ Symbol("tunnel proxy"); + function defaultProtocolPort(protocol) { + return protocol === "https:" ? 443 : 80; + } + function defaultFactory(origin, opts) { + return new Pool(origin, opts); + } + var noop = () => { + }; + function defaultAgentFactory(origin, opts) { + if (opts.connections === 1) { + return new Client(origin, opts); + } + return new Pool(origin, opts); + } + var Http1ProxyWrapper = class extends DispatcherBase { + #client; + constructor(proxyUrl, { headers = {}, connect, factory }) { + super(); + if (!proxyUrl) { + throw new InvalidArgumentError("Proxy URL is mandatory"); + } + this[kProxyHeaders] = headers; + if (factory) { + this.#client = factory(proxyUrl, { connect }); + } else { + this.#client = new Client(proxyUrl, { connect }); + } + } + [kDispatch](opts, handler) { + const onHeaders = handler.onHeaders; + handler.onHeaders = function(statusCode, data, resume) { + if (statusCode === 407) { + if (typeof handler.onError === "function") { + handler.onError(new InvalidArgumentError("Proxy Authentication Required (407)")); + } + return; + } + if (onHeaders) onHeaders.call(this, statusCode, data, resume); + }; + const { + origin, + path: path14 = "/", + headers = {} + } = opts; + opts.path = origin + path14; + if (!("host" in headers) && !("Host" in headers)) { + const { host: host2 } = new URL2(origin); + headers.host = host2; + } + opts.headers = { ...this[kProxyHeaders], ...headers }; + return this.#client[kDispatch](opts, handler); + } + async [kClose]() { + return this.#client.close(); + } + async [kDestroy](err) { + return this.#client.destroy(err); + } + }; + var ProxyAgent = class extends DispatcherBase { + constructor(opts) { + super(); + if (!opts || typeof opts === "object" && !(opts instanceof URL2) && !opts.uri) { + throw new InvalidArgumentError("Proxy uri is mandatory"); + } + const { clientFactory = defaultFactory } = opts; + if (typeof clientFactory !== "function") { + throw new InvalidArgumentError("Proxy opts.clientFactory must be a function."); + } + const { proxyTunnel = true } = opts; + const url = this.#getUrl(opts); + const { href, origin, port: port2, protocol, username, password, hostname: proxyHostname } = url; + this[kProxy] = { uri: href, protocol }; + this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) ? opts.interceptors.ProxyAgent : []; + this[kRequestTls] = opts.requestTls; + this[kProxyTls] = opts.proxyTls; + this[kProxyHeaders] = opts.headers || {}; + this[kTunnelProxy] = proxyTunnel; + if (opts.auth && opts.token) { + throw new InvalidArgumentError("opts.auth cannot be used in combination with opts.token"); + } else if (opts.auth) { + this[kProxyHeaders]["proxy-authorization"] = `Basic ${opts.auth}`; + } else if (opts.token) { + this[kProxyHeaders]["proxy-authorization"] = opts.token; + } else if (username && password) { + this[kProxyHeaders]["proxy-authorization"] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString("base64")}`; + } + const connect = buildConnector({ ...opts.proxyTls }); + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }); + const agentFactory = opts.factory || defaultAgentFactory; + const factory = (origin2, options) => { + const { protocol: protocol2 } = new URL2(origin2); + if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") { + return new Http1ProxyWrapper(this[kProxy].uri, { + headers: this[kProxyHeaders], + connect, + factory: agentFactory + }); + } + return agentFactory(origin2, options); + }; + this[kClient] = clientFactory(url, { connect }); + this[kAgent] = new Agent({ + ...opts, + factory, + connect: async (opts2, callback) => { + let requestedPath = opts2.host; + if (!opts2.port) { + requestedPath += `:${defaultProtocolPort(opts2.protocol)}`; + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port: port2, + path: requestedPath, + signal: opts2.signal, + headers: { + ...this[kProxyHeaders], + host: opts2.host + }, + servername: this[kProxyTls]?.servername || proxyHostname + }); + if (statusCode !== 200) { + socket.on("error", noop).destroy(); + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)); + } + if (opts2.protocol !== "https:") { + callback(null, socket); + return; + } + let servername; + if (this[kRequestTls]) { + servername = this[kRequestTls].servername; + } else { + servername = opts2.servername; + } + this[kConnectEndpoint]({ ...opts2, servername, httpSocket: socket }, callback); + } catch (err) { + if (err.code === "ERR_TLS_CERT_ALTNAME_INVALID") { + callback(new SecureProxyConnectionError(err)); + } else { + callback(err); + } + } + } + }); + } + dispatch(opts, handler) { + const headers = buildHeaders(opts.headers); + throwIfProxyAuthIsSent(headers); + if (headers && !("host" in headers) && !("Host" in headers)) { + const { host: host2 } = new URL2(opts.origin); + headers.host = host2; + } + return this[kAgent].dispatch( + { + ...opts, + headers + }, + handler + ); + } + /** + * @param {import('../types/proxy-agent').ProxyAgent.Options | string | URL} opts + * @returns {URL} + */ + #getUrl(opts) { + if (typeof opts === "string") { + return new URL2(opts); + } else if (opts instanceof URL2) { + return opts; + } else { + return new URL2(opts.uri); + } + } + async [kClose]() { + await this[kAgent].close(); + await this[kClient].close(); + } + async [kDestroy]() { + await this[kAgent].destroy(); + await this[kClient].destroy(); + } + }; + function buildHeaders(headers) { + if (Array.isArray(headers)) { + const headersPair = {}; + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1]; + } + return headersPair; + } + return headers; + } + function throwIfProxyAuthIsSent(headers) { + const existProxyAuth = headers && Object.keys(headers).find((key) => key.toLowerCase() === "proxy-authorization"); + if (existProxyAuth) { + throw new InvalidArgumentError("Proxy-Authorization should be sent in ProxyAgent constructor"); + } + } + module.exports = ProxyAgent; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +var require_env_http_proxy_agent = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js"(exports, module) { + "use strict"; + var DispatcherBase = require_dispatcher_base(); + var { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = require_symbols(); + var ProxyAgent = require_proxy_agent(); + var Agent = require_agent(); + var DEFAULT_PORTS = { + "http:": 80, + "https:": 443 + }; + var experimentalWarned = false; + var EnvHttpProxyAgent = class extends DispatcherBase { + #noProxyValue = null; + #noProxyEntries = null; + #opts = null; + constructor(opts = {}) { + super(); + this.#opts = opts; + if (!experimentalWarned) { + experimentalWarned = true; + process.emitWarning("EnvHttpProxyAgent is experimental, expect them to change at any time.", { + code: "UNDICI-EHPA" + }); + } + const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts; + this[kNoProxyAgent] = new Agent(agentOpts); + const HTTP_PROXY = httpProxy ?? process.env.http_proxy ?? process.env.HTTP_PROXY; + if (HTTP_PROXY) { + this[kHttpProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTP_PROXY }); + } else { + this[kHttpProxyAgent] = this[kNoProxyAgent]; + } + const HTTPS_PROXY = httpsProxy ?? process.env.https_proxy ?? process.env.HTTPS_PROXY; + if (HTTPS_PROXY) { + this[kHttpsProxyAgent] = new ProxyAgent({ ...agentOpts, uri: HTTPS_PROXY }); + } else { + this[kHttpsProxyAgent] = this[kHttpProxyAgent]; + } + this.#parseNoProxy(); + } + [kDispatch](opts, handler) { + const url = new URL(opts.origin); + const agent = this.#getProxyAgentForUrl(url); + return agent.dispatch(opts, handler); + } + async [kClose]() { + await this[kNoProxyAgent].close(); + if (!this[kHttpProxyAgent][kClosed]) { + await this[kHttpProxyAgent].close(); + } + if (!this[kHttpsProxyAgent][kClosed]) { + await this[kHttpsProxyAgent].close(); + } + } + async [kDestroy](err) { + await this[kNoProxyAgent].destroy(err); + if (!this[kHttpProxyAgent][kDestroyed]) { + await this[kHttpProxyAgent].destroy(err); + } + if (!this[kHttpsProxyAgent][kDestroyed]) { + await this[kHttpsProxyAgent].destroy(err); + } + } + #getProxyAgentForUrl(url) { + let { protocol, host: hostname, port: port2 } = url; + hostname = hostname.replace(/:\d*$/, "").toLowerCase(); + port2 = Number.parseInt(port2, 10) || DEFAULT_PORTS[protocol] || 0; + if (!this.#shouldProxy(hostname, port2)) { + return this[kNoProxyAgent]; + } + if (protocol === "https:") { + return this[kHttpsProxyAgent]; + } + return this[kHttpProxyAgent]; + } + #shouldProxy(hostname, port2) { + if (this.#noProxyChanged) { + this.#parseNoProxy(); + } + if (this.#noProxyEntries.length === 0) { + return true; + } + if (this.#noProxyValue === "*") { + return false; + } + for (let i = 0; i < this.#noProxyEntries.length; i++) { + const entry = this.#noProxyEntries[i]; + if (entry.port && entry.port !== port2) { + continue; + } + if (!/^[.*]/.test(entry.hostname)) { + if (hostname === entry.hostname) { + return false; + } + } else { + if (hostname.endsWith(entry.hostname.replace(/^\*/, ""))) { + return false; + } + } + } + return true; + } + #parseNoProxy() { + const noProxyValue = this.#opts.noProxy ?? this.#noProxyEnv; + const noProxySplit = noProxyValue.split(/[,\s]/); + const noProxyEntries = []; + for (let i = 0; i < noProxySplit.length; i++) { + const entry = noProxySplit[i]; + if (!entry) { + continue; + } + const parsed = entry.match(/^(.+):(\d+)$/); + noProxyEntries.push({ + hostname: (parsed ? parsed[1] : entry).toLowerCase(), + port: parsed ? Number.parseInt(parsed[2], 10) : 0 + }); + } + this.#noProxyValue = noProxyValue; + this.#noProxyEntries = noProxyEntries; + } + get #noProxyChanged() { + if (this.#opts.noProxy !== void 0) { + return false; + } + return this.#noProxyValue !== this.#noProxyEnv; + } + get #noProxyEnv() { + return process.env.no_proxy ?? process.env.NO_PROXY ?? ""; + } + }; + module.exports = EnvHttpProxyAgent; + } +}); + +// viewer/node_modules/undici/lib/handler/retry-handler.js +var require_retry_handler = __commonJS({ + "viewer/node_modules/undici/lib/handler/retry-handler.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { kRetryHandlerDefaultRetry } = require_symbols(); + var { RequestRetryError } = require_errors(); + var { + isDisturbed, + parseHeaders, + parseRangeHeader, + wrapRequestBody + } = require_util(); + function calculateRetryAfterHeader(retryAfter) { + const current = Date.now(); + return new Date(retryAfter).getTime() - current; + } + var RetryHandler = class _RetryHandler { + constructor(opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts; + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {}; + this.dispatch = handlers.dispatch; + this.handler = handlers.handler; + this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }; + this.abort = null; + this.aborted = false; + this.retryOpts = { + retry: retryFn ?? _RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1e3, + // 30s, + minTimeout: minTimeout ?? 500, + // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ["GET", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + "ECONNRESET", + "ECONNREFUSED", + "ENOTFOUND", + "ENETDOWN", + "ENETUNREACH", + "EHOSTDOWN", + "EHOSTUNREACH", + "EPIPE", + "UND_ERR_SOCKET" + ] + }; + this.retryCount = 0; + this.retryCountCheckpoint = 0; + this.start = 0; + this.end = null; + this.etag = null; + this.resume = null; + this.handler.onConnect((reason) => { + this.aborted = true; + if (this.abort) { + this.abort(reason); + } else { + this.reason = reason; + } + }); + } + onRequestSent() { + if (this.handler.onRequestSent) { + this.handler.onRequestSent(); + } + } + onUpgrade(statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket); + } + } + onConnect(abort) { + if (this.aborted) { + abort(this.reason); + } else { + this.abort = abort; + } + } + onBodySent(chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk); + } + static [kRetryHandlerDefaultRetry](err, { state, opts }, cb) { + const { statusCode, code, headers } = err; + const { method, retryOptions } = opts; + const { + maxRetries, + minTimeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions; + const { counter } = state; + if (code && code !== "UND_ERR_REQ_RETRY" && !errorCodes.includes(code)) { + cb(err); + return; + } + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err); + return; + } + if (statusCode != null && Array.isArray(statusCodes) && !statusCodes.includes(statusCode)) { + cb(err); + return; + } + if (counter > maxRetries) { + cb(err); + return; + } + let retryAfterHeader = headers?.["retry-after"]; + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader); + retryAfterHeader = Number.isNaN(retryAfterHeader) ? calculateRetryAfterHeader(retryAfterHeader) : retryAfterHeader * 1e3; + } + const retryTimeout = retryAfterHeader > 0 ? Math.min(retryAfterHeader, maxTimeout) : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout); + setTimeout(() => cb(null), retryTimeout); + } + onHeaders(statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders); + this.retryCount += 1; + if (statusCode >= 300) { + if (this.retryOpts.statusCodes.includes(statusCode) === false) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ); + } else { + this.abort( + new RequestRetryError("Request failed", statusCode, { + headers, + data: { + count: this.retryCount + } + }) + ); + return false; + } + } + if (this.resume != null) { + this.resume = null; + if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) { + this.abort( + new RequestRetryError("server does not support the range header and the payload was partially consumed", statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false; + } + const contentRange = parseRangeHeader(headers["content-range"]); + if (!contentRange) { + this.abort( + new RequestRetryError("Content-Range mismatch", statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false; + } + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError("ETag mismatch", statusCode, { + headers, + data: { count: this.retryCount } + }) + ); + return false; + } + const { start, size, end = size - 1 } = contentRange; + assert(this.start === start, "content-range mismatch"); + assert(this.end == null || this.end === end, "content-range mismatch"); + this.resume = resume; + return true; + } + if (this.end == null) { + if (statusCode === 206) { + const range = parseRangeHeader(headers["content-range"]); + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ); + } + const { start, size, end = size - 1 } = range; + assert( + start != null && Number.isFinite(start), + "content-range mismatch" + ); + assert(end != null && Number.isFinite(end), "invalid content-length"); + this.start = start; + this.end = end; + } + if (this.end == null) { + const contentLength = headers["content-length"]; + this.end = contentLength != null ? Number(contentLength) - 1 : null; + } + assert(Number.isFinite(this.start)); + assert( + this.end == null || Number.isFinite(this.end), + "invalid content-length" + ); + this.resume = resume; + this.etag = headers.etag != null ? headers.etag : null; + if (this.etag != null && this.etag.startsWith("W/")) { + this.etag = null; + } + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ); + } + const err = new RequestRetryError("Request failed", statusCode, { + headers, + data: { count: this.retryCount } + }); + this.abort(err); + return false; + } + onData(chunk) { + this.start += chunk.length; + return this.handler.onData(chunk); + } + onComplete(rawTrailers) { + this.retryCount = 0; + return this.handler.onComplete(rawTrailers); + } + onError(err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err); + } + if (this.retryCount - this.retryCountCheckpoint > 0) { + this.retryCount = this.retryCountCheckpoint + (this.retryCount - this.retryCountCheckpoint); + } else { + this.retryCount += 1; + } + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ); + function onRetry(err2) { + if (err2 != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err2); + } + if (this.start !== 0) { + const headers = { range: `bytes=${this.start}-${this.end ?? ""}` }; + if (this.etag != null) { + headers["if-match"] = this.etag; + } + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + ...headers + } + }; + } + try { + this.retryCountCheckpoint = this.retryCount; + this.dispatch(this.opts, this); + } catch (err3) { + this.handler.onError(err3); + } + } + } + }; + module.exports = RetryHandler; + } +}); + +// viewer/node_modules/undici/lib/dispatcher/retry-agent.js +var require_retry_agent = __commonJS({ + "viewer/node_modules/undici/lib/dispatcher/retry-agent.js"(exports, module) { + "use strict"; + var Dispatcher = require_dispatcher(); + var RetryHandler = require_retry_handler(); + var RetryAgent = class extends Dispatcher { + #agent = null; + #options = null; + constructor(agent, options = {}) { + super(options); + this.#agent = agent; + this.#options = options; + } + dispatch(opts, handler) { + const retry2 = new RetryHandler({ + ...opts, + retryOptions: this.#options + }, { + dispatch: this.#agent.dispatch.bind(this.#agent), + handler + }); + return this.#agent.dispatch(opts, retry2); + } + close() { + return this.#agent.close(); + } + destroy() { + return this.#agent.destroy(); + } + }; + module.exports = RetryAgent; + } +}); + +// viewer/node_modules/undici/lib/api/readable.js +var require_readable = __commonJS({ + "viewer/node_modules/undici/lib/api/readable.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { Readable: Readable2 } = __require("node:stream"); + var { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = require_errors(); + var util = require_util(); + var { ReadableStreamFrom } = require_util(); + var kConsume = /* @__PURE__ */ Symbol("kConsume"); + var kReading = /* @__PURE__ */ Symbol("kReading"); + var kBody = /* @__PURE__ */ Symbol("kBody"); + var kAbort = /* @__PURE__ */ Symbol("kAbort"); + var kContentType = /* @__PURE__ */ Symbol("kContentType"); + var kContentLength = /* @__PURE__ */ Symbol("kContentLength"); + var noop = () => { + }; + var BodyReadable = class extends Readable2 { + constructor({ + resume, + abort, + contentType = "", + contentLength, + highWaterMark = 64 * 1024 + // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }); + this._readableState.dataEmitted = false; + this[kAbort] = abort; + this[kConsume] = null; + this[kBody] = null; + this[kContentType] = contentType; + this[kContentLength] = contentLength; + this[kReading] = false; + } + destroy(err) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError(); + } + if (err) { + this[kAbort](); + } + return super.destroy(err); + } + _destroy(err, callback) { + if (!this[kReading]) { + setImmediate(() => { + callback(err); + }); + } else { + callback(err); + } + } + on(ev, ...args) { + if (ev === "data" || ev === "readable") { + this[kReading] = true; + } + return super.on(ev, ...args); + } + addListener(ev, ...args) { + return this.on(ev, ...args); + } + off(ev, ...args) { + const ret = super.off(ev, ...args); + if (ev === "data" || ev === "readable") { + this[kReading] = this.listenerCount("data") > 0 || this.listenerCount("readable") > 0; + } + return ret; + } + removeListener(ev, ...args) { + return this.off(ev, ...args); + } + push(chunk) { + if (this[kConsume] && chunk !== null) { + consumePush(this[kConsume], chunk); + return this[kReading] ? super.push(chunk) : true; + } + return super.push(chunk); + } + // https://fetch.spec.whatwg.org/#dom-body-text + async text() { + return consume(this, "text"); + } + // https://fetch.spec.whatwg.org/#dom-body-json + async json() { + return consume(this, "json"); + } + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob() { + return consume(this, "blob"); + } + // https://fetch.spec.whatwg.org/#dom-body-bytes + async bytes() { + return consume(this, "bytes"); + } + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer() { + return consume(this, "arrayBuffer"); + } + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData() { + throw new NotSupportedError(); + } + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed() { + return util.isDisturbed(this); + } + // https://fetch.spec.whatwg.org/#dom-body-body + get body() { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this); + if (this[kConsume]) { + this[kBody].getReader(); + assert(this[kBody].locked); + } + } + return this[kBody]; + } + async dump(opts) { + let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024; + const signal = opts?.signal; + if (signal != null && (typeof signal !== "object" || !("aborted" in signal))) { + throw new InvalidArgumentError("signal must be an AbortSignal"); + } + signal?.throwIfAborted(); + if (this._readableState.closeEmitted) { + return null; + } + return await new Promise((resolve, reject) => { + if (this[kContentLength] > limit) { + this.destroy(new AbortError()); + } + const onAbort = () => { + this.destroy(signal.reason ?? new AbortError()); + }; + signal?.addEventListener("abort", onAbort); + this.on("close", function() { + signal?.removeEventListener("abort", onAbort); + if (signal?.aborted) { + reject(signal.reason ?? new AbortError()); + } else { + resolve(null); + } + }).on("error", noop).on("data", function(chunk) { + limit -= chunk.length; + if (limit <= 0) { + this.destroy(); + } + }).resume(); + }); + } + }; + function isLocked(self) { + return self[kBody] && self[kBody].locked === true || self[kConsume]; + } + function isUnusable(self) { + return util.isDisturbed(self) || isLocked(self); + } + async function consume(stream, type) { + assert(!stream[kConsume]); + return new Promise((resolve, reject) => { + if (isUnusable(stream)) { + const rState = stream._readableState; + if (rState.destroyed && rState.closeEmitted === false) { + stream.on("error", (err) => { + reject(err); + }).on("close", () => { + reject(new TypeError("unusable")); + }); + } else { + reject(rState.errored ?? new TypeError("unusable")); + } + } else { + queueMicrotask(() => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + }; + stream.on("error", function(err) { + consumeFinish(this[kConsume], err); + }).on("close", function() { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()); + } + }); + consumeStart(stream[kConsume]); + }); + } + }); + } + function consumeStart(consume2) { + if (consume2.body === null) { + return; + } + const { _readableState: state } = consume2.stream; + if (state.bufferIndex) { + const start = state.bufferIndex; + const end = state.buffer.length; + for (let n = start; n < end; n++) { + consumePush(consume2, state.buffer[n]); + } + } else { + for (const chunk of state.buffer) { + consumePush(consume2, chunk); + } + } + if (state.endEmitted) { + consumeEnd(this[kConsume]); + } else { + consume2.stream.on("end", function() { + consumeEnd(this[kConsume]); + }); + } + consume2.stream.resume(); + while (consume2.stream.read() != null) { + } + } + function chunksDecode(chunks, length2) { + if (chunks.length === 0 || length2 === 0) { + return ""; + } + const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length2); + const bufferLength = buffer.length; + const start = bufferLength > 2 && buffer[0] === 239 && buffer[1] === 187 && buffer[2] === 191 ? 3 : 0; + return buffer.utf8Slice(start, bufferLength); + } + function chunksConcat(chunks, length2) { + if (chunks.length === 0 || length2 === 0) { + return new Uint8Array(0); + } + if (chunks.length === 1) { + return new Uint8Array(chunks[0]); + } + const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length2).buffer); + let offset = 0; + for (let i = 0; i < chunks.length; ++i) { + const chunk = chunks[i]; + buffer.set(chunk, offset); + offset += chunk.length; + } + return buffer; + } + function consumeEnd(consume2) { + const { type, body, resolve, stream, length: length2 } = consume2; + try { + if (type === "text") { + resolve(chunksDecode(body, length2)); + } else if (type === "json") { + resolve(JSON.parse(chunksDecode(body, length2))); + } else if (type === "arrayBuffer") { + resolve(chunksConcat(body, length2).buffer); + } else if (type === "blob") { + resolve(new Blob(body, { type: stream[kContentType] })); + } else if (type === "bytes") { + resolve(chunksConcat(body, length2)); + } + consumeFinish(consume2); + } catch (err) { + stream.destroy(err); + } + } + function consumePush(consume2, chunk) { + consume2.length += chunk.length; + consume2.body.push(chunk); + } + function consumeFinish(consume2, err) { + if (consume2.body === null) { + return; + } + if (err) { + consume2.reject(err); + } else { + consume2.resolve(); + } + consume2.type = null; + consume2.stream = null; + consume2.resolve = null; + consume2.reject = null; + consume2.length = 0; + consume2.body = null; + } + module.exports = { Readable: BodyReadable, chunksDecode }; + } +}); + +// viewer/node_modules/undici/lib/api/util.js +var require_util3 = __commonJS({ + "viewer/node_modules/undici/lib/api/util.js"(exports, module) { + var assert = __require("node:assert"); + var { + ResponseStatusCodeError + } = require_errors(); + var { chunksDecode } = require_readable(); + var CHUNK_LIMIT = 128 * 1024; + async function getResolveErrorBodyCallback({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body); + let chunks = []; + let length2 = 0; + try { + for await (const chunk of body) { + chunks.push(chunk); + length2 += chunk.length; + if (length2 > CHUNK_LIMIT) { + chunks = []; + length2 = 0; + break; + } + } + } catch { + chunks = []; + length2 = 0; + } + const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ""}`; + if (statusCode === 204 || !contentType || !length2) { + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers))); + return; + } + const stackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + let payload; + try { + if (isContentTypeApplicationJson(contentType)) { + payload = JSON.parse(chunksDecode(chunks, length2)); + } else if (isContentTypeText(contentType)) { + payload = chunksDecode(chunks, length2); + } + } catch { + } finally { + Error.stackTraceLimit = stackTraceLimit; + } + queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload))); + } + var isContentTypeApplicationJson = (contentType) => { + return contentType.length > 15 && contentType[11] === "/" && contentType[0] === "a" && contentType[1] === "p" && contentType[2] === "p" && contentType[3] === "l" && contentType[4] === "i" && contentType[5] === "c" && contentType[6] === "a" && contentType[7] === "t" && contentType[8] === "i" && contentType[9] === "o" && contentType[10] === "n" && contentType[12] === "j" && contentType[13] === "s" && contentType[14] === "o" && contentType[15] === "n"; + }; + var isContentTypeText = (contentType) => { + return contentType.length > 4 && contentType[4] === "/" && contentType[0] === "t" && contentType[1] === "e" && contentType[2] === "x" && contentType[3] === "t"; + }; + module.exports = { + getResolveErrorBodyCallback, + isContentTypeApplicationJson, + isContentTypeText + }; + } +}); + +// viewer/node_modules/undici/lib/api/api-request.js +var require_api_request = __commonJS({ + "viewer/node_modules/undici/lib/api/api-request.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { Readable: Readable2 } = require_readable(); + var { InvalidArgumentError, RequestAbortedError } = require_errors(); + var util = require_util(); + var { getResolveErrorBodyCallback } = require_util3(); + var { AsyncResource } = __require("node:async_hooks"); + var RequestHandler = class extends AsyncResource { + constructor(opts, callback) { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts; + try { + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + if (highWaterMark && (typeof highWaterMark !== "number" || highWaterMark < 0)) { + throw new InvalidArgumentError("invalid highWaterMark"); + } + if (signal && typeof signal.on !== "function" && typeof signal.addEventListener !== "function") { + throw new InvalidArgumentError("signal must be an EventEmitter or EventTarget"); + } + if (method === "CONNECT") { + throw new InvalidArgumentError("invalid method"); + } + if (onInfo && typeof onInfo !== "function") { + throw new InvalidArgumentError("invalid onInfo callback"); + } + super("UNDICI_REQUEST"); + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on("error", util.nop), err); + } + throw err; + } + this.method = method; + this.responseHeaders = responseHeaders || null; + this.opaque = opaque || null; + this.callback = callback; + this.res = null; + this.abort = null; + this.body = body; + this.trailers = {}; + this.context = null; + this.onInfo = onInfo || null; + this.throwOnError = throwOnError; + this.highWaterMark = highWaterMark; + this.signal = signal; + this.reason = null; + this.removeAbortListener = null; + if (util.isStream(body)) { + body.on("error", (err) => { + this.onError(err); + }); + } + if (this.signal) { + if (this.signal.aborted) { + this.reason = this.signal.reason ?? new RequestAbortedError(); + } else { + this.removeAbortListener = util.addAbortListener(this.signal, () => { + this.reason = this.signal.reason ?? new RequestAbortedError(); + if (this.res) { + util.destroy(this.res.on("error", util.nop), this.reason); + } else if (this.abort) { + this.abort(this.reason); + } + if (this.removeAbortListener) { + this.res?.off("close", this.removeAbortListener); + this.removeAbortListener(); + this.removeAbortListener = null; + } + }); + } + } + } + onConnect(abort, context) { + if (this.reason) { + abort(this.reason); + return; + } + assert(this.callback); + this.abort = abort; + this.context = context; + } + onHeaders(statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this; + const headers = responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }); + } + return; + } + const parsedHeaders = responseHeaders === "raw" ? util.parseHeaders(rawHeaders) : headers; + const contentType = parsedHeaders["content-type"]; + const contentLength = parsedHeaders["content-length"]; + const res = new Readable2({ + resume, + abort, + contentType, + contentLength: this.method !== "HEAD" && contentLength ? Number(contentLength) : null, + highWaterMark + }); + if (this.removeAbortListener) { + res.on("close", this.removeAbortListener); + } + this.callback = null; + this.res = res; + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope( + getResolveErrorBodyCallback, + null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ); + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body: res, + context + }); + } + } + } + onData(chunk) { + return this.res.push(chunk); + } + onComplete(trailers) { + util.parseHeaders(trailers, this.trailers); + this.res.push(null); + } + onError(err) { + const { res, callback, body, opaque } = this; + if (callback) { + this.callback = null; + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }); + }); + } + if (res) { + this.res = null; + queueMicrotask(() => { + util.destroy(res, err); + }); + } + if (body) { + this.body = null; + util.destroy(body, err); + } + if (this.removeAbortListener) { + res?.off("close", this.removeAbortListener); + this.removeAbortListener(); + this.removeAbortListener = null; + } + } + }; + function request(opts, callback) { + if (callback === void 0) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data); + }); + }); + } + try { + this.dispatch(opts, new RequestHandler(opts, callback)); + } catch (err) { + if (typeof callback !== "function") { + throw err; + } + const opaque = opts?.opaque; + queueMicrotask(() => callback(err, { opaque })); + } + } + module.exports = request; + module.exports.RequestHandler = RequestHandler; + } +}); + +// viewer/node_modules/undici/lib/api/abort-signal.js +var require_abort_signal = __commonJS({ + "viewer/node_modules/undici/lib/api/abort-signal.js"(exports, module) { + var { addAbortListener } = require_util(); + var { RequestAbortedError } = require_errors(); + var kListener = /* @__PURE__ */ Symbol("kListener"); + var kSignal = /* @__PURE__ */ Symbol("kSignal"); + function abort(self) { + if (self.abort) { + self.abort(self[kSignal]?.reason); + } else { + self.reason = self[kSignal]?.reason ?? new RequestAbortedError(); + } + removeSignal(self); + } + function addSignal(self, signal) { + self.reason = null; + self[kSignal] = null; + self[kListener] = null; + if (!signal) { + return; + } + if (signal.aborted) { + abort(self); + return; + } + self[kSignal] = signal; + self[kListener] = () => { + abort(self); + }; + addAbortListener(self[kSignal], self[kListener]); + } + function removeSignal(self) { + if (!self[kSignal]) { + return; + } + if ("removeEventListener" in self[kSignal]) { + self[kSignal].removeEventListener("abort", self[kListener]); + } else { + self[kSignal].removeListener("abort", self[kListener]); + } + self[kSignal] = null; + self[kListener] = null; + } + module.exports = { + addSignal, + removeSignal + }; + } +}); + +// viewer/node_modules/undici/lib/api/api-stream.js +var require_api_stream = __commonJS({ + "viewer/node_modules/undici/lib/api/api-stream.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { finished, PassThrough } = __require("node:stream"); + var { InvalidArgumentError, InvalidReturnValueError } = require_errors(); + var util = require_util(); + var { getResolveErrorBodyCallback } = require_util3(); + var { AsyncResource } = __require("node:async_hooks"); + var { addSignal, removeSignal } = require_abort_signal(); + var StreamHandler = class extends AsyncResource { + constructor(opts, factory, callback) { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts; + try { + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + if (typeof factory !== "function") { + throw new InvalidArgumentError("invalid factory"); + } + if (signal && typeof signal.on !== "function" && typeof signal.addEventListener !== "function") { + throw new InvalidArgumentError("signal must be an EventEmitter or EventTarget"); + } + if (method === "CONNECT") { + throw new InvalidArgumentError("invalid method"); + } + if (onInfo && typeof onInfo !== "function") { + throw new InvalidArgumentError("invalid onInfo callback"); + } + super("UNDICI_STREAM"); + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on("error", util.nop), err); + } + throw err; + } + this.responseHeaders = responseHeaders || null; + this.opaque = opaque || null; + this.factory = factory; + this.callback = callback; + this.res = null; + this.abort = null; + this.context = null; + this.trailers = null; + this.body = body; + this.onInfo = onInfo || null; + this.throwOnError = throwOnError || false; + if (util.isStream(body)) { + body.on("error", (err) => { + this.onError(err); + }); + } + addSignal(this, signal); + } + onConnect(abort, context) { + if (this.reason) { + abort(this.reason); + return; + } + assert(this.callback); + this.abort = abort; + this.context = context; + } + onHeaders(statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback, responseHeaders } = this; + const headers = responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }); + } + return; + } + this.factory = null; + let res; + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === "raw" ? util.parseHeaders(rawHeaders) : headers; + const contentType = parsedHeaders["content-type"]; + res = new PassThrough(); + this.callback = null; + this.runInAsyncScope( + getResolveErrorBodyCallback, + null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ); + } else { + if (factory === null) { + return; + } + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }); + if (!res || typeof res.write !== "function" || typeof res.end !== "function" || typeof res.on !== "function") { + throw new InvalidReturnValueError("expected Writable"); + } + finished(res, { readable: false }, (err) => { + const { callback: callback2, res: res2, opaque: opaque2, trailers, abort } = this; + this.res = null; + if (err || !res2.readable) { + util.destroy(res2, err); + } + this.callback = null; + this.runInAsyncScope(callback2, null, err || null, { opaque: opaque2, trailers }); + if (err) { + abort(); + } + }); + } + res.on("drain", resume); + this.res = res; + const needDrain = res.writableNeedDrain !== void 0 ? res.writableNeedDrain : res._writableState?.needDrain; + return needDrain !== true; + } + onData(chunk) { + const { res } = this; + return res ? res.write(chunk) : true; + } + onComplete(trailers) { + const { res } = this; + removeSignal(this); + if (!res) { + return; + } + this.trailers = util.parseHeaders(trailers); + res.end(); + } + onError(err) { + const { res, callback, opaque, body } = this; + removeSignal(this); + this.factory = null; + if (res) { + this.res = null; + util.destroy(res, err); + } else if (callback) { + this.callback = null; + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }); + }); + } + if (body) { + this.body = null; + util.destroy(body, err); + } + } + }; + function stream(opts, factory, callback) { + if (callback === void 0) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data); + }); + }); + } + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)); + } catch (err) { + if (typeof callback !== "function") { + throw err; + } + const opaque = opts?.opaque; + queueMicrotask(() => callback(err, { opaque })); + } + } + module.exports = stream; + } +}); + +// viewer/node_modules/undici/lib/api/api-pipeline.js +var require_api_pipeline = __commonJS({ + "viewer/node_modules/undici/lib/api/api-pipeline.js"(exports, module) { + "use strict"; + var { + Readable: Readable2, + Duplex, + PassThrough + } = __require("node:stream"); + var { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError + } = require_errors(); + var util = require_util(); + var { AsyncResource } = __require("node:async_hooks"); + var { addSignal, removeSignal } = require_abort_signal(); + var assert = __require("node:assert"); + var kResume = /* @__PURE__ */ Symbol("resume"); + var PipelineRequest = class extends Readable2 { + constructor() { + super({ autoDestroy: true }); + this[kResume] = null; + } + _read() { + const { [kResume]: resume } = this; + if (resume) { + this[kResume] = null; + resume(); + } + } + _destroy(err, callback) { + this._read(); + callback(err); + } + }; + var PipelineResponse = class extends Readable2 { + constructor(resume) { + super({ autoDestroy: true }); + this[kResume] = resume; + } + _read() { + this[kResume](); + } + _destroy(err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError(); + } + callback(err); + } + }; + var PipelineHandler = class extends AsyncResource { + constructor(opts, handler) { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + if (typeof handler !== "function") { + throw new InvalidArgumentError("invalid handler"); + } + const { signal, method, opaque, onInfo, responseHeaders } = opts; + if (signal && typeof signal.on !== "function" && typeof signal.addEventListener !== "function") { + throw new InvalidArgumentError("signal must be an EventEmitter or EventTarget"); + } + if (method === "CONNECT") { + throw new InvalidArgumentError("invalid method"); + } + if (onInfo && typeof onInfo !== "function") { + throw new InvalidArgumentError("invalid onInfo callback"); + } + super("UNDICI_PIPELINE"); + this.opaque = opaque || null; + this.responseHeaders = responseHeaders || null; + this.handler = handler; + this.abort = null; + this.context = null; + this.onInfo = onInfo || null; + this.req = new PipelineRequest().on("error", util.nop); + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this; + if (body?.resume) { + body.resume(); + } + }, + write: (chunk, encoding, callback) => { + const { req } = this; + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback(); + } else { + req[kResume] = callback; + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this; + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError(); + } + if (abort && err) { + abort(); + } + util.destroy(body, err); + util.destroy(req, err); + util.destroy(res, err); + removeSignal(this); + callback(err); + } + }).on("prefinish", () => { + const { req } = this; + req.push(null); + }); + this.res = null; + addSignal(this, signal); + } + onConnect(abort, context) { + const { ret, res } = this; + if (this.reason) { + abort(this.reason); + return; + } + assert(!res, "pipeline cannot be retried"); + assert(!ret.destroyed); + this.abort = abort; + this.context = context; + } + onHeaders(statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this; + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + this.onInfo({ statusCode, headers }); + } + return; + } + this.res = new PipelineResponse(resume); + let body; + try { + this.handler = null; + const headers = this.responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }); + } catch (err) { + this.res.on("error", util.nop); + throw err; + } + if (!body || typeof body.on !== "function") { + throw new InvalidReturnValueError("expected Readable"); + } + body.on("data", (chunk) => { + const { ret, body: body2 } = this; + if (!ret.push(chunk) && body2.pause) { + body2.pause(); + } + }).on("error", (err) => { + const { ret } = this; + util.destroy(ret, err); + }).on("end", () => { + const { ret } = this; + ret.push(null); + }).on("close", () => { + const { ret } = this; + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()); + } + }); + this.body = body; + } + onData(chunk) { + const { res } = this; + return res.push(chunk); + } + onComplete(trailers) { + const { res } = this; + res.push(null); + } + onError(err) { + const { ret } = this; + this.handler = null; + util.destroy(ret, err); + } + }; + function pipeline(opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler); + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler); + return pipelineHandler.ret; + } catch (err) { + return new PassThrough().destroy(err); + } + } + module.exports = pipeline; + } +}); + +// viewer/node_modules/undici/lib/api/api-upgrade.js +var require_api_upgrade = __commonJS({ + "viewer/node_modules/undici/lib/api/api-upgrade.js"(exports, module) { + "use strict"; + var { InvalidArgumentError, SocketError } = require_errors(); + var { AsyncResource } = __require("node:async_hooks"); + var util = require_util(); + var { addSignal, removeSignal } = require_abort_signal(); + var assert = __require("node:assert"); + var UpgradeHandler = class extends AsyncResource { + constructor(opts, callback) { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + const { signal, opaque, responseHeaders } = opts; + if (signal && typeof signal.on !== "function" && typeof signal.addEventListener !== "function") { + throw new InvalidArgumentError("signal must be an EventEmitter or EventTarget"); + } + super("UNDICI_UPGRADE"); + this.responseHeaders = responseHeaders || null; + this.opaque = opaque || null; + this.callback = callback; + this.abort = null; + this.context = null; + addSignal(this, signal); + } + onConnect(abort, context) { + if (this.reason) { + abort(this.reason); + return; + } + assert(this.callback); + this.abort = abort; + this.context = null; + } + onHeaders() { + throw new SocketError("bad upgrade", null); + } + onUpgrade(statusCode, rawHeaders, socket) { + assert(statusCode === 101); + const { callback, opaque, context } = this; + removeSignal(this); + this.callback = null; + const headers = this.responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }); + } + onError(err) { + const { callback, opaque } = this; + removeSignal(this); + if (callback) { + this.callback = null; + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }); + }); + } + } + }; + function upgrade(opts, callback) { + if (callback === void 0) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data); + }); + }); + } + try { + const upgradeHandler = new UpgradeHandler(opts, callback); + this.dispatch({ + ...opts, + method: opts.method || "GET", + upgrade: opts.protocol || "Websocket" + }, upgradeHandler); + } catch (err) { + if (typeof callback !== "function") { + throw err; + } + const opaque = opts?.opaque; + queueMicrotask(() => callback(err, { opaque })); + } + } + module.exports = upgrade; + } +}); + +// viewer/node_modules/undici/lib/api/api-connect.js +var require_api_connect = __commonJS({ + "viewer/node_modules/undici/lib/api/api-connect.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { AsyncResource } = __require("node:async_hooks"); + var { InvalidArgumentError, SocketError } = require_errors(); + var util = require_util(); + var { addSignal, removeSignal } = require_abort_signal(); + var ConnectHandler = class extends AsyncResource { + constructor(opts, callback) { + if (!opts || typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + if (typeof callback !== "function") { + throw new InvalidArgumentError("invalid callback"); + } + const { signal, opaque, responseHeaders } = opts; + if (signal && typeof signal.on !== "function" && typeof signal.addEventListener !== "function") { + throw new InvalidArgumentError("signal must be an EventEmitter or EventTarget"); + } + super("UNDICI_CONNECT"); + this.opaque = opaque || null; + this.responseHeaders = responseHeaders || null; + this.callback = callback; + this.abort = null; + addSignal(this, signal); + } + onConnect(abort, context) { + if (this.reason) { + abort(this.reason); + return; + } + assert(this.callback); + this.abort = abort; + this.context = context; + } + onHeaders() { + throw new SocketError("bad connect", null); + } + onUpgrade(statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this; + removeSignal(this); + this.callback = null; + let headers = rawHeaders; + if (headers != null) { + headers = this.responseHeaders === "raw" ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders); + } + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }); + } + onError(err) { + const { callback, opaque } = this; + removeSignal(this); + if (callback) { + this.callback = null; + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }); + }); + } + } + }; + function connect(opts, callback) { + if (callback === void 0) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data); + }); + }); + } + try { + const connectHandler = new ConnectHandler(opts, callback); + this.dispatch({ ...opts, method: "CONNECT" }, connectHandler); + } catch (err) { + if (typeof callback !== "function") { + throw err; + } + const opaque = opts?.opaque; + queueMicrotask(() => callback(err, { opaque })); + } + } + module.exports = connect; + } +}); + +// viewer/node_modules/undici/lib/api/index.js +var require_api = __commonJS({ + "viewer/node_modules/undici/lib/api/index.js"(exports, module) { + "use strict"; + module.exports.request = require_api_request(); + module.exports.stream = require_api_stream(); + module.exports.pipeline = require_api_pipeline(); + module.exports.upgrade = require_api_upgrade(); + module.exports.connect = require_api_connect(); + } +}); + +// viewer/node_modules/undici/lib/mock/mock-errors.js +var require_mock_errors = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-errors.js"(exports, module) { + "use strict"; + var { UndiciError } = require_errors(); + var kMockNotMatchedError = /* @__PURE__ */ Symbol.for("undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED"); + var MockNotMatchedError = class _MockNotMatchedError extends UndiciError { + constructor(message) { + super(message); + Error.captureStackTrace(this, _MockNotMatchedError); + this.name = "MockNotMatchedError"; + this.message = message || "The request does not match any registered mock dispatches"; + this.code = "UND_MOCK_ERR_MOCK_NOT_MATCHED"; + } + static [Symbol.hasInstance](instance) { + return instance && instance[kMockNotMatchedError] === true; + } + [kMockNotMatchedError] = true; + }; + module.exports = { + MockNotMatchedError + }; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-symbols.js +var require_mock_symbols = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-symbols.js"(exports, module) { + "use strict"; + module.exports = { + kAgent: /* @__PURE__ */ Symbol("agent"), + kOptions: /* @__PURE__ */ Symbol("options"), + kFactory: /* @__PURE__ */ Symbol("factory"), + kDispatches: /* @__PURE__ */ Symbol("dispatches"), + kDispatchKey: /* @__PURE__ */ Symbol("dispatch key"), + kDefaultHeaders: /* @__PURE__ */ Symbol("default headers"), + kDefaultTrailers: /* @__PURE__ */ Symbol("default trailers"), + kContentLength: /* @__PURE__ */ Symbol("content length"), + kMockAgent: /* @__PURE__ */ Symbol("mock agent"), + kMockAgentSet: /* @__PURE__ */ Symbol("mock agent set"), + kMockAgentGet: /* @__PURE__ */ Symbol("mock agent get"), + kMockDispatch: /* @__PURE__ */ Symbol("mock dispatch"), + kClose: /* @__PURE__ */ Symbol("close"), + kOriginalClose: /* @__PURE__ */ Symbol("original agent close"), + kOrigin: /* @__PURE__ */ Symbol("origin"), + kIsMockActive: /* @__PURE__ */ Symbol("is mock active"), + kNetConnect: /* @__PURE__ */ Symbol("net connect"), + kGetNetConnect: /* @__PURE__ */ Symbol("get net connect"), + kConnected: /* @__PURE__ */ Symbol("connected") + }; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-utils.js +var require_mock_utils = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-utils.js"(exports, module) { + "use strict"; + var { MockNotMatchedError } = require_mock_errors(); + var { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect + } = require_mock_symbols(); + var { buildURL } = require_util(); + var { STATUS_CODES } = __require("node:http"); + var { + types: { + isPromise + } + } = __require("node:util"); + function matchValue(match, value) { + if (typeof match === "string") { + return match === value; + } + if (match instanceof RegExp) { + return match.test(value); + } + if (typeof match === "function") { + return match(value) === true; + } + return false; + } + function lowerCaseEntries(headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue]; + }) + ); + } + function getHeaderByName(headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1]; + } + } + return void 0; + } else if (typeof headers.get === "function") { + return headers.get(key); + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()]; + } + } + function buildHeadersFromArray(headers) { + const clone = headers.slice(); + const entries = []; + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]); + } + return Object.fromEntries(entries); + } + function matchHeaders(mockDispatch2, headers) { + if (typeof mockDispatch2.headers === "function") { + if (Array.isArray(headers)) { + headers = buildHeadersFromArray(headers); + } + return mockDispatch2.headers(headers ? lowerCaseEntries(headers) : {}); + } + if (typeof mockDispatch2.headers === "undefined") { + return true; + } + if (typeof headers !== "object" || typeof mockDispatch2.headers !== "object") { + return false; + } + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch2.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName); + if (!matchValue(matchHeaderValue, headerValue)) { + return false; + } + } + return true; + } + function safeUrl(path14) { + if (typeof path14 !== "string") { + return path14; + } + const pathSegments = path14.split("?"); + if (pathSegments.length !== 2) { + return path14; + } + const qp = new URLSearchParams(pathSegments.pop()); + qp.sort(); + return [...pathSegments, qp.toString()].join("?"); + } + function matchKey(mockDispatch2, { path: path14, method, body, headers }) { + const pathMatch = matchValue(mockDispatch2.path, path14); + const methodMatch = matchValue(mockDispatch2.method, method); + const bodyMatch = typeof mockDispatch2.body !== "undefined" ? matchValue(mockDispatch2.body, body) : true; + const headersMatch = matchHeaders(mockDispatch2, headers); + return pathMatch && methodMatch && bodyMatch && headersMatch; + } + function getResponseData(data) { + if (Buffer.isBuffer(data)) { + return data; + } else if (data instanceof Uint8Array) { + return data; + } else if (data instanceof ArrayBuffer) { + return data; + } else if (typeof data === "object") { + return JSON.stringify(data); + } else { + return data.toString(); + } + } + function getMockDispatch(mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path; + const resolvedPath = typeof basePath === "string" ? safeUrl(basePath) : basePath; + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path14 }) => matchValue(safeUrl(path14), resolvedPath)); + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`); + } + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)); + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}' on path '${resolvedPath}'`); + } + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== "undefined" ? matchValue(body, key.body) : true); + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}' on path '${resolvedPath}'`); + } + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch2) => matchHeaders(mockDispatch2, key.headers)); + if (matchedMockDispatches.length === 0) { + const headers = typeof key.headers === "object" ? JSON.stringify(key.headers) : key.headers; + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${headers}' on path '${resolvedPath}'`); + } + return matchedMockDispatches[0]; + } + function addMockDispatch(mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false }; + const replyData = typeof data === "function" ? { callback: data } : { ...data }; + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } }; + mockDispatches.push(newMockDispatch); + return newMockDispatch; + } + function deleteMockDispatch(mockDispatches, key) { + const index = mockDispatches.findIndex((dispatch) => { + if (!dispatch.consumed) { + return false; + } + return matchKey(dispatch, key); + }); + if (index !== -1) { + mockDispatches.splice(index, 1); + } + } + function buildKey(opts) { + const { path: path14, method, body, headers, query } = opts; + return { + path: path14, + method, + body, + headers, + query + }; + } + function generateKeyValues(data) { + const keys = Object.keys(data); + const result = []; + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + const value = data[key]; + const name = Buffer.from(`${key}`); + if (Array.isArray(value)) { + for (let j = 0; j < value.length; ++j) { + result.push(name, Buffer.from(`${value[j]}`)); + } + } else { + result.push(name, Buffer.from(`${value}`)); + } + } + return result; + } + function getStatusText(statusCode) { + return STATUS_CODES[statusCode] || "unknown"; + } + async function getResponse(body) { + const buffers = []; + for await (const data of body) { + buffers.push(data); + } + return Buffer.concat(buffers).toString("utf8"); + } + function mockDispatch(opts, handler) { + const key = buildKey(opts); + const mockDispatch2 = getMockDispatch(this[kDispatches], key); + mockDispatch2.timesInvoked++; + if (mockDispatch2.data.callback) { + mockDispatch2.data = { ...mockDispatch2.data, ...mockDispatch2.data.callback(opts) }; + } + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch2; + const { timesInvoked, times } = mockDispatch2; + mockDispatch2.consumed = !persist && timesInvoked >= times; + mockDispatch2.pending = timesInvoked < times; + if (error !== null) { + deleteMockDispatch(this[kDispatches], key); + handler.onError(error); + return true; + } + if (typeof delay === "number" && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]); + }, delay); + } else { + handleReply(this[kDispatches]); + } + function handleReply(mockDispatches, _data = data) { + const optsHeaders = Array.isArray(opts.headers) ? buildHeadersFromArray(opts.headers) : opts.headers; + const body = typeof _data === "function" ? _data({ ...opts, headers: optsHeaders }) : _data; + if (isPromise(body)) { + body.then((newData) => handleReply(mockDispatches, newData)); + return; + } + const responseData = getResponseData(body); + const responseHeaders = generateKeyValues(headers); + const responseTrailers = generateKeyValues(trailers); + handler.onConnect?.((err) => handler.onError(err), null); + handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode)); + handler.onData?.(Buffer.from(responseData)); + handler.onComplete?.(responseTrailers); + deleteMockDispatch(mockDispatches, key); + } + function resume() { + } + return true; + } + function buildMockDispatch() { + const agent = this[kMockAgent]; + const origin = this[kOrigin]; + const originalDispatch = this[kOriginalDispatch]; + return function dispatch(opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler); + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect](); + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`); + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler); + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`); + } + } else { + throw error; + } + } + } else { + originalDispatch.call(this, opts, handler); + } + }; + } + function checkNetConnect(netConnect, origin) { + const url = new URL(origin); + if (netConnect === true) { + return true; + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true; + } + return false; + } + function buildMockOptions(opts) { + if (opts) { + const { agent, ...mockOptions } = opts; + return mockOptions; + } + } + module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName, + buildHeadersFromArray + }; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-interceptor.js +var require_mock_interceptor = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-interceptor.js"(exports, module) { + "use strict"; + var { getResponseData, buildKey, addMockDispatch } = require_mock_utils(); + var { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch + } = require_mock_symbols(); + var { InvalidArgumentError } = require_errors(); + var { buildURL } = require_util(); + var MockScope = class { + constructor(mockDispatch) { + this[kMockDispatch] = mockDispatch; + } + /** + * Delay a reply by a set amount in ms. + */ + delay(waitInMs) { + if (typeof waitInMs !== "number" || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError("waitInMs must be a valid integer > 0"); + } + this[kMockDispatch].delay = waitInMs; + return this; + } + /** + * For a defined reply, never mark as consumed. + */ + persist() { + this[kMockDispatch].persist = true; + return this; + } + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times(repeatTimes) { + if (typeof repeatTimes !== "number" || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError("repeatTimes must be a valid integer > 0"); + } + this[kMockDispatch].times = repeatTimes; + return this; + } + }; + var MockInterceptor = class { + constructor(opts, mockDispatches) { + if (typeof opts !== "object") { + throw new InvalidArgumentError("opts must be an object"); + } + if (typeof opts.path === "undefined") { + throw new InvalidArgumentError("opts.path must be defined"); + } + if (typeof opts.method === "undefined") { + opts.method = "GET"; + } + if (typeof opts.path === "string") { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query); + } else { + const parsedURL = new URL(opts.path, "data://"); + opts.path = parsedURL.pathname + parsedURL.search; + } + } + if (typeof opts.method === "string") { + opts.method = opts.method.toUpperCase(); + } + this[kDispatchKey] = buildKey(opts); + this[kDispatches] = mockDispatches; + this[kDefaultHeaders] = {}; + this[kDefaultTrailers] = {}; + this[kContentLength] = false; + } + createMockScopeDispatchData({ statusCode, data, responseOptions }) { + const responseData = getResponseData(data); + const contentLength = this[kContentLength] ? { "content-length": responseData.length } : {}; + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers }; + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers }; + return { statusCode, data, headers, trailers }; + } + validateReplyParameters(replyParameters) { + if (typeof replyParameters.statusCode === "undefined") { + throw new InvalidArgumentError("statusCode must be defined"); + } + if (typeof replyParameters.responseOptions !== "object" || replyParameters.responseOptions === null) { + throw new InvalidArgumentError("responseOptions must be an object"); + } + } + /** + * Mock an undici request with a defined reply. + */ + reply(replyOptionsCallbackOrStatusCode) { + if (typeof replyOptionsCallbackOrStatusCode === "function") { + const wrappedDefaultsCallback = (opts) => { + const resolvedData = replyOptionsCallbackOrStatusCode(opts); + if (typeof resolvedData !== "object" || resolvedData === null) { + throw new InvalidArgumentError("reply options callback must return an object"); + } + const replyParameters2 = { data: "", responseOptions: {}, ...resolvedData }; + this.validateReplyParameters(replyParameters2); + return { + ...this.createMockScopeDispatchData(replyParameters2) + }; + }; + const newMockDispatch2 = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback); + return new MockScope(newMockDispatch2); + } + const replyParameters = { + statusCode: replyOptionsCallbackOrStatusCode, + data: arguments[1] === void 0 ? "" : arguments[1], + responseOptions: arguments[2] === void 0 ? {} : arguments[2] + }; + this.validateReplyParameters(replyParameters); + const dispatchData = this.createMockScopeDispatchData(replyParameters); + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData); + return new MockScope(newMockDispatch); + } + /** + * Mock an undici request with a defined error. + */ + replyWithError(error) { + if (typeof error === "undefined") { + throw new InvalidArgumentError("error must be defined"); + } + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }); + return new MockScope(newMockDispatch); + } + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders(headers) { + if (typeof headers === "undefined") { + throw new InvalidArgumentError("headers must be defined"); + } + this[kDefaultHeaders] = headers; + return this; + } + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers(trailers) { + if (typeof trailers === "undefined") { + throw new InvalidArgumentError("trailers must be defined"); + } + this[kDefaultTrailers] = trailers; + return this; + } + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength() { + this[kContentLength] = true; + return this; + } + }; + module.exports.MockInterceptor = MockInterceptor; + module.exports.MockScope = MockScope; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-client.js +var require_mock_client = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-client.js"(exports, module) { + "use strict"; + var { promisify } = __require("node:util"); + var Client = require_client(); + var { buildMockDispatch } = require_mock_utils(); + var { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected + } = require_mock_symbols(); + var { MockInterceptor } = require_mock_interceptor(); + var Symbols = require_symbols(); + var { InvalidArgumentError } = require_errors(); + var MockClient = class extends Client { + constructor(origin, opts) { + super(origin, opts); + if (!opts || !opts.agent || typeof opts.agent.dispatch !== "function") { + throw new InvalidArgumentError("Argument opts.agent must implement Agent"); + } + this[kMockAgent] = opts.agent; + this[kOrigin] = origin; + this[kDispatches] = []; + this[kConnected] = 1; + this[kOriginalDispatch] = this.dispatch; + this[kOriginalClose] = this.close.bind(this); + this.dispatch = buildMockDispatch.call(this); + this.close = this[kClose]; + } + get [Symbols.kConnected]() { + return this[kConnected]; + } + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept(opts) { + return new MockInterceptor(opts, this[kDispatches]); + } + async [kClose]() { + await promisify(this[kOriginalClose])(); + this[kConnected] = 0; + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]); + } + }; + module.exports = MockClient; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-pool.js +var require_mock_pool = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-pool.js"(exports, module) { + "use strict"; + var { promisify } = __require("node:util"); + var Pool = require_pool(); + var { buildMockDispatch } = require_mock_utils(); + var { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected + } = require_mock_symbols(); + var { MockInterceptor } = require_mock_interceptor(); + var Symbols = require_symbols(); + var { InvalidArgumentError } = require_errors(); + var MockPool = class extends Pool { + constructor(origin, opts) { + super(origin, opts); + if (!opts || !opts.agent || typeof opts.agent.dispatch !== "function") { + throw new InvalidArgumentError("Argument opts.agent must implement Agent"); + } + this[kMockAgent] = opts.agent; + this[kOrigin] = origin; + this[kDispatches] = []; + this[kConnected] = 1; + this[kOriginalDispatch] = this.dispatch; + this[kOriginalClose] = this.close.bind(this); + this.dispatch = buildMockDispatch.call(this); + this.close = this[kClose]; + } + get [Symbols.kConnected]() { + return this[kConnected]; + } + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept(opts) { + return new MockInterceptor(opts, this[kDispatches]); + } + async [kClose]() { + await promisify(this[kOriginalClose])(); + this[kConnected] = 0; + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]); + } + }; + module.exports = MockPool; + } +}); + +// viewer/node_modules/undici/lib/mock/pluralizer.js +var require_pluralizer = __commonJS({ + "viewer/node_modules/undici/lib/mock/pluralizer.js"(exports, module) { + "use strict"; + var singulars = { + pronoun: "it", + is: "is", + was: "was", + this: "this" + }; + var plurals = { + pronoun: "they", + is: "are", + was: "were", + this: "these" + }; + module.exports = class Pluralizer { + constructor(singular, plural) { + this.singular = singular; + this.plural = plural; + } + pluralize(count) { + const one = count === 1; + const keys = one ? singulars : plurals; + const noun = one ? this.singular : this.plural; + return { ...keys, count, noun }; + } + }; + } +}); + +// viewer/node_modules/undici/lib/mock/pending-interceptors-formatter.js +var require_pending_interceptors_formatter = __commonJS({ + "viewer/node_modules/undici/lib/mock/pending-interceptors-formatter.js"(exports, module) { + "use strict"; + var { Transform } = __require("node:stream"); + var { Console } = __require("node:console"); + var PERSISTENT = process.versions.icu ? "\u2705" : "Y "; + var NOT_PERSISTENT = process.versions.icu ? "\u274C" : "N "; + module.exports = class PendingInterceptorsFormatter { + constructor({ disableColors } = {}) { + this.transform = new Transform({ + transform(chunk, _enc, cb) { + cb(null, chunk); + } + }); + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }); + } + format(pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path: path14, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path14, + "Status code": statusCode, + Persistent: persist ? PERSISTENT : NOT_PERSISTENT, + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + }) + ); + this.logger.table(withPrettyHeaders); + return this.transform.read().toString(); + } + }; + } +}); + +// viewer/node_modules/undici/lib/mock/mock-agent.js +var require_mock_agent = __commonJS({ + "viewer/node_modules/undici/lib/mock/mock-agent.js"(exports, module) { + "use strict"; + var { kClients } = require_symbols(); + var Agent = require_agent(); + var { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory + } = require_mock_symbols(); + var MockClient = require_mock_client(); + var MockPool = require_mock_pool(); + var { matchValue, buildMockOptions } = require_mock_utils(); + var { InvalidArgumentError, UndiciError } = require_errors(); + var Dispatcher = require_dispatcher(); + var Pluralizer = require_pluralizer(); + var PendingInterceptorsFormatter = require_pending_interceptors_formatter(); + var MockAgent = class extends Dispatcher { + constructor(opts) { + super(opts); + this[kNetConnect] = true; + this[kIsMockActive] = true; + if (opts?.agent && typeof opts.agent.dispatch !== "function") { + throw new InvalidArgumentError("Argument opts.agent must implement Agent"); + } + const agent = opts?.agent ? opts.agent : new Agent(opts); + this[kAgent] = agent; + this[kClients] = agent[kClients]; + this[kOptions] = buildMockOptions(opts); + } + get(origin) { + let dispatcher = this[kMockAgentGet](origin); + if (!dispatcher) { + dispatcher = this[kFactory](origin); + this[kMockAgentSet](origin, dispatcher); + } + return dispatcher; + } + dispatch(opts, handler) { + this.get(opts.origin); + return this[kAgent].dispatch(opts, handler); + } + async close() { + await this[kAgent].close(); + this[kClients].clear(); + } + deactivate() { + this[kIsMockActive] = false; + } + activate() { + this[kIsMockActive] = true; + } + enableNetConnect(matcher) { + if (typeof matcher === "string" || typeof matcher === "function" || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher); + } else { + this[kNetConnect] = [matcher]; + } + } else if (typeof matcher === "undefined") { + this[kNetConnect] = true; + } else { + throw new InvalidArgumentError("Unsupported matcher. Must be one of String|Function|RegExp."); + } + } + disableNetConnect() { + this[kNetConnect] = false; + } + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive() { + return this[kIsMockActive]; + } + [kMockAgentSet](origin, dispatcher) { + this[kClients].set(origin, dispatcher); + } + [kFactory](origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]); + return this[kOptions] && this[kOptions].connections === 1 ? new MockClient(origin, mockOptions) : new MockPool(origin, mockOptions); + } + [kMockAgentGet](origin) { + const client = this[kClients].get(origin); + if (client) { + return client; + } + if (typeof origin !== "string") { + const dispatcher = this[kFactory]("http://localhost:9999"); + this[kMockAgentSet](origin, dispatcher); + return dispatcher; + } + for (const [keyMatcher, nonExplicitDispatcher] of Array.from(this[kClients])) { + if (nonExplicitDispatcher && typeof keyMatcher !== "string" && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin); + this[kMockAgentSet](origin, dispatcher); + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches]; + return dispatcher; + } + } + } + [kGetNetConnect]() { + return this[kNetConnect]; + } + pendingInterceptors() { + const mockAgentClients = this[kClients]; + return Array.from(mockAgentClients.entries()).flatMap(([origin, scope]) => scope[kDispatches].map((dispatch) => ({ ...dispatch, origin }))).filter(({ pending }) => pending); + } + assertNoPendingInterceptors({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors(); + if (pending.length === 0) { + return; + } + const pluralizer = new Pluralizer("interceptor", "interceptors").pluralize(pending.length); + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()); + } + }; + module.exports = MockAgent; + } +}); + +// viewer/node_modules/undici/lib/global.js +var require_global2 = __commonJS({ + "viewer/node_modules/undici/lib/global.js"(exports, module) { + "use strict"; + var globalDispatcher = /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1"); + var { InvalidArgumentError } = require_errors(); + var Agent = require_agent(); + if (getGlobalDispatcher() === void 0) { + setGlobalDispatcher(new Agent()); + } + function setGlobalDispatcher(agent) { + if (!agent || typeof agent.dispatch !== "function") { + throw new InvalidArgumentError("Argument agent must implement Agent"); + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }); + } + function getGlobalDispatcher() { + return globalThis[globalDispatcher]; + } + module.exports = { + setGlobalDispatcher, + getGlobalDispatcher + }; + } +}); + +// viewer/node_modules/undici/lib/handler/decorator-handler.js +var require_decorator_handler = __commonJS({ + "viewer/node_modules/undici/lib/handler/decorator-handler.js"(exports, module) { + "use strict"; + module.exports = class DecoratorHandler { + #handler; + constructor(handler) { + if (typeof handler !== "object" || handler === null) { + throw new TypeError("handler must be an object"); + } + this.#handler = handler; + } + onConnect(...args) { + return this.#handler.onConnect?.(...args); + } + onError(...args) { + return this.#handler.onError?.(...args); + } + onUpgrade(...args) { + return this.#handler.onUpgrade?.(...args); + } + onResponseStarted(...args) { + return this.#handler.onResponseStarted?.(...args); + } + onHeaders(...args) { + return this.#handler.onHeaders?.(...args); + } + onData(...args) { + return this.#handler.onData?.(...args); + } + onComplete(...args) { + return this.#handler.onComplete?.(...args); + } + onBodySent(...args) { + return this.#handler.onBodySent?.(...args); + } + }; + } +}); + +// viewer/node_modules/undici/lib/interceptor/redirect.js +var require_redirect = __commonJS({ + "viewer/node_modules/undici/lib/interceptor/redirect.js"(exports, module) { + "use strict"; + var RedirectHandler = require_redirect_handler(); + module.exports = (opts) => { + const globalMaxRedirections = opts?.maxRedirections; + return (dispatch) => { + return function redirectInterceptor(opts2, handler) { + const { maxRedirections = globalMaxRedirections, ...baseOpts } = opts2; + if (!maxRedirections) { + return dispatch(opts2, handler); + } + const redirectHandler = new RedirectHandler( + dispatch, + maxRedirections, + opts2, + handler + ); + return dispatch(baseOpts, redirectHandler); + }; + }; + }; + } +}); + +// viewer/node_modules/undici/lib/interceptor/retry.js +var require_retry3 = __commonJS({ + "viewer/node_modules/undici/lib/interceptor/retry.js"(exports, module) { + "use strict"; + var RetryHandler = require_retry_handler(); + module.exports = (globalOpts) => { + return (dispatch) => { + return function retryInterceptor(opts, handler) { + return dispatch( + opts, + new RetryHandler( + { ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } }, + { + handler, + dispatch + } + ) + ); + }; + }; + }; + } +}); + +// viewer/node_modules/undici/lib/interceptor/dump.js +var require_dump = __commonJS({ + "viewer/node_modules/undici/lib/interceptor/dump.js"(exports, module) { + "use strict"; + var util = require_util(); + var { InvalidArgumentError, RequestAbortedError } = require_errors(); + var DecoratorHandler = require_decorator_handler(); + var DumpHandler = class extends DecoratorHandler { + #maxSize = 1024 * 1024; + #abort = null; + #dumped = false; + #aborted = false; + #size = 0; + #reason = null; + #handler = null; + constructor({ maxSize }, handler) { + super(handler); + if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) { + throw new InvalidArgumentError("maxSize must be a number greater than 0"); + } + this.#maxSize = maxSize ?? this.#maxSize; + this.#handler = handler; + } + onConnect(abort) { + this.#abort = abort; + this.#handler.onConnect(this.#customAbort.bind(this)); + } + #customAbort(reason) { + this.#aborted = true; + this.#reason = reason; + } + // TODO: will require adjustment after new hooks are out + onHeaders(statusCode, rawHeaders, resume, statusMessage) { + const headers = util.parseHeaders(rawHeaders); + const contentLength = headers["content-length"]; + if (contentLength != null && contentLength > this.#maxSize) { + throw new RequestAbortedError( + `Response size (${contentLength}) larger than maxSize (${this.#maxSize})` + ); + } + if (this.#aborted) { + return true; + } + return this.#handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ); + } + onError(err) { + if (this.#dumped) { + return; + } + err = this.#reason ?? err; + this.#handler.onError(err); + } + onData(chunk) { + this.#size = this.#size + chunk.length; + if (this.#size >= this.#maxSize) { + this.#dumped = true; + if (this.#aborted) { + this.#handler.onError(this.#reason); + } else { + this.#handler.onComplete([]); + } + } + return true; + } + onComplete(trailers) { + if (this.#dumped) { + return; + } + if (this.#aborted) { + this.#handler.onError(this.reason); + return; + } + this.#handler.onComplete(trailers); + } + }; + function createDumpInterceptor({ maxSize: defaultMaxSize } = { + maxSize: 1024 * 1024 + }) { + return (dispatch) => { + return function Intercept(opts, handler) { + const { dumpMaxSize = defaultMaxSize } = opts; + const dumpHandler = new DumpHandler( + { maxSize: dumpMaxSize }, + handler + ); + return dispatch(opts, dumpHandler); + }; + }; + } + module.exports = createDumpInterceptor; + } +}); + +// viewer/node_modules/undici/lib/interceptor/dns.js +var require_dns = __commonJS({ + "viewer/node_modules/undici/lib/interceptor/dns.js"(exports, module) { + "use strict"; + var { isIP } = __require("node:net"); + var { lookup } = __require("node:dns"); + var DecoratorHandler = require_decorator_handler(); + var { InvalidArgumentError, InformationalError } = require_errors(); + var maxInt = Math.pow(2, 31) - 1; + var DNSInstance = class { + #maxTTL = 0; + #maxItems = 0; + #records = /* @__PURE__ */ new Map(); + dualStack = true; + affinity = null; + lookup = null; + pick = null; + constructor(opts) { + this.#maxTTL = opts.maxTTL; + this.#maxItems = opts.maxItems; + this.dualStack = opts.dualStack; + this.affinity = opts.affinity; + this.lookup = opts.lookup ?? this.#defaultLookup; + this.pick = opts.pick ?? this.#defaultPick; + } + get full() { + return this.#records.size === this.#maxItems; + } + runLookup(origin, opts, cb) { + const ips = this.#records.get(origin.hostname); + if (ips == null && this.full) { + cb(null, origin.origin); + return; + } + const newOpts = { + affinity: this.affinity, + dualStack: this.dualStack, + lookup: this.lookup, + pick: this.pick, + ...opts.dns, + maxTTL: this.#maxTTL, + maxItems: this.#maxItems + }; + if (ips == null) { + this.lookup(origin, newOpts, (err, addresses) => { + if (err || addresses == null || addresses.length === 0) { + cb(err ?? new InformationalError("No DNS entries found")); + return; + } + this.setRecords(origin, addresses); + const records = this.#records.get(origin.hostname); + const ip = this.pick( + origin, + records, + newOpts.affinity + ); + let port2; + if (typeof ip.port === "number") { + port2 = `:${ip.port}`; + } else if (origin.port !== "") { + port2 = `:${origin.port}`; + } else { + port2 = ""; + } + cb( + null, + `${origin.protocol}//${ip.family === 6 ? `[${ip.address}]` : ip.address}${port2}` + ); + }); + } else { + const ip = this.pick( + origin, + ips, + newOpts.affinity + ); + if (ip == null) { + this.#records.delete(origin.hostname); + this.runLookup(origin, opts, cb); + return; + } + let port2; + if (typeof ip.port === "number") { + port2 = `:${ip.port}`; + } else if (origin.port !== "") { + port2 = `:${origin.port}`; + } else { + port2 = ""; + } + cb( + null, + `${origin.protocol}//${ip.family === 6 ? `[${ip.address}]` : ip.address}${port2}` + ); + } + } + #defaultLookup(origin, opts, cb) { + lookup( + origin.hostname, + { + all: true, + family: this.dualStack === false ? this.affinity : 0, + order: "ipv4first" + }, + (err, addresses) => { + if (err) { + return cb(err); + } + const results = /* @__PURE__ */ new Map(); + for (const addr of addresses) { + results.set(`${addr.address}:${addr.family}`, addr); + } + cb(null, results.values()); + } + ); + } + #defaultPick(origin, hostnameRecords, affinity) { + let ip = null; + const { records, offset } = hostnameRecords; + let family; + if (this.dualStack) { + if (affinity == null) { + if (offset == null || offset === maxInt) { + hostnameRecords.offset = 0; + affinity = 4; + } else { + hostnameRecords.offset++; + affinity = (hostnameRecords.offset & 1) === 1 ? 6 : 4; + } + } + if (records[affinity] != null && records[affinity].ips.length > 0) { + family = records[affinity]; + } else { + family = records[affinity === 4 ? 6 : 4]; + } + } else { + family = records[affinity]; + } + if (family == null || family.ips.length === 0) { + return ip; + } + if (family.offset == null || family.offset === maxInt) { + family.offset = 0; + } else { + family.offset++; + } + const position = family.offset % family.ips.length; + ip = family.ips[position] ?? null; + if (ip == null) { + return ip; + } + if (Date.now() - ip.timestamp > ip.ttl) { + family.ips.splice(position, 1); + return this.pick(origin, hostnameRecords, affinity); + } + return ip; + } + setRecords(origin, addresses) { + const timestamp = Date.now(); + const records = { records: { 4: null, 6: null } }; + for (const record of addresses) { + record.timestamp = timestamp; + if (typeof record.ttl === "number") { + record.ttl = Math.min(record.ttl, this.#maxTTL); + } else { + record.ttl = this.#maxTTL; + } + const familyRecords = records.records[record.family] ?? { ips: [] }; + familyRecords.ips.push(record); + records.records[record.family] = familyRecords; + } + this.#records.set(origin.hostname, records); + } + getHandler(meta, opts) { + return new DNSDispatchHandler(this, meta, opts); + } + }; + var DNSDispatchHandler = class extends DecoratorHandler { + #state = null; + #opts = null; + #dispatch = null; + #handler = null; + #origin = null; + constructor(state, { origin, handler, dispatch }, opts) { + super(handler); + this.#origin = origin; + this.#handler = handler; + this.#opts = { ...opts }; + this.#state = state; + this.#dispatch = dispatch; + } + onError(err) { + switch (err.code) { + case "ETIMEDOUT": + case "ECONNREFUSED": { + if (this.#state.dualStack) { + this.#state.runLookup(this.#origin, this.#opts, (err2, newOrigin) => { + if (err2) { + return this.#handler.onError(err2); + } + const dispatchOpts = { + ...this.#opts, + origin: newOrigin + }; + this.#dispatch(dispatchOpts, this); + }); + return; + } + this.#handler.onError(err); + return; + } + case "ENOTFOUND": + this.#state.deleteRecord(this.#origin); + // eslint-disable-next-line no-fallthrough + default: + this.#handler.onError(err); + break; + } + } + }; + module.exports = (interceptorOpts) => { + if (interceptorOpts?.maxTTL != null && (typeof interceptorOpts?.maxTTL !== "number" || interceptorOpts?.maxTTL < 0)) { + throw new InvalidArgumentError("Invalid maxTTL. Must be a positive number"); + } + if (interceptorOpts?.maxItems != null && (typeof interceptorOpts?.maxItems !== "number" || interceptorOpts?.maxItems < 1)) { + throw new InvalidArgumentError( + "Invalid maxItems. Must be a positive number and greater than zero" + ); + } + if (interceptorOpts?.affinity != null && interceptorOpts?.affinity !== 4 && interceptorOpts?.affinity !== 6) { + throw new InvalidArgumentError("Invalid affinity. Must be either 4 or 6"); + } + if (interceptorOpts?.dualStack != null && typeof interceptorOpts?.dualStack !== "boolean") { + throw new InvalidArgumentError("Invalid dualStack. Must be a boolean"); + } + if (interceptorOpts?.lookup != null && typeof interceptorOpts?.lookup !== "function") { + throw new InvalidArgumentError("Invalid lookup. Must be a function"); + } + if (interceptorOpts?.pick != null && typeof interceptorOpts?.pick !== "function") { + throw new InvalidArgumentError("Invalid pick. Must be a function"); + } + const dualStack = interceptorOpts?.dualStack ?? true; + let affinity; + if (dualStack) { + affinity = interceptorOpts?.affinity ?? null; + } else { + affinity = interceptorOpts?.affinity ?? 4; + } + const opts = { + maxTTL: interceptorOpts?.maxTTL ?? 1e4, + // Expressed in ms + lookup: interceptorOpts?.lookup ?? null, + pick: interceptorOpts?.pick ?? null, + dualStack, + affinity, + maxItems: interceptorOpts?.maxItems ?? Infinity + }; + const instance = new DNSInstance(opts); + return (dispatch) => { + return function dnsInterceptor(origDispatchOpts, handler) { + const origin = origDispatchOpts.origin.constructor === URL ? origDispatchOpts.origin : new URL(origDispatchOpts.origin); + if (isIP(origin.hostname) !== 0) { + return dispatch(origDispatchOpts, handler); + } + instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => { + if (err) { + return handler.onError(err); + } + let dispatchOpts = null; + dispatchOpts = { + ...origDispatchOpts, + servername: origin.hostname, + // For SNI on TLS + origin: newOrigin, + headers: { + host: origin.hostname, + ...origDispatchOpts.headers + } + }; + dispatch( + dispatchOpts, + instance.getHandler({ origin, dispatch, handler }, origDispatchOpts) + ); + }); + return true; + }; + }; + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/headers.js +var require_headers = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/headers.js"(exports, module) { + "use strict"; + var { kConstruct } = require_symbols(); + var { kEnumerableProperty } = require_util(); + var { + iteratorMixin, + isValidHeaderName, + isValidHeaderValue + } = require_util2(); + var { webidl } = require_webidl(); + var assert = __require("node:assert"); + var util = __require("node:util"); + var kHeadersMap = /* @__PURE__ */ Symbol("headers map"); + var kHeadersSortedMap = /* @__PURE__ */ Symbol("headers map sorted"); + function isHTTPWhiteSpaceCharCode(code) { + return code === 10 || code === 13 || code === 9 || code === 32; + } + function headerValueNormalize(potentialValue) { + let i = 0; + let j = potentialValue.length; + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j; + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i; + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j); + } + function fill(headers, object) { + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i]; + if (header.length !== 2) { + throw webidl.errors.exception({ + header: "Headers constructor", + message: `expected name/value pair to be length 2, found ${header.length}.` + }); + } + appendHeader(headers, header[0], header[1]); + } + } else if (typeof object === "object" && object !== null) { + const keys = Object.keys(object); + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]); + } + } else { + throw webidl.errors.conversionFailed({ + prefix: "Headers constructor", + argument: "Argument 1", + types: ["sequence>", "record"] + }); + } + } + function appendHeader(headers, name, value) { + value = headerValueNormalize(value); + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: "Headers.append", + value: name, + type: "header name" + }); + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: "Headers.append", + value, + type: "header value" + }); + } + if (getHeadersGuard(headers) === "immutable") { + throw new TypeError("immutable"); + } + return getHeadersList(headers).append(name, value, false); + } + function compareHeaderName(a, b) { + return a[0] < b[0] ? -1 : 1; + } + var HeadersList = class _HeadersList { + /** @type {[string, string][]|null} */ + cookies = null; + constructor(init) { + if (init instanceof _HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]); + this[kHeadersSortedMap] = init[kHeadersSortedMap]; + this.cookies = init.cookies === null ? null : [...init.cookies]; + } else { + this[kHeadersMap] = new Map(init); + this[kHeadersSortedMap] = null; + } + } + /** + * @see https://fetch.spec.whatwg.org/#header-list-contains + * @param {string} name + * @param {boolean} isLowerCase + */ + contains(name, isLowerCase) { + return this[kHeadersMap].has(isLowerCase ? name : name.toLowerCase()); + } + clear() { + this[kHeadersMap].clear(); + this[kHeadersSortedMap] = null; + this.cookies = null; + } + /** + * @see https://fetch.spec.whatwg.org/#concept-header-list-append + * @param {string} name + * @param {string} value + * @param {boolean} isLowerCase + */ + append(name, value, isLowerCase) { + this[kHeadersSortedMap] = null; + const lowercaseName = isLowerCase ? name : name.toLowerCase(); + const exists = this[kHeadersMap].get(lowercaseName); + if (exists) { + const delimiter = lowercaseName === "cookie" ? "; " : ", "; + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }); + } else { + this[kHeadersMap].set(lowercaseName, { name, value }); + } + if (lowercaseName === "set-cookie") { + (this.cookies ??= []).push(value); + } + } + /** + * @see https://fetch.spec.whatwg.org/#concept-header-list-set + * @param {string} name + * @param {string} value + * @param {boolean} isLowerCase + */ + set(name, value, isLowerCase) { + this[kHeadersSortedMap] = null; + const lowercaseName = isLowerCase ? name : name.toLowerCase(); + if (lowercaseName === "set-cookie") { + this.cookies = [value]; + } + this[kHeadersMap].set(lowercaseName, { name, value }); + } + /** + * @see https://fetch.spec.whatwg.org/#concept-header-list-delete + * @param {string} name + * @param {boolean} isLowerCase + */ + delete(name, isLowerCase) { + this[kHeadersSortedMap] = null; + if (!isLowerCase) name = name.toLowerCase(); + if (name === "set-cookie") { + this.cookies = null; + } + this[kHeadersMap].delete(name); + } + /** + * @see https://fetch.spec.whatwg.org/#concept-header-list-get + * @param {string} name + * @param {boolean} isLowerCase + * @returns {string | null} + */ + get(name, isLowerCase) { + return this[kHeadersMap].get(isLowerCase ? name : name.toLowerCase())?.value ?? null; + } + *[Symbol.iterator]() { + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { + yield [name, value]; + } + } + get entries() { + const headers = {}; + if (this[kHeadersMap].size !== 0) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value; + } + } + return headers; + } + rawValues() { + return this[kHeadersMap].values(); + } + get entriesList() { + const headers = []; + if (this[kHeadersMap].size !== 0) { + for (const { 0: lowerName, 1: { name, value } } of this[kHeadersMap]) { + if (lowerName === "set-cookie") { + for (const cookie of this.cookies) { + headers.push([name, cookie]); + } + } else { + headers.push([name, value]); + } + } + } + return headers; + } + // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set + toSortedArray() { + const size = this[kHeadersMap].size; + const array = new Array(size); + if (size <= 32) { + if (size === 0) { + return array; + } + const iterator = this[kHeadersMap][Symbol.iterator](); + const firstValue = iterator.next().value; + array[0] = [firstValue[0], firstValue[1].value]; + assert(firstValue[1].value !== null); + for (let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value; i < size; ++i) { + value = iterator.next().value; + x = array[i] = [value[0], value[1].value]; + assert(x[1] !== null); + left = 0; + right = i; + while (left < right) { + pivot = left + (right - left >> 1); + if (array[pivot][0] <= x[0]) { + left = pivot + 1; + } else { + right = pivot; + } + } + if (i !== pivot) { + j = i; + while (j > left) { + array[j] = array[--j]; + } + array[left] = x; + } + } + if (!iterator.next().done) { + throw new TypeError("Unreachable"); + } + return array; + } else { + let i = 0; + for (const { 0: name, 1: { value } } of this[kHeadersMap]) { + array[i++] = [name, value]; + assert(value !== null); + } + return array.sort(compareHeaderName); + } + } + }; + var Headers2 = class _Headers { + #guard; + #headersList; + constructor(init = void 0) { + webidl.util.markAsUncloneable(this); + if (init === kConstruct) { + return; + } + this.#headersList = new HeadersList(); + this.#guard = "none"; + if (init !== void 0) { + init = webidl.converters.HeadersInit(init, "Headers contructor", "init"); + fill(this, init); + } + } + // https://fetch.spec.whatwg.org/#dom-headers-append + append(name, value) { + webidl.brandCheck(this, _Headers); + webidl.argumentLengthCheck(arguments, 2, "Headers.append"); + const prefix = "Headers.append"; + name = webidl.converters.ByteString(name, prefix, "name"); + value = webidl.converters.ByteString(value, prefix, "value"); + return appendHeader(this, name, value); + } + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete(name) { + webidl.brandCheck(this, _Headers); + webidl.argumentLengthCheck(arguments, 1, "Headers.delete"); + const prefix = "Headers.delete"; + name = webidl.converters.ByteString(name, prefix, "name"); + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: "Headers.delete", + value: name, + type: "header name" + }); + } + if (this.#guard === "immutable") { + throw new TypeError("immutable"); + } + if (!this.#headersList.contains(name, false)) { + return; + } + this.#headersList.delete(name, false); + } + // https://fetch.spec.whatwg.org/#dom-headers-get + get(name) { + webidl.brandCheck(this, _Headers); + webidl.argumentLengthCheck(arguments, 1, "Headers.get"); + const prefix = "Headers.get"; + name = webidl.converters.ByteString(name, prefix, "name"); + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix, + value: name, + type: "header name" + }); + } + return this.#headersList.get(name, false); + } + // https://fetch.spec.whatwg.org/#dom-headers-has + has(name) { + webidl.brandCheck(this, _Headers); + webidl.argumentLengthCheck(arguments, 1, "Headers.has"); + const prefix = "Headers.has"; + name = webidl.converters.ByteString(name, prefix, "name"); + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix, + value: name, + type: "header name" + }); + } + return this.#headersList.contains(name, false); + } + // https://fetch.spec.whatwg.org/#dom-headers-set + set(name, value) { + webidl.brandCheck(this, _Headers); + webidl.argumentLengthCheck(arguments, 2, "Headers.set"); + const prefix = "Headers.set"; + name = webidl.converters.ByteString(name, prefix, "name"); + value = webidl.converters.ByteString(value, prefix, "value"); + value = headerValueNormalize(value); + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix, + value: name, + type: "header name" + }); + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix, + value, + type: "header value" + }); + } + if (this.#guard === "immutable") { + throw new TypeError("immutable"); + } + this.#headersList.set(name, value, false); + } + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie() { + webidl.brandCheck(this, _Headers); + const list2 = this.#headersList.cookies; + if (list2) { + return [...list2]; + } + return []; + } + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + get [kHeadersSortedMap]() { + if (this.#headersList[kHeadersSortedMap]) { + return this.#headersList[kHeadersSortedMap]; + } + const headers = []; + const names = this.#headersList.toSortedArray(); + const cookies = this.#headersList.cookies; + if (cookies === null || cookies.length === 1) { + return this.#headersList[kHeadersSortedMap] = names; + } + for (let i = 0; i < names.length; ++i) { + const { 0: name, 1: value } = names[i]; + if (name === "set-cookie") { + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]); + } + } else { + headers.push([name, value]); + } + } + return this.#headersList[kHeadersSortedMap] = headers; + } + [util.inspect.custom](depth, options) { + options.depth ??= depth; + return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`; + } + static getHeadersGuard(o) { + return o.#guard; + } + static setHeadersGuard(o, guard) { + o.#guard = guard; + } + static getHeadersList(o) { + return o.#headersList; + } + static setHeadersList(o, list2) { + o.#headersList = list2; + } + }; + var { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers2; + Reflect.deleteProperty(Headers2, "getHeadersGuard"); + Reflect.deleteProperty(Headers2, "setHeadersGuard"); + Reflect.deleteProperty(Headers2, "getHeadersList"); + Reflect.deleteProperty(Headers2, "setHeadersList"); + iteratorMixin("Headers", Headers2, kHeadersSortedMap, 0, 1); + Object.defineProperties(Headers2.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "Headers", + configurable: true + }, + [util.inspect.custom]: { + enumerable: false + } + }); + webidl.converters.HeadersInit = function(V, prefix, argument) { + if (webidl.util.Type(V) === "Object") { + const iterator = Reflect.get(V, Symbol.iterator); + if (!util.types.isProxy(V) && iterator === Headers2.prototype.entries) { + try { + return getHeadersList(V).entriesList; + } catch { + } + } + if (typeof iterator === "function") { + return webidl.converters["sequence>"](V, prefix, argument, iterator.bind(V)); + } + return webidl.converters["record"](V, prefix, argument); + } + throw webidl.errors.conversionFailed({ + prefix: "Headers constructor", + argument: "Argument 1", + types: ["sequence>", "record"] + }); + }; + module.exports = { + fill, + // for test. + compareHeaderName, + Headers: Headers2, + HeadersList, + getHeadersGuard, + setHeadersGuard, + setHeadersList, + getHeadersList + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/response.js +var require_response = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/response.js"(exports, module) { + "use strict"; + var { Headers: Headers2, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require_headers(); + var { extractBody, cloneBody, mixinBody, hasFinalizationRegistry, streamRegistry, bodyUnusable } = require_body(); + var util = require_util(); + var nodeUtil = __require("node:util"); + var { kEnumerableProperty } = util; + var { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike, + isomorphicEncode, + environmentSettingsObject: relevantRealm + } = require_util2(); + var { + redirectStatusSet, + nullBodyStatus + } = require_constants3(); + var { kState, kHeaders } = require_symbols2(); + var { webidl } = require_webidl(); + var { FormData } = require_formdata(); + var { URLSerializer } = require_data_url(); + var { kConstruct } = require_symbols(); + var assert = __require("node:assert"); + var { types } = __require("node:util"); + var textEncoder2 = new TextEncoder("utf-8"); + var Response2 = class _Response { + // Creates network error Response. + static error() { + const responseObject = fromInnerResponse(makeNetworkError(), "immutable"); + return responseObject; + } + // https://fetch.spec.whatwg.org/#dom-response-json + static json(data, init = {}) { + webidl.argumentLengthCheck(arguments, 1, "Response.json"); + if (init !== null) { + init = webidl.converters.ResponseInit(init); + } + const bytes2 = textEncoder2.encode( + serializeJavascriptValueToJSONString(data) + ); + const body = extractBody(bytes2); + const responseObject = fromInnerResponse(makeResponse({}), "response"); + initializeResponse(responseObject, init, { body: body[0], type: "application/json" }); + return responseObject; + } + // Creates a redirect Response that redirects to url with status status. + static redirect(url, status = 302) { + webidl.argumentLengthCheck(arguments, 1, "Response.redirect"); + url = webidl.converters.USVString(url); + status = webidl.converters["unsigned short"](status); + let parsedURL; + try { + parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl); + } catch (err) { + throw new TypeError(`Failed to parse URL from ${url}`, { cause: err }); + } + if (!redirectStatusSet.has(status)) { + throw new RangeError(`Invalid status code ${status}`); + } + const responseObject = fromInnerResponse(makeResponse({}), "immutable"); + responseObject[kState].status = status; + const value = isomorphicEncode(URLSerializer(parsedURL)); + responseObject[kState].headersList.append("location", value, true); + return responseObject; + } + // https://fetch.spec.whatwg.org/#dom-response + constructor(body = null, init = {}) { + webidl.util.markAsUncloneable(this); + if (body === kConstruct) { + return; + } + if (body !== null) { + body = webidl.converters.BodyInit(body); + } + init = webidl.converters.ResponseInit(init); + this[kState] = makeResponse({}); + this[kHeaders] = new Headers2(kConstruct); + setHeadersGuard(this[kHeaders], "response"); + setHeadersList(this[kHeaders], this[kState].headersList); + let bodyWithType = null; + if (body != null) { + const [extractedBody, type] = extractBody(body); + bodyWithType = { body: extractedBody, type }; + } + initializeResponse(this, init, bodyWithType); + } + // Returns response’s type, e.g., "cors". + get type() { + webidl.brandCheck(this, _Response); + return this[kState].type; + } + // Returns response’s URL, if it has one; otherwise the empty string. + get url() { + webidl.brandCheck(this, _Response); + const urlList = this[kState].urlList; + const url = urlList[urlList.length - 1] ?? null; + if (url === null) { + return ""; + } + return URLSerializer(url, true); + } + // Returns whether response was obtained through a redirect. + get redirected() { + webidl.brandCheck(this, _Response); + return this[kState].urlList.length > 1; + } + // Returns response’s status. + get status() { + webidl.brandCheck(this, _Response); + return this[kState].status; + } + // Returns whether response’s status is an ok status. + get ok() { + webidl.brandCheck(this, _Response); + return this[kState].status >= 200 && this[kState].status <= 299; + } + // Returns response’s status message. + get statusText() { + webidl.brandCheck(this, _Response); + return this[kState].statusText; + } + // Returns response’s headers as Headers. + get headers() { + webidl.brandCheck(this, _Response); + return this[kHeaders]; + } + get body() { + webidl.brandCheck(this, _Response); + return this[kState].body ? this[kState].body.stream : null; + } + get bodyUsed() { + webidl.brandCheck(this, _Response); + return !!this[kState].body && util.isDisturbed(this[kState].body.stream); + } + // Returns a clone of response. + clone() { + webidl.brandCheck(this, _Response); + if (bodyUnusable(this)) { + throw webidl.errors.exception({ + header: "Response.clone", + message: "Body has already been consumed." + }); + } + const clonedResponse = cloneResponse(this[kState]); + if (hasFinalizationRegistry && this[kState].body?.stream) { + streamRegistry.register(this, new WeakRef(this[kState].body.stream)); + } + return fromInnerResponse(clonedResponse, getHeadersGuard(this[kHeaders])); + } + [nodeUtil.inspect.custom](depth, options) { + if (options.depth === null) { + options.depth = 2; + } + options.colors ??= true; + const properties = { + status: this.status, + statusText: this.statusText, + headers: this.headers, + body: this.body, + bodyUsed: this.bodyUsed, + ok: this.ok, + redirected: this.redirected, + type: this.type, + url: this.url + }; + return `Response ${nodeUtil.formatWithOptions(options, properties)}`; + } + }; + mixinBody(Response2); + Object.defineProperties(Response2.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "Response", + configurable: true + } + }); + Object.defineProperties(Response2, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty + }); + function cloneResponse(response) { + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ); + } + const newResponse = makeResponse({ ...response, body: null }); + if (response.body != null) { + newResponse.body = cloneBody(newResponse, response.body); + } + return newResponse; + } + function makeResponse(init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: "default", + status: 200, + timingInfo: null, + cacheState: "", + statusText: "", + ...init, + headersList: init?.headersList ? new HeadersList(init?.headersList) : new HeadersList(), + urlList: init?.urlList ? [...init.urlList] : [] + }; + } + function makeNetworkError(reason) { + const isError2 = isErrorLike(reason); + return makeResponse({ + type: "error", + status: 0, + error: isError2 ? reason : new Error(reason ? String(reason) : reason), + aborted: reason && reason.name === "AbortError" + }); + } + function isNetworkError2(response) { + return ( + // A network error is a response whose type is "error", + response.type === "error" && // status is 0 + response.status === 0 + ); + } + function makeFilteredResponse(response, state) { + state = { + internalResponse: response, + ...state + }; + return new Proxy(response, { + get(target, p) { + return p in state ? state[p] : target[p]; + }, + set(target, p, value) { + assert(!(p in state)); + target[p] = value; + return true; + } + }); + } + function filterResponse(response, type) { + if (type === "basic") { + return makeFilteredResponse(response, { + type: "basic", + headersList: response.headersList + }); + } else if (type === "cors") { + return makeFilteredResponse(response, { + type: "cors", + headersList: response.headersList + }); + } else if (type === "opaque") { + return makeFilteredResponse(response, { + type: "opaque", + urlList: Object.freeze([]), + status: 0, + statusText: "", + body: null + }); + } else if (type === "opaqueredirect") { + return makeFilteredResponse(response, { + type: "opaqueredirect", + status: 0, + statusText: "", + headersList: [], + body: null + }); + } else { + assert(false); + } + } + function makeAppropriateNetworkError(fetchParams, err = null) { + assert(isCancelled(fetchParams)); + return isAborted(fetchParams) ? makeNetworkError(Object.assign(new DOMException("The operation was aborted.", "AbortError"), { cause: err })) : makeNetworkError(Object.assign(new DOMException("Request was cancelled."), { cause: err })); + } + function initializeResponse(response, init, body) { + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.'); + } + if ("statusText" in init && init.statusText != null) { + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError("Invalid statusText"); + } + } + if ("status" in init && init.status != null) { + response[kState].status = init.status; + } + if ("statusText" in init && init.statusText != null) { + response[kState].statusText = init.statusText; + } + if ("headers" in init && init.headers != null) { + fill(response[kHeaders], init.headers); + } + if (body) { + if (nullBodyStatus.includes(response.status)) { + throw webidl.errors.exception({ + header: "Response constructor", + message: `Invalid response status code ${response.status}` + }); + } + response[kState].body = body.body; + if (body.type != null && !response[kState].headersList.contains("content-type", true)) { + response[kState].headersList.append("content-type", body.type, true); + } + } + } + function fromInnerResponse(innerResponse, guard) { + const response = new Response2(kConstruct); + response[kState] = innerResponse; + response[kHeaders] = new Headers2(kConstruct); + setHeadersList(response[kHeaders], innerResponse.headersList); + setHeadersGuard(response[kHeaders], guard); + if (hasFinalizationRegistry && innerResponse.body?.stream) { + streamRegistry.register(response, new WeakRef(innerResponse.body.stream)); + } + return response; + } + webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream + ); + webidl.converters.FormData = webidl.interfaceConverter( + FormData + ); + webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams + ); + webidl.converters.XMLHttpRequestBodyInit = function(V, prefix, name) { + if (typeof V === "string") { + return webidl.converters.USVString(V, prefix, name); + } + if (isBlobLike(V)) { + return webidl.converters.Blob(V, prefix, name, { strict: false }); + } + if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + return webidl.converters.BufferSource(V, prefix, name); + } + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, prefix, name, { strict: false }); + } + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V, prefix, name); + } + return webidl.converters.DOMString(V, prefix, name); + }; + webidl.converters.BodyInit = function(V, prefix, argument) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V, prefix, argument); + } + if (V?.[Symbol.asyncIterator]) { + return V; + } + return webidl.converters.XMLHttpRequestBodyInit(V, prefix, argument); + }; + webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: "status", + converter: webidl.converters["unsigned short"], + defaultValue: () => 200 + }, + { + key: "statusText", + converter: webidl.converters.ByteString, + defaultValue: () => "" + }, + { + key: "headers", + converter: webidl.converters.HeadersInit + } + ]); + module.exports = { + isNetworkError: isNetworkError2, + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response: Response2, + cloneResponse, + fromInnerResponse + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/dispatcher-weakref.js +var require_dispatcher_weakref = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/dispatcher-weakref.js"(exports, module) { + "use strict"; + var { kConnected, kSize } = require_symbols(); + var CompatWeakRef = class { + constructor(value) { + this.value = value; + } + deref() { + return this.value[kConnected] === 0 && this.value[kSize] === 0 ? void 0 : this.value; + } + }; + var CompatFinalizer = class { + constructor(finalizer) { + this.finalizer = finalizer; + } + register(dispatcher, key) { + if (dispatcher.on) { + dispatcher.on("disconnect", () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key); + } + }); + } + } + unregister(key) { + } + }; + module.exports = function() { + if (process.env.NODE_V8_COVERAGE && process.version.startsWith("v18")) { + process._rawDebug("Using compatibility WeakRef and FinalizationRegistry"); + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + }; + } + return { WeakRef, FinalizationRegistry }; + }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/request.js +var require_request2 = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/request.js"(exports, module) { + "use strict"; + var { extractBody, mixinBody, cloneBody, bodyUnusable } = require_body(); + var { Headers: Headers2, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require_headers(); + var { FinalizationRegistry: FinalizationRegistry2 } = require_dispatcher_weakref()(); + var util = require_util(); + var nodeUtil = __require("node:util"); + var { + isValidHTTPToken, + sameOrigin, + environmentSettingsObject + } = require_util2(); + var { + forbiddenMethodsSet, + corsSafeListedMethodsSet, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + requestDuplex + } = require_constants3(); + var { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util; + var { kHeaders, kSignal, kState, kDispatcher } = require_symbols2(); + var { webidl } = require_webidl(); + var { URLSerializer } = require_data_url(); + var { kConstruct } = require_symbols(); + var assert = __require("node:assert"); + var { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = __require("node:events"); + var kAbortController = /* @__PURE__ */ Symbol("abortController"); + var requestFinalizer = new FinalizationRegistry2(({ signal, abort }) => { + signal.removeEventListener("abort", abort); + }); + var dependentControllerMap = /* @__PURE__ */ new WeakMap(); + function buildAbort(acRef) { + return abort; + function abort() { + const ac = acRef.deref(); + if (ac !== void 0) { + requestFinalizer.unregister(abort); + this.removeEventListener("abort", abort); + ac.abort(this.reason); + const controllerList = dependentControllerMap.get(ac.signal); + if (controllerList !== void 0) { + if (controllerList.size !== 0) { + for (const ref of controllerList) { + const ctrl = ref.deref(); + if (ctrl !== void 0) { + ctrl.abort(this.reason); + } + } + controllerList.clear(); + } + dependentControllerMap.delete(ac.signal); + } + } + } + } + var patchMethodWarning = false; + var Request2 = class _Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor(input, init = {}) { + webidl.util.markAsUncloneable(this); + if (input === kConstruct) { + return; + } + const prefix = "Request constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + input = webidl.converters.RequestInfo(input, prefix, "input"); + init = webidl.converters.RequestInit(init, prefix, "init"); + let request = null; + let fallbackMode = null; + const baseUrl = environmentSettingsObject.settingsObject.baseUrl; + let signal = null; + if (typeof input === "string") { + this[kDispatcher] = init.dispatcher; + let parsedURL; + try { + parsedURL = new URL(input, baseUrl); + } catch (err) { + throw new TypeError("Failed to parse URL from " + input, { cause: err }); + } + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + "Request cannot be constructed from a URL that includes credentials: " + input + ); + } + request = makeRequest({ urlList: [parsedURL] }); + fallbackMode = "cors"; + } else { + this[kDispatcher] = init.dispatcher || input[kDispatcher]; + assert(input instanceof _Request); + request = input[kState]; + signal = input[kSignal]; + } + const origin = environmentSettingsObject.settingsObject.origin; + let window2 = "client"; + if (request.window?.constructor?.name === "EnvironmentSettingsObject" && sameOrigin(request.window, origin)) { + window2 = request.window; + } + if (init.window != null) { + throw new TypeError(`'window' option '${window2}' must be null`); + } + if ("window" in init) { + window2 = "no-window"; + } + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: environmentSettingsObject.settingsObject, + // window window. + window: window2, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }); + const initHasKey = Object.keys(init).length !== 0; + if (initHasKey) { + if (request.mode === "navigate") { + request.mode = "same-origin"; + } + request.reloadNavigation = false; + request.historyNavigation = false; + request.origin = "client"; + request.referrer = "client"; + request.referrerPolicy = ""; + request.url = request.urlList[request.urlList.length - 1]; + request.urlList = [request.url]; + } + if (init.referrer !== void 0) { + const referrer = init.referrer; + if (referrer === "") { + request.referrer = "no-referrer"; + } else { + let parsedReferrer; + try { + parsedReferrer = new URL(referrer, baseUrl); + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }); + } + if (parsedReferrer.protocol === "about:" && parsedReferrer.hostname === "client" || origin && !sameOrigin(parsedReferrer, environmentSettingsObject.settingsObject.baseUrl)) { + request.referrer = "client"; + } else { + request.referrer = parsedReferrer; + } + } + } + if (init.referrerPolicy !== void 0) { + request.referrerPolicy = init.referrerPolicy; + } + let mode; + if (init.mode !== void 0) { + mode = init.mode; + } else { + mode = fallbackMode; + } + if (mode === "navigate") { + throw webidl.errors.exception({ + header: "Request constructor", + message: "invalid request mode navigate." + }); + } + if (mode != null) { + request.mode = mode; + } + if (init.credentials !== void 0) { + request.credentials = init.credentials; + } + if (init.cache !== void 0) { + request.cache = init.cache; + } + if (request.cache === "only-if-cached" && request.mode !== "same-origin") { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ); + } + if (init.redirect !== void 0) { + request.redirect = init.redirect; + } + if (init.integrity != null) { + request.integrity = String(init.integrity); + } + if (init.keepalive !== void 0) { + request.keepalive = Boolean(init.keepalive); + } + if (init.method !== void 0) { + let method = init.method; + const mayBeNormalized = normalizedMethodRecords[method]; + if (mayBeNormalized !== void 0) { + request.method = mayBeNormalized; + } else { + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`); + } + const upperCase = method.toUpperCase(); + if (forbiddenMethodsSet.has(upperCase)) { + throw new TypeError(`'${method}' HTTP method is unsupported.`); + } + method = normalizedMethodRecordsBase[upperCase] ?? method; + request.method = method; + } + if (!patchMethodWarning && request.method === "patch") { + process.emitWarning("Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.", { + code: "UNDICI-FETCH-patch" + }); + patchMethodWarning = true; + } + } + if (init.signal !== void 0) { + signal = init.signal; + } + this[kState] = request; + const ac = new AbortController(); + this[kSignal] = ac.signal; + if (signal != null) { + if (!signal || typeof signal.aborted !== "boolean" || typeof signal.addEventListener !== "function") { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ); + } + if (signal.aborted) { + ac.abort(signal.reason); + } else { + this[kAbortController] = ac; + const acRef = new WeakRef(ac); + const abort = buildAbort(acRef); + try { + if (typeof getMaxListeners === "function" && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(1500, signal); + } else if (getEventListeners(signal, "abort").length >= defaultMaxListeners) { + setMaxListeners(1500, signal); + } + } catch { + } + util.addAbortListener(signal, abort); + requestFinalizer.register(ac, { signal, abort }, abort); + } + } + this[kHeaders] = new Headers2(kConstruct); + setHeadersList(this[kHeaders], request.headersList); + setHeadersGuard(this[kHeaders], "request"); + if (mode === "no-cors") { + if (!corsSafeListedMethodsSet.has(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ); + } + setHeadersGuard(this[kHeaders], "request-no-cors"); + } + if (initHasKey) { + const headersList = getHeadersList(this[kHeaders]); + const headers = init.headers !== void 0 ? init.headers : new HeadersList(headersList); + headersList.clear(); + if (headers instanceof HeadersList) { + for (const { name, value } of headers.rawValues()) { + headersList.append(name, value, false); + } + headersList.cookies = headers.cookies; + } else { + fillHeaders(this[kHeaders], headers); + } + } + const inputBody = input instanceof _Request ? input[kState].body : null; + if ((init.body != null || inputBody != null) && (request.method === "GET" || request.method === "HEAD")) { + throw new TypeError("Request with GET/HEAD method cannot have body."); + } + let initBody = null; + if (init.body != null) { + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ); + initBody = extractedBody; + if (contentType && !getHeadersList(this[kHeaders]).contains("content-type", true)) { + this[kHeaders].append("content-type", contentType); + } + } + const inputOrInitBody = initBody ?? inputBody; + if (inputOrInitBody != null && inputOrInitBody.source == null) { + if (initBody != null && init.duplex == null) { + throw new TypeError("RequestInit: duplex option is required when sending a body."); + } + if (request.mode !== "same-origin" && request.mode !== "cors") { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ); + } + request.useCORSPreflightFlag = true; + } + let finalBody = inputOrInitBody; + if (initBody == null && inputBody != null) { + if (bodyUnusable(input)) { + throw new TypeError( + "Cannot construct a Request with a Request object that has already been used." + ); + } + const identityTransform = new TransformStream(); + inputBody.stream.pipeThrough(identityTransform); + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + }; + } + this[kState].body = finalBody; + } + // Returns request’s HTTP method, which is "GET" by default. + get method() { + webidl.brandCheck(this, _Request); + return this[kState].method; + } + // Returns the URL of request as a string. + get url() { + webidl.brandCheck(this, _Request); + return URLSerializer(this[kState].url); + } + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers() { + webidl.brandCheck(this, _Request); + return this[kHeaders]; + } + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination() { + webidl.brandCheck(this, _Request); + return this[kState].destination; + } + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer() { + webidl.brandCheck(this, _Request); + if (this[kState].referrer === "no-referrer") { + return ""; + } + if (this[kState].referrer === "client") { + return "about:client"; + } + return this[kState].referrer.toString(); + } + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy() { + webidl.brandCheck(this, _Request); + return this[kState].referrerPolicy; + } + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode() { + webidl.brandCheck(this, _Request); + return this[kState].mode; + } + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials() { + return this[kState].credentials; + } + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache() { + webidl.brandCheck(this, _Request); + return this[kState].cache; + } + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect() { + webidl.brandCheck(this, _Request); + return this[kState].redirect; + } + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity() { + webidl.brandCheck(this, _Request); + return this[kState].integrity; + } + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive() { + webidl.brandCheck(this, _Request); + return this[kState].keepalive; + } + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation() { + webidl.brandCheck(this, _Request); + return this[kState].reloadNavigation; + } + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-forward navigation). + get isHistoryNavigation() { + webidl.brandCheck(this, _Request); + return this[kState].historyNavigation; + } + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal() { + webidl.brandCheck(this, _Request); + return this[kSignal]; + } + get body() { + webidl.brandCheck(this, _Request); + return this[kState].body ? this[kState].body.stream : null; + } + get bodyUsed() { + webidl.brandCheck(this, _Request); + return !!this[kState].body && util.isDisturbed(this[kState].body.stream); + } + get duplex() { + webidl.brandCheck(this, _Request); + return "half"; + } + // Returns a clone of request. + clone() { + webidl.brandCheck(this, _Request); + if (bodyUnusable(this)) { + throw new TypeError("unusable"); + } + const clonedRequest = cloneRequest(this[kState]); + const ac = new AbortController(); + if (this.signal.aborted) { + ac.abort(this.signal.reason); + } else { + let list2 = dependentControllerMap.get(this.signal); + if (list2 === void 0) { + list2 = /* @__PURE__ */ new Set(); + dependentControllerMap.set(this.signal, list2); + } + const acRef = new WeakRef(ac); + list2.add(acRef); + util.addAbortListener( + ac.signal, + buildAbort(acRef) + ); + } + return fromInnerRequest(clonedRequest, ac.signal, getHeadersGuard(this[kHeaders])); + } + [nodeUtil.inspect.custom](depth, options) { + if (options.depth === null) { + options.depth = 2; + } + options.colors ??= true; + const properties = { + method: this.method, + url: this.url, + headers: this.headers, + destination: this.destination, + referrer: this.referrer, + referrerPolicy: this.referrerPolicy, + mode: this.mode, + credentials: this.credentials, + cache: this.cache, + redirect: this.redirect, + integrity: this.integrity, + keepalive: this.keepalive, + isReloadNavigation: this.isReloadNavigation, + isHistoryNavigation: this.isHistoryNavigation, + signal: this.signal + }; + return `Request ${nodeUtil.formatWithOptions(options, properties)}`; + } + }; + mixinBody(Request2); + function makeRequest(init) { + return { + method: init.method ?? "GET", + localURLsOnly: init.localURLsOnly ?? false, + unsafeRequest: init.unsafeRequest ?? false, + body: init.body ?? null, + client: init.client ?? null, + reservedClient: init.reservedClient ?? null, + replacesClientId: init.replacesClientId ?? "", + window: init.window ?? "client", + keepalive: init.keepalive ?? false, + serviceWorkers: init.serviceWorkers ?? "all", + initiator: init.initiator ?? "", + destination: init.destination ?? "", + priority: init.priority ?? null, + origin: init.origin ?? "client", + policyContainer: init.policyContainer ?? "client", + referrer: init.referrer ?? "client", + referrerPolicy: init.referrerPolicy ?? "", + mode: init.mode ?? "no-cors", + useCORSPreflightFlag: init.useCORSPreflightFlag ?? false, + credentials: init.credentials ?? "same-origin", + useCredentials: init.useCredentials ?? false, + cache: init.cache ?? "default", + redirect: init.redirect ?? "follow", + integrity: init.integrity ?? "", + cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? "", + parserMetadata: init.parserMetadata ?? "", + reloadNavigation: init.reloadNavigation ?? false, + historyNavigation: init.historyNavigation ?? false, + userActivation: init.userActivation ?? false, + taintedOrigin: init.taintedOrigin ?? false, + redirectCount: init.redirectCount ?? 0, + responseTainting: init.responseTainting ?? "basic", + preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false, + done: init.done ?? false, + timingAllowFailed: init.timingAllowFailed ?? false, + urlList: init.urlList, + url: init.urlList[0], + headersList: init.headersList ? new HeadersList(init.headersList) : new HeadersList() + }; + } + function cloneRequest(request) { + const newRequest = makeRequest({ ...request, body: null }); + if (request.body != null) { + newRequest.body = cloneBody(newRequest, request.body); + } + return newRequest; + } + function fromInnerRequest(innerRequest, signal, guard) { + const request = new Request2(kConstruct); + request[kState] = innerRequest; + request[kSignal] = signal; + request[kHeaders] = new Headers2(kConstruct); + setHeadersList(request[kHeaders], innerRequest.headersList); + setHeadersGuard(request[kHeaders], guard); + return request; + } + Object.defineProperties(Request2.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "Request", + configurable: true + } + }); + webidl.converters.Request = webidl.interfaceConverter( + Request2 + ); + webidl.converters.RequestInfo = function(V, prefix, argument) { + if (typeof V === "string") { + return webidl.converters.USVString(V, prefix, argument); + } + if (V instanceof Request2) { + return webidl.converters.Request(V, prefix, argument); + } + return webidl.converters.USVString(V, prefix, argument); + }; + webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal + ); + webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: "method", + converter: webidl.converters.ByteString + }, + { + key: "headers", + converter: webidl.converters.HeadersInit + }, + { + key: "body", + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: "referrer", + converter: webidl.converters.USVString + }, + { + key: "referrerPolicy", + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: referrerPolicy + }, + { + key: "mode", + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: requestMode + }, + { + key: "credentials", + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: requestCredentials + }, + { + key: "cache", + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: requestCache + }, + { + key: "redirect", + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: requestRedirect + }, + { + key: "integrity", + converter: webidl.converters.DOMString + }, + { + key: "keepalive", + converter: webidl.converters.boolean + }, + { + key: "signal", + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + "RequestInit", + "signal", + { strict: false } + ) + ) + }, + { + key: "window", + converter: webidl.converters.any + }, + { + key: "duplex", + converter: webidl.converters.DOMString, + allowedValues: requestDuplex + }, + { + key: "dispatcher", + // undici specific option + converter: webidl.converters.any + } + ]); + module.exports = { Request: Request2, makeRequest, fromInnerRequest, cloneRequest }; + } +}); + +// viewer/node_modules/undici/lib/web/fetch/index.js +var require_fetch = __commonJS({ + "viewer/node_modules/undici/lib/web/fetch/index.js"(exports, module) { + "use strict"; + var { + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse, + fromInnerResponse + } = require_response(); + var { HeadersList } = require_headers(); + var { Request: Request2, cloneRequest } = require_request2(); + var zlib = __require("node:zlib"); + var { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme, + clampAndCoarsenConnectionTimingInfo, + simpleRangeHeaderValue, + buildContentRange, + createInflate, + extractMimeType + } = require_util2(); + var { kState, kDispatcher } = require_symbols2(); + var assert = __require("node:assert"); + var { safelyExtractBody, extractBody } = require_body(); + var { + redirectStatusSet, + nullBodyStatus, + safeMethodsSet, + requestBodyHeader, + subresourceSet + } = require_constants3(); + var EE = __require("node:events"); + var { Readable: Readable2, pipeline, finished } = __require("node:stream"); + var { addAbortListener, isErrored, isReadable, bufferToLowerCasedHeaderName } = require_util(); + var { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require_data_url(); + var { getGlobalDispatcher } = require_global2(); + var { webidl } = require_webidl(); + var { STATUS_CODES } = __require("node:http"); + var GET_OR_HEAD = ["GET", "HEAD"]; + var defaultUserAgent = typeof __UNDICI_IS_NODE__ !== "undefined" || typeof esbuildDetection !== "undefined" ? "node" : "undici"; + var resolveObjectURL; + var Fetch = class extends EE { + constructor(dispatcher) { + super(); + this.dispatcher = dispatcher; + this.connection = null; + this.dump = false; + this.state = "ongoing"; + } + terminate(reason) { + if (this.state !== "ongoing") { + return; + } + this.state = "terminated"; + this.connection?.destroy(reason); + this.emit("terminated", reason); + } + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort(error) { + if (this.state !== "ongoing") { + return; + } + this.state = "aborted"; + if (!error) { + error = new DOMException("The operation was aborted.", "AbortError"); + } + this.serializedAbortReason = error; + this.connection?.destroy(error); + this.emit("terminated", error); + } + }; + function handleFetchDone(response) { + finalizeAndReportTiming(response, "fetch"); + } + function fetch3(input, init = void 0) { + webidl.argumentLengthCheck(arguments, 1, "globalThis.fetch"); + let p = createDeferredPromise(); + let requestObject; + try { + requestObject = new Request2(input, init); + } catch (e) { + p.reject(e); + return p.promise; + } + const request = requestObject[kState]; + if (requestObject.signal.aborted) { + abortFetch(p, request, null, requestObject.signal.reason); + return p.promise; + } + const globalObject = request.client.globalObject; + if (globalObject?.constructor?.name === "ServiceWorkerGlobalScope") { + request.serviceWorkers = "none"; + } + let responseObject = null; + let locallyAborted = false; + let controller = null; + addAbortListener( + requestObject.signal, + () => { + locallyAborted = true; + assert(controller != null); + controller.abort(requestObject.signal.reason); + const realResponse = responseObject?.deref(); + abortFetch(p, request, realResponse, requestObject.signal.reason); + } + ); + const processResponse = (response) => { + if (locallyAborted) { + return; + } + if (response.aborted) { + abortFetch(p, request, responseObject, controller.serializedAbortReason); + return; + } + if (response.type === "error") { + p.reject(new TypeError("fetch failed", { cause: response.error })); + return; + } + responseObject = new WeakRef(fromInnerResponse(response, "immutable")); + p.resolve(responseObject.deref()); + p = null; + }; + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: requestObject[kDispatcher] + // undici + }); + return p.promise; + } + function finalizeAndReportTiming(response, initiatorType = "other") { + if (response.type === "error" && response.aborted) { + return; + } + if (!response.urlList?.length) { + return; + } + const originalURL = response.urlList[0]; + let timingInfo = response.timingInfo; + let cacheState = response.cacheState; + if (!urlIsHttpHttpsScheme(originalURL)) { + return; + } + if (timingInfo === null) { + return; + } + if (!response.timingAllowPassed) { + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }); + cacheState = ""; + } + timingInfo.endTime = coarsenedSharedCurrentTime(); + response.timingInfo = timingInfo; + markResourceTiming( + timingInfo, + originalURL.href, + initiatorType, + globalThis, + cacheState + ); + } + var markResourceTiming = performance.markResourceTiming; + function abortFetch(p, request, responseObject, error) { + if (p) { + p.reject(error); + } + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === "ERR_INVALID_STATE") { + return; + } + throw err; + }); + } + if (responseObject == null) { + return; + } + const response = responseObject[kState]; + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === "ERR_INVALID_STATE") { + return; + } + throw err; + }); + } + } + function fetching({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher = getGlobalDispatcher() + // undici + }) { + assert(dispatcher); + let taskDestination = null; + let crossOriginIsolatedCapability = false; + if (request.client != null) { + taskDestination = request.client.globalObject; + crossOriginIsolatedCapability = request.client.crossOriginIsolatedCapability; + } + const currentTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability); + const timingInfo = createOpaqueTimingInfo({ + startTime: currentTime + }); + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + }; + assert(!request.body || request.body.stream); + if (request.window === "client") { + request.window = request.client?.globalObject?.constructor?.name === "Window" ? request.client : "no-window"; + } + if (request.origin === "client") { + request.origin = request.client.origin; + } + if (request.policyContainer === "client") { + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ); + } else { + request.policyContainer = makePolicyContainer(); + } + } + if (!request.headersList.contains("accept", true)) { + const value = "*/*"; + request.headersList.append("accept", value, true); + } + if (!request.headersList.contains("accept-language", true)) { + request.headersList.append("accept-language", "*", true); + } + if (request.priority === null) { + } + if (subresourceSet.has(request.destination)) { + } + mainFetch(fetchParams).catch((err) => { + fetchParams.controller.terminate(err); + }); + return fetchParams.controller; + } + async function mainFetch(fetchParams, recursive = false) { + const request = fetchParams.request; + let response = null; + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError("local URLs only"); + } + tryUpgradeRequestToAPotentiallyTrustworthyURL(request); + if (requestBadPort(request) === "blocked") { + response = makeNetworkError("bad port"); + } + if (request.referrerPolicy === "") { + request.referrerPolicy = request.policyContainer.referrerPolicy; + } + if (request.referrer !== "no-referrer") { + request.referrer = determineRequestsReferrer(request); + } + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request); + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + sameOrigin(currentURL, request.url) && request.responseTainting === "basic" || // request’s current URL’s scheme is "data" + currentURL.protocol === "data:" || // - request’s mode is "navigate" or "websocket" + (request.mode === "navigate" || request.mode === "websocket") + ) { + request.responseTainting = "basic"; + return await schemeFetch(fetchParams); + } + if (request.mode === "same-origin") { + return makeNetworkError('request mode cannot be "same-origin"'); + } + if (request.mode === "no-cors") { + if (request.redirect !== "follow") { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ); + } + request.responseTainting = "opaque"; + return await schemeFetch(fetchParams); + } + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + return makeNetworkError("URL scheme must be a HTTP(S) scheme"); + } + request.responseTainting = "cors"; + return await httpFetch(fetchParams); + })(); + } + if (recursive) { + return response; + } + if (response.status !== 0 && !response.internalResponse) { + if (request.responseTainting === "cors") { + } + if (request.responseTainting === "basic") { + response = filterResponse(response, "basic"); + } else if (request.responseTainting === "cors") { + response = filterResponse(response, "cors"); + } else if (request.responseTainting === "opaque") { + response = filterResponse(response, "opaque"); + } else { + assert(false); + } + } + let internalResponse = response.status === 0 ? response : response.internalResponse; + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList); + } + if (!request.timingAllowFailed) { + response.timingAllowPassed = true; + } + if (response.type === "opaque" && internalResponse.status === 206 && internalResponse.rangeRequested && !request.headers.contains("range", true)) { + response = internalResponse = makeNetworkError(); + } + if (response.status !== 0 && (request.method === "HEAD" || request.method === "CONNECT" || nullBodyStatus.includes(internalResponse.status))) { + internalResponse.body = null; + fetchParams.controller.dump = true; + } + if (request.integrity) { + const processBodyError = (reason) => fetchFinale(fetchParams, makeNetworkError(reason)); + if (request.responseTainting === "opaque" || response.body == null) { + processBodyError(response.error); + return; + } + const processBody = (bytes2) => { + if (!bytesMatch(bytes2, request.integrity)) { + processBodyError("integrity mismatch"); + return; + } + response.body = safelyExtractBody(bytes2)[0]; + fetchFinale(fetchParams, response); + }; + await fullyReadBody(response.body, processBody, processBodyError); + } else { + fetchFinale(fetchParams, response); + } + } + function schemeFetch(fetchParams) { + if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { + return Promise.resolve(makeAppropriateNetworkError(fetchParams)); + } + const { request } = fetchParams; + const { protocol: scheme } = requestCurrentURL(request); + switch (scheme) { + case "about:": { + return Promise.resolve(makeNetworkError("about scheme is not supported")); + } + case "blob:": { + if (!resolveObjectURL) { + resolveObjectURL = __require("node:buffer").resolveObjectURL; + } + const blobURLEntry = requestCurrentURL(request); + if (blobURLEntry.search.length !== 0) { + return Promise.resolve(makeNetworkError("NetworkError when attempting to fetch resource.")); + } + const blob = resolveObjectURL(blobURLEntry.toString()); + if (request.method !== "GET" || !isBlobLike(blob)) { + return Promise.resolve(makeNetworkError("invalid method")); + } + const response = makeResponse(); + const fullLength = blob.size; + const serializedFullLength = isomorphicEncode(`${fullLength}`); + const type = blob.type; + if (!request.headersList.contains("range", true)) { + const bodyWithType = extractBody(blob); + response.statusText = "OK"; + response.body = bodyWithType[0]; + response.headersList.set("content-length", serializedFullLength, true); + response.headersList.set("content-type", type, true); + } else { + response.rangeRequested = true; + const rangeHeader = request.headersList.get("range", true); + const rangeValue = simpleRangeHeaderValue(rangeHeader, true); + if (rangeValue === "failure") { + return Promise.resolve(makeNetworkError("failed to fetch the data URL")); + } + let { rangeStartValue: rangeStart, rangeEndValue: rangeEnd } = rangeValue; + if (rangeStart === null) { + rangeStart = fullLength - rangeEnd; + rangeEnd = rangeStart + rangeEnd - 1; + } else { + if (rangeStart >= fullLength) { + return Promise.resolve(makeNetworkError("Range start is greater than the blob's size.")); + } + if (rangeEnd === null || rangeEnd >= fullLength) { + rangeEnd = fullLength - 1; + } + } + const slicedBlob = blob.slice(rangeStart, rangeEnd, type); + const slicedBodyWithType = extractBody(slicedBlob); + response.body = slicedBodyWithType[0]; + const serializedSlicedLength = isomorphicEncode(`${slicedBlob.size}`); + const contentRange = buildContentRange(rangeStart, rangeEnd, fullLength); + response.status = 206; + response.statusText = "Partial Content"; + response.headersList.set("content-length", serializedSlicedLength, true); + response.headersList.set("content-type", type, true); + response.headersList.set("content-range", contentRange, true); + } + return Promise.resolve(response); + } + case "data:": { + const currentURL = requestCurrentURL(request); + const dataURLStruct = dataURLProcessor(currentURL); + if (dataURLStruct === "failure") { + return Promise.resolve(makeNetworkError("failed to fetch the data URL")); + } + const mimeType = serializeAMimeType(dataURLStruct.mimeType); + return Promise.resolve(makeResponse({ + statusText: "OK", + headersList: [ + ["content-type", { name: "Content-Type", value: mimeType }] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + })); + } + case "file:": { + return Promise.resolve(makeNetworkError("not implemented... yet...")); + } + case "http:": + case "https:": { + return httpFetch(fetchParams).catch((err) => makeNetworkError(err)); + } + default: { + return Promise.resolve(makeNetworkError("unknown scheme")); + } + } + } + function finalizeResponse(fetchParams, response) { + fetchParams.request.done = true; + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)); + } + } + function fetchFinale(fetchParams, response) { + let timingInfo = fetchParams.timingInfo; + const processResponseEndOfBody = () => { + const unsafeEndTime = Date.now(); + if (fetchParams.request.destination === "document") { + fetchParams.controller.fullTimingInfo = timingInfo; + } + fetchParams.controller.reportTimingSteps = () => { + if (fetchParams.request.url.protocol !== "https:") { + return; + } + timingInfo.endTime = unsafeEndTime; + let cacheState = response.cacheState; + const bodyInfo = response.bodyInfo; + if (!response.timingAllowPassed) { + timingInfo = createOpaqueTimingInfo(timingInfo); + cacheState = ""; + } + let responseStatus = 0; + if (fetchParams.request.mode !== "navigator" || !response.hasCrossOriginRedirects) { + responseStatus = response.status; + const mimeType = extractMimeType(response.headersList); + if (mimeType !== "failure") { + bodyInfo.contentType = minimizeSupportedMimeType(mimeType); + } + } + if (fetchParams.request.initiatorType != null) { + markResourceTiming(timingInfo, fetchParams.request.url.href, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus); + } + }; + const processResponseEndOfBodyTask = () => { + fetchParams.request.done = true; + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)); + } + if (fetchParams.request.initiatorType != null) { + fetchParams.controller.reportTimingSteps(); + } + }; + queueMicrotask(() => processResponseEndOfBodyTask()); + }; + if (fetchParams.processResponse != null) { + queueMicrotask(() => { + fetchParams.processResponse(response); + fetchParams.processResponse = null; + }); + } + const internalResponse = response.type === "error" ? response : response.internalResponse ?? response; + if (internalResponse.body == null) { + processResponseEndOfBody(); + } else { + finished(internalResponse.body.stream, () => { + processResponseEndOfBody(); + }); + } + } + async function httpFetch(fetchParams) { + const request = fetchParams.request; + let response = null; + let actualResponse = null; + const timingInfo = fetchParams.timingInfo; + if (request.serviceWorkers === "all") { + } + if (response === null) { + if (request.redirect === "follow") { + request.serviceWorkers = "none"; + } + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams); + if (request.responseTainting === "cors" && corsCheck(request, response) === "failure") { + return makeNetworkError("cors failure"); + } + if (TAOCheck(request, response) === "failure") { + request.timingAllowFailed = true; + } + } + if ((request.responseTainting === "opaque" || response.type === "opaque") && crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === "blocked") { + return makeNetworkError("blocked"); + } + if (redirectStatusSet.has(actualResponse.status)) { + if (request.redirect !== "manual") { + fetchParams.controller.connection.destroy(void 0, false); + } + if (request.redirect === "error") { + response = makeNetworkError("unexpected redirect"); + } else if (request.redirect === "manual") { + response = actualResponse; + } else if (request.redirect === "follow") { + response = await httpRedirectFetch(fetchParams, response); + } else { + assert(false); + } + } + response.timingInfo = timingInfo; + return response; + } + function httpRedirectFetch(fetchParams, response) { + const request = fetchParams.request; + const actualResponse = response.internalResponse ? response.internalResponse : response; + let locationURL; + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ); + if (locationURL == null) { + return response; + } + } catch (err) { + return Promise.resolve(makeNetworkError(err)); + } + if (!urlIsHttpHttpsScheme(locationURL)) { + return Promise.resolve(makeNetworkError("URL scheme must be a HTTP(S) scheme")); + } + if (request.redirectCount === 20) { + return Promise.resolve(makeNetworkError("redirect count exceeded")); + } + request.redirectCount += 1; + if (request.mode === "cors" && (locationURL.username || locationURL.password) && !sameOrigin(request, locationURL)) { + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')); + } + if (request.responseTainting === "cors" && (locationURL.username || locationURL.password)) { + return Promise.resolve(makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + )); + } + if (actualResponse.status !== 303 && request.body != null && request.body.source == null) { + return Promise.resolve(makeNetworkError()); + } + if ([301, 302].includes(actualResponse.status) && request.method === "POST" || actualResponse.status === 303 && !GET_OR_HEAD.includes(request.method)) { + request.method = "GET"; + request.body = null; + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName); + } + } + if (!sameOrigin(requestCurrentURL(request), locationURL)) { + request.headersList.delete("authorization", true); + request.headersList.delete("proxy-authorization", true); + request.headersList.delete("cookie", true); + request.headersList.delete("host", true); + } + if (request.body != null) { + assert(request.body.source != null); + request.body = safelyExtractBody(request.body.source)[0]; + } + const timingInfo = fetchParams.timingInfo; + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability); + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime; + } + request.urlList.push(locationURL); + setRequestReferrerPolicyOnRedirect(request, actualResponse); + return mainFetch(fetchParams, true); + } + async function httpNetworkOrCacheFetch(fetchParams, isAuthenticationFetch = false, isNewConnectionFetch = false) { + const request = fetchParams.request; + let httpFetchParams = null; + let httpRequest = null; + let response = null; + const httpCache = null; + const revalidatingFlag = false; + if (request.window === "no-window" && request.redirect === "error") { + httpFetchParams = fetchParams; + httpRequest = request; + } else { + httpRequest = cloneRequest(request); + httpFetchParams = { ...fetchParams }; + httpFetchParams.request = httpRequest; + } + const includeCredentials = request.credentials === "include" || request.credentials === "same-origin" && request.responseTainting === "basic"; + const contentLength = httpRequest.body ? httpRequest.body.length : null; + let contentLengthHeaderValue = null; + if (httpRequest.body == null && ["POST", "PUT"].includes(httpRequest.method)) { + contentLengthHeaderValue = "0"; + } + if (contentLength != null) { + contentLengthHeaderValue = isomorphicEncode(`${contentLength}`); + } + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append("content-length", contentLengthHeaderValue, true); + } + if (contentLength != null && httpRequest.keepalive) { + } + if (httpRequest.referrer instanceof URL) { + httpRequest.headersList.append("referer", isomorphicEncode(httpRequest.referrer.href), true); + } + appendRequestOriginHeader(httpRequest); + appendFetchMetadata(httpRequest); + if (!httpRequest.headersList.contains("user-agent", true)) { + httpRequest.headersList.append("user-agent", defaultUserAgent); + } + if (httpRequest.cache === "default" && (httpRequest.headersList.contains("if-modified-since", true) || httpRequest.headersList.contains("if-none-match", true) || httpRequest.headersList.contains("if-unmodified-since", true) || httpRequest.headersList.contains("if-match", true) || httpRequest.headersList.contains("if-range", true))) { + httpRequest.cache = "no-store"; + } + if (httpRequest.cache === "no-cache" && !httpRequest.preventNoCacheCacheControlHeaderModification && !httpRequest.headersList.contains("cache-control", true)) { + httpRequest.headersList.append("cache-control", "max-age=0", true); + } + if (httpRequest.cache === "no-store" || httpRequest.cache === "reload") { + if (!httpRequest.headersList.contains("pragma", true)) { + httpRequest.headersList.append("pragma", "no-cache", true); + } + if (!httpRequest.headersList.contains("cache-control", true)) { + httpRequest.headersList.append("cache-control", "no-cache", true); + } + } + if (httpRequest.headersList.contains("range", true)) { + httpRequest.headersList.append("accept-encoding", "identity", true); + } + if (!httpRequest.headersList.contains("accept-encoding", true)) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { + httpRequest.headersList.append("accept-encoding", "br, gzip, deflate", true); + } else { + httpRequest.headersList.append("accept-encoding", "gzip, deflate", true); + } + } + httpRequest.headersList.delete("host", true); + if (includeCredentials) { + } + if (httpCache == null) { + httpRequest.cache = "no-store"; + } + if (httpRequest.cache !== "no-store" && httpRequest.cache !== "reload") { + } + if (response == null) { + if (httpRequest.cache === "only-if-cached") { + return makeNetworkError("only if cached"); + } + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ); + if (!safeMethodsSet.has(httpRequest.method) && forwardResponse.status >= 200 && forwardResponse.status <= 399) { + } + if (revalidatingFlag && forwardResponse.status === 304) { + } + if (response == null) { + response = forwardResponse; + } + } + response.urlList = [...httpRequest.urlList]; + if (httpRequest.headersList.contains("range", true)) { + response.rangeRequested = true; + } + response.requestIncludesCredentials = includeCredentials; + if (response.status === 407) { + if (request.window === "no-window") { + return makeNetworkError(); + } + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams); + } + return makeNetworkError("proxy authentication required"); + } + if ( + // response’s status is 421 + response.status === 421 && // isNewConnectionFetch is false + !isNewConnectionFetch && // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams); + } + fetchParams.controller.connection.destroy(); + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ); + } + if (isAuthenticationFetch) { + } + return response; + } + async function httpNetworkFetch(fetchParams, includeCredentials = false, forceNewConnection = false) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed); + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy(err, abort = true) { + if (!this.destroyed) { + this.destroyed = true; + if (abort) { + this.abort?.(err ?? new DOMException("The operation was aborted.", "AbortError")); + } + } + } + }; + const request = fetchParams.request; + let response = null; + const timingInfo = fetchParams.timingInfo; + const httpCache = null; + if (httpCache == null) { + request.cache = "no-store"; + } + const newConnection = forceNewConnection ? "yes" : "no"; + if (request.mode === "websocket") { + } else { + } + let requestBody = null; + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()); + } else if (request.body != null) { + const processBodyChunk = async function* (bytes2) { + if (isCancelled(fetchParams)) { + return; + } + yield bytes2; + fetchParams.processRequestBodyChunkLength?.(bytes2.byteLength); + }; + const processEndOfBody = () => { + if (isCancelled(fetchParams)) { + return; + } + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody(); + } + }; + const processBodyError = (e) => { + if (isCancelled(fetchParams)) { + return; + } + if (e.name === "AbortError") { + fetchParams.controller.abort(); + } else { + fetchParams.controller.terminate(e); + } + }; + requestBody = (async function* () { + try { + for await (const bytes2 of request.body.stream) { + yield* processBodyChunk(bytes2); + } + processEndOfBody(); + } catch (err) { + processBodyError(err); + } + })(); + } + try { + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }); + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }); + } else { + const iterator = body[Symbol.asyncIterator](); + fetchParams.controller.next = () => iterator.next(); + response = makeResponse({ status, statusText, headersList }); + } + } catch (err) { + if (err.name === "AbortError") { + fetchParams.controller.connection.destroy(); + return makeAppropriateNetworkError(fetchParams, err); + } + return makeNetworkError(err); + } + const pullAlgorithm = async () => { + await fetchParams.controller.resume(); + }; + const cancelAlgorithm = (reason) => { + if (!isCancelled(fetchParams)) { + fetchParams.controller.abort(reason); + } + }; + const stream = new ReadableStream( + { + async start(controller) { + fetchParams.controller.controller = controller; + }, + async pull(controller) { + await pullAlgorithm(controller); + }, + async cancel(reason) { + await cancelAlgorithm(reason); + }, + type: "bytes" + } + ); + response.body = { stream, source: null, length: null }; + fetchParams.controller.onAborted = onAborted; + fetchParams.controller.on("terminated", onAborted); + fetchParams.controller.resume = async () => { + while (true) { + let bytes2; + let isFailure; + try { + const { done, value } = await fetchParams.controller.next(); + if (isAborted(fetchParams)) { + break; + } + bytes2 = done ? void 0 : value; + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + bytes2 = void 0; + } else { + bytes2 = err; + isFailure = true; + } + } + if (bytes2 === void 0) { + readableStreamClose(fetchParams.controller.controller); + finalizeResponse(fetchParams, response); + return; + } + timingInfo.decodedBodySize += bytes2?.byteLength ?? 0; + if (isFailure) { + fetchParams.controller.terminate(bytes2); + return; + } + const buffer = new Uint8Array(bytes2); + if (buffer.byteLength) { + fetchParams.controller.controller.enqueue(buffer); + } + if (isErrored(stream)) { + fetchParams.controller.terminate(); + return; + } + if (fetchParams.controller.controller.desiredSize <= 0) { + return; + } + } + }; + function onAborted(reason) { + if (isAborted(fetchParams)) { + response.aborted = true; + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ); + } + } else { + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError("terminated", { + cause: isErrorLike(reason) ? reason : void 0 + })); + } + } + fetchParams.controller.connection.destroy(); + } + return response; + function dispatch({ body }) { + const url = requestCurrentURL(request); + const agent = fetchParams.controller.dispatcher; + return new Promise((resolve, reject) => agent.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + headers: request.headersList.entries, + maxRedirections: 0, + upgrade: request.mode === "websocket" ? "websocket" : void 0 + }, + { + body: null, + abort: null, + onConnect(abort) { + const { connection } = fetchParams.controller; + timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(void 0, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability); + if (connection.destroyed) { + abort(new DOMException("The operation was aborted.", "AbortError")); + } else { + fetchParams.controller.on("terminated", abort); + this.abort = connection.abort = abort; + } + timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability); + }, + onResponseStarted() { + timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability); + }, + onHeaders(status, rawHeaders, resume, statusText) { + if (status < 200) { + return; + } + let location = ""; + const headersList = new HeadersList(); + for (let i = 0; i < rawHeaders.length; i += 2) { + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); + } + location = headersList.get("location", true); + this.body = new Readable2({ read: resume }); + const decoders = []; + const willFollow = location && request.redirect === "follow" && redirectStatusSet.has(status); + if (request.method !== "HEAD" && request.method !== "CONNECT" && !nullBodyStatus.includes(status) && !willFollow) { + const contentEncoding = headersList.get("content-encoding", true); + const codings = contentEncoding ? contentEncoding.toLowerCase().split(",") : []; + const maxContentEncodings = 5; + if (codings.length > maxContentEncodings) { + reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`)); + return true; + } + for (let i = codings.length - 1; i >= 0; --i) { + const coding = codings[i].trim(); + if (coding === "x-gzip" || coding === "gzip") { + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })); + } else if (coding === "deflate") { + decoders.push(createInflate({ + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })); + } else if (coding === "br") { + decoders.push(zlib.createBrotliDecompress({ + flush: zlib.constants.BROTLI_OPERATION_FLUSH, + finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH + })); + } else { + decoders.length = 0; + break; + } + } + } + const onError = this.onError.bind(this); + resolve({ + status, + statusText, + headersList, + body: decoders.length ? pipeline(this.body, ...decoders, (err) => { + if (err) { + this.onError(err); + } + }).on("error", onError) : this.body.on("error", onError) + }); + return true; + }, + onData(chunk) { + if (fetchParams.controller.dump) { + return; + } + const bytes2 = chunk; + timingInfo.encodedBodySize += bytes2.byteLength; + return this.body.push(bytes2); + }, + onComplete() { + if (this.abort) { + fetchParams.controller.off("terminated", this.abort); + } + if (fetchParams.controller.onAborted) { + fetchParams.controller.off("terminated", fetchParams.controller.onAborted); + } + fetchParams.controller.ended = true; + this.body.push(null); + }, + onError(error) { + if (this.abort) { + fetchParams.controller.off("terminated", this.abort); + } + this.body?.destroy(error); + fetchParams.controller.terminate(error); + reject(error); + }, + onUpgrade(status, rawHeaders, socket) { + if (status !== 101) { + return; + } + const headersList = new HeadersList(); + for (let i = 0; i < rawHeaders.length; i += 2) { + headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString("latin1"), true); + } + resolve({ + status, + statusText: STATUS_CODES[status], + headersList, + socket + }); + return true; + } + } + )); + } + } + module.exports = { + fetch: fetch3, + Fetch, + fetching, + finalizeAndReportTiming + }; + } +}); + +// viewer/node_modules/undici/lib/web/fileapi/symbols.js +var require_symbols3 = __commonJS({ + "viewer/node_modules/undici/lib/web/fileapi/symbols.js"(exports, module) { + "use strict"; + module.exports = { + kState: /* @__PURE__ */ Symbol("FileReader state"), + kResult: /* @__PURE__ */ Symbol("FileReader result"), + kError: /* @__PURE__ */ Symbol("FileReader error"), + kLastProgressEventFired: /* @__PURE__ */ Symbol("FileReader last progress event fired timestamp"), + kEvents: /* @__PURE__ */ Symbol("FileReader events"), + kAborted: /* @__PURE__ */ Symbol("FileReader aborted") + }; + } +}); + +// viewer/node_modules/undici/lib/web/fileapi/progressevent.js +var require_progressevent = __commonJS({ + "viewer/node_modules/undici/lib/web/fileapi/progressevent.js"(exports, module) { + "use strict"; + var { webidl } = require_webidl(); + var kState = /* @__PURE__ */ Symbol("ProgressEvent state"); + var ProgressEvent = class _ProgressEvent extends Event { + constructor(type, eventInitDict = {}) { + type = webidl.converters.DOMString(type, "ProgressEvent constructor", "type"); + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}); + super(type, eventInitDict); + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + }; + } + get lengthComputable() { + webidl.brandCheck(this, _ProgressEvent); + return this[kState].lengthComputable; + } + get loaded() { + webidl.brandCheck(this, _ProgressEvent); + return this[kState].loaded; + } + get total() { + webidl.brandCheck(this, _ProgressEvent); + return this[kState].total; + } + }; + webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: "lengthComputable", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "loaded", + converter: webidl.converters["unsigned long long"], + defaultValue: () => 0 + }, + { + key: "total", + converter: webidl.converters["unsigned long long"], + defaultValue: () => 0 + }, + { + key: "bubbles", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "cancelable", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "composed", + converter: webidl.converters.boolean, + defaultValue: () => false + } + ]); + module.exports = { + ProgressEvent + }; + } +}); + +// viewer/node_modules/undici/lib/web/fileapi/encoding.js +var require_encoding = __commonJS({ + "viewer/node_modules/undici/lib/web/fileapi/encoding.js"(exports, module) { + "use strict"; + function getEncoding(label) { + if (!label) { + return "failure"; + } + switch (label.trim().toLowerCase()) { + case "unicode-1-1-utf-8": + case "unicode11utf8": + case "unicode20utf8": + case "utf-8": + case "utf8": + case "x-unicode20utf8": + return "UTF-8"; + case "866": + case "cp866": + case "csibm866": + case "ibm866": + return "IBM866"; + case "csisolatin2": + case "iso-8859-2": + case "iso-ir-101": + case "iso8859-2": + case "iso88592": + case "iso_8859-2": + case "iso_8859-2:1987": + case "l2": + case "latin2": + return "ISO-8859-2"; + case "csisolatin3": + case "iso-8859-3": + case "iso-ir-109": + case "iso8859-3": + case "iso88593": + case "iso_8859-3": + case "iso_8859-3:1988": + case "l3": + case "latin3": + return "ISO-8859-3"; + case "csisolatin4": + case "iso-8859-4": + case "iso-ir-110": + case "iso8859-4": + case "iso88594": + case "iso_8859-4": + case "iso_8859-4:1988": + case "l4": + case "latin4": + return "ISO-8859-4"; + case "csisolatincyrillic": + case "cyrillic": + case "iso-8859-5": + case "iso-ir-144": + case "iso8859-5": + case "iso88595": + case "iso_8859-5": + case "iso_8859-5:1988": + return "ISO-8859-5"; + case "arabic": + case "asmo-708": + case "csiso88596e": + case "csiso88596i": + case "csisolatinarabic": + case "ecma-114": + case "iso-8859-6": + case "iso-8859-6-e": + case "iso-8859-6-i": + case "iso-ir-127": + case "iso8859-6": + case "iso88596": + case "iso_8859-6": + case "iso_8859-6:1987": + return "ISO-8859-6"; + case "csisolatingreek": + case "ecma-118": + case "elot_928": + case "greek": + case "greek8": + case "iso-8859-7": + case "iso-ir-126": + case "iso8859-7": + case "iso88597": + case "iso_8859-7": + case "iso_8859-7:1987": + case "sun_eu_greek": + return "ISO-8859-7"; + case "csiso88598e": + case "csisolatinhebrew": + case "hebrew": + case "iso-8859-8": + case "iso-8859-8-e": + case "iso-ir-138": + case "iso8859-8": + case "iso88598": + case "iso_8859-8": + case "iso_8859-8:1988": + case "visual": + return "ISO-8859-8"; + case "csiso88598i": + case "iso-8859-8-i": + case "logical": + return "ISO-8859-8-I"; + case "csisolatin6": + case "iso-8859-10": + case "iso-ir-157": + case "iso8859-10": + case "iso885910": + case "l6": + case "latin6": + return "ISO-8859-10"; + case "iso-8859-13": + case "iso8859-13": + case "iso885913": + return "ISO-8859-13"; + case "iso-8859-14": + case "iso8859-14": + case "iso885914": + return "ISO-8859-14"; + case "csisolatin9": + case "iso-8859-15": + case "iso8859-15": + case "iso885915": + case "iso_8859-15": + case "l9": + return "ISO-8859-15"; + case "iso-8859-16": + return "ISO-8859-16"; + case "cskoi8r": + case "koi": + case "koi8": + case "koi8-r": + case "koi8_r": + return "KOI8-R"; + case "koi8-ru": + case "koi8-u": + return "KOI8-U"; + case "csmacintosh": + case "mac": + case "macintosh": + case "x-mac-roman": + return "macintosh"; + case "iso-8859-11": + case "iso8859-11": + case "iso885911": + case "tis-620": + case "windows-874": + return "windows-874"; + case "cp1250": + case "windows-1250": + case "x-cp1250": + return "windows-1250"; + case "cp1251": + case "windows-1251": + case "x-cp1251": + return "windows-1251"; + case "ansi_x3.4-1968": + case "ascii": + case "cp1252": + case "cp819": + case "csisolatin1": + case "ibm819": + case "iso-8859-1": + case "iso-ir-100": + case "iso8859-1": + case "iso88591": + case "iso_8859-1": + case "iso_8859-1:1987": + case "l1": + case "latin1": + case "us-ascii": + case "windows-1252": + case "x-cp1252": + return "windows-1252"; + case "cp1253": + case "windows-1253": + case "x-cp1253": + return "windows-1253"; + case "cp1254": + case "csisolatin5": + case "iso-8859-9": + case "iso-ir-148": + case "iso8859-9": + case "iso88599": + case "iso_8859-9": + case "iso_8859-9:1989": + case "l5": + case "latin5": + case "windows-1254": + case "x-cp1254": + return "windows-1254"; + case "cp1255": + case "windows-1255": + case "x-cp1255": + return "windows-1255"; + case "cp1256": + case "windows-1256": + case "x-cp1256": + return "windows-1256"; + case "cp1257": + case "windows-1257": + case "x-cp1257": + return "windows-1257"; + case "cp1258": + case "windows-1258": + case "x-cp1258": + return "windows-1258"; + case "x-mac-cyrillic": + case "x-mac-ukrainian": + return "x-mac-cyrillic"; + case "chinese": + case "csgb2312": + case "csiso58gb231280": + case "gb2312": + case "gb_2312": + case "gb_2312-80": + case "gbk": + case "iso-ir-58": + case "x-gbk": + return "GBK"; + case "gb18030": + return "gb18030"; + case "big5": + case "big5-hkscs": + case "cn-big5": + case "csbig5": + case "x-x-big5": + return "Big5"; + case "cseucpkdfmtjapanese": + case "euc-jp": + case "x-euc-jp": + return "EUC-JP"; + case "csiso2022jp": + case "iso-2022-jp": + return "ISO-2022-JP"; + case "csshiftjis": + case "ms932": + case "ms_kanji": + case "shift-jis": + case "shift_jis": + case "sjis": + case "windows-31j": + case "x-sjis": + return "Shift_JIS"; + case "cseuckr": + case "csksc56011987": + case "euc-kr": + case "iso-ir-149": + case "korean": + case "ks_c_5601-1987": + case "ks_c_5601-1989": + case "ksc5601": + case "ksc_5601": + case "windows-949": + return "EUC-KR"; + case "csiso2022kr": + case "hz-gb-2312": + case "iso-2022-cn": + case "iso-2022-cn-ext": + case "iso-2022-kr": + case "replacement": + return "replacement"; + case "unicodefffe": + case "utf-16be": + return "UTF-16BE"; + case "csunicode": + case "iso-10646-ucs-2": + case "ucs-2": + case "unicode": + case "unicodefeff": + case "utf-16": + case "utf-16le": + return "UTF-16LE"; + case "x-user-defined": + return "x-user-defined"; + default: + return "failure"; + } + } + module.exports = { + getEncoding + }; + } +}); + +// viewer/node_modules/undici/lib/web/fileapi/util.js +var require_util4 = __commonJS({ + "viewer/node_modules/undici/lib/web/fileapi/util.js"(exports, module) { + "use strict"; + var { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired + } = require_symbols3(); + var { ProgressEvent } = require_progressevent(); + var { getEncoding } = require_encoding(); + var { serializeAMimeType, parseMIMEType } = require_data_url(); + var { types } = __require("node:util"); + var { StringDecoder } = __require("string_decoder"); + var { btoa: btoa2 } = __require("node:buffer"); + var staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false + }; + function readOperation(fr, blob, type, encodingName) { + if (fr[kState] === "loading") { + throw new DOMException("Invalid state", "InvalidStateError"); + } + fr[kState] = "loading"; + fr[kResult] = null; + fr[kError] = null; + const stream = blob.stream(); + const reader = stream.getReader(); + const bytes2 = []; + let chunkPromise = reader.read(); + let isFirstChunk = true; + (async () => { + while (!fr[kAborted]) { + try { + const { done, value } = await chunkPromise; + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent("loadstart", fr); + }); + } + isFirstChunk = false; + if (!done && types.isUint8Array(value)) { + bytes2.push(value); + if ((fr[kLastProgressEventFired] === void 0 || Date.now() - fr[kLastProgressEventFired] >= 50) && !fr[kAborted]) { + fr[kLastProgressEventFired] = Date.now(); + queueMicrotask(() => { + fireAProgressEvent("progress", fr); + }); + } + chunkPromise = reader.read(); + } else if (done) { + queueMicrotask(() => { + fr[kState] = "done"; + try { + const result = packageData(bytes2, type, blob.type, encodingName); + if (fr[kAborted]) { + return; + } + fr[kResult] = result; + fireAProgressEvent("load", fr); + } catch (error) { + fr[kError] = error; + fireAProgressEvent("error", fr); + } + if (fr[kState] !== "loading") { + fireAProgressEvent("loadend", fr); + } + }); + break; + } + } catch (error) { + if (fr[kAborted]) { + return; + } + queueMicrotask(() => { + fr[kState] = "done"; + fr[kError] = error; + fireAProgressEvent("error", fr); + if (fr[kState] !== "loading") { + fireAProgressEvent("loadend", fr); + } + }); + break; + } + } + })(); + } + function fireAProgressEvent(e, reader) { + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }); + reader.dispatchEvent(event); + } + function packageData(bytes2, type, mimeType, encodingName) { + switch (type) { + case "DataURL": { + let dataURL = "data:"; + const parsed = parseMIMEType(mimeType || "application/octet-stream"); + if (parsed !== "failure") { + dataURL += serializeAMimeType(parsed); + } + dataURL += ";base64,"; + const decoder = new StringDecoder("latin1"); + for (const chunk of bytes2) { + dataURL += btoa2(decoder.write(chunk)); + } + dataURL += btoa2(decoder.end()); + return dataURL; + } + case "Text": { + let encoding = "failure"; + if (encodingName) { + encoding = getEncoding(encodingName); + } + if (encoding === "failure" && mimeType) { + const type2 = parseMIMEType(mimeType); + if (type2 !== "failure") { + encoding = getEncoding(type2.parameters.get("charset")); + } + } + if (encoding === "failure") { + encoding = "UTF-8"; + } + return decode(bytes2, encoding); + } + case "ArrayBuffer": { + const sequence = combineByteSequences(bytes2); + return sequence.buffer; + } + case "BinaryString": { + let binaryString = ""; + const decoder = new StringDecoder("latin1"); + for (const chunk of bytes2) { + binaryString += decoder.write(chunk); + } + binaryString += decoder.end(); + return binaryString; + } + } + } + function decode(ioQueue, encoding) { + const bytes2 = combineByteSequences(ioQueue); + const BOMEncoding = BOMSniffing(bytes2); + let slice = 0; + if (BOMEncoding !== null) { + encoding = BOMEncoding; + slice = BOMEncoding === "UTF-8" ? 3 : 2; + } + const sliced = bytes2.slice(slice); + return new TextDecoder(encoding).decode(sliced); + } + function BOMSniffing(ioQueue) { + const [a, b, c] = ioQueue; + if (a === 239 && b === 187 && c === 191) { + return "UTF-8"; + } else if (a === 254 && b === 255) { + return "UTF-16BE"; + } else if (a === 255 && b === 254) { + return "UTF-16LE"; + } + return null; + } + function combineByteSequences(sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength; + }, 0); + let offset = 0; + return sequences.reduce((a, b) => { + a.set(b, offset); + offset += b.byteLength; + return a; + }, new Uint8Array(size)); + } + module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent + }; + } +}); + +// viewer/node_modules/undici/lib/web/fileapi/filereader.js +var require_filereader = __commonJS({ + "viewer/node_modules/undici/lib/web/fileapi/filereader.js"(exports, module) { + "use strict"; + var { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent + } = require_util4(); + var { + kState, + kError, + kResult, + kEvents, + kAborted + } = require_symbols3(); + var { webidl } = require_webidl(); + var { kEnumerableProperty } = require_util(); + var FileReader = class _FileReader extends EventTarget { + constructor() { + super(); + this[kState] = "empty"; + this[kResult] = null; + this[kError] = null; + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + }; + } + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer(blob) { + webidl.brandCheck(this, _FileReader); + webidl.argumentLengthCheck(arguments, 1, "FileReader.readAsArrayBuffer"); + blob = webidl.converters.Blob(blob, { strict: false }); + readOperation(this, blob, "ArrayBuffer"); + } + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString(blob) { + webidl.brandCheck(this, _FileReader); + webidl.argumentLengthCheck(arguments, 1, "FileReader.readAsBinaryString"); + blob = webidl.converters.Blob(blob, { strict: false }); + readOperation(this, blob, "BinaryString"); + } + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText(blob, encoding = void 0) { + webidl.brandCheck(this, _FileReader); + webidl.argumentLengthCheck(arguments, 1, "FileReader.readAsText"); + blob = webidl.converters.Blob(blob, { strict: false }); + if (encoding !== void 0) { + encoding = webidl.converters.DOMString(encoding, "FileReader.readAsText", "encoding"); + } + readOperation(this, blob, "Text", encoding); + } + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL(blob) { + webidl.brandCheck(this, _FileReader); + webidl.argumentLengthCheck(arguments, 1, "FileReader.readAsDataURL"); + blob = webidl.converters.Blob(blob, { strict: false }); + readOperation(this, blob, "DataURL"); + } + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort() { + if (this[kState] === "empty" || this[kState] === "done") { + this[kResult] = null; + return; + } + if (this[kState] === "loading") { + this[kState] = "done"; + this[kResult] = null; + } + this[kAborted] = true; + fireAProgressEvent("abort", this); + if (this[kState] !== "loading") { + fireAProgressEvent("loadend", this); + } + } + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState() { + webidl.brandCheck(this, _FileReader); + switch (this[kState]) { + case "empty": + return this.EMPTY; + case "loading": + return this.LOADING; + case "done": + return this.DONE; + } + } + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result() { + webidl.brandCheck(this, _FileReader); + return this[kResult]; + } + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error() { + webidl.brandCheck(this, _FileReader); + return this[kError]; + } + get onloadend() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].loadend; + } + set onloadend(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].loadend) { + this.removeEventListener("loadend", this[kEvents].loadend); + } + if (typeof fn === "function") { + this[kEvents].loadend = fn; + this.addEventListener("loadend", fn); + } else { + this[kEvents].loadend = null; + } + } + get onerror() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].error; + } + set onerror(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].error) { + this.removeEventListener("error", this[kEvents].error); + } + if (typeof fn === "function") { + this[kEvents].error = fn; + this.addEventListener("error", fn); + } else { + this[kEvents].error = null; + } + } + get onloadstart() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].loadstart; + } + set onloadstart(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].loadstart) { + this.removeEventListener("loadstart", this[kEvents].loadstart); + } + if (typeof fn === "function") { + this[kEvents].loadstart = fn; + this.addEventListener("loadstart", fn); + } else { + this[kEvents].loadstart = null; + } + } + get onprogress() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].progress; + } + set onprogress(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].progress) { + this.removeEventListener("progress", this[kEvents].progress); + } + if (typeof fn === "function") { + this[kEvents].progress = fn; + this.addEventListener("progress", fn); + } else { + this[kEvents].progress = null; + } + } + get onload() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].load; + } + set onload(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].load) { + this.removeEventListener("load", this[kEvents].load); + } + if (typeof fn === "function") { + this[kEvents].load = fn; + this.addEventListener("load", fn); + } else { + this[kEvents].load = null; + } + } + get onabort() { + webidl.brandCheck(this, _FileReader); + return this[kEvents].abort; + } + set onabort(fn) { + webidl.brandCheck(this, _FileReader); + if (this[kEvents].abort) { + this.removeEventListener("abort", this[kEvents].abort); + } + if (typeof fn === "function") { + this[kEvents].abort = fn; + this.addEventListener("abort", fn); + } else { + this[kEvents].abort = null; + } + } + }; + FileReader.EMPTY = FileReader.prototype.EMPTY = 0; + FileReader.LOADING = FileReader.prototype.LOADING = 1; + FileReader.DONE = FileReader.prototype.DONE = 2; + Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "FileReader", + writable: false, + enumerable: false, + configurable: true + } + }); + Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors + }); + module.exports = { + FileReader + }; + } +}); + +// viewer/node_modules/undici/lib/web/cache/symbols.js +var require_symbols4 = __commonJS({ + "viewer/node_modules/undici/lib/web/cache/symbols.js"(exports, module) { + "use strict"; + module.exports = { + kConstruct: require_symbols().kConstruct + }; + } +}); + +// viewer/node_modules/undici/lib/web/cache/util.js +var require_util5 = __commonJS({ + "viewer/node_modules/undici/lib/web/cache/util.js"(exports, module) { + "use strict"; + var assert = __require("node:assert"); + var { URLSerializer } = require_data_url(); + var { isValidHeaderName } = require_util2(); + function urlEquals(A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment); + const serializedB = URLSerializer(B, excludeFragment); + return serializedA === serializedB; + } + function getFieldValues(header) { + assert(header !== null); + const values = []; + for (let value of header.split(",")) { + value = value.trim(); + if (isValidHeaderName(value)) { + values.push(value); + } + } + return values; + } + module.exports = { + urlEquals, + getFieldValues + }; + } +}); + +// viewer/node_modules/undici/lib/web/cache/cache.js +var require_cache = __commonJS({ + "viewer/node_modules/undici/lib/web/cache/cache.js"(exports, module) { + "use strict"; + var { kConstruct } = require_symbols4(); + var { urlEquals, getFieldValues } = require_util5(); + var { kEnumerableProperty, isDisturbed } = require_util(); + var { webidl } = require_webidl(); + var { Response: Response2, cloneResponse, fromInnerResponse } = require_response(); + var { Request: Request2, fromInnerRequest } = require_request2(); + var { kState } = require_symbols2(); + var { fetching } = require_fetch(); + var { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = require_util2(); + var assert = __require("node:assert"); + var Cache = class _Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList; + constructor() { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor(); + } + webidl.util.markAsUncloneable(this); + this.#relevantRequestResponseList = arguments[1]; + } + async match(request, options = {}) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.match"; + webidl.argumentLengthCheck(arguments, 1, prefix); + request = webidl.converters.RequestInfo(request, prefix, "request"); + options = webidl.converters.CacheQueryOptions(options, prefix, "options"); + const p = this.#internalMatchAll(request, options, 1); + if (p.length === 0) { + return; + } + return p[0]; + } + async matchAll(request = void 0, options = {}) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.matchAll"; + if (request !== void 0) request = webidl.converters.RequestInfo(request, prefix, "request"); + options = webidl.converters.CacheQueryOptions(options, prefix, "options"); + return this.#internalMatchAll(request, options); + } + async add(request) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.add"; + webidl.argumentLengthCheck(arguments, 1, prefix); + request = webidl.converters.RequestInfo(request, prefix, "request"); + const requests = [request]; + const responseArrayPromise = this.addAll(requests); + return await responseArrayPromise; + } + async addAll(requests) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.addAll"; + webidl.argumentLengthCheck(arguments, 1, prefix); + const responsePromises = []; + const requestList = []; + for (let request of requests) { + if (request === void 0) { + throw webidl.errors.conversionFailed({ + prefix, + argument: "Argument 1", + types: ["undefined is not allowed"] + }); + } + request = webidl.converters.RequestInfo(request); + if (typeof request === "string") { + continue; + } + const r = request[kState]; + if (!urlIsHttpHttpsScheme(r.url) || r.method !== "GET") { + throw webidl.errors.exception({ + header: prefix, + message: "Expected http/s scheme when method is not GET." + }); + } + } + const fetchControllers = []; + for (const request of requests) { + const r = new Request2(request)[kState]; + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: prefix, + message: "Expected http/s scheme." + }); + } + r.initiator = "fetch"; + r.destination = "subresource"; + requestList.push(r); + const responsePromise = createDeferredPromise(); + fetchControllers.push(fetching({ + request: r, + processResponse(response) { + if (response.type === "error" || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: "Cache.addAll", + message: "Received an invalid status code or the request failed." + })); + } else if (response.headersList.contains("vary")) { + const fieldValues = getFieldValues(response.headersList.get("vary")); + for (const fieldValue of fieldValues) { + if (fieldValue === "*") { + responsePromise.reject(webidl.errors.exception({ + header: "Cache.addAll", + message: "invalid vary field value" + })); + for (const controller of fetchControllers) { + controller.abort(); + } + return; + } + } + } + }, + processResponseEndOfBody(response) { + if (response.aborted) { + responsePromise.reject(new DOMException("aborted", "AbortError")); + return; + } + responsePromise.resolve(response); + } + })); + responsePromises.push(responsePromise.promise); + } + const p = Promise.all(responsePromises); + const responses = await p; + const operations = []; + let index = 0; + for (const response of responses) { + const operation = { + type: "put", + // 7.3.2 + request: requestList[index], + // 7.3.3 + response + // 7.3.4 + }; + operations.push(operation); + index++; + } + const cacheJobPromise = createDeferredPromise(); + let errorData = null; + try { + this.#batchCacheOperations(operations); + } catch (e) { + errorData = e; + } + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(void 0); + } else { + cacheJobPromise.reject(errorData); + } + }); + return cacheJobPromise.promise; + } + async put(request, response) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.put"; + webidl.argumentLengthCheck(arguments, 2, prefix); + request = webidl.converters.RequestInfo(request, prefix, "request"); + response = webidl.converters.Response(response, prefix, "response"); + let innerRequest = null; + if (request instanceof Request2) { + innerRequest = request[kState]; + } else { + innerRequest = new Request2(request)[kState]; + } + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== "GET") { + throw webidl.errors.exception({ + header: prefix, + message: "Expected an http/s scheme when method is not GET" + }); + } + const innerResponse = response[kState]; + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: prefix, + message: "Got 206 status" + }); + } + if (innerResponse.headersList.contains("vary")) { + const fieldValues = getFieldValues(innerResponse.headersList.get("vary")); + for (const fieldValue of fieldValues) { + if (fieldValue === "*") { + throw webidl.errors.exception({ + header: prefix, + message: "Got * vary field value" + }); + } + } + } + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: prefix, + message: "Response body is locked or disturbed" + }); + } + const clonedResponse = cloneResponse(innerResponse); + const bodyReadPromise = createDeferredPromise(); + if (innerResponse.body != null) { + const stream = innerResponse.body.stream; + const reader = stream.getReader(); + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject); + } else { + bodyReadPromise.resolve(void 0); + } + const operations = []; + const operation = { + type: "put", + // 14. + request: innerRequest, + // 15. + response: clonedResponse + // 16. + }; + operations.push(operation); + const bytes2 = await bodyReadPromise.promise; + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes2; + } + const cacheJobPromise = createDeferredPromise(); + let errorData = null; + try { + this.#batchCacheOperations(operations); + } catch (e) { + errorData = e; + } + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(); + } else { + cacheJobPromise.reject(errorData); + } + }); + return cacheJobPromise.promise; + } + async delete(request, options = {}) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.delete"; + webidl.argumentLengthCheck(arguments, 1, prefix); + request = webidl.converters.RequestInfo(request, prefix, "request"); + options = webidl.converters.CacheQueryOptions(options, prefix, "options"); + let r = null; + if (request instanceof Request2) { + r = request[kState]; + if (r.method !== "GET" && !options.ignoreMethod) { + return false; + } + } else { + assert(typeof request === "string"); + r = new Request2(request)[kState]; + } + const operations = []; + const operation = { + type: "delete", + request: r, + options + }; + operations.push(operation); + const cacheJobPromise = createDeferredPromise(); + let errorData = null; + let requestResponses; + try { + requestResponses = this.#batchCacheOperations(operations); + } catch (e) { + errorData = e; + } + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length); + } else { + cacheJobPromise.reject(errorData); + } + }); + return cacheJobPromise.promise; + } + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {Promise} + */ + async keys(request = void 0, options = {}) { + webidl.brandCheck(this, _Cache); + const prefix = "Cache.keys"; + if (request !== void 0) request = webidl.converters.RequestInfo(request, prefix, "request"); + options = webidl.converters.CacheQueryOptions(options, prefix, "options"); + let r = null; + if (request !== void 0) { + if (request instanceof Request2) { + r = request[kState]; + if (r.method !== "GET" && !options.ignoreMethod) { + return []; + } + } else if (typeof request === "string") { + r = new Request2(request)[kState]; + } + } + const promise = createDeferredPromise(); + const requests = []; + if (request === void 0) { + for (const requestResponse of this.#relevantRequestResponseList) { + requests.push(requestResponse[0]); + } + } else { + const requestResponses = this.#queryCache(r, options); + for (const requestResponse of requestResponses) { + requests.push(requestResponse[0]); + } + } + queueMicrotask(() => { + const requestList = []; + for (const request2 of requests) { + const requestObject = fromInnerRequest( + request2, + new AbortController().signal, + "immutable" + ); + requestList.push(requestObject); + } + promise.resolve(Object.freeze(requestList)); + }); + return promise.promise; + } + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations(operations) { + const cache = this.#relevantRequestResponseList; + const backupCache = [...cache]; + const addedItems = []; + const resultList = []; + try { + for (const operation of operations) { + if (operation.type !== "delete" && operation.type !== "put") { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: 'operation type does not match "delete" or "put"' + }); + } + if (operation.type === "delete" && operation.response != null) { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: "delete operation should not have an associated response" + }); + } + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException("???", "InvalidStateError"); + } + let requestResponses; + if (operation.type === "delete") { + requestResponses = this.#queryCache(operation.request, operation.options); + if (requestResponses.length === 0) { + return []; + } + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse); + assert(idx !== -1); + cache.splice(idx, 1); + } + } else if (operation.type === "put") { + if (operation.response == null) { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: "put operation should have an associated response" + }); + } + const r = operation.request; + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: "expected http or https scheme" + }); + } + if (r.method !== "GET") { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: "not get method" + }); + } + if (operation.options != null) { + throw webidl.errors.exception({ + header: "Cache.#batchCacheOperations", + message: "options must not be defined" + }); + } + requestResponses = this.#queryCache(operation.request); + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse); + assert(idx !== -1); + cache.splice(idx, 1); + } + cache.push([operation.request, operation.response]); + addedItems.push([operation.request, operation.response]); + } + resultList.push([operation.request, operation.response]); + } + return resultList; + } catch (e) { + this.#relevantRequestResponseList.length = 0; + this.#relevantRequestResponseList = backupCache; + throw e; + } + } + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache(requestQuery, options, targetStorage) { + const resultList = []; + const storage = targetStorage ?? this.#relevantRequestResponseList; + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse; + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse); + } + } + return resultList; + } + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem(requestQuery, request, response = null, options) { + const queryURL = new URL(requestQuery.url); + const cachedURL = new URL(request.url); + if (options?.ignoreSearch) { + cachedURL.search = ""; + queryURL.search = ""; + } + if (!urlEquals(queryURL, cachedURL, true)) { + return false; + } + if (response == null || options?.ignoreVary || !response.headersList.contains("vary")) { + return true; + } + const fieldValues = getFieldValues(response.headersList.get("vary")); + for (const fieldValue of fieldValues) { + if (fieldValue === "*") { + return false; + } + const requestValue = request.headersList.get(fieldValue); + const queryValue = requestQuery.headersList.get(fieldValue); + if (requestValue !== queryValue) { + return false; + } + } + return true; + } + #internalMatchAll(request, options, maxResponses = Infinity) { + let r = null; + if (request !== void 0) { + if (request instanceof Request2) { + r = request[kState]; + if (r.method !== "GET" && !options.ignoreMethod) { + return []; + } + } else if (typeof request === "string") { + r = new Request2(request)[kState]; + } + } + const responses = []; + if (request === void 0) { + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]); + } + } else { + const requestResponses = this.#queryCache(r, options); + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]); + } + } + const responseList = []; + for (const response of responses) { + const responseObject = fromInnerResponse(response, "immutable"); + responseList.push(responseObject.clone()); + if (responseList.length >= maxResponses) { + break; + } + } + return Object.freeze(responseList); + } + }; + Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: "Cache", + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty + }); + var cacheQueryOptionConverters = [ + { + key: "ignoreSearch", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "ignoreMethod", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "ignoreVary", + converter: webidl.converters.boolean, + defaultValue: () => false + } + ]; + webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters); + webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: "cacheName", + converter: webidl.converters.DOMString + } + ]); + webidl.converters.Response = webidl.interfaceConverter(Response2); + webidl.converters["sequence"] = webidl.sequenceConverter( + webidl.converters.RequestInfo + ); + module.exports = { + Cache + }; + } +}); + +// viewer/node_modules/undici/lib/web/cache/cachestorage.js +var require_cachestorage = __commonJS({ + "viewer/node_modules/undici/lib/web/cache/cachestorage.js"(exports, module) { + "use strict"; + var { kConstruct } = require_symbols4(); + var { Cache } = require_cache(); + var { webidl } = require_webidl(); + var { kEnumerableProperty } = require_util(); + var CacheStorage = class _CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has(cacheName) { + webidl.brandCheck(this, _CacheStorage); + const prefix = "CacheStorage.has"; + webidl.argumentLengthCheck(arguments, 1, prefix); + cacheName = webidl.converters.DOMString(cacheName, prefix, "cacheName"); + return this.#caches.has(cacheName); + } + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open(cacheName) { + webidl.brandCheck(this, _CacheStorage); + const prefix = "CacheStorage.open"; + webidl.argumentLengthCheck(arguments, 1, prefix); + cacheName = webidl.converters.DOMString(cacheName, prefix, "cacheName"); + if (this.#caches.has(cacheName)) { + const cache2 = this.#caches.get(cacheName); + return new Cache(kConstruct, cache2); + } + const cache = []; + this.#caches.set(cacheName, cache); + return new Cache(kConstruct, cache); + } + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete(cacheName) { + webidl.brandCheck(this, _CacheStorage); + const prefix = "CacheStorage.delete"; + webidl.argumentLengthCheck(arguments, 1, prefix); + cacheName = webidl.converters.DOMString(cacheName, prefix, "cacheName"); + return this.#caches.delete(cacheName); + } + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {Promise} + */ + async keys() { + webidl.brandCheck(this, _CacheStorage); + const keys = this.#caches.keys(); + return [...keys]; + } + }; + Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: "CacheStorage", + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty + }); + module.exports = { + CacheStorage + }; + } +}); + +// viewer/node_modules/undici/lib/web/cookies/constants.js +var require_constants4 = __commonJS({ + "viewer/node_modules/undici/lib/web/cookies/constants.js"(exports, module) { + "use strict"; + var maxAttributeValueSize = 1024; + var maxNameValuePairSize = 4096; + module.exports = { + maxAttributeValueSize, + maxNameValuePairSize + }; + } +}); + +// viewer/node_modules/undici/lib/web/cookies/util.js +var require_util6 = __commonJS({ + "viewer/node_modules/undici/lib/web/cookies/util.js"(exports, module) { + "use strict"; + function isCTLExcludingHtab(value) { + for (let i = 0; i < value.length; ++i) { + const code = value.charCodeAt(i); + if (code >= 0 && code <= 8 || code >= 10 && code <= 31 || code === 127) { + return true; + } + } + return false; + } + function validateCookieName(name) { + for (let i = 0; i < name.length; ++i) { + const code = name.charCodeAt(i); + if (code < 33 || // exclude CTLs (0-31), SP and HT + code > 126 || // exclude non-ascii and DEL + code === 34 || // " + code === 40 || // ( + code === 41 || // ) + code === 60 || // < + code === 62 || // > + code === 64 || // @ + code === 44 || // , + code === 59 || // ; + code === 58 || // : + code === 92 || // \ + code === 47 || // / + code === 91 || // [ + code === 93 || // ] + code === 63 || // ? + code === 61 || // = + code === 123 || // { + code === 125) { + throw new Error("Invalid cookie name"); + } + } + } + function validateCookieValue(value) { + let len = value.length; + let i = 0; + if (value[0] === '"') { + if (len === 1 || value[len - 1] !== '"') { + throw new Error("Invalid cookie value"); + } + --len; + ++i; + } + while (i < len) { + const code = value.charCodeAt(i++); + if (code < 33 || // exclude CTLs (0-31) + code > 126 || // non-ascii and DEL (127) + code === 34 || // " + code === 44 || // , + code === 59 || // ; + code === 92) { + throw new Error("Invalid cookie value"); + } + } + } + function validateCookiePath(path14) { + for (let i = 0; i < path14.length; ++i) { + const code = path14.charCodeAt(i); + if (code < 32 || // exclude CTLs (0-31) + code === 127 || // DEL + code === 59) { + throw new Error("Invalid cookie path"); + } + } + } + function validateCookieDomain(domain) { + if (domain.startsWith("-") || domain.endsWith(".") || domain.endsWith("-")) { + throw new Error("Invalid cookie domain"); + } + } + var IMFDays = [ + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat" + ]; + var IMFMonths = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ]; + var IMFPaddedNumbers = Array(61).fill(0).map((_, i) => i.toString().padStart(2, "0")); + function toIMFDate(date) { + if (typeof date === "number") { + date = new Date(date); + } + return `${IMFDays[date.getUTCDay()]}, ${IMFPaddedNumbers[date.getUTCDate()]} ${IMFMonths[date.getUTCMonth()]} ${date.getUTCFullYear()} ${IMFPaddedNumbers[date.getUTCHours()]}:${IMFPaddedNumbers[date.getUTCMinutes()]}:${IMFPaddedNumbers[date.getUTCSeconds()]} GMT`; + } + function validateCookieMaxAge(maxAge) { + if (maxAge < 0) { + throw new Error("Invalid cookie max-age"); + } + } + function stringify(cookie) { + if (cookie.name.length === 0) { + return null; + } + validateCookieName(cookie.name); + validateCookieValue(cookie.value); + const out = [`${cookie.name}=${cookie.value}`]; + if (cookie.name.startsWith("__Secure-")) { + cookie.secure = true; + } + if (cookie.name.startsWith("__Host-")) { + cookie.secure = true; + cookie.domain = null; + cookie.path = "/"; + } + if (cookie.secure) { + out.push("Secure"); + } + if (cookie.httpOnly) { + out.push("HttpOnly"); + } + if (typeof cookie.maxAge === "number") { + validateCookieMaxAge(cookie.maxAge); + out.push(`Max-Age=${cookie.maxAge}`); + } + if (cookie.domain) { + validateCookieDomain(cookie.domain); + out.push(`Domain=${cookie.domain}`); + } + if (cookie.path) { + validateCookiePath(cookie.path); + out.push(`Path=${cookie.path}`); + } + if (cookie.expires && cookie.expires.toString() !== "Invalid Date") { + out.push(`Expires=${toIMFDate(cookie.expires)}`); + } + if (cookie.sameSite) { + out.push(`SameSite=${cookie.sameSite}`); + } + for (const part of cookie.unparsed) { + if (!part.includes("=")) { + throw new Error("Invalid unparsed"); + } + const [key, ...value] = part.split("="); + out.push(`${key.trim()}=${value.join("=")}`); + } + return out.join("; "); + } + module.exports = { + isCTLExcludingHtab, + validateCookieName, + validateCookiePath, + validateCookieValue, + toIMFDate, + stringify + }; + } +}); + +// viewer/node_modules/undici/lib/web/cookies/parse.js +var require_parse = __commonJS({ + "viewer/node_modules/undici/lib/web/cookies/parse.js"(exports, module) { + "use strict"; + var { maxNameValuePairSize, maxAttributeValueSize } = require_constants4(); + var { isCTLExcludingHtab } = require_util6(); + var { collectASequenceOfCodePointsFast } = require_data_url(); + var assert = __require("node:assert"); + function parseSetCookie(header) { + if (isCTLExcludingHtab(header)) { + return null; + } + let nameValuePair = ""; + let unparsedAttributes = ""; + let name = ""; + let value = ""; + if (header.includes(";")) { + const position = { position: 0 }; + nameValuePair = collectASequenceOfCodePointsFast(";", header, position); + unparsedAttributes = header.slice(position.position); + } else { + nameValuePair = header; + } + if (!nameValuePair.includes("=")) { + value = nameValuePair; + } else { + const position = { position: 0 }; + name = collectASequenceOfCodePointsFast( + "=", + nameValuePair, + position + ); + value = nameValuePair.slice(position.position + 1); + } + name = name.trim(); + value = value.trim(); + if (name.length + value.length > maxNameValuePairSize) { + return null; + } + return { + name, + value, + ...parseUnparsedAttributes(unparsedAttributes) + }; + } + function parseUnparsedAttributes(unparsedAttributes, cookieAttributeList = {}) { + if (unparsedAttributes.length === 0) { + return cookieAttributeList; + } + assert(unparsedAttributes[0] === ";"); + unparsedAttributes = unparsedAttributes.slice(1); + let cookieAv = ""; + if (unparsedAttributes.includes(";")) { + cookieAv = collectASequenceOfCodePointsFast( + ";", + unparsedAttributes, + { position: 0 } + ); + unparsedAttributes = unparsedAttributes.slice(cookieAv.length); + } else { + cookieAv = unparsedAttributes; + unparsedAttributes = ""; + } + let attributeName = ""; + let attributeValue = ""; + if (cookieAv.includes("=")) { + const position = { position: 0 }; + attributeName = collectASequenceOfCodePointsFast( + "=", + cookieAv, + position + ); + attributeValue = cookieAv.slice(position.position + 1); + } else { + attributeName = cookieAv; + } + attributeName = attributeName.trim(); + attributeValue = attributeValue.trim(); + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList); + } + const attributeNameLowercase = attributeName.toLowerCase(); + if (attributeNameLowercase === "expires") { + const expiryTime = new Date(attributeValue); + cookieAttributeList.expires = expiryTime; + } else if (attributeNameLowercase === "max-age") { + const charCode = attributeValue.charCodeAt(0); + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== "-") { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList); + } + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList); + } + const deltaSeconds = Number(attributeValue); + cookieAttributeList.maxAge = deltaSeconds; + } else if (attributeNameLowercase === "domain") { + let cookieDomain = attributeValue; + if (cookieDomain[0] === ".") { + cookieDomain = cookieDomain.slice(1); + } + cookieDomain = cookieDomain.toLowerCase(); + cookieAttributeList.domain = cookieDomain; + } else if (attributeNameLowercase === "path") { + let cookiePath = ""; + if (attributeValue.length === 0 || attributeValue[0] !== "/") { + cookiePath = "/"; + } else { + cookiePath = attributeValue; + } + cookieAttributeList.path = cookiePath; + } else if (attributeNameLowercase === "secure") { + cookieAttributeList.secure = true; + } else if (attributeNameLowercase === "httponly") { + cookieAttributeList.httpOnly = true; + } else if (attributeNameLowercase === "samesite") { + let enforcement = "Default"; + const attributeValueLowercase = attributeValue.toLowerCase(); + if (attributeValueLowercase.includes("none")) { + enforcement = "None"; + } + if (attributeValueLowercase.includes("strict")) { + enforcement = "Strict"; + } + if (attributeValueLowercase.includes("lax")) { + enforcement = "Lax"; + } + cookieAttributeList.sameSite = enforcement; + } else { + cookieAttributeList.unparsed ??= []; + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`); + } + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList); + } + module.exports = { + parseSetCookie, + parseUnparsedAttributes + }; + } +}); + +// viewer/node_modules/undici/lib/web/cookies/index.js +var require_cookies = __commonJS({ + "viewer/node_modules/undici/lib/web/cookies/index.js"(exports, module) { + "use strict"; + var { parseSetCookie } = require_parse(); + var { stringify } = require_util6(); + var { webidl } = require_webidl(); + var { Headers: Headers2 } = require_headers(); + function getCookies(headers) { + webidl.argumentLengthCheck(arguments, 1, "getCookies"); + webidl.brandCheck(headers, Headers2, { strict: false }); + const cookie = headers.get("cookie"); + const out = {}; + if (!cookie) { + return out; + } + for (const piece of cookie.split(";")) { + const [name, ...value] = piece.split("="); + out[name.trim()] = value.join("="); + } + return out; + } + function deleteCookie(headers, name, attributes) { + webidl.brandCheck(headers, Headers2, { strict: false }); + const prefix = "deleteCookie"; + webidl.argumentLengthCheck(arguments, 2, prefix); + name = webidl.converters.DOMString(name, prefix, "name"); + attributes = webidl.converters.DeleteCookieAttributes(attributes); + setCookie(headers, { + name, + value: "", + expires: /* @__PURE__ */ new Date(0), + ...attributes + }); + } + function getSetCookies(headers) { + webidl.argumentLengthCheck(arguments, 1, "getSetCookies"); + webidl.brandCheck(headers, Headers2, { strict: false }); + const cookies = headers.getSetCookie(); + if (!cookies) { + return []; + } + return cookies.map((pair) => parseSetCookie(pair)); + } + function setCookie(headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, "setCookie"); + webidl.brandCheck(headers, Headers2, { strict: false }); + cookie = webidl.converters.Cookie(cookie); + const str = stringify(cookie); + if (str) { + headers.append("Set-Cookie", str); + } + } + webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: "path", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: "domain", + defaultValue: () => null + } + ]); + webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: "name" + }, + { + converter: webidl.converters.DOMString, + key: "value" + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === "number") { + return webidl.converters["unsigned long long"](value); + } + return new Date(value); + }), + key: "expires", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters["long long"]), + key: "maxAge", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: "domain", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: "path", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: "secure", + defaultValue: () => null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: "httpOnly", + defaultValue: () => null + }, + { + converter: webidl.converters.USVString, + key: "sameSite", + allowedValues: ["Strict", "Lax", "None"] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: "unparsed", + defaultValue: () => new Array(0) + } + ]); + module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/events.js +var require_events = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/events.js"(exports, module) { + "use strict"; + var { webidl } = require_webidl(); + var { kEnumerableProperty } = require_util(); + var { kConstruct } = require_symbols(); + var { MessagePort } = __require("node:worker_threads"); + var MessageEvent = class _MessageEvent extends Event { + #eventInit; + constructor(type, eventInitDict = {}) { + if (type === kConstruct) { + super(arguments[1], arguments[2]); + webidl.util.markAsUncloneable(this); + return; + } + const prefix = "MessageEvent constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + type = webidl.converters.DOMString(type, prefix, "type"); + eventInitDict = webidl.converters.MessageEventInit(eventInitDict, prefix, "eventInitDict"); + super(type, eventInitDict); + this.#eventInit = eventInitDict; + webidl.util.markAsUncloneable(this); + } + get data() { + webidl.brandCheck(this, _MessageEvent); + return this.#eventInit.data; + } + get origin() { + webidl.brandCheck(this, _MessageEvent); + return this.#eventInit.origin; + } + get lastEventId() { + webidl.brandCheck(this, _MessageEvent); + return this.#eventInit.lastEventId; + } + get source() { + webidl.brandCheck(this, _MessageEvent); + return this.#eventInit.source; + } + get ports() { + webidl.brandCheck(this, _MessageEvent); + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports); + } + return this.#eventInit.ports; + } + initMessageEvent(type, bubbles = false, cancelable = false, data = null, origin = "", lastEventId = "", source = null, ports = []) { + webidl.brandCheck(this, _MessageEvent); + webidl.argumentLengthCheck(arguments, 1, "MessageEvent.initMessageEvent"); + return new _MessageEvent(type, { + bubbles, + cancelable, + data, + origin, + lastEventId, + source, + ports + }); + } + static createFastMessageEvent(type, init) { + const messageEvent = new _MessageEvent(kConstruct, type, init); + messageEvent.#eventInit = init; + messageEvent.#eventInit.data ??= null; + messageEvent.#eventInit.origin ??= ""; + messageEvent.#eventInit.lastEventId ??= ""; + messageEvent.#eventInit.source ??= null; + messageEvent.#eventInit.ports ??= []; + return messageEvent; + } + }; + var { createFastMessageEvent } = MessageEvent; + delete MessageEvent.createFastMessageEvent; + var CloseEvent = class _CloseEvent extends Event { + #eventInit; + constructor(type, eventInitDict = {}) { + const prefix = "CloseEvent constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + type = webidl.converters.DOMString(type, prefix, "type"); + eventInitDict = webidl.converters.CloseEventInit(eventInitDict); + super(type, eventInitDict); + this.#eventInit = eventInitDict; + webidl.util.markAsUncloneable(this); + } + get wasClean() { + webidl.brandCheck(this, _CloseEvent); + return this.#eventInit.wasClean; + } + get code() { + webidl.brandCheck(this, _CloseEvent); + return this.#eventInit.code; + } + get reason() { + webidl.brandCheck(this, _CloseEvent); + return this.#eventInit.reason; + } + }; + var ErrorEvent = class _ErrorEvent extends Event { + #eventInit; + constructor(type, eventInitDict) { + const prefix = "ErrorEvent constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + super(type, eventInitDict); + webidl.util.markAsUncloneable(this); + type = webidl.converters.DOMString(type, prefix, "type"); + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}); + this.#eventInit = eventInitDict; + } + get message() { + webidl.brandCheck(this, _ErrorEvent); + return this.#eventInit.message; + } + get filename() { + webidl.brandCheck(this, _ErrorEvent); + return this.#eventInit.filename; + } + get lineno() { + webidl.brandCheck(this, _ErrorEvent); + return this.#eventInit.lineno; + } + get colno() { + webidl.brandCheck(this, _ErrorEvent); + return this.#eventInit.colno; + } + get error() { + webidl.brandCheck(this, _ErrorEvent); + return this.#eventInit.error; + } + }; + Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: "MessageEvent", + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty + }); + Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: "CloseEvent", + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty + }); + Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: "ErrorEvent", + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty + }); + webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort); + webidl.converters["sequence"] = webidl.sequenceConverter( + webidl.converters.MessagePort + ); + var eventInit = [ + { + key: "bubbles", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "cancelable", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "composed", + converter: webidl.converters.boolean, + defaultValue: () => false + } + ]; + webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: "data", + converter: webidl.converters.any, + defaultValue: () => null + }, + { + key: "origin", + converter: webidl.converters.USVString, + defaultValue: () => "" + }, + { + key: "lastEventId", + converter: webidl.converters.DOMString, + defaultValue: () => "" + }, + { + key: "source", + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: () => null + }, + { + key: "ports", + converter: webidl.converters["sequence"], + defaultValue: () => new Array(0) + } + ]); + webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: "wasClean", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "code", + converter: webidl.converters["unsigned short"], + defaultValue: () => 0 + }, + { + key: "reason", + converter: webidl.converters.USVString, + defaultValue: () => "" + } + ]); + webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: "message", + converter: webidl.converters.DOMString, + defaultValue: () => "" + }, + { + key: "filename", + converter: webidl.converters.USVString, + defaultValue: () => "" + }, + { + key: "lineno", + converter: webidl.converters["unsigned long"], + defaultValue: () => 0 + }, + { + key: "colno", + converter: webidl.converters["unsigned long"], + defaultValue: () => 0 + }, + { + key: "error", + converter: webidl.converters.any + } + ]); + module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent, + createFastMessageEvent + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/constants.js +var require_constants5 = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/constants.js"(exports, module) { + "use strict"; + var uid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + var staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false + }; + var states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 + }; + var sentCloseFrameState = { + NOT_SENT: 0, + PROCESSING: 1, + SENT: 2 + }; + var opcodes = { + CONTINUATION: 0, + TEXT: 1, + BINARY: 2, + CLOSE: 8, + PING: 9, + PONG: 10 + }; + var maxUnsigned16Bit = 2 ** 16 - 1; + var parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 + }; + var emptyBuffer = Buffer.allocUnsafe(0); + var sendHints = { + string: 1, + typedArray: 2, + arrayBuffer: 3, + blob: 4 + }; + module.exports = { + uid, + sentCloseFrameState, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates, + emptyBuffer, + sendHints + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/symbols.js +var require_symbols5 = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/symbols.js"(exports, module) { + "use strict"; + module.exports = { + kWebSocketURL: /* @__PURE__ */ Symbol("url"), + kReadyState: /* @__PURE__ */ Symbol("ready state"), + kController: /* @__PURE__ */ Symbol("controller"), + kResponse: /* @__PURE__ */ Symbol("response"), + kBinaryType: /* @__PURE__ */ Symbol("binary type"), + kSentClose: /* @__PURE__ */ Symbol("sent close"), + kReceivedClose: /* @__PURE__ */ Symbol("received close"), + kByteParser: /* @__PURE__ */ Symbol("byte parser") + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/util.js +var require_util7 = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/util.js"(exports, module) { + "use strict"; + var { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require_symbols5(); + var { states, opcodes } = require_constants5(); + var { ErrorEvent, createFastMessageEvent } = require_events(); + var { isUtf8 } = __require("node:buffer"); + var { collectASequenceOfCodePointsFast, removeHTTPWhitespace } = require_data_url(); + function isConnecting(ws) { + return ws[kReadyState] === states.CONNECTING; + } + function isEstablished(ws) { + return ws[kReadyState] === states.OPEN; + } + function isClosing(ws) { + return ws[kReadyState] === states.CLOSING; + } + function isClosed(ws) { + return ws[kReadyState] === states.CLOSED; + } + function fireEvent(e, target, eventFactory = (type, init) => new Event(type, init), eventInitDict = {}) { + const event = eventFactory(e, eventInitDict); + target.dispatchEvent(event); + } + function websocketMessageReceived(ws, type, data) { + if (ws[kReadyState] !== states.OPEN) { + return; + } + let dataForEvent; + if (type === opcodes.TEXT) { + try { + dataForEvent = utf8Decode(data); + } catch { + failWebsocketConnection(ws, "Received invalid UTF-8 in text frame."); + return; + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === "blob") { + dataForEvent = new Blob([data]); + } else { + dataForEvent = toArrayBuffer(data); + } + } + fireEvent("message", ws, createFastMessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }); + } + function toArrayBuffer(buffer) { + if (buffer.byteLength === buffer.buffer.byteLength) { + return buffer.buffer; + } + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); + } + function isValidSubprotocol(protocol) { + if (protocol.length === 0) { + return false; + } + for (let i = 0; i < protocol.length; ++i) { + const code = protocol.charCodeAt(i); + if (code < 33 || // CTL, contains SP (0x20) and HT (0x09) + code > 126 || code === 34 || // " + code === 40 || // ( + code === 41 || // ) + code === 44 || // , + code === 47 || // / + code === 58 || // : + code === 59 || // ; + code === 60 || // < + code === 61 || // = + code === 62 || // > + code === 63 || // ? + code === 64 || // @ + code === 91 || // [ + code === 92 || // \ + code === 93 || // ] + code === 123 || // { + code === 125) { + return false; + } + } + return true; + } + function isValidStatusCode(code) { + if (code >= 1e3 && code < 1015) { + return code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006; + } + return code >= 3e3 && code <= 4999; + } + function failWebsocketConnection(ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws; + controller.abort(); + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy(); + } + if (reason) { + fireEvent("error", ws, (type, init) => new ErrorEvent(type, init), { + error: new Error(reason), + message: reason + }); + } + } + function isControlFrame(opcode) { + return opcode === opcodes.CLOSE || opcode === opcodes.PING || opcode === opcodes.PONG; + } + function isContinuationFrame(opcode) { + return opcode === opcodes.CONTINUATION; + } + function isTextBinaryFrame(opcode) { + return opcode === opcodes.TEXT || opcode === opcodes.BINARY; + } + function isValidOpcode(opcode) { + return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode); + } + function parseExtensions(extensions) { + const position = { position: 0 }; + const extensionList = /* @__PURE__ */ new Map(); + while (position.position < extensions.length) { + const pair = collectASequenceOfCodePointsFast(";", extensions, position); + const [name, value = ""] = pair.split("="); + extensionList.set( + removeHTTPWhitespace(name, true, false), + removeHTTPWhitespace(value, false, true) + ); + position.position++; + } + return extensionList; + } + function isValidClientWindowBits(value) { + if (value.length === 0) { + return false; + } + for (let i = 0; i < value.length; i++) { + const byte = value.charCodeAt(i); + if (byte < 48 || byte > 57) { + return false; + } + } + const num = Number.parseInt(value, 10); + return num >= 8 && num <= 15; + } + var hasIntl = typeof process.versions.icu === "string"; + var fatalDecoder = hasIntl ? new TextDecoder("utf-8", { fatal: true }) : void 0; + var utf8Decode = hasIntl ? fatalDecoder.decode.bind(fatalDecoder) : function(buffer) { + if (isUtf8(buffer)) { + return buffer.toString("utf-8"); + } + throw new TypeError("Invalid utf-8 received."); + }; + module.exports = { + isConnecting, + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived, + utf8Decode, + isControlFrame, + isContinuationFrame, + isTextBinaryFrame, + isValidOpcode, + parseExtensions, + isValidClientWindowBits + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/frame.js +var require_frame = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/frame.js"(exports, module) { + "use strict"; + var { maxUnsigned16Bit } = require_constants5(); + var BUFFER_SIZE = 16386; + var crypto2; + var buffer = null; + var bufIdx = BUFFER_SIZE; + try { + crypto2 = __require("node:crypto"); + } catch { + crypto2 = { + // not full compatibility, but minimum. + randomFillSync: function randomFillSync(buffer2, _offset, _size) { + for (let i = 0; i < buffer2.length; ++i) { + buffer2[i] = Math.random() * 255 | 0; + } + return buffer2; + } + }; + } + function generateMask() { + if (bufIdx === BUFFER_SIZE) { + bufIdx = 0; + crypto2.randomFillSync(buffer ??= Buffer.allocUnsafe(BUFFER_SIZE), 0, BUFFER_SIZE); + } + return [buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++], buffer[bufIdx++]]; + } + var WebsocketFrameSend = class { + /** + * @param {Buffer|undefined} data + */ + constructor(data) { + this.frameData = data; + } + createFrame(opcode) { + const frameData = this.frameData; + const maskKey = generateMask(); + const bodyLength = frameData?.byteLength ?? 0; + let payloadLength = bodyLength; + let offset = 6; + if (bodyLength > maxUnsigned16Bit) { + offset += 8; + payloadLength = 127; + } else if (bodyLength > 125) { + offset += 2; + payloadLength = 126; + } + const buffer2 = Buffer.allocUnsafe(bodyLength + offset); + buffer2[0] = buffer2[1] = 0; + buffer2[0] |= 128; + buffer2[0] = (buffer2[0] & 240) + opcode; + buffer2[offset - 4] = maskKey[0]; + buffer2[offset - 3] = maskKey[1]; + buffer2[offset - 2] = maskKey[2]; + buffer2[offset - 1] = maskKey[3]; + buffer2[1] = payloadLength; + if (payloadLength === 126) { + buffer2.writeUInt16BE(bodyLength, 2); + } else if (payloadLength === 127) { + buffer2[2] = buffer2[3] = 0; + buffer2.writeUIntBE(bodyLength, 4, 6); + } + buffer2[1] |= 128; + for (let i = 0; i < bodyLength; ++i) { + buffer2[offset + i] = frameData[i] ^ maskKey[i & 3]; + } + return buffer2; + } + }; + module.exports = { + WebsocketFrameSend + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/connection.js +var require_connection = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/connection.js"(exports, module) { + "use strict"; + var { uid, states, sentCloseFrameState, emptyBuffer, opcodes } = require_constants5(); + var { + kReadyState, + kSentClose, + kByteParser, + kReceivedClose, + kResponse + } = require_symbols5(); + var { fireEvent, failWebsocketConnection, isClosing, isClosed, isEstablished, parseExtensions } = require_util7(); + var { channels } = require_diagnostics(); + var { CloseEvent } = require_events(); + var { makeRequest } = require_request2(); + var { fetching } = require_fetch(); + var { Headers: Headers2, getHeadersList } = require_headers(); + var { getDecodeSplit } = require_util2(); + var { WebsocketFrameSend } = require_frame(); + var crypto2; + try { + crypto2 = __require("node:crypto"); + } catch { + } + function establishWebSocketConnection(url, protocols, client, ws, onEstablish, options) { + const requestURL = url; + requestURL.protocol = url.protocol === "ws:" ? "http:" : "https:"; + const request = makeRequest({ + urlList: [requestURL], + client, + serviceWorkers: "none", + referrer: "no-referrer", + mode: "websocket", + credentials: "include", + cache: "no-store", + redirect: "error" + }); + if (options.headers) { + const headersList = getHeadersList(new Headers2(options.headers)); + request.headersList = headersList; + } + const keyValue = crypto2.randomBytes(16).toString("base64"); + request.headersList.append("sec-websocket-key", keyValue); + request.headersList.append("sec-websocket-version", "13"); + for (const protocol of protocols) { + request.headersList.append("sec-websocket-protocol", protocol); + } + const permessageDeflate = "permessage-deflate; client_max_window_bits"; + request.headersList.append("sec-websocket-extensions", permessageDeflate); + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: options.dispatcher, + processResponse(response) { + if (response.type === "error" || response.status !== 101) { + failWebsocketConnection(ws, "Received network error or non-101 status code."); + return; + } + if (protocols.length !== 0 && !response.headersList.get("Sec-WebSocket-Protocol")) { + failWebsocketConnection(ws, "Server did not respond with sent protocols."); + return; + } + if (response.headersList.get("Upgrade")?.toLowerCase() !== "websocket") { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".'); + return; + } + if (response.headersList.get("Connection")?.toLowerCase() !== "upgrade") { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".'); + return; + } + const secWSAccept = response.headersList.get("Sec-WebSocket-Accept"); + const digest = crypto2.createHash("sha1").update(keyValue + uid).digest("base64"); + if (secWSAccept !== digest) { + failWebsocketConnection(ws, "Incorrect hash received in Sec-WebSocket-Accept header."); + return; + } + const secExtension = response.headersList.get("Sec-WebSocket-Extensions"); + let extensions; + if (secExtension !== null) { + extensions = parseExtensions(secExtension); + if (!extensions.has("permessage-deflate")) { + failWebsocketConnection(ws, "Sec-WebSocket-Extensions header does not match."); + return; + } + } + const secProtocol = response.headersList.get("Sec-WebSocket-Protocol"); + if (secProtocol !== null) { + const requestProtocols = getDecodeSplit("sec-websocket-protocol", request.headersList); + if (!requestProtocols.includes(secProtocol)) { + failWebsocketConnection(ws, "Protocol was not set in the opening handshake."); + return; + } + } + response.socket.on("data", onSocketData); + response.socket.on("close", onSocketClose); + response.socket.on("error", onSocketError); + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }); + } + onEstablish(response, extensions); + } + }); + return controller; + } + function closeWebSocketConnection(ws, code, reason, reasonByteLength) { + if (isClosing(ws) || isClosed(ws)) { + } else if (!isEstablished(ws)) { + failWebsocketConnection(ws, "Connection was closed before it was established."); + ws[kReadyState] = states.CLOSING; + } else if (ws[kSentClose] === sentCloseFrameState.NOT_SENT) { + ws[kSentClose] = sentCloseFrameState.PROCESSING; + const frame = new WebsocketFrameSend(); + if (code !== void 0 && reason === void 0) { + frame.frameData = Buffer.allocUnsafe(2); + frame.frameData.writeUInt16BE(code, 0); + } else if (code !== void 0 && reason !== void 0) { + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength); + frame.frameData.writeUInt16BE(code, 0); + frame.frameData.write(reason, 2, "utf-8"); + } else { + frame.frameData = emptyBuffer; + } + const socket = ws[kResponse].socket; + socket.write(frame.createFrame(opcodes.CLOSE)); + ws[kSentClose] = sentCloseFrameState.SENT; + ws[kReadyState] = states.CLOSING; + } else { + ws[kReadyState] = states.CLOSING; + } + } + function onSocketData(chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause(); + } + } + function onSocketClose() { + const { ws } = this; + const { [kResponse]: response } = ws; + response.socket.off("data", onSocketData); + response.socket.off("close", onSocketClose); + response.socket.off("error", onSocketError); + const wasClean = ws[kSentClose] === sentCloseFrameState.SENT && ws[kReceivedClose]; + let code = 1005; + let reason = ""; + const result = ws[kByteParser].closingInfo; + if (result && !result.error) { + code = result.code ?? 1005; + reason = result.reason; + } else if (!ws[kReceivedClose]) { + code = 1006; + } + ws[kReadyState] = states.CLOSED; + fireEvent("close", ws, (type, init) => new CloseEvent(type, init), { + wasClean, + code, + reason + }); + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }); + } + } + function onSocketError(error) { + const { ws } = this; + ws[kReadyState] = states.CLOSING; + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error); + } + this.destroy(); + } + module.exports = { + establishWebSocketConnection, + closeWebSocketConnection + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/permessage-deflate.js +var require_permessage_deflate = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/permessage-deflate.js"(exports, module) { + "use strict"; + var { createInflateRaw, Z_DEFAULT_WINDOWBITS } = __require("node:zlib"); + var { isValidClientWindowBits } = require_util7(); + var { MessageSizeExceededError } = require_errors(); + var tail = Buffer.from([0, 0, 255, 255]); + var kBuffer = /* @__PURE__ */ Symbol("kBuffer"); + var kLength = /* @__PURE__ */ Symbol("kLength"); + var PerMessageDeflate = class { + /** @type {import('node:zlib').InflateRaw} */ + #inflate; + #options = {}; + #maxPayloadSize = 0; + /** + * @param {Map} extensions + */ + constructor(extensions, options) { + this.#options.serverNoContextTakeover = extensions.has("server_no_context_takeover"); + this.#options.serverMaxWindowBits = extensions.get("server_max_window_bits"); + this.#maxPayloadSize = options.maxPayloadSize; + } + /** + * Decompress a compressed payload. + * @param {Buffer} chunk Compressed data + * @param {boolean} fin Final fragment flag + * @param {Function} callback Callback function + */ + decompress(chunk, fin, callback) { + if (!this.#inflate) { + let windowBits = Z_DEFAULT_WINDOWBITS; + if (this.#options.serverMaxWindowBits) { + if (!isValidClientWindowBits(this.#options.serverMaxWindowBits)) { + callback(new Error("Invalid server_max_window_bits")); + return; + } + windowBits = Number.parseInt(this.#options.serverMaxWindowBits); + } + try { + this.#inflate = createInflateRaw({ windowBits }); + } catch (err) { + callback(err); + return; + } + this.#inflate[kBuffer] = []; + this.#inflate[kLength] = 0; + this.#inflate.on("data", (data) => { + this.#inflate[kLength] += data.length; + if (this.#maxPayloadSize > 0 && this.#inflate[kLength] > this.#maxPayloadSize) { + callback(new MessageSizeExceededError()); + this.#inflate.removeAllListeners(); + this.#inflate = null; + return; + } + this.#inflate[kBuffer].push(data); + }); + this.#inflate.on("error", (err) => { + this.#inflate = null; + callback(err); + }); + } + this.#inflate.write(chunk); + if (fin) { + this.#inflate.write(tail); + } + this.#inflate.flush(() => { + if (!this.#inflate) { + return; + } + const full = Buffer.concat(this.#inflate[kBuffer], this.#inflate[kLength]); + this.#inflate[kBuffer].length = 0; + this.#inflate[kLength] = 0; + callback(null, full); + }); + } + }; + module.exports = { PerMessageDeflate }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/receiver.js +var require_receiver = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/receiver.js"(exports, module) { + "use strict"; + var { Writable } = __require("node:stream"); + var assert = __require("node:assert"); + var { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require_constants5(); + var { kReadyState, kSentClose, kResponse, kReceivedClose } = require_symbols5(); + var { channels } = require_diagnostics(); + var { + isValidStatusCode, + isValidOpcode, + failWebsocketConnection, + websocketMessageReceived, + utf8Decode, + isControlFrame, + isTextBinaryFrame, + isContinuationFrame + } = require_util7(); + var { WebsocketFrameSend } = require_frame(); + var { closeWebSocketConnection } = require_connection(); + var { PerMessageDeflate } = require_permessage_deflate(); + var { MessageSizeExceededError } = require_errors(); + var ByteParser = class extends Writable { + #buffers = []; + #fragmentsBytes = 0; + #byteOffset = 0; + #loop = false; + #state = parserStates.INFO; + #info = {}; + #fragments = []; + /** @type {Map} */ + #extensions; + /** @type {number} */ + #maxPayloadSize; + /** + * @param {import('./websocket').WebSocket} ws + * @param {Map|null} extensions + * @param {{ maxPayloadSize?: number }} [options] + */ + constructor(ws, extensions, options = {}) { + super(); + this.ws = ws; + this.#extensions = extensions == null ? /* @__PURE__ */ new Map() : extensions; + this.#maxPayloadSize = options.maxPayloadSize ?? 0; + if (this.#extensions.has("permessage-deflate")) { + this.#extensions.set("permessage-deflate", new PerMessageDeflate(extensions, options)); + } + } + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write(chunk, _, callback) { + this.#buffers.push(chunk); + this.#byteOffset += chunk.length; + this.#loop = true; + this.run(callback); + } + #validatePayloadLength() { + if (this.#maxPayloadSize > 0 && !isControlFrame(this.#info.opcode) && this.#info.payloadLength > this.#maxPayloadSize) { + failWebsocketConnection(this.ws, "Payload size exceeds maximum allowed size"); + return false; + } + return true; + } + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run(callback) { + while (this.#loop) { + if (this.#state === parserStates.INFO) { + if (this.#byteOffset < 2) { + return callback(); + } + const buffer = this.consume(2); + const fin = (buffer[0] & 128) !== 0; + const opcode = buffer[0] & 15; + const masked = (buffer[1] & 128) === 128; + const fragmented = !fin && opcode !== opcodes.CONTINUATION; + const payloadLength = buffer[1] & 127; + const rsv1 = buffer[0] & 64; + const rsv2 = buffer[0] & 32; + const rsv3 = buffer[0] & 16; + if (!isValidOpcode(opcode)) { + failWebsocketConnection(this.ws, "Invalid opcode received"); + return callback(); + } + if (masked) { + failWebsocketConnection(this.ws, "Frame cannot be masked"); + return callback(); + } + if (rsv1 !== 0 && !this.#extensions.has("permessage-deflate")) { + failWebsocketConnection(this.ws, "Expected RSV1 to be clear."); + return; + } + if (rsv2 !== 0 || rsv3 !== 0) { + failWebsocketConnection(this.ws, "RSV1, RSV2, RSV3 must be clear"); + return; + } + if (fragmented && !isTextBinaryFrame(opcode)) { + failWebsocketConnection(this.ws, "Invalid frame type was fragmented."); + return; + } + if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) { + failWebsocketConnection(this.ws, "Expected continuation frame"); + return; + } + if (this.#info.fragmented && fragmented) { + failWebsocketConnection(this.ws, "Fragmented frame exceeded 125 bytes."); + return; + } + if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) { + failWebsocketConnection(this.ws, "Control frame either too large or fragmented"); + return; + } + if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) { + failWebsocketConnection(this.ws, "Unexpected continuation frame"); + return; + } + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength; + this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16; + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64; + } + if (isTextBinaryFrame(opcode)) { + this.#info.binaryType = opcode; + this.#info.compressed = rsv1 !== 0; + } + this.#info.opcode = opcode; + this.#info.masked = masked; + this.#info.fin = fin; + this.#info.fragmented = fragmented; + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback(); + } + const buffer = this.consume(2); + this.#info.payloadLength = buffer.readUInt16BE(0); + this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback(); + } + const buffer = this.consume(8); + const upper = buffer.readUInt32BE(0); + const lower = buffer.readUInt32BE(4); + if (upper !== 0 || lower > 2 ** 31 - 1) { + failWebsocketConnection(this.ws, "Received payload length > 2^31 bytes."); + return; + } + this.#info.payloadLength = lower; + this.#state = parserStates.READ_DATA; + if (!this.#validatePayloadLength()) { + return; + } + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + return callback(); + } + const body = this.consume(this.#info.payloadLength); + if (isControlFrame(this.#info.opcode)) { + this.#loop = this.parseControlFrame(body); + this.#state = parserStates.INFO; + } else { + if (!this.#info.compressed) { + this.writeFragments(body); + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnection(this.ws, new MessageSizeExceededError().message); + return; + } + if (!this.#info.fragmented && this.#info.fin) { + websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments()); + } + this.#state = parserStates.INFO; + } else { + this.#extensions.get("permessage-deflate").decompress( + body, + this.#info.fin, + (error, data) => { + if (error) { + failWebsocketConnection(this.ws, error.message); + return; + } + this.writeFragments(data); + if (this.#maxPayloadSize > 0 && this.#fragmentsBytes > this.#maxPayloadSize) { + failWebsocketConnection(this.ws, new MessageSizeExceededError().message); + return; + } + if (!this.#info.fin) { + this.#state = parserStates.INFO; + this.#loop = true; + this.run(callback); + return; + } + websocketMessageReceived(this.ws, this.#info.binaryType, this.consumeFragments()); + this.#loop = true; + this.#state = parserStates.INFO; + this.run(callback); + } + ); + this.#loop = false; + break; + } + } + } + } + } + /** + * Take n bytes from the buffered Buffers + * @param {number} n + * @returns {Buffer} + */ + consume(n) { + if (n > this.#byteOffset) { + throw new Error("Called consume() before buffers satiated."); + } else if (n === 0) { + return emptyBuffer; + } + if (this.#buffers[0].length === n) { + this.#byteOffset -= this.#buffers[0].length; + return this.#buffers.shift(); + } + const buffer = Buffer.allocUnsafe(n); + let offset = 0; + while (offset !== n) { + const next = this.#buffers[0]; + const { length: length2 } = next; + if (length2 + offset === n) { + buffer.set(this.#buffers.shift(), offset); + break; + } else if (length2 + offset > n) { + buffer.set(next.subarray(0, n - offset), offset); + this.#buffers[0] = next.subarray(n - offset); + break; + } else { + buffer.set(this.#buffers.shift(), offset); + offset += next.length; + } + } + this.#byteOffset -= n; + return buffer; + } + writeFragments(fragment) { + this.#fragmentsBytes += fragment.length; + this.#fragments.push(fragment); + } + consumeFragments() { + const fragments = this.#fragments; + if (fragments.length === 1) { + this.#fragmentsBytes = 0; + return fragments.shift(); + } + const output = Buffer.concat(fragments, this.#fragmentsBytes); + this.#fragments = []; + this.#fragmentsBytes = 0; + return output; + } + parseCloseBody(data) { + assert(data.length !== 1); + let code; + if (data.length >= 2) { + code = data.readUInt16BE(0); + } + if (code !== void 0 && !isValidStatusCode(code)) { + return { code: 1002, reason: "Invalid status code", error: true }; + } + let reason = data.subarray(2); + if (reason[0] === 239 && reason[1] === 187 && reason[2] === 191) { + reason = reason.subarray(3); + } + try { + reason = utf8Decode(reason); + } catch { + return { code: 1007, reason: "Invalid UTF-8", error: true }; + } + return { code, reason, error: false }; + } + /** + * Parses control frames. + * @param {Buffer} body + */ + parseControlFrame(body) { + const { opcode, payloadLength } = this.#info; + if (opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, "Received close frame with a 1-byte body."); + return false; + } + this.#info.closeInfo = this.parseCloseBody(body); + if (this.#info.closeInfo.error) { + const { code, reason } = this.#info.closeInfo; + closeWebSocketConnection(this.ws, code, reason, reason.length); + failWebsocketConnection(this.ws, reason); + return false; + } + if (this.ws[kSentClose] !== sentCloseFrameState.SENT) { + let body2 = emptyBuffer; + if (this.#info.closeInfo.code) { + body2 = Buffer.allocUnsafe(2); + body2.writeUInt16BE(this.#info.closeInfo.code, 0); + } + const closeFrame = new WebsocketFrameSend(body2); + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = sentCloseFrameState.SENT; + } + } + ); + } + this.ws[kReadyState] = states.CLOSING; + this.ws[kReceivedClose] = true; + return false; + } else if (opcode === opcodes.PING) { + if (!this.ws[kReceivedClose]) { + const frame = new WebsocketFrameSend(body); + this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)); + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body + }); + } + } + } else if (opcode === opcodes.PONG) { + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body + }); + } + } + return true; + } + get closingInfo() { + return this.#info.closeInfo; + } + }; + module.exports = { + ByteParser + }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/sender.js +var require_sender = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/sender.js"(exports, module) { + "use strict"; + var { WebsocketFrameSend } = require_frame(); + var { opcodes, sendHints } = require_constants5(); + var FixedQueue = require_fixed_queue(); + var FastBuffer = Buffer[Symbol.species]; + var SendQueue = class { + /** + * @type {FixedQueue} + */ + #queue = new FixedQueue(); + /** + * @type {boolean} + */ + #running = false; + /** @type {import('node:net').Socket} */ + #socket; + constructor(socket) { + this.#socket = socket; + } + add(item, cb, hint) { + if (hint !== sendHints.blob) { + const frame = createFrame(item, hint); + if (!this.#running) { + this.#socket.write(frame, cb); + } else { + const node2 = { + promise: null, + callback: cb, + frame + }; + this.#queue.push(node2); + } + return; + } + const node = { + promise: item.arrayBuffer().then((ab) => { + node.promise = null; + node.frame = createFrame(ab, hint); + }), + callback: cb, + frame: null + }; + this.#queue.push(node); + if (!this.#running) { + this.#run(); + } + } + async #run() { + this.#running = true; + const queue = this.#queue; + while (!queue.isEmpty()) { + const node = queue.shift(); + if (node.promise !== null) { + await node.promise; + } + this.#socket.write(node.frame, node.callback); + node.callback = node.frame = null; + } + this.#running = false; + } + }; + function createFrame(data, hint) { + return new WebsocketFrameSend(toBuffer(data, hint)).createFrame(hint === sendHints.string ? opcodes.TEXT : opcodes.BINARY); + } + function toBuffer(data, hint) { + switch (hint) { + case sendHints.string: + return Buffer.from(data); + case sendHints.arrayBuffer: + case sendHints.blob: + return new FastBuffer(data); + case sendHints.typedArray: + return new FastBuffer(data.buffer, data.byteOffset, data.byteLength); + } + } + module.exports = { SendQueue }; + } +}); + +// viewer/node_modules/undici/lib/web/websocket/websocket.js +var require_websocket = __commonJS({ + "viewer/node_modules/undici/lib/web/websocket/websocket.js"(exports, module) { + "use strict"; + var { webidl } = require_webidl(); + var { URLSerializer } = require_data_url(); + var { environmentSettingsObject } = require_util2(); + var { staticPropertyDescriptors, states, sentCloseFrameState, sendHints } = require_constants5(); + var { + kWebSocketURL, + kReadyState, + kController, + kBinaryType, + kResponse, + kSentClose, + kByteParser + } = require_symbols5(); + var { + isConnecting, + isEstablished, + isClosing, + isValidSubprotocol, + fireEvent + } = require_util7(); + var { establishWebSocketConnection, closeWebSocketConnection } = require_connection(); + var { ByteParser } = require_receiver(); + var { kEnumerableProperty, isBlobLike } = require_util(); + var { getGlobalDispatcher } = require_global2(); + var { types } = __require("node:util"); + var { ErrorEvent, CloseEvent } = require_events(); + var { SendQueue } = require_sender(); + var WebSocket = class _WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + }; + #bufferedAmount = 0; + #protocol = ""; + #extensions = ""; + /** @type {SendQueue} */ + #sendQueue; + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor(url, protocols = []) { + super(); + webidl.util.markAsUncloneable(this); + const prefix = "WebSocket constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + const options = webidl.converters["DOMString or sequence or WebSocketInit"](protocols, prefix, "options"); + url = webidl.converters.USVString(url, prefix, "url"); + protocols = options.protocols; + const baseURL = environmentSettingsObject.settingsObject.baseUrl; + let urlRecord; + try { + urlRecord = new URL(url, baseURL); + } catch (e) { + throw new DOMException(e, "SyntaxError"); + } + if (urlRecord.protocol === "http:") { + urlRecord.protocol = "ws:"; + } else if (urlRecord.protocol === "https:") { + urlRecord.protocol = "wss:"; + } + if (urlRecord.protocol !== "ws:" && urlRecord.protocol !== "wss:") { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + "SyntaxError" + ); + } + if (urlRecord.hash || urlRecord.href.endsWith("#")) { + throw new DOMException("Got fragment", "SyntaxError"); + } + if (typeof protocols === "string") { + protocols = [protocols]; + } + if (protocols.length !== new Set(protocols.map((p) => p.toLowerCase())).size) { + throw new DOMException("Invalid Sec-WebSocket-Protocol value", "SyntaxError"); + } + if (protocols.length > 0 && !protocols.every((p) => isValidSubprotocol(p))) { + throw new DOMException("Invalid Sec-WebSocket-Protocol value", "SyntaxError"); + } + this[kWebSocketURL] = new URL(urlRecord.href); + const client = environmentSettingsObject.settingsObject; + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + client, + this, + (response, extensions) => this.#onConnectionEstablished(response, extensions), + options + ); + this[kReadyState] = _WebSocket.CONNECTING; + this[kSentClose] = sentCloseFrameState.NOT_SENT; + this[kBinaryType] = "blob"; + } + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close(code = void 0, reason = void 0) { + webidl.brandCheck(this, _WebSocket); + const prefix = "WebSocket.close"; + if (code !== void 0) { + code = webidl.converters["unsigned short"](code, prefix, "code", { clamp: true }); + } + if (reason !== void 0) { + reason = webidl.converters.USVString(reason, prefix, "reason"); + } + if (code !== void 0) { + if (code !== 1e3 && (code < 3e3 || code > 4999)) { + throw new DOMException("invalid code", "InvalidAccessError"); + } + } + let reasonByteLength = 0; + if (reason !== void 0) { + reasonByteLength = Buffer.byteLength(reason); + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + "SyntaxError" + ); + } + } + closeWebSocketConnection(this, code, reason, reasonByteLength); + } + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send(data) { + webidl.brandCheck(this, _WebSocket); + const prefix = "WebSocket.send"; + webidl.argumentLengthCheck(arguments, 1, prefix); + data = webidl.converters.WebSocketSendData(data, prefix, "data"); + if (isConnecting(this)) { + throw new DOMException("Sent before connected.", "InvalidStateError"); + } + if (!isEstablished(this) || isClosing(this)) { + return; + } + if (typeof data === "string") { + const length2 = Buffer.byteLength(data); + this.#bufferedAmount += length2; + this.#sendQueue.add(data, () => { + this.#bufferedAmount -= length2; + }, sendHints.string); + } else if (types.isArrayBuffer(data)) { + this.#bufferedAmount += data.byteLength; + this.#sendQueue.add(data, () => { + this.#bufferedAmount -= data.byteLength; + }, sendHints.arrayBuffer); + } else if (ArrayBuffer.isView(data)) { + this.#bufferedAmount += data.byteLength; + this.#sendQueue.add(data, () => { + this.#bufferedAmount -= data.byteLength; + }, sendHints.typedArray); + } else if (isBlobLike(data)) { + this.#bufferedAmount += data.size; + this.#sendQueue.add(data, () => { + this.#bufferedAmount -= data.size; + }, sendHints.blob); + } + } + get readyState() { + webidl.brandCheck(this, _WebSocket); + return this[kReadyState]; + } + get bufferedAmount() { + webidl.brandCheck(this, _WebSocket); + return this.#bufferedAmount; + } + get url() { + webidl.brandCheck(this, _WebSocket); + return URLSerializer(this[kWebSocketURL]); + } + get extensions() { + webidl.brandCheck(this, _WebSocket); + return this.#extensions; + } + get protocol() { + webidl.brandCheck(this, _WebSocket); + return this.#protocol; + } + get onopen() { + webidl.brandCheck(this, _WebSocket); + return this.#events.open; + } + set onopen(fn) { + webidl.brandCheck(this, _WebSocket); + if (this.#events.open) { + this.removeEventListener("open", this.#events.open); + } + if (typeof fn === "function") { + this.#events.open = fn; + this.addEventListener("open", fn); + } else { + this.#events.open = null; + } + } + get onerror() { + webidl.brandCheck(this, _WebSocket); + return this.#events.error; + } + set onerror(fn) { + webidl.brandCheck(this, _WebSocket); + if (this.#events.error) { + this.removeEventListener("error", this.#events.error); + } + if (typeof fn === "function") { + this.#events.error = fn; + this.addEventListener("error", fn); + } else { + this.#events.error = null; + } + } + get onclose() { + webidl.brandCheck(this, _WebSocket); + return this.#events.close; + } + set onclose(fn) { + webidl.brandCheck(this, _WebSocket); + if (this.#events.close) { + this.removeEventListener("close", this.#events.close); + } + if (typeof fn === "function") { + this.#events.close = fn; + this.addEventListener("close", fn); + } else { + this.#events.close = null; + } + } + get onmessage() { + webidl.brandCheck(this, _WebSocket); + return this.#events.message; + } + set onmessage(fn) { + webidl.brandCheck(this, _WebSocket); + if (this.#events.message) { + this.removeEventListener("message", this.#events.message); + } + if (typeof fn === "function") { + this.#events.message = fn; + this.addEventListener("message", fn); + } else { + this.#events.message = null; + } + } + get binaryType() { + webidl.brandCheck(this, _WebSocket); + return this[kBinaryType]; + } + set binaryType(type) { + webidl.brandCheck(this, _WebSocket); + if (type !== "blob" && type !== "arraybuffer") { + this[kBinaryType] = "blob"; + } else { + this[kBinaryType] = type; + } + } + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished(response, parsedExtensions) { + this[kResponse] = response; + const maxPayloadSize = this[kController]?.dispatcher?.webSocketOptions?.maxPayloadSize; + const parser = new ByteParser(this, parsedExtensions, { + maxPayloadSize + }); + parser.on("drain", onParserDrain); + parser.on("error", onParserError.bind(this)); + response.socket.ws = this; + this[kByteParser] = parser; + this.#sendQueue = new SendQueue(response.socket); + this[kReadyState] = states.OPEN; + const extensions = response.headersList.get("sec-websocket-extensions"); + if (extensions !== null) { + this.#extensions = extensions; + } + const protocol = response.headersList.get("sec-websocket-protocol"); + if (protocol !== null) { + this.#protocol = protocol; + } + fireEvent("open", this); + } + }; + WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING; + WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN; + WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING; + WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED; + Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: "WebSocket", + writable: false, + enumerable: false, + configurable: true + } + }); + Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors + }); + webidl.converters["sequence"] = webidl.sequenceConverter( + webidl.converters.DOMString + ); + webidl.converters["DOMString or sequence"] = function(V, prefix, argument) { + if (webidl.util.Type(V) === "Object" && Symbol.iterator in V) { + return webidl.converters["sequence"](V); + } + return webidl.converters.DOMString(V, prefix, argument); + }; + webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: "protocols", + converter: webidl.converters["DOMString or sequence"], + defaultValue: () => new Array(0) + }, + { + key: "dispatcher", + converter: webidl.converters.any, + defaultValue: () => getGlobalDispatcher() + }, + { + key: "headers", + converter: webidl.nullableConverter(webidl.converters.HeadersInit) + } + ]); + webidl.converters["DOMString or sequence or WebSocketInit"] = function(V) { + if (webidl.util.Type(V) === "Object" && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V); + } + return { protocols: webidl.converters["DOMString or sequence"](V) }; + }; + webidl.converters.WebSocketSendData = function(V) { + if (webidl.util.Type(V) === "Object") { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }); + } + if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + return webidl.converters.BufferSource(V); + } + } + return webidl.converters.USVString(V); + }; + function onParserDrain() { + this.ws[kResponse].socket.resume(); + } + function onParserError(err) { + let message; + let code; + if (err instanceof CloseEvent) { + message = err.reason; + code = err.code; + } else { + message = err.message; + } + fireEvent("error", this, () => new ErrorEvent("error", { error: err, message })); + closeWebSocketConnection(this, code); + } + module.exports = { + WebSocket + }; + } +}); + +// viewer/node_modules/undici/lib/web/eventsource/util.js +var require_util8 = __commonJS({ + "viewer/node_modules/undici/lib/web/eventsource/util.js"(exports, module) { + "use strict"; + function isValidLastEventId(value) { + return value.indexOf("\0") === -1; + } + function isASCIINumber(value) { + if (value.length === 0) return false; + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) < 48 || value.charCodeAt(i) > 57) return false; + } + return true; + } + function delay(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms).unref(); + }); + } + module.exports = { + isValidLastEventId, + isASCIINumber, + delay + }; + } +}); + +// viewer/node_modules/undici/lib/web/eventsource/eventsource-stream.js +var require_eventsource_stream = __commonJS({ + "viewer/node_modules/undici/lib/web/eventsource/eventsource-stream.js"(exports, module) { + "use strict"; + var { Transform } = __require("node:stream"); + var { isASCIINumber, isValidLastEventId } = require_util8(); + var BOM = [239, 187, 191]; + var LF = 10; + var CR = 13; + var COLON = 58; + var SPACE = 32; + var EventSourceStream = class extends Transform { + /** + * @type {eventSourceSettings} + */ + state = null; + /** + * Leading byte-order-mark check. + * @type {boolean} + */ + checkBOM = true; + /** + * @type {boolean} + */ + crlfCheck = false; + /** + * @type {boolean} + */ + eventEndCheck = false; + /** + * @type {Buffer} + */ + buffer = null; + pos = 0; + event = { + data: void 0, + event: void 0, + id: void 0, + retry: void 0 + }; + /** + * @param {object} options + * @param {eventSourceSettings} options.eventSourceSettings + * @param {Function} [options.push] + */ + constructor(options = {}) { + options.readableObjectMode = true; + super(options); + this.state = options.eventSourceSettings || {}; + if (options.push) { + this.push = options.push; + } + } + /** + * @param {Buffer} chunk + * @param {string} _encoding + * @param {Function} callback + * @returns {void} + */ + _transform(chunk, _encoding, callback) { + if (chunk.length === 0) { + callback(); + return; + } + if (this.buffer) { + this.buffer = Buffer.concat([this.buffer, chunk]); + } else { + this.buffer = chunk; + } + if (this.checkBOM) { + switch (this.buffer.length) { + case 1: + if (this.buffer[0] === BOM[0]) { + callback(); + return; + } + this.checkBOM = false; + callback(); + return; + case 2: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1]) { + callback(); + return; + } + this.checkBOM = false; + break; + case 3: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1] && this.buffer[2] === BOM[2]) { + this.buffer = Buffer.alloc(0); + this.checkBOM = false; + callback(); + return; + } + this.checkBOM = false; + break; + default: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1] && this.buffer[2] === BOM[2]) { + this.buffer = this.buffer.subarray(3); + } + this.checkBOM = false; + break; + } + } + while (this.pos < this.buffer.length) { + if (this.eventEndCheck) { + if (this.crlfCheck) { + if (this.buffer[this.pos] === LF) { + this.buffer = this.buffer.subarray(this.pos + 1); + this.pos = 0; + this.crlfCheck = false; + continue; + } + this.crlfCheck = false; + } + if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (this.buffer[this.pos] === CR) { + this.crlfCheck = true; + } + this.buffer = this.buffer.subarray(this.pos + 1); + this.pos = 0; + if (this.event.data !== void 0 || this.event.event || this.event.id || this.event.retry) { + this.processEvent(this.event); + } + this.clearEvent(); + continue; + } + this.eventEndCheck = false; + continue; + } + if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (this.buffer[this.pos] === CR) { + this.crlfCheck = true; + } + this.parseLine(this.buffer.subarray(0, this.pos), this.event); + this.buffer = this.buffer.subarray(this.pos + 1); + this.pos = 0; + this.eventEndCheck = true; + continue; + } + this.pos++; + } + callback(); + } + /** + * @param {Buffer} line + * @param {EventStreamEvent} event + */ + parseLine(line, event) { + if (line.length === 0) { + return; + } + const colonPosition = line.indexOf(COLON); + if (colonPosition === 0) { + return; + } + let field = ""; + let value = ""; + if (colonPosition !== -1) { + field = line.subarray(0, colonPosition).toString("utf8"); + let valueStart = colonPosition + 1; + if (line[valueStart] === SPACE) { + ++valueStart; + } + value = line.subarray(valueStart).toString("utf8"); + } else { + field = line.toString("utf8"); + value = ""; + } + switch (field) { + case "data": + if (event[field] === void 0) { + event[field] = value; + } else { + event[field] += ` +${value}`; + } + break; + case "retry": + if (isASCIINumber(value)) { + event[field] = value; + } + break; + case "id": + if (isValidLastEventId(value)) { + event[field] = value; + } + break; + case "event": + if (value.length > 0) { + event[field] = value; + } + break; + } + } + /** + * @param {EventSourceStreamEvent} event + */ + processEvent(event) { + if (event.retry && isASCIINumber(event.retry)) { + this.state.reconnectionTime = parseInt(event.retry, 10); + } + if (event.id && isValidLastEventId(event.id)) { + this.state.lastEventId = event.id; + } + if (event.data !== void 0) { + this.push({ + type: event.event || "message", + options: { + data: event.data, + lastEventId: this.state.lastEventId, + origin: this.state.origin + } + }); + } + } + clearEvent() { + this.event = { + data: void 0, + event: void 0, + id: void 0, + retry: void 0 + }; + } + }; + module.exports = { + EventSourceStream + }; + } +}); + +// viewer/node_modules/undici/lib/web/eventsource/eventsource.js +var require_eventsource = __commonJS({ + "viewer/node_modules/undici/lib/web/eventsource/eventsource.js"(exports, module) { + "use strict"; + var { pipeline } = __require("node:stream"); + var { fetching } = require_fetch(); + var { makeRequest } = require_request2(); + var { webidl } = require_webidl(); + var { EventSourceStream } = require_eventsource_stream(); + var { parseMIMEType } = require_data_url(); + var { createFastMessageEvent } = require_events(); + var { isNetworkError: isNetworkError2 } = require_response(); + var { delay } = require_util8(); + var { kEnumerableProperty } = require_util(); + var { environmentSettingsObject } = require_util2(); + var experimentalWarned = false; + var defaultReconnectionTime = 3e3; + var CONNECTING = 0; + var OPEN = 1; + var CLOSED = 2; + var ANONYMOUS = "anonymous"; + var USE_CREDENTIALS = "use-credentials"; + var EventSource = class _EventSource extends EventTarget { + #events = { + open: null, + error: null, + message: null + }; + #url = null; + #withCredentials = false; + #readyState = CONNECTING; + #request = null; + #controller = null; + #dispatcher; + /** + * @type {import('./eventsource-stream').eventSourceSettings} + */ + #state; + /** + * Creates a new EventSource object. + * @param {string} url + * @param {EventSourceInit} [eventSourceInitDict] + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + */ + constructor(url, eventSourceInitDict = {}) { + super(); + webidl.util.markAsUncloneable(this); + const prefix = "EventSource constructor"; + webidl.argumentLengthCheck(arguments, 1, prefix); + if (!experimentalWarned) { + experimentalWarned = true; + process.emitWarning("EventSource is experimental, expect them to change at any time.", { + code: "UNDICI-ES" + }); + } + url = webidl.converters.USVString(url, prefix, "url"); + eventSourceInitDict = webidl.converters.EventSourceInitDict(eventSourceInitDict, prefix, "eventSourceInitDict"); + this.#dispatcher = eventSourceInitDict.dispatcher; + this.#state = { + lastEventId: "", + reconnectionTime: defaultReconnectionTime + }; + const settings = environmentSettingsObject; + let urlRecord; + try { + urlRecord = new URL(url, settings.settingsObject.baseUrl); + this.#state.origin = urlRecord.origin; + } catch (e) { + throw new DOMException(e, "SyntaxError"); + } + this.#url = urlRecord.href; + let corsAttributeState = ANONYMOUS; + if (eventSourceInitDict.withCredentials) { + corsAttributeState = USE_CREDENTIALS; + this.#withCredentials = true; + } + const initRequest = { + redirect: "follow", + keepalive: true, + // @see https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes + mode: "cors", + credentials: corsAttributeState === "anonymous" ? "same-origin" : "omit", + referrer: "no-referrer" + }; + initRequest.client = environmentSettingsObject.settingsObject; + initRequest.headersList = [["accept", { name: "accept", value: "text/event-stream" }]]; + initRequest.cache = "no-store"; + initRequest.initiator = "other"; + initRequest.urlList = [new URL(this.#url)]; + this.#request = makeRequest(initRequest); + this.#connect(); + } + /** + * Returns the state of this EventSource object's connection. It can have the + * values described below. + * @returns {0|1|2} + * @readonly + */ + get readyState() { + return this.#readyState; + } + /** + * Returns the URL providing the event stream. + * @readonly + * @returns {string} + */ + get url() { + return this.#url; + } + /** + * Returns a boolean indicating whether the EventSource object was + * instantiated with CORS credentials set (true), or not (false, the default). + */ + get withCredentials() { + return this.#withCredentials; + } + #connect() { + if (this.#readyState === CLOSED) return; + this.#readyState = CONNECTING; + const fetchParams = { + request: this.#request, + dispatcher: this.#dispatcher + }; + const processEventSourceEndOfBody = (response) => { + if (isNetworkError2(response)) { + this.dispatchEvent(new Event("error")); + this.close(); + } + this.#reconnect(); + }; + fetchParams.processResponseEndOfBody = processEventSourceEndOfBody; + fetchParams.processResponse = (response) => { + if (isNetworkError2(response)) { + if (response.aborted) { + this.close(); + this.dispatchEvent(new Event("error")); + return; + } else { + this.#reconnect(); + return; + } + } + const contentType = response.headersList.get("content-type", true); + const mimeType = contentType !== null ? parseMIMEType(contentType) : "failure"; + const contentTypeValid = mimeType !== "failure" && mimeType.essence === "text/event-stream"; + if (response.status !== 200 || contentTypeValid === false) { + this.close(); + this.dispatchEvent(new Event("error")); + return; + } + this.#readyState = OPEN; + this.dispatchEvent(new Event("open")); + this.#state.origin = response.urlList[response.urlList.length - 1].origin; + const eventSourceStream = new EventSourceStream({ + eventSourceSettings: this.#state, + push: (event) => { + this.dispatchEvent(createFastMessageEvent( + event.type, + event.options + )); + } + }); + pipeline( + response.body.stream, + eventSourceStream, + (error) => { + if (error?.aborted === false) { + this.close(); + this.dispatchEvent(new Event("error")); + } + } + ); + }; + this.#controller = fetching(fetchParams); + } + /** + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#sse-processing-model + * @returns {Promise} + */ + async #reconnect() { + if (this.#readyState === CLOSED) return; + this.#readyState = CONNECTING; + this.dispatchEvent(new Event("error")); + await delay(this.#state.reconnectionTime); + if (this.#readyState !== CONNECTING) return; + if (this.#state.lastEventId.length) { + this.#request.headersList.set("last-event-id", this.#state.lastEventId, true); + } + this.#connect(); + } + /** + * Closes the connection, if any, and sets the readyState attribute to + * CLOSED. + */ + close() { + webidl.brandCheck(this, _EventSource); + if (this.#readyState === CLOSED) return; + this.#readyState = CLOSED; + this.#controller.abort(); + this.#request = null; + } + get onopen() { + return this.#events.open; + } + set onopen(fn) { + if (this.#events.open) { + this.removeEventListener("open", this.#events.open); + } + if (typeof fn === "function") { + this.#events.open = fn; + this.addEventListener("open", fn); + } else { + this.#events.open = null; + } + } + get onmessage() { + return this.#events.message; + } + set onmessage(fn) { + if (this.#events.message) { + this.removeEventListener("message", this.#events.message); + } + if (typeof fn === "function") { + this.#events.message = fn; + this.addEventListener("message", fn); + } else { + this.#events.message = null; + } + } + get onerror() { + return this.#events.error; + } + set onerror(fn) { + if (this.#events.error) { + this.removeEventListener("error", this.#events.error); + } + if (typeof fn === "function") { + this.#events.error = fn; + this.addEventListener("error", fn); + } else { + this.#events.error = null; + } + } + }; + var constantsPropertyDescriptors = { + CONNECTING: { + __proto__: null, + configurable: false, + enumerable: true, + value: CONNECTING, + writable: false + }, + OPEN: { + __proto__: null, + configurable: false, + enumerable: true, + value: OPEN, + writable: false + }, + CLOSED: { + __proto__: null, + configurable: false, + enumerable: true, + value: CLOSED, + writable: false + } + }; + Object.defineProperties(EventSource, constantsPropertyDescriptors); + Object.defineProperties(EventSource.prototype, constantsPropertyDescriptors); + Object.defineProperties(EventSource.prototype, { + close: kEnumerableProperty, + onerror: kEnumerableProperty, + onmessage: kEnumerableProperty, + onopen: kEnumerableProperty, + readyState: kEnumerableProperty, + url: kEnumerableProperty, + withCredentials: kEnumerableProperty + }); + webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ + { + key: "withCredentials", + converter: webidl.converters.boolean, + defaultValue: () => false + }, + { + key: "dispatcher", + // undici only + converter: webidl.converters.any + } + ]); + module.exports = { + EventSource, + defaultReconnectionTime + }; + } +}); + +// viewer/node_modules/undici/index.js +var require_undici = __commonJS({ + "viewer/node_modules/undici/index.js"(exports, module) { + "use strict"; + var Client = require_client(); + var Dispatcher = require_dispatcher(); + var Pool = require_pool(); + var BalancedPool = require_balanced_pool(); + var Agent = require_agent(); + var ProxyAgent = require_proxy_agent(); + var EnvHttpProxyAgent = require_env_http_proxy_agent(); + var RetryAgent = require_retry_agent(); + var errors = require_errors(); + var util = require_util(); + var { InvalidArgumentError } = errors; + var api = require_api(); + var buildConnector = require_connect(); + var MockClient = require_mock_client(); + var MockAgent = require_mock_agent(); + var MockPool = require_mock_pool(); + var mockErrors = require_mock_errors(); + var RetryHandler = require_retry_handler(); + var { getGlobalDispatcher, setGlobalDispatcher } = require_global2(); + var DecoratorHandler = require_decorator_handler(); + var RedirectHandler = require_redirect_handler(); + var createRedirectInterceptor = require_redirect_interceptor(); + Object.assign(Dispatcher.prototype, api); + module.exports.Dispatcher = Dispatcher; + module.exports.Client = Client; + module.exports.Pool = Pool; + module.exports.BalancedPool = BalancedPool; + module.exports.Agent = Agent; + module.exports.ProxyAgent = ProxyAgent; + module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent; + module.exports.RetryAgent = RetryAgent; + module.exports.RetryHandler = RetryHandler; + module.exports.DecoratorHandler = DecoratorHandler; + module.exports.RedirectHandler = RedirectHandler; + module.exports.createRedirectInterceptor = createRedirectInterceptor; + module.exports.interceptors = { + redirect: require_redirect(), + retry: require_retry3(), + dump: require_dump(), + dns: require_dns() + }; + module.exports.buildConnector = buildConnector; + module.exports.errors = errors; + module.exports.util = { + parseHeaders: util.parseHeaders, + headerNameToString: util.headerNameToString + }; + function makeDispatcher(fn) { + return (url, opts, handler) => { + if (typeof opts === "function") { + handler = opts; + opts = null; + } + if (!url || typeof url !== "string" && typeof url !== "object" && !(url instanceof URL)) { + throw new InvalidArgumentError("invalid url"); + } + if (opts != null && typeof opts !== "object") { + throw new InvalidArgumentError("invalid opts"); + } + if (opts && opts.path != null) { + if (typeof opts.path !== "string") { + throw new InvalidArgumentError("invalid opts.path"); + } + let path14 = opts.path; + if (!opts.path.startsWith("/")) { + path14 = `/${path14}`; + } + url = new URL(util.parseOrigin(url).origin + path14); + } else { + if (!opts) { + opts = typeof url === "object" ? url : {}; + } + url = util.parseURL(url); + } + const { agent, dispatcher = getGlobalDispatcher() } = opts; + if (agent) { + throw new InvalidArgumentError("unsupported opts.agent. Did you mean opts.client?"); + } + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? "PUT" : "GET") + }, handler); + }; + } + module.exports.setGlobalDispatcher = setGlobalDispatcher; + module.exports.getGlobalDispatcher = getGlobalDispatcher; + var fetchImpl = require_fetch().fetch; + module.exports.fetch = async function fetch3(init, options = void 0) { + try { + return await fetchImpl(init, options); + } catch (err) { + if (err && typeof err === "object") { + Error.captureStackTrace(err); + } + throw err; + } + }; + module.exports.Headers = require_headers().Headers; + module.exports.Response = require_response().Response; + module.exports.Request = require_request2().Request; + module.exports.FormData = require_formdata().FormData; + module.exports.File = globalThis.File ?? __require("node:buffer").File; + module.exports.FileReader = require_filereader().FileReader; + var { setGlobalOrigin, getGlobalOrigin } = require_global(); + module.exports.setGlobalOrigin = setGlobalOrigin; + module.exports.getGlobalOrigin = getGlobalOrigin; + var { CacheStorage } = require_cachestorage(); + var { kConstruct } = require_symbols4(); + module.exports.caches = new CacheStorage(kConstruct); + var { deleteCookie, getCookies, getSetCookies, setCookie } = require_cookies(); + module.exports.deleteCookie = deleteCookie; + module.exports.getCookies = getCookies; + module.exports.getSetCookies = getSetCookies; + module.exports.setCookie = setCookie; + var { parseMIMEType, serializeAMimeType } = require_data_url(); + module.exports.parseMIMEType = parseMIMEType; + module.exports.serializeAMimeType = serializeAMimeType; + var { CloseEvent, ErrorEvent, MessageEvent } = require_events(); + module.exports.WebSocket = require_websocket().WebSocket; + module.exports.CloseEvent = CloseEvent; + module.exports.ErrorEvent = ErrorEvent; + module.exports.MessageEvent = MessageEvent; + module.exports.request = makeDispatcher(api.request); + module.exports.stream = makeDispatcher(api.stream); + module.exports.pipeline = makeDispatcher(api.pipeline); + module.exports.connect = makeDispatcher(api.connect); + module.exports.upgrade = makeDispatcher(api.upgrade); + module.exports.MockClient = MockClient; + module.exports.MockPool = MockPool; + module.exports.MockAgent = MockAgent; + module.exports.mockErrors = mockErrors; + var { EventSource } = require_eventsource(); + module.exports.EventSource = EventSource; + } +}); + +// viewer/node_modules/throttleit/index.js +var require_throttleit = __commonJS({ + "viewer/node_modules/throttleit/index.js"(exports, module) { + function throttle3(function_, wait) { + if (typeof function_ !== "function") { + throw new TypeError(`Expected the first argument to be a \`function\`, got \`${typeof function_}\`.`); + } + let timeoutId; + let lastCallTime = 0; + return function throttled(...arguments_) { + clearTimeout(timeoutId); + const now = Date.now(); + const timeSinceLastCall = now - lastCallTime; + const delayForNextCall = wait - timeSinceLastCall; + if (delayForNextCall <= 0) { + lastCallTime = now; + function_.apply(this, arguments_); + } else { + timeoutId = setTimeout(() => { + lastCallTime = Date.now(); + function_.apply(this, arguments_); + }, delayForNextCall); + } + }; + } + module.exports = throttle3; + } +}); + +// viewer/node_modules/@vercel/blob/dist/chunk-3D2SZ6M2.js +import { Readable } from "stream"; +async function toReadableStream(value) { + if (value instanceof ReadableStream) { + return value; + } + if (value instanceof Blob) { + return value.stream(); + } + if (isNodeJsReadableStream(value)) { + return Readable.toWeb(value); + } + let streamValue; + if (value instanceof ArrayBuffer) { + streamValue = new Uint8Array(value); + } else if (isNodeJsBuffer(value)) { + streamValue = value; + } else { + streamValue = stringToUint8Array(value); + } + if (await supportsNewBlobFromArrayBuffer) { + return new Blob([streamValue]).stream(); + } + return new ReadableStream({ + start(controller) { + controller.enqueue(streamValue); + controller.close(); + } + }); +} +function isNodeJsReadableStream(value) { + return typeof value === "object" && typeof value.pipe === "function" && value.readable && typeof value._read === "function" && // @ts-expect-error _readableState does exists on Readable + typeof value._readableState === "object"; +} +function stringToUint8Array(s) { + const enc = new TextEncoder(); + return enc.encode(s); +} +function isNodeJsBuffer(value) { + return (0, import_is_buffer.default)(value); +} +function readEnv(name) { + try { + const value = process.env[name]; + return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0; + } catch { + return void 0; + } +} +function getVercelOidcToken() { + var _a3; + const tokenFromContext = (_a3 = getContext().headers) == null ? void 0 : _a3["x-vercel-oidc-token"]; + if (typeof tokenFromContext === "string" && tokenFromContext.trim() !== "") { + return tokenFromContext.trim(); + } + return readEnv("VERCEL_OIDC_TOKEN"); +} +function bytes(val) { + if (typeof val === "number" && !Number.isNaN(val)) { + return val; + } + if (typeof val !== "string") { + return null; + } + const results = parseRegExp.exec(val); + let floatValue; + let unit = "b"; + if (!results) { + floatValue = parseInt(val, 10); + } else { + const [, res, , , unitMatch] = results; + if (!res) { + return null; + } + floatValue = parseFloat(res); + if (unitMatch) { + unit = unitMatch.toLowerCase(); + } + } + if (Number.isNaN(floatValue)) { + return null; + } + return Math.floor(map[unit] * floatValue); +} +function readEnv2(name) { + try { + const value = process.env[name]; + return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0; + } catch { + return void 0; + } +} +function parseStoreIdFromReadWriteToken(token) { + const [, , , storeId = ""] = token.split("_"); + return storeId; +} +function base64UrlDecodeDelegationSegment(segment) { + let base64 = segment.replace(/-/g, "+").replace(/_/g, "/"); + const padding = 4 - base64.length % 4; + if (padding !== 4) { + base64 += "=".repeat(padding); + } + if (typeof atob === "function") { + return atob(base64); + } + if (typeof Buffer !== "undefined") { + return Buffer.from(base64, "base64").toString("utf8"); + } + throw new BlobError("Cannot decode base64: no atob or Buffer available."); +} +function parseStoreIdFromDelegationToken(delegationToken) { + const dot3 = delegationToken.indexOf("."); + if (dot3 < 0) { + throw new BlobError("Invalid delegation token format."); + } + const payloadSeg = delegationToken.slice(0, dot3); + let parsed; + try { + parsed = JSON.parse(base64UrlDecodeDelegationSegment(payloadSeg)); + } catch { + throw new BlobError("Invalid delegation token payload."); + } + if (!parsed.storeId || typeof parsed.storeId !== "string") { + throw new BlobError("Delegation token payload is missing `storeId`."); + } + return normalizeStoreId(parsed.storeId); +} +function parseStoreIdFromPresignedUrl(presignedUrlPayload) { + const delegation = presignedUrlPayload.delegationToken; + return parseStoreIdFromDelegationToken(delegation); +} +function normalizeStoreId(storeId) { + return storeId.startsWith("store_") ? storeId.slice("store_".length) : storeId; +} +function resolveBlobAuth(options) { + var _a3, _b2; + if (options == null ? void 0 : options.presignedUrlPayload) { + const storeId = parseStoreIdFromDelegationToken( + options.presignedUrlPayload.delegationToken + ); + return { kind: "presigned", storeId }; + } + if (options == null ? void 0 : options.token) { + const storeId = parseStoreIdFromReadWriteToken(options.token); + return { kind: "readWrite", token: options.token, storeId }; + } + const manualOidcToken = (_a3 = options == null ? void 0 : options.oidcToken) == null ? void 0 : _a3.trim(); + const oidcToken = manualOidcToken || getVercelOidcToken(); + if (oidcToken) { + const manualStoreId = (_b2 = options == null ? void 0 : options.storeId) == null ? void 0 : _b2.trim(); + if (manualStoreId) { + return { + kind: "oidc", + token: oidcToken, + storeId: normalizeStoreId(manualStoreId) + }; + } + const blobStoreId = readEnv2("BLOB_STORE_ID"); + if (blobStoreId) { + return { + kind: "oidc", + token: oidcToken, + storeId: normalizeStoreId(blobStoreId) + }; + } + if (manualOidcToken) { + throw new BlobError( + "oidcToken was passed, but no storeId was found. Pass a `storeId` option or set `BLOB_STORE_ID` to use OIDC auth" + ); + } + } + const readWrite = readEnv2("BLOB_READ_WRITE_TOKEN"); + if (readWrite) { + const storeId = parseStoreIdFromReadWriteToken(readWrite); + return { kind: "readWrite", token: readWrite, storeId }; + } + throw new BlobError( + "No blob credentials found. Pass a `token` option, set `BLOB_READ_WRITE_TOKEN`, or use `oidcToken` (or `VERCEL_OIDC_TOKEN`) with `storeId` or `BLOB_STORE_ID`." + ); +} +function getDownloadUrl(blobUrl) { + const url = new URL(blobUrl); + url.searchParams.set("download", "1"); + return url.toString(); +} +function isPlainObject(value) { + if (typeof value !== "object" || value === null) { + return false; + } + const prototype = Object.getPrototypeOf(value); + return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value); +} +function getApiUrl(pathname = "") { + let baseUrl = null; + try { + baseUrl = process.env.VERCEL_BLOB_API_URL || process.env.NEXT_PUBLIC_VERCEL_BLOB_API_URL; + } catch { + } + return `${baseUrl || defaultVercelBlobApiUrl}${pathname}`; +} +function computeBodyLength(body) { + if (!body) { + return 0; + } + if (typeof body === "string") { + if (TEXT_ENCODER) { + return TEXT_ENCODER.encode(body).byteLength; + } + return new Blob([body]).size; + } + if ("byteLength" in body && typeof body.byteLength === "number") { + return body.byteLength; + } + if ("size" in body && typeof body.size === "number") { + return body.size; + } + return 0; +} +function isReadableStream(value) { + return globalThis.ReadableStream && // TODO: Can be removed once Node.js 16 is no more required internally + value instanceof ReadableStream; +} +function isStream(value) { + if (isReadableStream(value)) { + return true; + } + if (isNodeJsReadableStream(value)) { + return true; + } + return false; +} +function isUrl(urlOrPathname) { + return urlOrPathname.startsWith("http://") || urlOrPathname.startsWith("https://"); +} +function constructBlobUrl(storeId, pathname, access) { + return `https://${storeId}.${access}.blob.vercel-storage.com/${pathname}`; +} +function debug(message, ...args) { + if (debugIsActive) { + console.debug(`vercel-blob: ${message}`, ...args); + } +} +function isNetworkError(error) { + const isValid = error && isError(error) && error.name === "TypeError" && typeof error.message === "string"; + if (!isValid) { + return false; + } + if (error.message === "Load failed") { + return error.stack === void 0; + } + return errorMessages.has(error.message); +} +function getApiVersion() { + let versionOverride = null; + try { + versionOverride = process.env.VERCEL_BLOB_API_VERSION_OVERRIDE || process.env.NEXT_PUBLIC_VERCEL_BLOB_API_VERSION_OVERRIDE; + } catch { + } + return `${versionOverride != null ? versionOverride : BLOB_API_VERSION}`; +} +function getRetries() { + try { + const retries = process.env.VERCEL_BLOB_RETRIES || "10"; + return parseInt(retries, 10); + } catch { + return 10; + } +} +function createBlobServiceRateLimited(response) { + const retryAfter = response.headers.get("retry-after"); + return new BlobServiceRateLimited( + retryAfter ? parseInt(retryAfter, 10) : void 0 + ); +} +async function getBlobError(response) { + var _a3, _b2, _c; + let code; + let message; + try { + const data = await response.json(); + code = (_b2 = (_a3 = data.error) == null ? void 0 : _a3.code) != null ? _b2 : "unknown_error"; + message = (_c = data.error) == null ? void 0 : _c.message; + } catch { + code = "unknown_error"; + } + if ((message == null ? void 0 : message.includes("contentType")) && message.includes("is not allowed")) { + code = "content_type_not_allowed"; + } + if ((message == null ? void 0 : message.includes('"pathname"')) && message.includes("does not match the token payload")) { + code = "client_token_pathname_mismatch"; + } + if (message === "Token expired") { + code = "client_token_expired"; + } + if (message == null ? void 0 : message.includes("the file length cannot be greater than")) { + code = "file_too_large"; + } + if ((message == null ? void 0 : message.startsWith("OIDC is enabled for this project, but not for the")) && message.includes("environment.")) { + code = "oidc_environment_not_allowed"; + } + let error; + switch (code) { + case "store_suspended": + error = new BlobStoreSuspendedError(); + break; + case "forbidden": + error = new BlobAccessError(); + break; + case "oidc_environment_not_allowed": + error = new BlobOidcEnvironmentNotAllowedError(message); + break; + case "content_type_not_allowed": + error = new BlobContentTypeNotAllowedError(message); + break; + case "client_token_pathname_mismatch": + error = new BlobPathnameMismatchError(message); + break; + case "client_token_expired": + error = new BlobClientTokenExpiredError(); + break; + case "file_too_large": + error = new BlobFileTooLargeError(message); + break; + case "not_found": + error = new BlobNotFoundError(); + break; + case "client_token_not_allowed": + error = new BlobError( + message != null ? message : "This operation is not available when using a client token. Use a read\u2013write or OIDC token on the server." + ); + break; + case "store_not_found": + error = new BlobStoreNotFoundError(); + break; + case "bad_request": + error = new BlobError(message != null ? message : "Bad request"); + break; + case "service_unavailable": + error = new BlobServiceNotAvailable(); + break; + case "rate_limited": + error = createBlobServiceRateLimited(response); + break; + case "precondition_failed": + error = new BlobPreconditionFailedError(); + break; + case "unknown_error": + case "not_allowed": + default: + error = new BlobUnknownError(); + break; + } + return { code, error }; +} +async function requestApi(pathname, init, commandOptions) { + const apiVersion = getApiVersion(); + const auth = resolveBlobAuth(commandOptions); + const bearerToken = auth.kind === "presigned" ? void 0 : auth.token; + const extraHeaders = getProxyThroughAlternativeApiHeaderFromEnv(); + let requestInput = getApiUrl(pathname); + if (commandOptions == null ? void 0 : commandOptions.presignedUrlPayload) { + requestInput = addPresignedParams( + requestInput, + commandOptions.presignedUrlPayload + ); + } + const requestId = `${auth.storeId}:${Date.now()}:${Math.random().toString(16).slice(2)}`; + let retryCount = 0; + let bodyLength = 0; + let totalLoaded = 0; + const sendBodyLength = (commandOptions == null ? void 0 : commandOptions.onUploadProgress) || shouldUseXContentLength(); + if (init.body && // 1. For upload progress we always need to know the total size of the body + // 2. In development we need the header for put() to work correctly when passing a stream + sendBodyLength) { + bodyLength = computeBodyLength(init.body); + } + if (commandOptions == null ? void 0 : commandOptions.onUploadProgress) { + commandOptions.onUploadProgress({ + loaded: 0, + total: bodyLength, + percentage: 0 + }); + } + const apiResponse = await (0, import_async_retry.default)( + async (bail) => { + let res; + try { + res = await blobRequest({ + input: requestInput, + init: { + ...init, + headers: { + "x-api-blob-request-id": requestId, + // Store ID is not encoded in OIDC token, so pass it separately as a header + "x-vercel-blob-store-id": auth.storeId, + "x-api-blob-request-attempt": String(retryCount), + "x-api-version": apiVersion, + ...sendBodyLength ? { "x-content-length": String(bodyLength) } : {}, + ...bearerToken !== void 0 ? { authorization: `Bearer ${bearerToken}` } : {}, + ...extraHeaders, + ...init.headers + } + }, + onUploadProgress: (commandOptions == null ? void 0 : commandOptions.onUploadProgress) ? (loaded) => { + var _a3; + const total = bodyLength !== 0 ? bodyLength : loaded; + totalLoaded = loaded; + const percentage = bodyLength > 0 ? Number((loaded / total * 100).toFixed(2)) : 0; + if (percentage === 100 && bodyLength > 0) { + return; + } + (_a3 = commandOptions.onUploadProgress) == null ? void 0 : _a3.call(commandOptions, { + loaded, + // When passing a stream to put(), we have no way to know the total size of the body. + // Instead of defining total as total?: number we decided to set the total to the currently + // loaded number. This is not inaccurate and way more practical for DX. + // Passing down a stream to put() is very rare + total, + percentage + }); + } : void 0 + }); + } catch (error2) { + if (error2 instanceof DOMException2 && error2.name === "AbortError") { + bail(new BlobRequestAbortedError()); + return; + } + if (isNetworkError(error2)) { + throw error2; + } + if (error2 instanceof TypeError) { + bail(error2); + return; + } + throw error2; + } + if (res.ok) { + return res; + } + const { code, error } = await getBlobError(res); + if (code === "unknown_error" || code === "service_unavailable" || code === "internal_server_error") { + throw error; + } + bail(error); + }, + { + retries: getRetries(), + onRetry: (error) => { + if (error instanceof Error) { + debug(`retrying API request to ${pathname}`, error.message); + } + retryCount = retryCount + 1; + } + } + ); + if (!apiResponse) { + throw new BlobUnknownError(); + } + if (commandOptions == null ? void 0 : commandOptions.onUploadProgress) { + commandOptions.onUploadProgress({ + loaded: totalLoaded, + total: totalLoaded, + percentage: 100 + }); + } + return await apiResponse.json(); +} +function getProxyThroughAlternativeApiHeaderFromEnv() { + const extraHeaders = {}; + try { + if ("VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API" in process.env && process.env.VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API !== void 0) { + extraHeaders["x-proxy-through-alternative-api"] = process.env.VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API; + } else if ("NEXT_PUBLIC_VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API" in process.env && process.env.NEXT_PUBLIC_VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API !== void 0) { + extraHeaders["x-proxy-through-alternative-api"] = process.env.NEXT_PUBLIC_VERCEL_BLOB_PROXY_THROUGH_ALTERNATIVE_API; + } + } catch { + } + return extraHeaders; +} +function shouldUseXContentLength() { + try { + return process.env.VERCEL_BLOB_USE_X_CONTENT_LENGTH === "1"; + } catch { + return false; + } +} +function createPutHeaders(allowedOptions, options) { + const headers = {}; + headers[putOptionHeaderMap.access] = options.access; + if (allowedOptions.includes("contentType") && options.contentType) { + headers[putOptionHeaderMap.contentType] = options.contentType; + } + if (allowedOptions.includes("addRandomSuffix") && options.addRandomSuffix !== void 0) { + headers[putOptionHeaderMap.addRandomSuffix] = options.addRandomSuffix ? "1" : "0"; + } + if (allowedOptions.includes("ifMatch") && options.ifMatch) { + if (options.allowOverwrite === false) { + throw new BlobError( + "ifMatch and allowOverwrite: false are contradictory. ifMatch is used for conditional overwrites, which requires allowOverwrite to be true." + ); + } + headers[putOptionHeaderMap.ifMatch] = options.ifMatch; + if (allowedOptions.includes("allowOverwrite") && options.allowOverwrite === void 0) { + headers[putOptionHeaderMap.allowOverwrite] = "1"; + } + } + if (allowedOptions.includes("allowOverwrite") && options.allowOverwrite !== void 0) { + headers[putOptionHeaderMap.allowOverwrite] = options.allowOverwrite ? "1" : "0"; + } + if (allowedOptions.includes("cacheControlMaxAge") && options.cacheControlMaxAge !== void 0) { + headers[putOptionHeaderMap.cacheControlMaxAge] = options.cacheControlMaxAge.toString(); + } + return headers; +} +async function createPutOptions({ + pathname, + options, + extraChecks, + getToken +}) { + if (!pathname) { + throw new BlobError("pathname is required"); + } + if (pathname.length > MAXIMUM_PATHNAME_LENGTH) { + throw new BlobError( + `pathname is too long, maximum length is ${MAXIMUM_PATHNAME_LENGTH}` + ); + } + for (const invalidCharacter of disallowedPathnameCharacters) { + if (pathname.includes(invalidCharacter)) { + throw new BlobError( + `pathname cannot contain "${invalidCharacter}", please encode it if needed` + ); + } + } + if (!options) { + throw new BlobError("missing options, see usage"); + } + if (options.access !== "public" && options.access !== "private") { + throw new BlobError( + 'access must be "private" or "public", see https://vercel.com/docs/vercel-blob' + ); + } + if (extraChecks) { + extraChecks(options); + } + if (getToken) { + options.token = await getToken(pathname, options); + } + return options; +} +function createCompleteMultipartUploadMethod({ allowedOptions, getToken, extraChecks }) { + return async (pathname, parts, optionsInput) => { + const options = await createPutOptions({ + pathname, + options: optionsInput, + extraChecks, + getToken + }); + const headers = createPutHeaders(allowedOptions, options); + return completeMultipartUpload({ + uploadId: options.uploadId, + key: options.key, + pathname, + headers, + options, + parts + }); + }; +} +async function completeMultipartUpload({ + uploadId, + key, + pathname, + parts, + headers, + options +}) { + const params = new URLSearchParams({ pathname }); + try { + const response = await requestApi( + `/mpu?${params.toString()}`, + { + method: "POST", + headers: { + ...headers, + "content-type": "application/json", + "x-mpu-action": "complete", + "x-mpu-upload-id": uploadId, + // key can be any utf8 character so we need to encode it as HTTP headers can only be us-ascii + // https://www.rfc-editor.org/rfc/rfc7230#swection-3.2.4 + "x-mpu-key": encodeURIComponent(key) + }, + body: JSON.stringify(parts), + signal: options.abortSignal + }, + options + ); + debug("mpu: complete", response); + return response; + } catch (error) { + if (error instanceof TypeError && (error.message === "Failed to fetch" || error.message === "fetch failed")) { + throw new BlobServiceNotAvailable(); + } else { + throw error; + } + } +} +function createCreateMultipartUploadMethod({ allowedOptions, getToken, extraChecks }) { + return async (pathname, optionsInput) => { + const options = await createPutOptions({ + pathname, + options: optionsInput, + extraChecks, + getToken + }); + const headers = createPutHeaders(allowedOptions, options); + const createMultipartUploadResponse = await createMultipartUpload( + pathname, + headers, + options + ); + return { + key: createMultipartUploadResponse.key, + uploadId: createMultipartUploadResponse.uploadId + }; + }; +} +async function createMultipartUpload(pathname, headers, options) { + debug("mpu: create", "pathname:", pathname); + const params = new URLSearchParams({ pathname }); + try { + const response = await requestApi( + `/mpu?${params.toString()}`, + { + method: "POST", + headers: { + ...headers, + "x-mpu-action": "create" + }, + signal: options.abortSignal + }, + options + ); + debug("mpu: create", response); + return response; + } catch (error) { + if (error instanceof TypeError && (error.message === "Failed to fetch" || error.message === "fetch failed")) { + throw new BlobServiceNotAvailable(); + } + throw error; + } +} +function createUploadPartMethod({ allowedOptions, getToken, extraChecks }) { + return async (pathname, body, optionsInput) => { + const options = await createPutOptions({ + pathname, + options: optionsInput, + extraChecks, + getToken + }); + const headers = createPutHeaders(allowedOptions, options); + if (isPlainObject(body)) { + throw new BlobError( + "Body must be a string, buffer or stream. You sent a plain JavaScript object, double check what you're trying to upload." + ); + } + const result = await uploadPart({ + uploadId: options.uploadId, + key: options.key, + pathname, + part: { blob: body, partNumber: options.partNumber }, + headers, + options + }); + return { + etag: result.etag, + partNumber: options.partNumber + }; + }; +} +async function uploadPart({ + uploadId, + key, + pathname, + headers, + options, + internalAbortController = new AbortController(), + part +}) { + var _a3, _b2, _c; + const params = new URLSearchParams({ pathname }); + const responsePromise = requestApi( + `/mpu?${params.toString()}`, + { + signal: internalAbortController.signal, + method: "POST", + headers: { + ...headers, + "x-mpu-action": "upload", + "x-mpu-key": encodeURIComponent(key), + "x-mpu-upload-id": uploadId, + "x-mpu-part-number": part.partNumber.toString() + }, + // weird things between undici types and native fetch types + body: part.blob + }, + options + ); + function handleAbort() { + internalAbortController.abort(); + } + if ((_a3 = options.abortSignal) == null ? void 0 : _a3.aborted) { + handleAbort(); + } else { + (_b2 = options.abortSignal) == null ? void 0 : _b2.addEventListener("abort", handleAbort); + } + const response = await responsePromise; + (_c = options.abortSignal) == null ? void 0 : _c.removeEventListener("abort", handleAbort); + return response; +} +function uploadAllParts({ + uploadId, + key, + pathname, + stream, + headers, + options, + totalToLoad +}) { + debug("mpu: upload init", "key:", key); + const internalAbortController = new AbortController(); + return new Promise((resolve, reject) => { + const partsToUpload = []; + const completedParts = []; + const reader = stream.getReader(); + let activeUploads = 0; + let reading = false; + let currentPartNumber = 1; + let rejected = false; + let currentBytesInMemory = 0; + let doneReading = false; + let bytesSent = 0; + let arrayBuffers = []; + let currentPartBytesRead = 0; + let onUploadProgress; + const totalLoadedPerPartNumber = {}; + if (options.onUploadProgress) { + onUploadProgress = (0, import_throttleit.default)(() => { + var _a3; + const loaded = Object.values(totalLoadedPerPartNumber).reduce( + (acc, cur) => { + return acc + cur; + }, + 0 + ); + const total = totalToLoad || loaded; + const percentage = totalToLoad > 0 ? Number(((loaded / totalToLoad || loaded) * 100).toFixed(2)) : 0; + (_a3 = options.onUploadProgress) == null ? void 0 : _a3.call(options, { loaded, total, percentage }); + }, 150); + } + read().catch(cancel); + async function read() { + debug( + "mpu: upload read start", + "activeUploads:", + activeUploads, + "currentBytesInMemory:", + `${bytes(currentBytesInMemory)}/${bytes(maxBytesInMemory)}`, + "bytesSent:", + bytes(bytesSent) + ); + reading = true; + while (currentBytesInMemory < maxBytesInMemory && !rejected) { + try { + const { value, done } = await reader.read(); + if (done) { + doneReading = true; + debug("mpu: upload read consumed the whole stream"); + if (arrayBuffers.length > 0) { + partsToUpload.push({ + partNumber: currentPartNumber++, + blob: new Blob(arrayBuffers, { + type: "application/octet-stream" + }) + }); + sendParts(); + } else if (activeUploads === 0) { + reader.releaseLock(); + resolve(completedParts); + } + reading = false; + return; + } + currentBytesInMemory += value.byteLength; + let valueOffset = 0; + while (valueOffset < value.byteLength) { + const remainingPartSize = partSizeInBytes - currentPartBytesRead; + const endOffset = Math.min( + valueOffset + remainingPartSize, + value.byteLength + ); + const chunk = value.slice(valueOffset, endOffset); + arrayBuffers.push(chunk); + currentPartBytesRead += chunk.byteLength; + valueOffset = endOffset; + if (currentPartBytesRead === partSizeInBytes) { + partsToUpload.push({ + partNumber: currentPartNumber++, + blob: new Blob(arrayBuffers, { + type: "application/octet-stream" + }) + }); + arrayBuffers = []; + currentPartBytesRead = 0; + sendParts(); + } + } + } catch (error) { + cancel(error); + } + } + debug( + "mpu: upload read end", + "activeUploads:", + activeUploads, + "currentBytesInMemory:", + `${bytes(currentBytesInMemory)}/${bytes(maxBytesInMemory)}`, + "bytesSent:", + bytes(bytesSent) + ); + reading = false; + } + async function sendPart(part) { + activeUploads++; + debug( + "mpu: upload send part start", + "partNumber:", + part.partNumber, + "size:", + part.blob.size, + "activeUploads:", + activeUploads, + "currentBytesInMemory:", + `${bytes(currentBytesInMemory)}/${bytes(maxBytesInMemory)}`, + "bytesSent:", + bytes(bytesSent) + ); + try { + const uploadProgressForPart = options.onUploadProgress ? (event) => { + totalLoadedPerPartNumber[part.partNumber] = event.loaded; + if (onUploadProgress) { + onUploadProgress(); + } + } : void 0; + const completedPart = await uploadPart({ + uploadId, + key, + pathname, + headers, + options: { + ...options, + onUploadProgress: uploadProgressForPart + }, + internalAbortController, + part + }); + debug( + "mpu: upload send part end", + "partNumber:", + part.partNumber, + "activeUploads", + activeUploads, + "currentBytesInMemory:", + `${bytes(currentBytesInMemory)}/${bytes(maxBytesInMemory)}`, + "bytesSent:", + bytes(bytesSent) + ); + if (rejected) { + return; + } + completedParts.push({ + partNumber: part.partNumber, + etag: completedPart.etag + }); + currentBytesInMemory -= part.blob.size; + activeUploads--; + bytesSent += part.blob.size; + if (partsToUpload.length > 0) { + sendParts(); + } + if (doneReading) { + if (activeUploads === 0) { + reader.releaseLock(); + resolve(completedParts); + } + return; + } + if (!reading) { + read().catch(cancel); + } + } catch (error) { + cancel(error); + } + } + function sendParts() { + if (rejected) { + return; + } + debug( + "send parts", + "activeUploads", + activeUploads, + "partsToUpload", + partsToUpload.length + ); + while (activeUploads < maxConcurrentUploads && partsToUpload.length > 0) { + const partToSend = partsToUpload.shift(); + if (partToSend) { + void sendPart(partToSend); + } + } + } + function cancel(error) { + if (rejected) { + return; + } + rejected = true; + internalAbortController.abort(); + reader.releaseLock(); + if (error instanceof TypeError && (error.message === "Failed to fetch" || error.message === "fetch failed")) { + reject(new BlobServiceNotAvailable()); + } else { + reject(error); + } + } + }); +} +function createCreateMultipartUploaderMethod({ allowedOptions, getToken, extraChecks }) { + return async (pathname, optionsInput) => { + const options = await createPutOptions({ + pathname, + options: optionsInput, + extraChecks, + getToken + }); + const headers = createPutHeaders(allowedOptions, options); + const createMultipartUploadResponse = await createMultipartUpload( + pathname, + headers, + options + ); + return { + key: createMultipartUploadResponse.key, + uploadId: createMultipartUploadResponse.uploadId, + async uploadPart(partNumber, body) { + if (isPlainObject(body)) { + throw new BlobError( + "Body must be a string, buffer or stream. You sent a plain JavaScript object, double check what you're trying to upload." + ); + } + const result = await uploadPart({ + uploadId: createMultipartUploadResponse.uploadId, + key: createMultipartUploadResponse.key, + pathname, + part: { partNumber, blob: body }, + headers, + options + }); + return { + etag: result.etag, + partNumber + }; + }, + async complete(parts) { + return completeMultipartUpload({ + uploadId: createMultipartUploadResponse.uploadId, + key: createMultipartUploadResponse.key, + pathname, + parts, + headers, + options + }); + } + }; + }; +} +async function uncontrolledMultipartUpload(pathname, body, headers, options) { + debug("mpu: init", "pathname:", pathname, "headers:", headers); + const optionsWithoutOnUploadProgress = { + ...options, + onUploadProgress: void 0 + }; + if (options.maximumSizeInBytes !== void 0 && !isStream(body) && computeBodyLength(body) > options.maximumSizeInBytes) { + throw new BlobError( + `Body size of ${computeBodyLength(body)} bytes exceeds the maximum allowed size of ${options.maximumSizeInBytes} bytes` + ); + } + const createMultipartUploadResponse = await createMultipartUpload( + pathname, + headers, + optionsWithoutOnUploadProgress + ); + const totalToLoad = computeBodyLength(body); + const stream = await toReadableStream(body); + const parts = await uploadAllParts({ + uploadId: createMultipartUploadResponse.uploadId, + key: createMultipartUploadResponse.key, + pathname, + // @ts-expect-error ReadableStream is compatible at runtime + stream, + headers, + options, + totalToLoad + }); + const blob = await completeMultipartUpload({ + uploadId: createMultipartUploadResponse.uploadId, + key: createMultipartUploadResponse.key, + pathname, + parts, + headers, + options: optionsWithoutOnUploadProgress + }); + return blob; +} +function createPutMethod({ + allowedOptions, + getToken, + getPresignedUrlPayload, + extraChecks +}) { + return async function put2(pathname, body, optionsInput) { + if (!body) { + throw new BlobError("body is required"); + } + if (isPlainObject(body)) { + throw new BlobError( + "Body must be a string, buffer or stream. You sent a plain JavaScript object, double check what you're trying to upload." + ); + } + const options = await createPutOptions({ + pathname, + options: optionsInput, + extraChecks, + getToken + }); + const presignedUrlPayload = await (getPresignedUrlPayload == null ? void 0 : getPresignedUrlPayload( + pathname, + options + )); + const optionsWithPresignedUrlPayload = { + ...options, + presignedUrlPayload + }; + const headers = createPutHeaders(allowedOptions, options); + if (options.multipart === true) { + return uncontrolledMultipartUpload( + pathname, + body, + headers, + optionsWithPresignedUrlPayload + ); + } + const onUploadProgress = options.onUploadProgress ? (0, import_throttleit2.default)(options.onUploadProgress, 100) : void 0; + const params = new URLSearchParams({ pathname }); + const response = await requestApi( + `/?${params.toString()}`, + { + method: "PUT", + body, + headers, + signal: options.abortSignal + }, + { + ...optionsWithPresignedUrlPayload, + onUploadProgress + } + ); + return { + url: response.url, + downloadUrl: response.downloadUrl, + pathname: response.pathname, + contentType: response.contentType, + contentDisposition: response.contentDisposition, + etag: response.etag + }; + }; +} +function contentTypeAllowedByList(contentType, allowed) { + const [type] = contentType.split("/"); + const wildcard = `${type}/*`; + return allowed.includes(contentType) || (type ? allowed.includes(wildcard) : false); +} +function assertAllowedContentTypesSubset(optionsTypes, delegationTypes, label) { + if (!(optionsTypes == null ? void 0 : optionsTypes.length)) { + return; + } + if (!(delegationTypes == null ? void 0 : delegationTypes.length)) { + return; + } + for (const ct of optionsTypes) { + if (!contentTypeAllowedByList(ct, delegationTypes)) { + throw new Error( + `${label}: allowedContentTypes entry "${ct}" is not permitted by the delegation token.` + ); + } + } +} +function assertNumberSubset(name, optionVal, delegationVal, label, mode) { + if (optionVal === void 0) { + return; + } + if (delegationVal === void 0) { + return; + } + if (mode === "lte" && optionVal > delegationVal) { + throw new Error( + `${label}: ${name} must be \u2264 delegation (${String(delegationVal)}).` + ); + } +} +function isPlausibleAbsoluteUrl(s) { + if (typeof URL !== "undefined" && typeof URL.canParse === "function") { + return URL.canParse(s); + } + try { + new URL(s); + return true; + } catch { + return false; + } +} +function validatePresignUrlOnUploadCompletedWire(opt, label) { + if (!opt) { + return; + } + if (typeof opt.callbackUrl !== "string" || opt.callbackUrl.length === 0) { + throw new Error( + `${label}: onUploadCompleted.callbackUrl must be a non-empty string.` + ); + } + if (opt.callbackUrl.length > MAX_PRESIGN_CALLBACK_URL_CHARS) { + throw new Error(`${label}: onUploadCompleted.callbackUrl is too long.`); + } + if (!isPlausibleAbsoluteUrl(opt.callbackUrl)) { + throw new Error( + `${label}: onUploadCompleted.callbackUrl must be a valid URL.` + ); + } + if (opt.tokenPayload !== void 0 && opt.tokenPayload !== null) { + if (typeof opt.tokenPayload !== "string") { + throw new Error( + `${label}: onUploadCompleted.tokenPayload must be a string.` + ); + } + if (opt.tokenPayload.length > MAX_PRESIGN_CALLBACK_TOKEN_PAYLOAD_CHARS) { + throw new Error(`${label}: onUploadCompleted.tokenPayload is too long.`); + } + } +} +function validateUrlOnlyPresignUploadOptions(urlOptions, label) { + if (urlOptions.cacheControlMaxAge !== void 0) { + const n = urlOptions.cacheControlMaxAge; + if (!Number.isInteger(n) || n < 0 || n > MAX_PRESIGN_CACHE_CONTROL_MAX_AGE_SECONDS) { + throw new Error( + `${label}: cacheControlMaxAge must be an integer between 0 and ${MAX_PRESIGN_CACHE_CONTROL_MAX_AGE_SECONDS}.` + ); + } + } + if (urlOptions.ifMatch !== void 0) { + const im = urlOptions.ifMatch; + if (typeof im !== "string" || im.length === 0) { + throw new Error(`${label}: ifMatch must be a non-empty string.`); + } + if (im.length > MAX_PRESIGN_IF_MATCH_LENGTH) { + throw new Error(`${label}: ifMatch is too long.`); + } + if (IF_MATCH_CONTROL_CHARS_RE.test(im)) { + throw new Error( + `${label}: ifMatch contains disallowed control characters.` + ); + } + } +} +function sortedContentTypesCsv(types) { + return [...types].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join(","); +} +function resolvePresignUrlValidUntilMs(args) { + const { delegationValidUntil, urlOptions, nowMs } = args; + let t; + if ((urlOptions == null ? void 0 : urlOptions.validUntil) !== void 0) { + if (typeof urlOptions.validUntil !== "number" || !Number.isFinite(urlOptions.validUntil)) { + throw new Error("presignUrl: validUntil must be a finite number (ms)."); + } + t = Math.trunc(urlOptions.validUntil); + } else { + t = Math.trunc(delegationValidUntil); + } + if (Number.isFinite(delegationValidUntil)) { + t = Math.min(t, Math.trunc(delegationValidUntil)); + } + if (t <= nowMs) { + throw new Error( + "presignUrl: resolved URL expiry is not after the current time; issue a new delegation token or pass a later validUntil." + ); + } + return t; +} +function buildPresignCanonicalQueryEntries(args) { + const { operation, delegation, urlOptions, nowMs } = args; + const label = "presignUrl"; + const resolvedUntil = resolvePresignUrlValidUntilMs({ + delegationValidUntil: delegation.validUntil, + urlOptions, + nowMs + }); + const delegUntil = Math.trunc(delegation.validUntil); + const entries = []; + if (resolvedUntil < delegUntil) { + entries.push([BLOB_PRESIGN_QUERY_VALID_UNTIL, String(resolvedUntil)]); + } + if (operation === "delete") { + if ((urlOptions == null ? void 0 : urlOptions.ifMatch) !== void 0) { + entries.push([BLOB_PRESIGN_QUERY_IF_MATCH, urlOptions.ifMatch]); + } + return entries; + } + if (operation !== "put" || !urlOptions) { + return entries; + } + assertAllowedContentTypesSubset( + urlOptions.allowedContentTypes, + delegation.allowedContentTypes, + label + ); + assertNumberSubset( + "maximumSizeInBytes", + urlOptions.maximumSizeInBytes, + delegation.maximumSizeInBytes, + label, + "lte" + ); + validateUrlOnlyPresignUploadOptions(urlOptions, label); + validatePresignUrlOnUploadCompletedWire(urlOptions.onUploadCompleted, label); + if (urlOptions.allowedContentTypes !== void 0) { + const csv = sortedContentTypesCsv(urlOptions.allowedContentTypes); + if (csv.length > 16384) { + throw new Error(`${label}: allowedContentTypes query value is too long.`); + } + entries.push([BLOB_PRESIGN_QUERY_ALLOWED_CONTENT_TYPES, csv]); + } + if (urlOptions.maximumSizeInBytes !== void 0) { + entries.push([ + BLOB_PRESIGN_QUERY_MAXIMUM_SIZE, + String(Math.trunc(urlOptions.maximumSizeInBytes)) + ]); + } + if (urlOptions.addRandomSuffix !== void 0) { + entries.push([ + BLOB_PRESIGN_QUERY_ADD_RANDOM_SUFFIX, + urlOptions.addRandomSuffix ? "true" : "false" + ]); + } + if (urlOptions.allowOverwrite !== void 0) { + entries.push([ + BLOB_PRESIGN_QUERY_ALLOW_OVERWRITE, + urlOptions.allowOverwrite ? "true" : "false" + ]); + } + if (urlOptions.cacheControlMaxAge !== void 0) { + entries.push([ + BLOB_PRESIGN_QUERY_CACHE_CONTROL_MAX_AGE, + String(Math.trunc(urlOptions.cacheControlMaxAge)) + ]); + } + if (urlOptions.ifMatch !== void 0) { + entries.push([BLOB_PRESIGN_QUERY_IF_MATCH, urlOptions.ifMatch]); + } + if (urlOptions.onUploadCompleted !== void 0) { + const { callbackUrl, tokenPayload } = urlOptions.onUploadCompleted; + if (callbackUrl.length > MAX_PRESIGN_CALLBACK_URL_CHARS) { + throw new Error(`${label}: onUploadCompleted.callbackUrl is too long.`); + } + entries.push([BLOB_PRESIGN_QUERY_CALLBACK_URL, callbackUrl]); + if (tokenPayload !== void 0 && tokenPayload !== null && tokenPayload !== "") { + if (tokenPayload.length > MAX_PRESIGN_CALLBACK_TOKEN_PAYLOAD_CHARS) { + throw new Error( + `${label}: onUploadCompleted.tokenPayload is too long.` + ); + } + entries.push([BLOB_PRESIGN_QUERY_CALLBACK_TOKEN_PAYLOAD, tokenPayload]); + } + } + return entries; +} +function assertIssueSignedTokenValidUntilOption(validUntil) { + const now = Date.now(); + if (typeof validUntil !== "number" || !Number.isInteger(validUntil) || !Number.isFinite(validUntil)) { + throw new BlobError( + "`issueSignedToken`: validUntil must be an integer milliseconds timestamp." + ); + } + if (validUntil <= now) { + throw new BlobError( + "`issueSignedToken`: validUntil must be in the future." + ); + } +} +async function issueSignedToken(options) { + if (!options) { + throw new BlobError("`issueSignedToken` requires an options object"); + } + const body = {}; + if (options.pathname !== void 0) { + body.pathname = options.pathname; + } + if (options.operations !== void 0) { + if (options.operations.length === 0) { + throw new BlobError("`operations` must be a non-empty array if provided"); + } + body.operations = dedupeOps(options.operations); + } + if (options.validUntil !== void 0) { + assertIssueSignedTokenValidUntilOption(options.validUntil); + body.validUntil = options.validUntil; + } + if (options.maximumSizeInBytes !== void 0) { + body.maximumSizeInBytes = options.maximumSizeInBytes; + } + if (options.allowedContentTypes !== void 0) { + body.allowedContentTypes = options.allowedContentTypes; + } + return requestApi( + "/signed-token", + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + signal: options.abortSignal + }, + options + ); +} +function dedupeOps(operations) { + return Array.from(new Set(operations)); +} +function base64UrlDecodeToString(segment) { + let base64 = segment.replace(/-/g, "+").replace(/_/g, "/"); + const padding = 4 - base64.length % 4; + if (padding !== 4) { + base64 += "=".repeat(padding); + } + if (typeof atob === "function") { + return atob(base64); + } + if (typeof Buffer !== "undefined") { + return Buffer.from(base64, "base64").toString("utf8"); + } + throw new BlobError("Cannot decode base64: no atob or Buffer available."); +} +function tryDecodePayload(delegationToken) { + const dot3 = delegationToken.indexOf("."); + if (dot3 < 0) { + return null; + } + const payloadSeg = delegationToken.slice(0, dot3); + try { + return JSON.parse( + base64UrlDecodeToString(payloadSeg) + ); + } catch { + return null; + } +} +function uint8ToBase64(bytes2) { + if (typeof Buffer !== "undefined") { + return Buffer.from( + bytes2.buffer, + bytes2.byteOffset, + bytes2.byteLength + ).toString("base64"); + } + let s = ""; + for (let i = 0; i < bytes2.length; i++) { + s += String.fromCharCode(bytes2[i]); + } + return btoa(s); +} +async function hmacSha256Base64Url(key, data) { + var _a3; + if (!((_a3 = globalThis.crypto) == null ? void 0 : _a3.subtle)) { + throw new BlobError( + "HMAC is not available: expected globalThis.crypto.subtle (Node 20+ or a modern browser)." + ); + } + const enc = new TextEncoder(); + const cryptoKey = await globalThis.crypto.subtle.importKey( + "raw", + enc.encode(key), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + const buf = await globalThis.crypto.subtle.sign( + "HMAC", + cryptoKey, + enc.encode(data) + ); + return toBase64Url(uint8ToBase64(new Uint8Array(buf))); +} +function toBase64Url(base64) { + return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} +async function presign(signedToken, options) { + var _a3, _b2, _c, _d; + if (!(signedToken == null ? void 0 : signedToken.clientSigningToken) || !(signedToken == null ? void 0 : signedToken.delegationToken)) { + throw new BlobError( + "`clientSigningToken` and `delegationToken` from `issueSignedToken` are required." + ); + } + const scope = tryDecodePayload(signedToken.delegationToken); + if (!scope) { + throw new BlobError("Invalid or unreadable `delegationToken` payload."); + } + const p = scope.pathname; + if (p && p !== "*") { + if (options.pathname !== p) { + throw new BlobError( + `Blob path does not match the signed token scope; expected \`${p}\`, got \`${options.pathname}\`.` + ); + } + } + if (Number.isFinite(scope.validUntil) && Date.now() > scope.validUntil) { + throw new BlobError( + "The signed delegation has expired; issue a new token first." + ); + } + if (options.operation === "get" && !((_a3 = scope.operations) == null ? void 0 : _a3.includes("get"))) { + throw new BlobError( + 'The delegation token is not valid for `GET` requests. Include `"get"` in `operations` when calling `issueSignedToken`.' + ); + } + if (options.operation === "head" && !((_b2 = scope.operations) == null ? void 0 : _b2.includes("head"))) { + throw new BlobError( + 'The delegation token is not valid for `HEAD` requests. Include `"head"` in `operations` when calling `issueSignedToken`.' + ); + } + if (options.operation === "put" && !((_c = scope.operations) == null ? void 0 : _c.includes("put"))) { + throw new BlobError( + 'The delegation token is not valid for presigned write requests. Include `"put"` in `operations` when calling `issueSignedToken`.' + ); + } + if (options.operation === "delete" && !((_d = scope.operations) == null ? void 0 : _d.includes("delete"))) { + throw new BlobError( + 'The delegation token is not valid for presigned delete requests. Include `"delete"` in `operations` when calling `issueSignedToken`.' + ); + } + const delegationForOptions = { + validUntil: scope.validUntil, + maximumSizeInBytes: scope.maximumSizeInBytes, + allowedContentTypes: scope.allowedContentTypes + }; + let presignEntries; + try { + presignEntries = buildPresignCanonicalQueryEntries({ + operation: options.operation, + delegation: delegationForOptions, + urlOptions: options, + nowMs: Date.now() + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + throw new BlobError(msg); + } + const canonical = canonicalString( + options.pathname, + presignEntries, + options.operation + ); + const signature = await hmacSha256Base64Url( + signedToken.clientSigningToken, + canonical + ); + return { + delegationToken: signedToken.delegationToken, + signature, + params: Object.fromEntries(presignEntries) + }; +} +function buildPresignedGetUrl(pathnameOrUrl, presignedUrlPayload, options) { + const storeId = parseStoreIdFromDelegationToken( + presignedUrlPayload.delegationToken + ); + const blobUrl = isUrl(pathnameOrUrl) ? pathnameOrUrl : constructBlobUrl(storeId, pathnameOrUrl, options.access); + return addPresignedParams(blobUrl, presignedUrlPayload); +} +function buildPresignedPutUrl(pathname, presignedUrlPayload) { + const params = new URLSearchParams({ pathname }); + const apiUrl = getApiUrl(`/?${params.toString()}`); + return addPresignedParams(apiUrl, presignedUrlPayload); +} +function buildPresignedDeleteUrl(pathname, presignedUrlPayload) { + const params = new URLSearchParams({ pathname }); + const apiUrl = getApiUrl(`/?${params.toString()}`); + return addPresignedParams(apiUrl, presignedUrlPayload); +} +async function presignUrl(signedToken, options) { + const payload = await presign(signedToken, options); + if (options.operation === "get" || options.operation === "head") { + return { + presignedUrl: buildPresignedGetUrl(options.pathname, payload, options) + }; + } + if (options.operation === "put") { + return { + presignedUrl: buildPresignedPutUrl(options.pathname, payload) + }; + } + if (options.operation === "delete") { + return { + presignedUrl: buildPresignedDeleteUrl(options.pathname, payload) + }; + } + throw new BlobError(`Unknown operation`); +} +function canonicalString(pathname, presignEntries, operation) { + var _a3; + const lines = [`operation=${operation}`, `pathname=${pathname}`]; + for (const k of PRESIGN_CANONICAL_QUERY_KEYS) { + const v = (_a3 = presignEntries.find(([key]) => key === k)) == null ? void 0 : _a3[1]; + if (v) { + lines.push(`${k}=${v}`); + } + } + lines.sort((a, b) => compareUtf8(a, b)); + return lines.join("\n"); +} +function compareUtf8(a, b) { + const ab = utf8Encoder.encode(a); + const bb = utf8Encoder.encode(b); + const n = Math.min(ab.length, bb.length); + for (let i = 0; i < n; i++) { + const d = ab[i] - bb[i]; + if (d !== 0) { + return d; + } + } + return ab.length - bb.length; +} +async function createFolder(pathname, options = { access: "public" }) { + var _a3; + const access = (_a3 = options.access) != null ? _a3 : "public"; + const folderPathname = pathname.endsWith("/") ? pathname : `${pathname}/`; + const headers = {}; + headers[putOptionHeaderMap.access] = access; + headers[putOptionHeaderMap.addRandomSuffix] = "0"; + const params = new URLSearchParams({ pathname: folderPathname }); + const response = await requestApi( + `/?${params.toString()}`, + { + method: "PUT", + headers, + signal: options.abortSignal + }, + options + ); + return { + url: response.url, + pathname: response.pathname + }; +} +var import_is_buffer, import_async_retry, import_undici, import_throttleit, import_throttleit2, supportsNewBlobFromArrayBuffer, SYMBOL_FOR_REQ_CONTEXT, getContext, parseRegExp, map, defaultVercelBlobApiUrl, BlobError, disallowedPathnameCharacters, supportsRequestStreams, TEXT_ENCODER, createChunkTransformStream, addPresignedParams, debugIsActive, _a, _b, _a2, DOMException2, objectToString, isError, errorMessages, hasFetch, hasFetchWithUploadProgress, CHUNK_SIZE, blobFetch, hasXhr, blobXhr, blobRequest, MAXIMUM_PATHNAME_LENGTH, BlobAccessError, BlobOidcEnvironmentNotAllowedError, BlobContentTypeNotAllowedError, BlobPathnameMismatchError, BlobClientTokenExpiredError, BlobFileTooLargeError, BlobStoreNotFoundError, BlobStoreSuspendedError, BlobUnknownError, BlobNotFoundError, BlobServiceNotAvailable, BlobServiceRateLimited, BlobRequestAbortedError, BlobPreconditionFailedError, BLOB_API_VERSION, putOptionHeaderMap, maxConcurrentUploads, partSizeInBytes, maxBytesInMemory, BLOB_PRESIGN_QUERY_VALID_UNTIL, BLOB_PRESIGN_QUERY_MAXIMUM_SIZE, BLOB_PRESIGN_QUERY_ALLOWED_CONTENT_TYPES, BLOB_PRESIGN_QUERY_ADD_RANDOM_SUFFIX, BLOB_PRESIGN_QUERY_ALLOW_OVERWRITE, BLOB_PRESIGN_QUERY_CACHE_CONTROL_MAX_AGE, BLOB_PRESIGN_QUERY_IF_MATCH, BLOB_PRESIGN_QUERY_CALLBACK_URL, BLOB_PRESIGN_QUERY_CALLBACK_TOKEN_PAYLOAD, PRESIGN_CANONICAL_QUERY_KEYS, MAX_PRESIGN_CALLBACK_URL_CHARS, MAX_PRESIGN_CALLBACK_TOKEN_PAYLOAD_CHARS, MAX_PRESIGN_CACHE_CONTROL_MAX_AGE_SECONDS, MAX_PRESIGN_IF_MATCH_LENGTH, IF_MATCH_CONTROL_CHARS_RE, utf8Encoder; +var init_chunk_3D2SZ6M2 = __esm({ + "viewer/node_modules/@vercel/blob/dist/chunk-3D2SZ6M2.js"() { + init_lib(); + import_is_buffer = __toESM(require_is_buffer(), 1); + import_async_retry = __toESM(require_lib(), 1); + import_undici = __toESM(require_undici(), 1); + import_throttleit = __toESM(require_throttleit(), 1); + import_throttleit2 = __toESM(require_throttleit(), 1); + supportsNewBlobFromArrayBuffer = new Promise((resolve) => { + try { + const helloAsArrayBuffer = new Uint8Array([104, 101, 108, 108, 111]); + const blob = new Blob([helloAsArrayBuffer]); + blob.text().then((text) => { + resolve(text === "hello"); + }).catch(() => { + resolve(false); + }); + } catch { + resolve(false); + } + }); + SYMBOL_FOR_REQ_CONTEXT = /* @__PURE__ */ Symbol.for("@vercel/request-context"); + getContext = () => { + var _a3, _b2, _c; + const fromSymbol = globalThis; + return (_c = (_b2 = (_a3 = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]) == null ? void 0 : _a3.get) == null ? void 0 : _b2.call(_a3)) != null ? _c : {}; + }; + parseRegExp = /^((-|\+)?(\d+(?:\.\d+)?)) *(kb|mb|gb|tb|pb)$/i; + map = { + b: 1, + kb: 1 << 10, + mb: 1 << 20, + gb: 1 << 30, + tb: 1024 ** 4, + pb: 1024 ** 5 + }; + defaultVercelBlobApiUrl = "https://vercel.com/api/blob"; + BlobError = class extends Error { + constructor(message) { + super(`Vercel Blob: ${message}`); + } + }; + disallowedPathnameCharacters = ["//"]; + supportsRequestStreams = (() => { + if (isNodeProcess()) { + return true; + } + const apiUrl = getApiUrl(); + if (apiUrl.startsWith("http://localhost")) { + return false; + } + let duplexAccessed = false; + const hasContentType = new Request(getApiUrl(), { + body: new ReadableStream(), + method: "POST", + // @ts-expect-error -- TypeScript doesn't yet have duplex but it's in the spec: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1729 + get duplex() { + duplexAccessed = true; + return "half"; + } + }).headers.has("Content-Type"); + return duplexAccessed && !hasContentType; + })(); + TEXT_ENCODER = typeof TextEncoder === "function" ? new TextEncoder() : null; + createChunkTransformStream = (chunkSize, onProgress) => { + let buffer = new Uint8Array(0); + return new TransformStream({ + transform(chunk, controller) { + const newBuffer = new Uint8Array(buffer.length + chunk.byteLength); + newBuffer.set(buffer); + newBuffer.set(new Uint8Array(chunk), buffer.length); + buffer = newBuffer; + while (buffer.length >= chunkSize) { + const newChunk = buffer.slice(0, chunkSize); + controller.enqueue(newChunk); + onProgress == null ? void 0 : onProgress(newChunk.byteLength); + buffer = buffer.slice(chunkSize); + } + }, + flush(controller) { + if (buffer.length > 0) { + controller.enqueue(buffer); + onProgress == null ? void 0 : onProgress(buffer.byteLength); + } + } + }); + }; + addPresignedParams = (url, presignedUrlPayload) => { + const urlObj = new URL(url); + for (const [key, value] of Object.entries(presignedUrlPayload.params)) { + urlObj.searchParams.set(key, value); + } + urlObj.searchParams.set( + "vercel-blob-delegation", + presignedUrlPayload.delegationToken + ); + urlObj.searchParams.set( + "vercel-blob-signature", + presignedUrlPayload.signature + ); + return urlObj.toString(); + }; + debugIsActive = false; + try { + if (((_a = process.env.DEBUG) == null ? void 0 : _a.includes("blob")) || ((_b = process.env.NEXT_PUBLIC_DEBUG) == null ? void 0 : _b.includes("blob"))) { + debugIsActive = true; + } + } catch { + } + DOMException2 = (_a2 = globalThis.DOMException) != null ? _a2 : (() => { + try { + atob("~"); + } catch (err) { + return Object.getPrototypeOf(err).constructor; + } + })(); + objectToString = Object.prototype.toString; + isError = (value) => objectToString.call(value) === "[object Error]"; + errorMessages = /* @__PURE__ */ new Set([ + "network error", + // Chrome + "Failed to fetch", + // Chrome + "NetworkError when attempting to fetch resource.", + // Firefox + "The Internet connection appears to be offline.", + // Safari 16 + "Load failed", + // Safari 17+ + "Network request failed", + // `cross-fetch` + "fetch failed", + // Undici (Node.js) + "terminated" + // Undici (Node.js) + ]); + hasFetch = typeof import_undici.fetch === "function"; + hasFetchWithUploadProgress = hasFetch && supportsRequestStreams; + CHUNK_SIZE = 64 * 1024; + blobFetch = async ({ + input, + init, + onUploadProgress + }) => { + debug("using fetch"); + let body; + if (init.body) { + if (onUploadProgress) { + const stream = await toReadableStream(init.body); + let loaded = 0; + const chunkTransformStream = createChunkTransformStream( + CHUNK_SIZE, + (newLoaded) => { + loaded += newLoaded; + onUploadProgress(loaded); + } + ); + body = stream.pipeThrough(chunkTransformStream); + } else { + body = init.body; + } + } + const duplex = supportsRequestStreams && body && isStream(body) ? "half" : void 0; + return (0, import_undici.fetch)( + input, + // @ts-expect-error -- Blob and Nodejs Blob are triggering type errors, fine with it + { + ...init, + ...init.body ? { body } : {}, + duplex + } + ); + }; + hasXhr = typeof XMLHttpRequest !== "undefined"; + blobXhr = async ({ + input, + init, + onUploadProgress + }) => { + debug("using xhr"); + let body = null; + if (init.body) { + if (isReadableStream(init.body)) { + body = await new Response(init.body).blob(); + } else { + body = init.body; + } + } + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open(init.method || "GET", input.toString(), true); + if (onUploadProgress) { + xhr.upload.addEventListener("progress", (event) => { + if (event.lengthComputable) { + onUploadProgress(event.loaded); + } + }); + } + xhr.onload = () => { + var _a3; + if ((_a3 = init.signal) == null ? void 0 : _a3.aborted) { + reject(new DOMException("The user aborted the request.", "AbortError")); + return; + } + const headers = new Headers(); + const rawHeaders = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/); + rawHeaders.forEach((line) => { + const parts = line.split(": "); + const key = parts.shift(); + const value = parts.join(": "); + if (key) headers.set(key.toLowerCase(), value); + }); + const response = new Response(xhr.response, { + status: xhr.status, + statusText: xhr.statusText, + headers + }); + resolve(response); + }; + xhr.onerror = () => { + reject(new TypeError("Network request failed")); + }; + xhr.ontimeout = () => { + reject(new TypeError("Network request timed out")); + }; + xhr.onabort = () => { + reject(new DOMException("The user aborted a request.", "AbortError")); + }; + if (init.headers) { + const headers = new Headers(init.headers); + headers.forEach((value, key) => { + xhr.setRequestHeader(key, value); + }); + } + if (init.signal) { + init.signal.addEventListener("abort", () => { + xhr.abort(); + }); + if (init.signal.aborted) { + xhr.abort(); + return; + } + } + xhr.send(body); + }); + }; + blobRequest = async ({ + input, + init, + onUploadProgress + }) => { + if (onUploadProgress) { + if (hasFetchWithUploadProgress) { + return blobFetch({ input, init, onUploadProgress }); + } + if (hasXhr) { + return blobXhr({ input, init, onUploadProgress }); + } + } + if (hasFetch) { + return blobFetch({ input, init }); + } + if (hasXhr) { + return blobXhr({ input, init }); + } + throw new Error("No request implementation available"); + }; + MAXIMUM_PATHNAME_LENGTH = 950; + BlobAccessError = class extends BlobError { + constructor() { + super("Access denied, please provide a valid token for this resource."); + } + }; + BlobOidcEnvironmentNotAllowedError = class extends BlobError { + constructor(message) { + super( + message != null ? message : "OIDC is enabled for this project, but not for this token's environment." + ); + } + }; + BlobContentTypeNotAllowedError = class extends BlobError { + constructor(message) { + super(`Content type mismatch, ${message}.`); + } + }; + BlobPathnameMismatchError = class extends BlobError { + constructor(message) { + super( + `Pathname mismatch, ${message}. Check the pathname used in upload() or put() matches the one from the client token.` + ); + } + }; + BlobClientTokenExpiredError = class extends BlobError { + constructor() { + super("Client token has expired."); + } + }; + BlobFileTooLargeError = class extends BlobError { + constructor(message) { + super(`File is too large, ${message}.`); + } + }; + BlobStoreNotFoundError = class extends BlobError { + constructor() { + super("This store does not exist."); + } + }; + BlobStoreSuspendedError = class extends BlobError { + constructor() { + super("This store has been suspended."); + } + }; + BlobUnknownError = class extends BlobError { + constructor() { + super("Unknown error, please visit https://vercel.com/help."); + } + }; + BlobNotFoundError = class extends BlobError { + constructor() { + super("The requested blob does not exist"); + } + }; + BlobServiceNotAvailable = class extends BlobError { + constructor() { + super("The blob service is currently not available. Please try again."); + } + }; + BlobServiceRateLimited = class extends BlobError { + constructor(seconds) { + super( + `Too many requests please lower the number of concurrent requests ${seconds ? ` - try again in ${seconds} seconds` : ""}.` + ); + this.retryAfter = seconds != null ? seconds : 0; + } + }; + BlobRequestAbortedError = class extends BlobError { + constructor() { + super("The request was aborted."); + } + }; + BlobPreconditionFailedError = class extends BlobError { + constructor() { + super("Precondition failed: ETag mismatch."); + } + }; + BLOB_API_VERSION = 12; + putOptionHeaderMap = { + cacheControlMaxAge: "x-cache-control-max-age", + addRandomSuffix: "x-add-random-suffix", + allowOverwrite: "x-allow-overwrite", + contentType: "x-content-type", + access: "x-vercel-blob-access", + ifMatch: "x-if-match" + }; + maxConcurrentUploads = typeof window !== "undefined" ? 6 : 8; + partSizeInBytes = 8 * 1024 * 1024; + maxBytesInMemory = maxConcurrentUploads * partSizeInBytes * 2; + BLOB_PRESIGN_QUERY_VALID_UNTIL = "vercel-blob-valid-until"; + BLOB_PRESIGN_QUERY_MAXIMUM_SIZE = "vercel-blob-maximum-size-in-bytes"; + BLOB_PRESIGN_QUERY_ALLOWED_CONTENT_TYPES = "vercel-blob-allowed-content-types"; + BLOB_PRESIGN_QUERY_ADD_RANDOM_SUFFIX = "vercel-blob-add-random-suffix"; + BLOB_PRESIGN_QUERY_ALLOW_OVERWRITE = "vercel-blob-allow-overwrite"; + BLOB_PRESIGN_QUERY_CACHE_CONTROL_MAX_AGE = "vercel-blob-cache-control-max-age"; + BLOB_PRESIGN_QUERY_IF_MATCH = "vercel-blob-if-match"; + BLOB_PRESIGN_QUERY_CALLBACK_URL = "vercel-blob-callback-url"; + BLOB_PRESIGN_QUERY_CALLBACK_TOKEN_PAYLOAD = "vercel-blob-callback-token-payload"; + PRESIGN_CANONICAL_QUERY_KEYS = [ + BLOB_PRESIGN_QUERY_ADD_RANDOM_SUFFIX, + BLOB_PRESIGN_QUERY_ALLOW_OVERWRITE, + BLOB_PRESIGN_QUERY_ALLOWED_CONTENT_TYPES, + BLOB_PRESIGN_QUERY_CACHE_CONTROL_MAX_AGE, + BLOB_PRESIGN_QUERY_CALLBACK_TOKEN_PAYLOAD, + BLOB_PRESIGN_QUERY_CALLBACK_URL, + BLOB_PRESIGN_QUERY_IF_MATCH, + BLOB_PRESIGN_QUERY_MAXIMUM_SIZE, + BLOB_PRESIGN_QUERY_VALID_UNTIL + ]; + MAX_PRESIGN_CALLBACK_URL_CHARS = 4096; + MAX_PRESIGN_CALLBACK_TOKEN_PAYLOAD_CHARS = 8192; + MAX_PRESIGN_CACHE_CONTROL_MAX_AGE_SECONDS = 365 * 24 * 60 * 60; + MAX_PRESIGN_IF_MATCH_LENGTH = 256; + IF_MATCH_CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/; + utf8Encoder = new TextEncoder(); + } +}); + +// viewer/node_modules/@vercel/blob/dist/index.js +var dist_exports = {}; +__export(dist_exports, { + BlobAccessError: () => BlobAccessError, + BlobClientTokenExpiredError: () => BlobClientTokenExpiredError, + BlobContentTypeNotAllowedError: () => BlobContentTypeNotAllowedError, + BlobError: () => BlobError, + BlobFileTooLargeError: () => BlobFileTooLargeError, + BlobNotFoundError: () => BlobNotFoundError, + BlobPathnameMismatchError: () => BlobPathnameMismatchError, + BlobPreconditionFailedError: () => BlobPreconditionFailedError, + BlobRequestAbortedError: () => BlobRequestAbortedError, + BlobServiceNotAvailable: () => BlobServiceNotAvailable, + BlobServiceRateLimited: () => BlobServiceRateLimited, + BlobStoreNotFoundError: () => BlobStoreNotFoundError, + BlobStoreSuspendedError: () => BlobStoreSuspendedError, + BlobUnknownError: () => BlobUnknownError, + completeMultipartUpload: () => completeMultipartUpload2, + copy: () => copy, + createFolder: () => createFolder, + createMultipartUpload: () => createMultipartUpload2, + createMultipartUploader: () => createMultipartUploader, + del: () => del, + get: () => get, + getDownloadUrl: () => getDownloadUrl, + head: () => head, + issueSignedToken: () => issueSignedToken, + list: () => list, + parseStoreIdFromDelegationToken: () => parseStoreIdFromDelegationToken, + parseStoreIdFromPresignedUrl: () => parseStoreIdFromPresignedUrl, + presignUrl: () => presignUrl, + put: () => put, + uploadPart: () => uploadPart2 +}); +async function del(urlOrPathname, options) { + const urls = Array.isArray(urlOrPathname) ? urlOrPathname : [urlOrPathname]; + if ((options == null ? void 0 : options.ifMatch) && urls.length > 1) { + throw new BlobError("ifMatch can only be used when deleting a single URL."); + } + const headers = { + "content-type": "application/json" + }; + if (options == null ? void 0 : options.ifMatch) { + headers["x-if-match"] = options.ifMatch; + } + await requestApi( + "/delete", + { + method: "POST", + headers, + body: JSON.stringify({ urls }), + signal: options == null ? void 0 : options.abortSignal + }, + options + ); +} +async function head(urlOrPathname, options) { + const searchParams = new URLSearchParams({ url: urlOrPathname }); + const response = await requestApi( + `?${searchParams.toString()}`, + // HEAD can't have body as a response, so we use GET + { + method: "GET", + signal: options == null ? void 0 : options.abortSignal + }, + options + ); + return { + url: response.url, + downloadUrl: response.downloadUrl, + pathname: response.pathname, + size: response.size, + contentType: response.contentType, + contentDisposition: response.contentDisposition, + cacheControl: response.cacheControl, + uploadedAt: new Date(response.uploadedAt), + etag: response.etag + }; +} +function extractPathnameFromUrl(url) { + try { + const parsedUrl = new URL(url); + return parsedUrl.pathname.slice(1); + } catch { + return url; + } +} +async function get(urlOrPathname, options) { + if (!urlOrPathname) { + throw new BlobError("url or pathname is required"); + } + if (!options) { + throw new BlobError("missing options, see usage"); + } + if (options.access !== "public" && options.access !== "private") { + throw new BlobError( + 'access must be "private" or "public", see https://vercel.com/docs/vercel-blob' + ); + } + const auth = resolveBlobAuth(options); + if (auth.kind === "presigned") { + throw new BlobError("Presigned URLs are not supported for the get method"); + } + let blobUrl; + let pathname; + const access = options.access; + if (isUrl(urlOrPathname)) { + blobUrl = urlOrPathname; + pathname = extractPathnameFromUrl(urlOrPathname); + try { + const { hostname } = new URL(blobUrl); + if (!hostname.endsWith(".blob.vercel-storage.com")) { + throw new BlobError( + "Invalid URL: the URL does not point to a Vercel Blob store. Use a pathname instead, see https://vercel.com/docs/vercel-blob" + ); + } + } catch (error) { + if (error instanceof BlobError) throw error; + throw new BlobError("Invalid URL: unable to parse the provided URL"); + } + } else { + if (!auth.storeId) { + throw new BlobError("Invalid token: unable to extract store ID"); + } + pathname = urlOrPathname; + blobUrl = constructBlobUrl(auth.storeId, pathname, access); + } + const requestHeaders = { + ...options.ifNoneMatch ? { "If-None-Match": options.ifNoneMatch } : {}, + authorization: `Bearer ${auth.token}`, + ...options.headers + // low-level escape hatch, applied last to override anything + }; + let fetchUrl = blobUrl; + if (options.useCache === false) { + const url = new URL(blobUrl); + url.searchParams.set("cache", "0"); + fetchUrl = url.toString(); + } + const response = await (0, import_undici2.fetch)(fetchUrl, { + method: "GET", + headers: requestHeaders, + signal: options.abortSignal + }); + if (response.status === 304) { + const downloadUrlObj = new URL(blobUrl); + downloadUrlObj.searchParams.set("download", "1"); + const lastModified2 = response.headers.get("last-modified"); + return { + statusCode: 304, + stream: null, + headers: response.headers, + blob: { + url: blobUrl, + downloadUrl: downloadUrlObj.toString(), + pathname, + contentType: null, + contentDisposition: response.headers.get("content-disposition") || "", + cacheControl: response.headers.get("cache-control") || "", + size: null, + uploadedAt: lastModified2 ? new Date(lastModified2) : /* @__PURE__ */ new Date(), + etag: response.headers.get("etag") || "" + } + }; + } + if (response.status === 404) { + return null; + } + if (!response.ok) { + throw new BlobError( + `Failed to fetch blob: ${response.status} ${response.statusText}` + ); + } + const stream = response.body; + if (!stream) { + throw new BlobError("Response body is null"); + } + const contentLength = response.headers.get("content-length"); + const lastModified = response.headers.get("last-modified"); + const downloadUrl = new URL(blobUrl); + downloadUrl.searchParams.set("download", "1"); + return { + statusCode: 200, + stream, + headers: response.headers, + blob: { + url: blobUrl, + downloadUrl: downloadUrl.toString(), + pathname, + contentType: response.headers.get("content-type") || "application/octet-stream", + contentDisposition: response.headers.get("content-disposition") || "", + cacheControl: response.headers.get("cache-control") || "", + size: contentLength ? parseInt(contentLength, 10) : 0, + uploadedAt: lastModified ? new Date(lastModified) : /* @__PURE__ */ new Date(), + etag: response.headers.get("etag") || "" + } + }; +} +async function list(options) { + var _a3; + const searchParams = new URLSearchParams(); + if (options == null ? void 0 : options.limit) { + searchParams.set("limit", options.limit.toString()); + } + if (options == null ? void 0 : options.prefix) { + searchParams.set("prefix", options.prefix); + } + if (options == null ? void 0 : options.cursor) { + searchParams.set("cursor", options.cursor); + } + if (options == null ? void 0 : options.mode) { + searchParams.set("mode", options.mode); + } + const response = await requestApi( + `?${searchParams.toString()}`, + { + method: "GET", + signal: options == null ? void 0 : options.abortSignal + }, + options + ); + if ((options == null ? void 0 : options.mode) === "folded") { + return { + folders: (_a3 = response.folders) != null ? _a3 : [], + cursor: response.cursor, + hasMore: response.hasMore, + blobs: response.blobs.map(mapBlobResult) + }; + } + return { + cursor: response.cursor, + hasMore: response.hasMore, + blobs: response.blobs.map(mapBlobResult) + }; +} +function mapBlobResult(blobResult) { + return { + url: blobResult.url, + downloadUrl: blobResult.downloadUrl, + pathname: blobResult.pathname, + size: blobResult.size, + uploadedAt: new Date(blobResult.uploadedAt), + etag: blobResult.etag + }; +} +async function copy(fromUrlOrPathname, toPathname, options) { + if (!options) { + throw new BlobError("missing options, see usage"); + } + if (options.access !== "public" && options.access !== "private") { + throw new BlobError( + 'access must be "private" or "public", see https://vercel.com/docs/vercel-blob' + ); + } + if (toPathname.length > MAXIMUM_PATHNAME_LENGTH) { + throw new BlobError( + `pathname is too long, maximum length is ${MAXIMUM_PATHNAME_LENGTH}` + ); + } + for (const invalidCharacter of disallowedPathnameCharacters) { + if (toPathname.includes(invalidCharacter)) { + throw new BlobError( + `pathname cannot contain "${invalidCharacter}", please encode it if needed` + ); + } + } + const headers = {}; + headers["x-vercel-blob-access"] = options.access; + if (options.addRandomSuffix !== void 0) { + headers["x-add-random-suffix"] = options.addRandomSuffix ? "1" : "0"; + } + if (options.allowOverwrite !== void 0) { + headers["x-allow-overwrite"] = options.allowOverwrite ? "1" : "0"; + } + if (options.contentType) { + headers["x-content-type"] = options.contentType; + } + if (options.cacheControlMaxAge !== void 0) { + headers["x-cache-control-max-age"] = options.cacheControlMaxAge.toString(); + } + if (options.ifMatch) { + headers["x-if-match"] = options.ifMatch; + } + const params = new URLSearchParams({ + pathname: toPathname, + fromUrl: fromUrlOrPathname + }); + const response = await requestApi( + `?${params.toString()}`, + { + method: "PUT", + headers, + signal: options.abortSignal + }, + options + ); + return { + url: response.url, + downloadUrl: response.downloadUrl, + pathname: response.pathname, + contentType: response.contentType, + contentDisposition: response.contentDisposition, + etag: response.etag + }; +} +var import_undici2, put, createMultipartUpload2, createMultipartUploader, uploadPart2, completeMultipartUpload2; +var init_dist = __esm({ + "viewer/node_modules/@vercel/blob/dist/index.js"() { + init_chunk_3D2SZ6M2(); + import_undici2 = __toESM(require_undici(), 1); + put = createPutMethod({ + allowedOptions: [ + "cacheControlMaxAge", + "addRandomSuffix", + "allowOverwrite", + "contentType", + "ifMatch" + ] + }); + createMultipartUpload2 = createCreateMultipartUploadMethod({ + allowedOptions: [ + "cacheControlMaxAge", + "addRandomSuffix", + "allowOverwrite", + "contentType", + "ifMatch" + ] + }); + createMultipartUploader = createCreateMultipartUploaderMethod({ + allowedOptions: [ + "cacheControlMaxAge", + "addRandomSuffix", + "allowOverwrite", + "contentType", + "ifMatch" + ] + }); + uploadPart2 = createUploadPartMethod({ + allowedOptions: [ + "cacheControlMaxAge", + "addRandomSuffix", + "allowOverwrite", + "contentType" + ] + }); + completeMultipartUpload2 = createCompleteMultipartUploadMethod({ + allowedOptions: [ + "cacheControlMaxAge", + "addRandomSuffix", + "allowOverwrite", + "contentType" + ] + }); + } +}); + +// viewer/src/server/server.mjs +import fs9 from "node:fs"; +import http from "node:http"; +import path13 from "node:path"; +import { fileURLToPath as fileURLToPath2 } from "node:url"; + +// viewer/src/server/localAssetBackend.mjs +import fs7 from "node:fs"; +import path8 from "node:path"; +import { spawn as spawn2 } from "node:child_process"; + +// viewer/src/server/catalog/cadDirectoryScanner.mjs +import crypto from "node:crypto"; +import fs2 from "node:fs"; +import path3 from "node:path"; + +// viewer/packages/cadjs/src/common/stepSidecars.mjs +import path from "node:path"; +function isPerStepViewerDirectoryName(name) { + const normalized = String(name || "").toLowerCase(); + return normalized.startsWith(".") && (normalized.endsWith(".step") || normalized.endsWith(".stp")); +} +function isInlineStepGlbArtifactPath(filePath) { + const name = path.basename(String(filePath || "")).toLowerCase(); + return name.startsWith(".") && (name.endsWith(".step.glb") || name.endsWith(".stp.glb")); +} +function isInlineStepParameterPath(filePath) { + const name = path.basename(String(filePath || "")).toLowerCase(); + return name.startsWith(".") && name.endsWith(".step.js"); +} +function isPathInsidePerStepViewerDirectory(filePath) { + return String(filePath || "").split(path.sep).some((part) => isPerStepViewerDirectoryName(part)); +} +function inlineStepGlbArtifactPathForSource(sourcePath) { + return path.join(path.dirname(sourcePath), `.${path.basename(sourcePath)}.glb`); +} +function stepParameterPathForStepSource(sourcePath) { + const stem = path.basename(sourcePath, path.extname(sourcePath)); + return path.join(path.dirname(sourcePath), `.${stem}.step.js`); +} + +// viewer/packages/cadjs/src/common/stepTopology.mjs +var STEP_TOPOLOGY_EXTENSION = "STEP_topology"; +var STEP_TOPOLOGY_SCHEMA_VERSION = 2; +var STEP_EDGE_BARYCENTRIC_ATTRIBUTE = "_CAD_EDGE_BARYCENTRIC"; +var STEP_EDGE_CLASS_ATTRIBUTE = "_CAD_EDGE_CLASS"; +var STEP_EDGE_FLAGS = Object.freeze({ + DEGENERATE: 1 << 1, + SEAM: 1 << 2, + NOT_REFERENCEABLE: 1 << 3, + BOUNDARY: 1 << 4, + NON_MANIFOLD: 1 << 5, + HARD: 1 << 6, + TANGENT: 1 << 7, + UNKNOWN_CONTINUITY: 1 << 8 +}); +var STEP_EDGE_VISIBILITY_CLASSES = Object.freeze({ + FEATURE: "feature", + TANGENT: "tangent", + SEAM: "seam", + DEGENERATE: "degenerate", + BOUNDARY: "boundary", + NON_MANIFOLD: "nonManifold", + UNKNOWN: "unknown" +}); +var STEP_EDGE_SURFACE_CLASS_CODES = Object.freeze({ + none: 0, + feature: 1, + tangent: 2, + seam: 3, + degenerate: 4, + boundary: 5, + nonManifold: 6, + unknown: 7 +}); +var STEP_EDGE_RENDER_VISIBILITY_CLASSES = Object.freeze([ + STEP_EDGE_VISIBILITY_CLASSES.FEATURE, + STEP_EDGE_VISIBILITY_CLASSES.TANGENT, + STEP_EDGE_VISIBILITY_CLASSES.SEAM, + STEP_EDGE_VISIBILITY_CLASSES.DEGENERATE +]); +function isCurrentStepTopologySchemaVersion(value) { + return Number(value) === STEP_TOPOLOGY_SCHEMA_VERSION; +} + +// viewer/packages/cadjs/src/lib/pathUtils.mjs +import path2 from "node:path"; +function toPosixPath(value) { + return String(value || "").split(path2.sep).join("/"); +} +function relativePathStaysInsideRoot(relativePath) { + return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path2.sep}`) && !path2.isAbsolute(relativePath); +} +function pathIsInside(childPath, parentPath) { + const relativePath = path2.relative(path2.resolve(parentPath), path2.resolve(childPath)); + return Boolean(relativePath) && relativePathStaysInsideRoot(relativePath); +} + +// viewer/src/server/step/stepMetadata.mjs +import fs from "node:fs"; +var TEXT_TO_CAD_GENERATOR_PROPERTY = "cadpy:generator"; +var TEXT_TO_CAD_ENTRY_KIND_PROPERTY = "cadpy:entryKind"; +var TEXT_TO_CAD_SOURCE_PATH_PROPERTY = "cadpy:sourcePath"; +var TEXT_TO_CAD_SOURCE_HASH_PROPERTY = "cadpy:sourceHash"; +var STEP_STRING_PATTERN = "'(?:''|[^'])*'"; +function unescapeStepString(value) { + const raw = String(value || "").trim(); + const unquoted = raw.startsWith("'") && raw.endsWith("'") ? raw.slice(1, -1) : raw; + return unquoted.replace(/''/g, "'"); +} +function normalizeTextToCadEntryKind(value) { + const normalized = String(value || "").trim().toLowerCase(); + return normalized === "part" || normalized === "assembly" ? normalized : ""; +} +function readTextToCadStepMetadataText(stepText) { + const text = String(stepText || ""); + const descriptiveItems = /* @__PURE__ */ new Map(); + const descriptivePattern = new RegExp( + `#(\\d+)\\s*=\\s*DESCRIPTIVE_REPRESENTATION_ITEM\\s*\\(\\s*(${STEP_STRING_PATTERN})\\s*,\\s*(${STEP_STRING_PATTERN})\\s*\\)\\s*;`, + "gis" + ); + for (const match of text.matchAll(descriptivePattern)) { + descriptiveItems.set(`#${match[1]}`, { + value: unescapeStepString(match[3]) + }); + } + const representations = /* @__PURE__ */ new Map(); + const representationPattern = new RegExp( + `#(\\d+)\\s*=\\s*REPRESENTATION\\s*\\(\\s*(${STEP_STRING_PATTERN})\\s*,\\s*\\(([^)]*)\\)\\s*,\\s*#\\d+\\s*\\)\\s*;`, + "gis" + ); + for (const match of text.matchAll(representationPattern)) { + representations.set(`#${match[1]}`, { + itemRefs: Array.from(String(match[3] || "").matchAll(/#\d+/g), (itemMatch) => itemMatch[0]) + }); + } + const propertyDefinitions = /* @__PURE__ */ new Map(); + const propertyPattern = new RegExp( + `#(\\d+)\\s*=\\s*PROPERTY_DEFINITION\\s*\\(\\s*(${STEP_STRING_PATTERN})\\s*,\\s*(${STEP_STRING_PATTERN})\\s*,\\s*#[0-9]+\\s*\\)\\s*;`, + "gis" + ); + for (const match of text.matchAll(propertyPattern)) { + const propertyName = unescapeStepString(match[3]); + if (propertyName === TEXT_TO_CAD_GENERATOR_PROPERTY || propertyName === TEXT_TO_CAD_ENTRY_KIND_PROPERTY || propertyName === TEXT_TO_CAD_SOURCE_PATH_PROPERTY || propertyName === TEXT_TO_CAD_SOURCE_HASH_PROPERTY || propertyName === "cadpy:entry_kind") { + propertyDefinitions.set(`#${match[1]}`, propertyName); + } + } + const metadata = {}; + const linkPattern = /#\d+\s*=\s*PROPERTY_DEFINITION_REPRESENTATION\s*\(\s*(#\d+)\s*,\s*(#\d+)\s*\)\s*;/gis; + for (const match of text.matchAll(linkPattern)) { + const propertyName = propertyDefinitions.get(match[1]); + const representation = representations.get(match[2]); + if (!propertyName || !representation) { + continue; + } + const item = representation.itemRefs.map((itemRef) => descriptiveItems.get(itemRef)).find(Boolean); + if (!item) { + continue; + } + if (propertyName === TEXT_TO_CAD_GENERATOR_PROPERTY) { + metadata.generator = item.value; + } else if (propertyName === TEXT_TO_CAD_ENTRY_KIND_PROPERTY || propertyName === "cadpy:entry_kind") { + const entryKind = normalizeTextToCadEntryKind(item.value); + if (entryKind) { + metadata.entryKind = entryKind; + } + } else if (propertyName === TEXT_TO_CAD_SOURCE_HASH_PROPERTY) { + metadata.sourceHash = item.value; + } else if (propertyName === TEXT_TO_CAD_SOURCE_PATH_PROPERTY) { + metadata.sourcePath = item.value; + } + } + return metadata; +} +function readTextToCadStepMetadataFile(stepPath) { + return readTextToCadStepMetadataText(fs.readFileSync(stepPath, "utf-8")); +} + +// viewer/src/server/catalog/cadDirectoryScanner.mjs +var DEFAULT_VIEWER_ROOT_DIR = ""; +var CAD_CATALOG_SCHEMA_VERSION = 4; +var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".step", ".stp", ".stl", ".3mf", ".glb", ".gcode", ".dxf", ".urdf", ".srdf", ".sdf"]); +var IMPLICIT_CAD_EXTENSIONS = Object.freeze([".implicit.js", ".implicit.mjs"]); +var REGENERATE_STEP_COMMAND = "python -m cadpy.step_artifact --repo-root . --step"; +var VIEWER_SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([ + ".agents", + ".cache", + ".viewer", + ".git", + ".venv", + "__pycache__", + "build", + "coverage", + "dist", + "node_modules", + "viewer" +]); +var SRDF_URDF_METADATA_PATTERN = /<\s*(?:tcad|explorer):urdf\b[^>]*\bpath\s*=\s*["']([^"']+)["'][^>]*>/i; +var STEP_EDGE_RENDER_CLASS_ORDER = Object.freeze(["feature", "tangent", "seam", "degenerate"]); +var TEXT_TO_CAD_COMMENT_METADATA_RE = //g; +var PYTHON_GENERATOR_BY_KIND = Object.freeze({ + dxf: "gen_dxf", + step: "gen_step", + stp: "gen_step", + urdf: "gen_urdf", + srdf: "gen_srdf", + sdf: "gen_sdf" +}); +function encodeUrlPath(repoRelativePath2) { + return `/${repoRelativePath2.split("/").map((part) => encodeURIComponent(part)).join("/")}`; +} +function pathIsImplicitCadSource(value = "") { + const pathname = String(value || "").split(/[?#]/, 1)[0].toLowerCase(); + return IMPLICIT_CAD_EXTENSIONS.some((extension) => pathname.endsWith(extension)); +} +function relativePathStaysInsideRoot2(relativePath) { + return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path3.sep}`) && !path3.isAbsolute(relativePath); +} +function normalizeStepEdgeRenderVisibilityClasses(value) { + const rawValues = Array.isArray(value) ? value : value === void 0 || value === null ? [] : [value]; + const validValues = new Set(Object.values(STEP_EDGE_VISIBILITY_CLASSES)); + const normalized = []; + for (const raw of rawValues) { + const classId = String(raw || "").trim(); + if (validValues.has(classId) && !normalized.includes(classId)) { + normalized.push(classId); + } + } + if (!normalized.includes(STEP_EDGE_VISIBILITY_CLASSES.FEATURE)) { + normalized.unshift(STEP_EDGE_VISIBILITY_CLASSES.FEATURE); + } + return [ + ...STEP_EDGE_RENDER_CLASS_ORDER.filter((classId) => normalized.includes(classId)), + ...normalized.filter((classId) => !STEP_EDGE_RENDER_CLASS_ORDER.includes(classId)) + ]; +} +function normalizeViewerRootDir(value = DEFAULT_VIEWER_ROOT_DIR) { + const rawValue = String(value ?? "").trim(); + const slashNormalized = rawValue.replace(/\\/g, "/"); + const normalized = path3.posix.normalize(slashNormalized); + if (!normalized || normalized === ".") { + return DEFAULT_VIEWER_ROOT_DIR; + } + if (normalized === ".." || normalized.startsWith("../")) { + throw new Error(`CAD Viewer root directory must stay inside the directory root: ${rawValue}`); + } + return normalized.replace(/(?!^\/)\/+$/, ""); +} +function resolveViewerRoot(repoRoot, rootDir = DEFAULT_VIEWER_ROOT_DIR) { + const normalizedDir = normalizeViewerRootDir(rootDir); + const resolvedRepoRoot = path3.resolve(repoRoot); + const rootPath = normalizedDir ? path3.resolve(resolvedRepoRoot, normalizedDir) : resolvedRepoRoot; + const relativePath = path3.relative(resolvedRepoRoot, rootPath); + if (!relativePathStaysInsideRoot2(relativePath)) { + throw new Error(`CAD Viewer root directory must stay inside the directory root: ${normalizedDir}`); + } + return { + dir: normalizedDir, + rootPath, + rootName: normalizedDir ? path3.basename(rootPath) : path3.basename(resolvedRepoRoot) + }; +} +function repoRelativePath(repoRoot, filePath) { + return toPosixPath(path3.relative(path3.resolve(repoRoot), path3.resolve(filePath))); +} +function scanRelativePath(rootPath, filePath) { + return toPosixPath(path3.relative(path3.resolve(rootPath), path3.resolve(filePath))); +} +function fileStats(filePath) { + try { + const stats = fs2.statSync(filePath, { bigint: true }); + return stats.isFile() ? stats : null; + } catch { + return null; + } +} +function sha256File(filePath) { + const hash = crypto.createHash("sha256"); + const fd = fs2.openSync(filePath, "r"); + try { + const buffer = Buffer.alloc(1024 * 1024); + let bytesRead = 0; + do { + bytesRead = fs2.readSync(fd, buffer, 0, buffer.length, null); + if (bytesRead > 0) { + hash.update(buffer.subarray(0, bytesRead)); + } + } while (bytesRead > 0); + } finally { + fs2.closeSync(fd); + } + return hash.digest("hex"); +} +function pathIsInside2(filePath, rootPath) { + const relative = path3.relative(path3.resolve(rootPath), path3.resolve(filePath)); + return relativePathStaysInsideRoot2(relative); +} +function dedupePaths(paths) { + const result = []; + const seen = /* @__PURE__ */ new Set(); + for (const rawPath of paths) { + const resolved = path3.resolve(rawPath); + if (!seen.has(resolved)) { + seen.add(resolved); + result.push(resolved); + } + } + return result; +} +function normalizeManifestPath(manifestPath) { + const value = String(manifestPath || "").trim(); + if (!value || value.includes("\0")) { + return ""; + } + return value.replace(/\\/g, "/"); +} +function resolveManifestSourcePath(repoRoot, manifestPath, baseDir = repoRoot) { + const value = normalizeManifestPath(manifestPath); + if (!value) { + return null; + } + const resolved = path3.isAbsolute(value) ? path3.resolve(value) : path3.resolve(baseDir, value); + if (!pathIsInside2(resolved, repoRoot)) { + return null; + } + return resolved; +} +function normalizeManifestRelativePath(manifestPath) { + const value = String(manifestPath || "").trim().replace(/\\/g, "/").replace(/^\/+/, ""); + if (!value || value === "." || value.startsWith("../")) { + return ""; + } + return value; +} +function manifestIdentityRootForStep(repoRoot, actualStepPath, manifestStepPath) { + const resolvedRepoRoot = path3.resolve(repoRoot); + const normalizedStepPath = normalizeManifestRelativePath(manifestStepPath); + if (!normalizedStepPath || !actualStepPath) { + return resolvedRepoRoot; + } + const actualRepoPath = repoRelativePath(resolvedRepoRoot, actualStepPath); + if (actualRepoPath === normalizedStepPath) { + return resolvedRepoRoot; + } + if (!actualRepoPath.endsWith(`/${normalizedStepPath}`)) { + return resolvedRepoRoot; + } + const prefix = actualRepoPath.slice(0, actualRepoPath.length - normalizedStepPath.length).replace(/\/+$/, ""); + if (!prefix) { + return resolvedRepoRoot; + } + const resolvedPrefix = path3.resolve(resolvedRepoRoot, ...prefix.split("/").filter(Boolean)); + return pathIsInside2(resolvedPrefix, resolvedRepoRoot) ? resolvedPrefix : resolvedRepoRoot; +} +function sourcePathFromManifest(repoRoot, manifestPath, { identityRoot = null, baseDir = null } = {}) { + const value = normalizeManifestPath(manifestPath); + if (!value) { + return { sourcePath: "", manifestSourcePath: "", filePath: null, identityRoot: path3.resolve(repoRoot) }; + } + const resolvedRepoRoot = path3.resolve(repoRoot); + const resolvedIdentityRoot = identityRoot && pathIsInside2(identityRoot, resolvedRepoRoot) ? path3.resolve(identityRoot) : resolvedRepoRoot; + const roots = dedupePaths([ + baseDir ? path3.resolve(baseDir) : null, + resolvedIdentityRoot, + resolvedRepoRoot + ].filter(Boolean)); + const candidates = []; + for (const root of roots) { + if (!pathIsInside2(root, resolvedRepoRoot)) { + continue; + } + const filePath2 = resolveManifestSourcePath(resolvedRepoRoot, value, root); + if (!filePath2) { + continue; + } + candidates.push({ + sourcePath: repoRelativePath(resolvedRepoRoot, filePath2), + manifestSourcePath: value, + filePath: filePath2, + identityRoot: resolvedIdentityRoot + }); + } + const candidate = candidates.find((entry) => fileStats(entry.filePath)) || candidates[0]; + if (candidate) { + return candidate; + } + const filePath = resolveManifestSourcePath(resolvedRepoRoot, value, resolvedRepoRoot); + return { + sourcePath: value, + manifestSourcePath: value, + filePath, + identityRoot: resolvedIdentityRoot + }; +} +function fileHasGenStep(filePath) { + try { + return /\bgen_step\s*\(/.test(fs2.readFileSync(filePath, "utf-8")); + } catch { + return false; + } +} +function generatorSourcePathFromManifest(repoRoot, manifestPath, { identityRoot = null, baseDir = null } = {}) { + const candidate = sourcePathFromManifest(repoRoot, manifestPath, { identityRoot, baseDir }); + if (candidate.sourcePath && candidate.filePath && path3.extname(candidate.filePath).toLowerCase() === ".py" && path3.basename(candidate.filePath) !== "__init__.py" && fileHasGenStep(candidate.filePath)) { + return candidate; + } + return { + sourcePath: "", + manifestSourcePath: candidate.manifestSourcePath || "", + filePath: null, + identityRoot: candidate.identityRoot || path3.resolve(repoRoot) + }; +} +function fileHasPythonGenerator(filePath, generatorName) { + if (!generatorName) { + return false; + } + try { + const source = fs2.readFileSync(filePath, "utf-8"); + return new RegExp(`\\b${generatorName}\\s*\\(`).test(source); + } catch { + return false; + } +} +function generatorSourcePathFromMetadata(repoRoot, manifestPath, generatorName, { baseDir = null } = {}) { + const candidate = sourcePathFromManifest(repoRoot, manifestPath, { baseDir }); + if (candidate.sourcePath && candidate.filePath && path3.extname(candidate.filePath).toLowerCase() === ".py" && path3.basename(candidate.filePath) !== "__init__.py" && fileHasPythonGenerator(candidate.filePath, generatorName)) { + return candidate; + } + return { sourcePath: "", filePath: null }; +} +function readXmlTextToCadMetadata(filePath) { + let text = ""; + try { + text = fs2.readFileSync(filePath, "utf-8"); + } catch { + return {}; + } + const metadata = {}; + for (const match of text.matchAll(TEXT_TO_CAD_COMMENT_METADATA_RE)) { + metadata[String(match[1] || "").trim()] = String(match[2] || "").trim(); + } + return metadata; +} +function readDxfTextToCadMetadata(filePath) { + let lines = []; + try { + lines = fs2.readFileSync(filePath, "utf-8").split(/\r?\n/); + } catch { + return {}; + } + const metadata = {}; + for (let index = 0; index + 1 < lines.length; index += 1) { + if (String(lines[index] || "").trim() !== "999") { + continue; + } + const value = String(lines[index + 1] || "").trim(); + if (!value.startsWith("cadpy:")) { + continue; + } + const [key, ...rest] = value.slice("cadpy:".length).split("="); + const normalizedKey = String(key || "").trim(); + if (rest.length && /^[A-Za-z][A-Za-z0-9]*$/.test(normalizedKey)) { + metadata[normalizedKey] = rest.join("=").trim(); + } + } + return metadata; +} +function readGeneratedFileMetadata(filePath, kind) { + const normalizedKind = String(kind || "").trim().toLowerCase(); + if (normalizedKind === "dxf") { + return readDxfTextToCadMetadata(filePath); + } + if (["urdf", "srdf", "sdf"].includes(normalizedKind)) { + return readXmlTextToCadMetadata(filePath); + } + if (normalizedKind === "step" || normalizedKind === "stp") { + try { + return readTextToCadStepMetadataFile(filePath); + } catch { + return {}; + } + } + return {}; +} +function generatedSourceStatusForFile({ repoRoot, sourcePath, kind }) { + const normalizedKind = String(kind || "").trim().toLowerCase(); + const generatorName = PYTHON_GENERATOR_BY_KIND[normalizedKind] || ""; + if (!generatorName) { + return null; + } + const metadata = readGeneratedFileMetadata(sourcePath, normalizedKind); + const metadataSourcePath = String(metadata.sourcePath || "").trim(); + if (!metadataSourcePath) { + return null; + } + const sourceIdentity = generatorSourcePathFromMetadata( + repoRoot, + metadataSourcePath, + generatorName, + { baseDir: path3.dirname(sourcePath) } + ); + const base = { + sourceKind: "python", + source: { + file: sourceIdentity.sourcePath || metadataSourcePath, + sourcePath: sourceIdentity.sourcePath || metadataSourcePath, + ...metadata.sourceHash ? { sourceHash: String(metadata.sourceHash) } : {} + } + }; + if (!sourceIdentity.filePath) { + return { + ...base, + sourceStatus: { + ok: false, + status: "missing", + stale: false, + sourceKind: "python", + sourcePath: metadataSourcePath, + message: "Python generator source is unavailable." + } + }; + } + return { + ...base, + source: { + ...base.source, + file: sourceIdentity.sourcePath, + sourcePath: sourceIdentity.sourcePath, + sourceHash: String(metadata.sourceHash || "") + } + }; +} +function assetForPath(repoRoot, filePath) { + const stats = fileStats(filePath); + if (!stats) { + return null; + } + const version = `${stats.size.toString(36)}-${stats.mtimeNs.toString(36)}`; + const repoPath = repoRelativePath(repoRoot, filePath); + return { + url: `${encodeUrlPath(repoPath)}?v=${encodeURIComponent(version)}`, + hash: sha256File(filePath), + bytes: Number(stats.size) + }; +} +function assetUrlForPath(repoRoot, filePath) { + return encodeUrlPath(repoRelativePath(repoRoot, filePath)); +} +function readExact(fd, length2, position) { + const buffer = Buffer.alloc(length2); + const bytesRead = fs2.readSync(fd, buffer, 0, length2, position); + return bytesRead === length2 ? buffer : null; +} +function glbBufferViewRange(gltf, binOffset, binLength, viewIndex) { + const view = Array.isArray(gltf?.bufferViews) ? gltf.bufferViews[Number(viewIndex)] : null; + if (!view || Number(view.buffer || 0) !== 0) { + return null; + } + const byteOffset = binOffset + Number(view.byteOffset || 0); + const byteLength = Number(view.byteLength || 0); + if (!Number.isFinite(byteOffset) || !Number.isFinite(byteLength) || byteLength < 0) { + return null; + } + if (byteOffset < binOffset || byteOffset + byteLength > binOffset + binLength) { + return null; + } + return { byteOffset, byteLength }; +} +function parseJsonBufferView(fd, gltf, binOffset, binLength, viewIndex, encoding = "utf-8") { + const range = glbBufferViewRange(gltf, binOffset, binLength, viewIndex); + if (!range) { + throw new Error("STEP topology buffer view range is invalid"); + } + const bytes2 = readExact(fd, range.byteLength, range.byteOffset); + if (!bytes2) { + throw new Error("STEP topology buffer view range is invalid"); + } + const payload = JSON.parse(bytes2.toString(String(encoding || "utf-8"))); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + throw new Error("STEP topology JSON buffer view is not an object"); + } + return payload; +} +function gltfPrimitivesHaveSurfaceEdgeAttributes(gltf) { + const meshes = Array.isArray(gltf?.meshes) ? gltf.meshes : []; + let primitiveCount = 0; + for (const mesh of meshes) { + for (const primitive of Array.isArray(mesh?.primitives) ? mesh.primitives : []) { + primitiveCount += 1; + const attributes = primitive?.attributes || {}; + if (attributes[STEP_EDGE_BARYCENTRIC_ATTRIBUTE] === void 0 || attributes[STEP_EDGE_CLASS_ATTRIBUTE] === void 0) { + return false; + } + } + } + return primitiveCount > 0; +} +function readGlbTopologyContainer(filePath) { + let fd = null; + try { + fd = fs2.openSync(filePath, "r"); + const header = readExact(fd, 12, 0); + if (!header || header.readUInt32LE(0) !== 1179937895 || header.readUInt32LE(4) !== 2) { + throw new Error("Not a GLB v2 file"); + } + const totalLength = Math.min(header.readUInt32LE(8), fs2.fstatSync(fd).size); + let offset = 12; + let gltf = null; + let binOffset = 0; + let binLength = 0; + while (offset + 8 <= totalLength) { + const chunkHeader = readExact(fd, 8, offset); + if (!chunkHeader) { + throw new Error("Invalid GLB chunk header"); + } + const chunkLength = chunkHeader.readUInt32LE(0); + const chunkType = chunkHeader.toString("latin1", 4, 8); + offset += 8; + if (offset + chunkLength > totalLength) { + throw new Error("Invalid GLB chunk length"); + } + if (chunkType === "JSON") { + const jsonBytes = readExact(fd, chunkLength, offset); + if (!jsonBytes) { + throw new Error("GLB is missing JSON chunk"); + } + gltf = JSON.parse(jsonBytes.toString("utf8").trim()); + } else if (chunkType === "BIN\0") { + binOffset = offset; + binLength = chunkLength; + } + offset += chunkLength; + } + return { + fd, + gltf, + binOffset, + binLength + }; + } catch { + if (fd !== null) { + fs2.closeSync(fd); + } + throw new Error("Invalid GLB topology container"); + } +} +function stepArtifactError({ code, reason, repoRoot, cadPath, sourcePath, glbPath, details = {} }) { + const glbRelPath = repoRelativePath(repoRoot, glbPath); + return { + ok: false, + error: { + code, + message: `${reason}: ${glbRelPath}.`, + cadPath, + stepPath: repoRelativePath(repoRoot, sourcePath), + glbPath: glbRelPath, + regenerateCommand: REGENERATE_STEP_COMMAND, + ...details + } + }; +} +function staleStepArtifactError({ + repoRoot, + cadPath, + sourcePath, + glbPath, + manifestSourcePath = "", + sourceKind = "step", + artifactHash, + currentHash +}) { + return stepArtifactError({ + code: "stale_step_artifact", + reason: "Generated GLB doesn't match the hash of the STEP file", + repoRoot, + cadPath, + sourcePath, + glbPath, + details: { + stale: true, + sourceKind, + ...manifestSourcePath ? { sourcePath: manifestSourcePath } : {}, + artifactHash, + currentHash + } + }); +} +function validateStepTopologyArtifact({ repoRoot, sourcePath, cadPath }) { + const glbPath = inlineStepGlbArtifactPathForSource(sourcePath); + let stepHash = ""; + let sourceHash = ""; + let artifactSourcePath = ""; + const fail = (code, reason) => ({ + topology: null, + stepArtifact: stepArtifactError({ code, reason, repoRoot, cadPath, sourcePath, glbPath }), + glbPath, + stepHash, + sourceHash + }); + if (!fileStats(glbPath)) { + return fail( + "missing_glb", + "STEP topology validation requires the generated GLB artifact, but it is missing" + ); + } + let container = null; + try { + container = readGlbTopologyContainer(glbPath); + const extension = container.gltf?.extensions?.[STEP_TOPOLOGY_EXTENSION]; + if (!extension || typeof extension !== "object" || Array.isArray(extension)) { + return fail( + "missing_step_topology", + "STEP topology validation requires readable STEP_topology indexView in the GLB" + ); + } + if (!isCurrentStepTopologySchemaVersion(extension.schemaVersion)) { + return fail( + "unsupported_step_topology", + `STEP topology validation requires STEP_topology schemaVersion ${STEP_TOPOLOGY_SCHEMA_VERSION} in the GLB` + ); + } + const manifest = parseJsonBufferView( + container.fd, + container.gltf, + container.binOffset, + container.binLength, + extension.indexView, + extension.encoding + ); + const topology = { + index: manifest, + entryKind: String(extension.entryKind || manifest.entryKind || "").trim().toLowerCase(), + hasSelector: false, + hasDisplayEdges: false + }; + if (!isCurrentStepTopologySchemaVersion(manifest.schemaVersion)) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "unsupported_step_topology", + reason: `STEP topology validation requires STEP_topology schemaVersion ${STEP_TOPOLOGY_SCHEMA_VERSION} in the GLB`, + repoRoot, + cadPath, + sourcePath, + glbPath + }), + glbPath, + stepHash + }; + } + const artifactSourceKind = String(manifest.sourceKind || "step").trim().toLowerCase(); + const artifactUsesPythonSource = artifactSourceKind === "python"; + const artifactNormalizedSourceKind = artifactUsesPythonSource ? "python" : "step"; + const artifactIdentityDetails = () => ({ + sourceKind: artifactNormalizedSourceKind, + ...artifactSourcePath ? { sourcePath: artifactSourcePath } : {} + }); + const manifestIdentityRoot = manifestIdentityRootForStep(repoRoot, sourcePath, manifest.stepPath); + const sourceIdentity = artifactUsesPythonSource ? generatorSourcePathFromManifest(repoRoot, manifest.sourcePath, { + identityRoot: manifestIdentityRoot, + baseDir: path3.dirname(glbPath) + }) : sourcePathFromManifest(repoRoot, manifest.sourcePath, { + identityRoot: manifestIdentityRoot, + baseDir: path3.dirname(glbPath) + }); + artifactSourcePath = sourceIdentity.sourcePath; + if (!artifactSourcePath || !sourceIdentity.filePath) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_source_path", + reason: "GLB STEP_topology is missing required sourcePath identity", + repoRoot, + cadPath, + sourcePath, + glbPath, + details: { + sourceKind: artifactNormalizedSourceKind + } + }), + glbPath, + stepHash, + sourceHash + }; + } + stepHash = String(manifest.stepHash || "").trim(); + sourceHash = String(manifest.sourceHash || "").trim(); + if (!stepHash && fileStats(sourcePath)) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_step_hash", + reason: "GLB STEP_topology is missing STEP file identity", + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + const currentStepHash = fileStats(sourcePath) ? sha256File(sourcePath) : ""; + if (currentStepHash && stepHash !== currentStepHash) { + return { + topology, + stepArtifact: staleStepArtifactError({ + repoRoot, + cadPath, + sourcePath, + glbPath, + manifestSourcePath: artifactSourcePath, + sourceKind: artifactNormalizedSourceKind, + artifactHash: stepHash, + currentHash: currentStepHash + }), + glbPath, + stepHash, + sourceHash + }; + } + let edgeRendering = null; + try { + const edgeManifest = parseJsonBufferView( + container.fd, + container.gltf, + container.binOffset, + container.binLength, + extension.edgeView, + extension.encoding + ); + const edgeVisibilityClasses = edgeManifest?.edgeRendering && typeof edgeManifest.edgeRendering === "object" ? normalizeStepEdgeRenderVisibilityClasses(edgeManifest.edgeRendering.visibilityClasses) : []; + const indexEdgeVisibilityClasses = manifest?.edgeRendering && typeof manifest.edgeRendering === "object" ? normalizeStepEdgeRenderVisibilityClasses(manifest.edgeRendering.visibilityClasses) : []; + if (!isCurrentStepTopologySchemaVersion(edgeManifest.schemaVersion) || String(edgeManifest.profile || "") !== "surface-edges" || String(edgeManifest.sourcePath || "").trim() !== String(manifest.sourcePath || "").trim() || stepHash && String(edgeManifest.stepHash || "").trim() !== stepHash || !edgeVisibilityClasses.length || edgeVisibilityClasses.join("\n") !== indexEdgeVisibilityClasses.join("\n") || !edgeVisibilityClasses.includes(STEP_EDGE_VISIBILITY_CLASSES.FEATURE) || !edgeManifest?.buffers?.views?.surfaceHalfEdges) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_edge_topology", + reason: `STEP topology validation requires STEP_topology edgeView schemaVersion ${STEP_TOPOLOGY_SCHEMA_VERSION} in the GLB`, + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + edgeRendering = edgeManifest.edgeRendering && typeof edgeManifest.edgeRendering === "object" ? { + visibilityClasses: edgeVisibilityClasses, + generatedVisibilityClasses: (Array.isArray(edgeManifest.edgeRendering.generatedVisibilityClasses) ? edgeManifest.edgeRendering.generatedVisibilityClasses : edgeVisibilityClasses).map((classId) => String(classId || "").trim()).filter((classId, index, list2) => edgeVisibilityClasses.includes(classId) && STEP_EDGE_RENDER_CLASS_ORDER.includes(classId) && list2.indexOf(classId) === index), + visibilityClassCounts: edgeManifest.edgeRendering.visibilityClassCounts || {}, + generatedVisibilityClassCounts: edgeManifest.edgeRendering.generatedVisibilityClassCounts || {} + } : null; + if (!gltfPrimitivesHaveSurfaceEdgeAttributes(container.gltf)) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_surface_edge_attributes", + reason: `STEP topology validation requires ${STEP_EDGE_BARYCENTRIC_ATTRIBUTE} and ${STEP_EDGE_CLASS_ATTRIBUTE} on every STEP mesh primitive`, + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + } catch { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_edge_topology", + reason: "STEP topology validation requires readable STEP_topology edgeView in the GLB", + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + topology.hasDisplayEdges = true; + try { + const selectorManifest = parseJsonBufferView( + container.fd, + container.gltf, + container.binOffset, + container.binLength, + extension.selectorView, + extension.encoding + ); + if (!isCurrentStepTopologySchemaVersion(selectorManifest.schemaVersion)) { + return { + topology, + stepArtifact: stepArtifactError({ + code: "unsupported_step_topology", + reason: `STEP topology validation requires STEP_topology schemaVersion ${STEP_TOPOLOGY_SCHEMA_VERSION} in the GLB`, + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + } catch { + return { + topology, + stepArtifact: stepArtifactError({ + code: "missing_selector_topology", + reason: "STEP topology validation requires readable STEP_topology selectorView in the GLB", + repoRoot, + cadPath, + sourcePath, + glbPath, + details: artifactIdentityDetails() + }), + glbPath, + stepHash, + sourceHash + }; + } + topology.hasSelector = true; + const stepArtifact = { + ok: true, + glbPath: repoRelativePath(repoRoot, glbPath), + ...artifactSourcePath ? { sourcePath: artifactSourcePath } : {}, + sourceKind: artifactUsesPythonSource ? "python" : "step", + ...edgeRendering ? { edgeRendering } : {}, + ...artifactUsesPythonSource ? { + sourceHash, + ...stepHash ? { stepHash } : {} + } : { + stepHash + } + }; + return { + topology, + stepArtifact, + glbPath, + stepHash, + sourceHash + }; + } catch { + return fail( + "missing_step_topology", + "STEP topology validation requires readable STEP_topology indexView in the GLB" + ); + } finally { + if (container?.fd !== null && container?.fd !== void 0) { + try { + fs2.closeSync(container.fd); + } catch { + } + } + } +} +function readStepSourceStatus({ + repoRoot, + stepPath, + pythonSourcePath = "", + cadPath = "" +} = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + if (!stepPath) { + throw new Error("stepPath is required"); + } + const resolvedRepoRoot = path3.resolve(repoRoot); + const resolvedStepPath = path3.resolve(stepPath); + const extension = path3.extname(resolvedStepPath) || ".step"; + const normalizedCadPath = cadPath || cadPathForStepSource(resolvedRepoRoot, resolvedStepPath, extension); + const validation = validateStepTopologyArtifact({ + repoRoot: resolvedRepoRoot, + sourcePath: resolvedStepPath, + cadPath: normalizedCadPath + }); + const stepArtifact = validation.stepArtifact || {}; + const artifact = catalogArtifactFromValidation(stepArtifact) || null; + const normalizedPythonSourcePath = pythonSourcePath ? repoRelativePath( + resolvedRepoRoot, + path3.isAbsolute(pythonSourcePath) ? path3.resolve(pythonSourcePath) : path3.resolve(resolvedRepoRoot, pythonSourcePath) + ) : ""; + const hasPythonSource = Boolean(normalizedPythonSourcePath) || String(stepArtifact.sourceKind || artifact?.sourceKind || "").trim().toLowerCase() === "python"; + const sourceKind = hasPythonSource ? "python" : "step"; + const file = repoRelativePath(resolvedRepoRoot, resolvedStepPath); + const sourcePath = String(stepArtifact.sourcePath || artifact?.sourcePath || normalizedPythonSourcePath || "").trim(); + const base = { + ok: true, + file, + stepPath: file, + sourceKind, + artifact, + ...sourcePath ? { sourcePath } : {} + }; + if (!fileStats(resolvedStepPath)) { + return { + ...base, + ok: false, + step: { + ok: false, + status: "missing", + missing: true, + stale: false, + message: "STEP file is missing." + } + }; + } + return { + ...base, + step: { + ok: true, + status: "current", + missing: false, + stale: false, + currentHash: sha256File(resolvedStepPath) + } + }; +} +function stepKindFromTopology(topology) { + const index = topology?.index && typeof topology.index === "object" ? topology.index : topology; + if (topology?.entryKind === "assembly" || index?.entryKind === "assembly") { + return "assembly"; + } + return index?.assembly?.root && typeof index.assembly.root === "object" ? "assembly" : "part"; +} +function sourceFormatFromExtension(extension) { + const normalized = extension.toLowerCase().replace(/^\./, ""); + return normalized === "stp" ? "stp" : normalized; +} +function sourceFormatForPath(sourcePath, extension = path3.extname(sourcePath)) { + return pathIsImplicitCadSource(sourcePath) ? "implicit" : sourceFormatFromExtension(extension); +} +function isPerUrdfViewerDirectoryName(name) { + const normalized = String(name || "").toLowerCase(); + return normalized.startsWith(".") && normalized.endsWith(".urdf"); +} +function isHiddenDirectoryName(name) { + return String(name || "").startsWith("."); +} +function isPathInsidePerUrdfViewerDirectory(filePath) { + return String(filePath || "").split(path3.sep).some((part) => isPerUrdfViewerDirectoryName(part)); +} +function fileRefForSource(rootPath, sourcePath) { + return scanRelativePath(rootPath, sourcePath); +} +function cadPathForStepSource(repoRoot, sourcePath, extension) { + const relativePath = repoRelativePath(repoRoot, sourcePath); + return relativePath.slice(0, -extension.length); +} +function sourcePathForInlineStepGlbArtifact(glbPath) { + const name = path3.basename(glbPath); + if (!isInlineStepGlbArtifactPath(glbPath)) { + return null; + } + return path3.join(path3.dirname(glbPath), name.slice(1, -".glb".length)); +} +function sourcePathForInlineStepParameter(parameterPath) { + const name = path3.basename(parameterPath); + if (!isInlineStepParameterPath(parameterPath)) { + return null; + } + return path3.join(path3.dirname(parameterPath), name.slice(1, -".js".length)); +} +function catalogArtifactFromValidation(stepArtifact) { + if (!stepArtifact || typeof stepArtifact !== "object") { + return void 0; + } + if (stepArtifact.ok) { + return void 0; + } + const rawError = stepArtifact.error && typeof stepArtifact.error === "object" ? stepArtifact.error : {}; + const error = String(rawError.code || stepArtifact.error || "step_artifact_error").trim(); + const message = String(rawError.message || "").trim(); + const sourceKind = String(rawError.sourceKind || "").trim(); + const artifactHash = String(rawError.artifactHash || "").trim(); + const currentHash = String(rawError.currentHash || "").trim(); + const sourceHash = String(rawError.sourceHash || "").trim(); + const stepPath = String(rawError.stepPath || "").trim(); + const glbPath = String(rawError.glbPath || "").trim(); + const cadPath = String(rawError.cadPath || "").trim(); + return { + ok: false, + error, + ...rawError.stale ? { stale: true } : {}, + ...stepPath ? { stepPath } : {}, + ...glbPath ? { glbPath } : {}, + ...cadPath ? { cadPath } : {}, + ...sourceKind ? { sourceKind } : {}, + ...sourceHash ? { sourceHash } : {}, + ...artifactHash ? { artifactHash } : {}, + ...currentHash ? { currentHash } : {}, + ...message ? { message } : {} + }; +} +function readStepCatalogMetadata({ repoRoot, glbPath, sourcePath = "" } = {}) { + if (!fileStats(glbPath)) { + return {}; + } + let container = null; + try { + container = readGlbTopologyContainer(glbPath); + const extension = container.gltf?.extensions?.[STEP_TOPOLOGY_EXTENSION]; + if (!extension || typeof extension !== "object" || Array.isArray(extension)) { + return {}; + } + if (!isCurrentStepTopologySchemaVersion(extension.schemaVersion)) { + return {}; + } + const manifest = parseJsonBufferView( + container.fd, + container.gltf, + container.binOffset, + container.binLength, + extension.indexView, + extension.encoding + ); + const sourceKind = String(manifest?.sourceKind || "step").trim().toLowerCase() === "python" ? "python" : "step"; + const manifestIdentityRoot = manifestIdentityRootForStep(repoRoot, sourcePathForInlineStepGlbArtifact(glbPath) || sourcePath, manifest?.stepPath); + const sourceIdentity = sourceKind === "python" ? generatorSourcePathFromManifest(repoRoot, manifest?.sourcePath, { + identityRoot: manifestIdentityRoot, + baseDir: path3.dirname(glbPath) + }) : sourcePathFromManifest(repoRoot, manifest?.sourcePath, { + identityRoot: manifestIdentityRoot, + baseDir: path3.dirname(glbPath) + }); + return { + topology: { + index: manifest, + entryKind: String(extension.entryKind || manifest?.entryKind || "").trim().toLowerCase(), + hasSelector: Boolean(extension.selectorView), + hasDisplayEdges: Boolean(extension.edgeView) + }, + sourceKind, + sourcePath: sourceIdentity.sourcePath, + sourceHash: String(manifest?.sourceHash || ""), + stepHash: String(manifest?.stepHash || "") + }; + } catch { + return {}; + } finally { + if (container?.fd !== null && container?.fd !== void 0) { + try { + fs2.closeSync(container.fd); + } catch { + } + } + } +} +function pythonStepSourceFromStepMetadata(repoRoot, stepPath) { + const metadata = readGeneratedFileMetadata(stepPath, "step"); + const metadataSourcePath = String(metadata.sourcePath || "").trim(); + if (!metadataSourcePath) { + return null; + } + const sourceIdentity = sourcePathFromManifest(repoRoot, metadataSourcePath, { baseDir: path3.dirname(stepPath) }); + if (!sourceIdentity.sourcePath) { + return null; + } + const sourceExtension = path3.extname(sourceIdentity.filePath || sourceIdentity.sourcePath).toLowerCase(); + if (sourceExtension !== ".py") { + return null; + } + return { + sourcePath: sourceIdentity.sourcePath, + sourceHash: String(metadata.sourceHash || "") + }; +} +function createStepEntry({ repoRoot, rootPath, sourcePath, extension, includeArtifactStatus = true }) { + const cadPath = cadPathForStepSource(repoRoot, sourcePath, extension); + const validation = includeArtifactStatus ? validateStepTopologyArtifact({ + repoRoot, + sourcePath, + cadPath + }) : null; + const glbPath = validation?.glbPath || inlineStepGlbArtifactPathForSource(sourcePath); + const catalogMetadata = includeArtifactStatus ? {} : readStepCatalogMetadata({ repoRoot, glbPath, sourcePath }); + const topology = validation?.topology || catalogMetadata.topology || null; + const stepArtifact = validation?.stepArtifact || {}; + const glbAsset = assetForPath(repoRoot, glbPath); + const stepModuleAsset = assetForPath(repoRoot, stepParameterPathForStepSource(sourcePath)); + const artifact = includeArtifactStatus ? catalogArtifactFromValidation(stepArtifact) : void 0; + const artifactSourceKind = String( + stepArtifact.sourceKind || stepArtifact.error?.sourceKind || catalogMetadata.sourceKind || artifact?.sourceKind || "" + ).trim().toLowerCase(); + const metadataPythonSource = (!includeArtifactStatus || artifact) && artifactSourceKind !== "python" ? pythonStepSourceFromStepMetadata(repoRoot, sourcePath) : null; + const sourceKind = artifactSourceKind === "python" || metadataPythonSource ? "python" : "step"; + const artifactSourcePath = String( + stepArtifact.sourcePath || stepArtifact.error?.sourcePath || catalogMetadata.sourcePath || "" + ).trim(); + const pythonSourcePath = artifactSourcePath || metadataPythonSource?.sourcePath || ""; + return { + file: fileRefForSource(rootPath, sourcePath), + kind: stepKindFromTopology(topology), + url: glbAsset?.url || assetUrlForPath(repoRoot, glbPath), + hash: glbAsset?.hash || "", + bytes: glbAsset?.bytes || 0, + sourceKind, + ...sourceKind === "python" && pythonSourcePath ? { + source: { + file: pythonSourcePath, + sourcePath: pythonSourcePath, + ...stepArtifact.sourceHash || catalogMetadata.sourceHash || metadataPythonSource?.sourceHash ? { sourceHash: stepArtifact.sourceHash || catalogMetadata.sourceHash || metadataPythonSource.sourceHash } : {} + } + } : {}, + ...stepModuleAsset ? { moduleUrl: stepModuleAsset.url } : {}, + ...artifact ? { artifact } : {} + }; +} +function linkedUrdfPathForSrdf(sourcePath, repoRoot) { + let xmlText = ""; + try { + xmlText = fs2.readFileSync(sourcePath, "utf-8"); + } catch { + return null; + } + const match = SRDF_URDF_METADATA_PATTERN.exec(xmlText); + const rawRef = String(match?.[1] || "").trim(); + if (!rawRef || rawRef.includes("\\") || rawRef.startsWith("/")) { + return null; + } + const resolved = path3.resolve(path3.dirname(sourcePath), rawRef); + const relativeToRepo = path3.relative(path3.resolve(repoRoot), resolved); + if (!relativePathStaysInsideRoot2(relativeToRepo) || path3.extname(resolved).toLowerCase() !== ".urdf") { + return null; + } + return fileStats(resolved) ? resolved : null; +} +function createSingleAssetEntry({ repoRoot, rootPath, sourcePath, extension }) { + const kind = sourceFormatForPath(sourcePath, extension); + const asset = assetForPath(repoRoot, sourcePath); + const file = fileRefForSource(rootPath, sourcePath); + const generatedSource = generatedSourceStatusForFile({ repoRoot, sourcePath, kind }); + const entry = { + file, + kind, + url: asset?.url || assetUrlForPath(repoRoot, sourcePath), + hash: asset?.hash || "", + bytes: asset?.bytes || 0, + ...generatedSource?.sourceKind === "python" ? { sourceKind: "python" } : {}, + ...generatedSource?.source ? { source: generatedSource.source } : {}, + ...generatedSource?.sourceStatus ? { sourceStatus: generatedSource.sourceStatus } : {} + }; + if (kind === "srdf") { + const linkedUrdfPath = linkedUrdfPathForSrdf(sourcePath, repoRoot); + if (linkedUrdfPath) { + const urdfAsset = assetForPath(repoRoot, linkedUrdfPath); + if (urdfAsset) { + entry.relations = { + urdf: { + file: fileRefForSource(rootPath, linkedUrdfPath), + ...urdfAsset + } + }; + } + } + } + return entry; +} +function shouldSkipDirectory(name) { + return VIEWER_SKIPPED_DIRECTORIES.has(name) || isHiddenDirectoryName(name) || isPerStepViewerDirectoryName(name) || isPerUrdfViewerDirectoryName(name); +} +function scanPathIncluded(includePath, rootPath, entryPath, isDirectory) { + if (typeof includePath !== "function") { + return true; + } + return includePath({ + filePath: entryPath, + relativePath: scanRelativePath(rootPath, entryPath), + isDirectory + }) !== false; +} +function collectCadSourceFiles(rootPath, { scanRootPath = rootPath, includePath = null } = {}, result = []) { + let entries = []; + try { + entries = fs2.readdirSync(rootPath, { withFileTypes: true }); + } catch { + return result; + } + for (const entry of entries) { + const entryPath = path3.join(rootPath, entry.name); + if (entry.isDirectory()) { + if (!shouldSkipDirectory(entry.name) && scanPathIncluded(includePath, scanRootPath, entryPath, true)) { + collectCadSourceFiles(entryPath, { scanRootPath, includePath }, result); + } + continue; + } + if (!entry.isFile()) { + continue; + } + if (!scanPathIncluded(includePath, scanRootPath, entryPath, false)) { + continue; + } + const extension = path3.extname(entry.name).toLowerCase(); + if (isInlineStepGlbArtifactPath(entryPath)) { + const sourcePath = sourcePathForInlineStepGlbArtifact(entryPath); + if (sourcePath && !fileStats(sourcePath)) { + result.push(entryPath); + } + continue; + } + if ((SOURCE_EXTENSIONS.has(extension) || pathIsImplicitCadSource(entryPath)) && !isInlineStepGlbArtifactPath(entryPath)) { + result.push(entryPath); + continue; + } + } + return result; +} +function compareEntries(a, b) { + return String(a.file || "").localeCompare(String(b.file || ""), void 0, { + numeric: true, + sensitivity: "base" + }); +} +function pathHasSkippedDirectory(rootPath, filePath) { + const relativePath = scanRelativePath(rootPath, filePath); + if (!relativePathStaysInsideRoot2(relativePath)) { + return true; + } + const parts = relativePath.split("/").filter(Boolean); + return parts.slice(0, -1).some((part) => shouldSkipDirectory(part)); +} +function logicalStepSourcePathForInlineArtifactPath(filePath) { + if (isInlineStepGlbArtifactPath(filePath)) { + return sourcePathForInlineStepGlbArtifact(filePath); + } + if (isInlineStepParameterPath(filePath)) { + return sourcePathForInlineStepParameter(filePath); + } + return null; +} +function logicalStepSourceExistsForSidecar(sourcePath) { + return Boolean( + sourcePath && (fileStats(sourcePath) || fileStats(inlineStepGlbArtifactPathForSource(sourcePath)) || fileStats(stepParameterPathForStepSource(sourcePath))) + ); +} +function catalogFileRefForPath({ repoRoot, rootDir = DEFAULT_VIEWER_ROOT_DIR, filePath } = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + if (!filePath) { + throw new Error("filePath is required"); + } + const resolved = resolveViewerRoot(repoRoot, rootDir); + const resolvedFilePath = path3.resolve(filePath); + if (!pathIsInside2(resolvedFilePath, resolved.rootPath) || pathHasSkippedDirectory(resolved.rootPath, resolvedFilePath)) { + return ""; + } + const logicalStepSourcePath = logicalStepSourcePathForInlineArtifactPath(resolvedFilePath); + if (logicalStepSourcePath) { + return fileRefForSource(resolved.rootPath, logicalStepSourcePath); + } + if (isPathInsidePerStepViewerDirectory(resolvedFilePath) || isPathInsidePerUrdfViewerDirectory(resolvedFilePath)) { + return ""; + } + const extension = path3.extname(resolvedFilePath).toLowerCase(); + return SOURCE_EXTENSIONS.has(extension) || pathIsImplicitCadSource(resolvedFilePath) ? fileRefForSource(resolved.rootPath, resolvedFilePath) : ""; +} +function scanCadFile({ + repoRoot, + rootDir = DEFAULT_VIEWER_ROOT_DIR, + filePath, + includeArtifactStatus = true +} = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + if (!filePath) { + throw new Error("filePath is required"); + } + const resolved = resolveViewerRoot(repoRoot, rootDir); + const resolvedFilePath = path3.resolve(filePath); + if (!pathIsInside2(resolvedFilePath, resolved.rootPath) || pathHasSkippedDirectory(resolved.rootPath, resolvedFilePath)) { + return null; + } + const logicalStepSourcePath = logicalStepSourcePathForInlineArtifactPath(resolvedFilePath); + if (logicalStepSourcePath) { + if (!logicalStepSourceExistsForSidecar(logicalStepSourcePath)) { + return null; + } + return createStepEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath: logicalStepSourcePath, + extension: path3.extname(logicalStepSourcePath).toLowerCase(), + includeArtifactStatus + }); + } + if (isPathInsidePerStepViewerDirectory(resolvedFilePath) || isPathInsidePerUrdfViewerDirectory(resolvedFilePath)) { + return null; + } + const extension = path3.extname(resolvedFilePath).toLowerCase(); + if (!SOURCE_EXTENSIONS.has(extension) && !pathIsImplicitCadSource(resolvedFilePath) || !fileStats(resolvedFilePath)) { + if ((extension === ".step" || extension === ".stp") && fileStats(inlineStepGlbArtifactPathForSource(resolvedFilePath))) { + return createStepEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath: resolvedFilePath, + extension, + includeArtifactStatus + }); + } + return null; + } + if (extension === ".step" || extension === ".stp") { + return createStepEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath: resolvedFilePath, + extension, + includeArtifactStatus + }); + } + return createSingleAssetEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath: resolvedFilePath, + extension + }); +} +function scanCadDirectory({ + repoRoot, + rootDir = DEFAULT_VIEWER_ROOT_DIR, + includePath = null, + includeArtifactStatus = true +} = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + const resolved = resolveViewerRoot(repoRoot, rootDir); + const entries = collectCadSourceFiles(resolved.rootPath, { + scanRootPath: resolved.rootPath, + includePath + }).map((sourcePath) => { + const logicalSourcePath = isInlineStepGlbArtifactPath(sourcePath) ? sourcePathForInlineStepGlbArtifact(sourcePath) : sourcePath; + const extension = path3.extname(logicalSourcePath).toLowerCase(); + if (extension === ".step" || extension === ".stp") { + return createStepEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath: logicalSourcePath, + extension, + includeArtifactStatus + }); + } + return createSingleAssetEntry({ + repoRoot, + rootPath: resolved.rootPath, + sourcePath, + extension + }); + }).sort(compareEntries); + return { + schemaVersion: CAD_CATALOG_SCHEMA_VERSION, + entries + }; +} +function sortCatalogEntries(entries) { + return [...Array.isArray(entries) ? entries : []].sort(compareEntries); +} +function isServedCadAsset(filePath) { + const extension = path3.extname(filePath).toLowerCase(); + if (isInlineStepGlbArtifactPath(filePath)) { + return true; + } + if (isInlineStepParameterPath(filePath)) { + return true; + } + if (isPathInsidePerStepViewerDirectory(filePath)) { + return false; + } + if (isPathInsidePerUrdfViewerDirectory(filePath)) { + return false; + } + if (SOURCE_EXTENSIONS.has(extension) || pathIsImplicitCadSource(filePath)) { + return true; + } + return false; +} + +// viewer/src/server/catalog/generationStatus.mjs +import fs3 from "node:fs"; +import path4 from "node:path"; +var GENERATION_STATUS_SCHEMA_VERSION = 1; +var GENERATION_LOCK_SUFFIX = ".generation.lock.json"; +var DEFAULT_ACTIVE_STATUS_MAX_AGE_MS = 3e4; +var IGNORED_STATUS_SCAN_DIRS = /* @__PURE__ */ new Set([ + ".cache", + ".git", + ".hg", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "build", + "dist", + "env", + "node_modules", + "site-packages", + "venv", + "__pycache__" +]); +function emptyGenerationStatus() { + return { + schemaVersion: GENERATION_STATUS_SCHEMA_VERSION, + runs: [], + files: {} + }; +} +function generationStatusDir(repoRoot, rootDir = DEFAULT_VIEWER_ROOT_DIR) { + const resolvedRepoRoot = path4.resolve(repoRoot); + return resolveViewerRoot(resolvedRepoRoot, normalizeViewerRootDir(rootDir)).rootPath; +} +function pathIsInside3(filePath, rootPath) { + const relative = path4.relative(path4.resolve(rootPath), path4.resolve(filePath)); + return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative); +} +function isGenerationLockFileName(name) { + return String(name || "").startsWith(".") && String(name || "").endsWith(GENERATION_LOCK_SUFFIX); +} +function resolveStatusPath(repoRoot, value, { statusPath = "", rootPath = "" } = {}) { + const raw = String(value || "").trim(); + if (!raw) { + return ""; + } + if (raw.includes("\0")) { + return ""; + } + if (path4.isAbsolute(raw)) { + return path4.resolve(raw); + } + const normalized = raw.replace(/\\/g, "/"); + const statusDir = statusPath ? path4.dirname(statusPath) : ""; + const repoCandidate = path4.resolve(repoRoot, normalized); + const shouldPreferStatusDir = statusDir && (normalized.startsWith("../") || normalized.startsWith("./") || !normalized.includes("/")); + if (shouldPreferStatusDir) { + return path4.resolve(statusDir, normalized); + } + if (rootPath && pathIsInside3(repoCandidate, rootPath)) { + return repoCandidate; + } + return repoCandidate; +} +function processIsAlive(pid) { + const normalizedPid = Number(pid); + if (!Number.isInteger(normalizedPid) || normalizedPid <= 0) { + return false; + } + try { + process.kill(normalizedPid, 0); + return true; + } catch (error) { + return error?.code === "EPERM"; + } +} +function timestampMs(value) { + const ms = Date.parse(String(value || "")); + return Number.isFinite(ms) ? ms : 0; +} +function statusIsActive(payload, { nowMs = Date.now(), maxAgeMs = DEFAULT_ACTIVE_STATUS_MAX_AGE_MS } = {}) { + if (String(payload?.status || "").trim().toLowerCase() !== "running") { + return false; + } + if (!processIsAlive(payload?.pid)) { + return false; + } + const updatedAtMs = timestampMs(payload?.updatedAt || payload?.startedAt); + return !updatedAtMs || nowMs - updatedAtMs <= maxAgeMs; +} +function readStatusPayload(statusPath) { + try { + const payload = JSON.parse(fs3.readFileSync(statusPath, "utf8")); + return payload && typeof payload === "object" ? payload : null; + } catch { + return null; + } +} +function listStatusFiles(repoRoot, rootDir = DEFAULT_VIEWER_ROOT_DIR) { + const statusRoot = generationStatusDir(repoRoot, rootDir); + const statusFiles = []; + const visit = (directory) => { + let entries = []; + try { + entries = fs3.readdirSync(directory, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const entryPath = path4.join(directory, entry.name); + if (entry.isDirectory()) { + if (!IGNORED_STATUS_SCAN_DIRS.has(entry.name)) { + visit(entryPath); + } + continue; + } + if (entry.isFile() && isGenerationLockFileName(entry.name)) { + statusFiles.push(entryPath); + } + } + }; + visit(statusRoot); + return statusFiles; +} +function outputEntries(payload) { + return (Array.isArray(payload?.outputs) ? payload.outputs : []).map((output) => { + if (typeof output === "string") { + return { path: output, kind: path4.extname(output).toLowerCase().replace(".", "") }; + } + return { + path: String(output?.path || "").trim(), + kind: String(output?.kind || "").trim().toLowerCase() + }; + }).filter((output) => output.path); +} +function readGenerationStatus({ + repoRoot, + rootDir = DEFAULT_VIEWER_ROOT_DIR, + nowMs = Date.now(), + maxAgeMs = DEFAULT_ACTIVE_STATUS_MAX_AGE_MS +} = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + const resolvedRepoRoot = path4.resolve(repoRoot); + const resolvedRoot = resolveViewerRoot(resolvedRepoRoot, normalizeViewerRootDir(rootDir)); + const status = emptyGenerationStatus(); + const runsById = /* @__PURE__ */ new Map(); + for (const statusPath of listStatusFiles(resolvedRepoRoot, rootDir)) { + const payload = readStatusPayload(statusPath); + if (!payload || !statusIsActive(payload, { nowMs, maxAgeMs })) { + continue; + } + const runId = String(payload.id || path4.basename(statusPath, GENERATION_LOCK_SUFFIX)); + let run = runsById.get(runId); + if (!run) { + run = { + id: runId, + pid: Number(payload.pid) || 0, + startedAt: String(payload.startedAt || ""), + updatedAt: String(payload.updatedAt || ""), + sourcePath: sourcePathFromStatusPayload(resolvedRepoRoot, payload.sourcePath, statusPath), + generator: String(payload.generator || ""), + files: [] + }; + runsById.set(runId, run); + } + for (const output of outputEntries(payload)) { + const outputPath = resolveStatusPath(resolvedRepoRoot, output.path, { + statusPath, + rootPath: resolvedRoot.rootPath + }); + if (!outputPath || !pathIsInside3(outputPath, resolvedRoot.rootPath)) { + continue; + } + const file = toPosixPath(path4.relative(resolvedRoot.rootPath, outputPath)); + if (!file || file.startsWith("../")) { + continue; + } + const fileStatus = { + running: true, + runId: run.id, + pid: run.pid, + startedAt: run.startedAt, + updatedAt: run.updatedAt, + sourcePath: run.sourcePath, + generator: run.generator, + kind: output.kind + }; + status.files[file] = fileStatus; + if (!run.files.includes(file)) { + run.files.push(file); + } + } + } + status.runs = Array.from(runsById.values()).filter((run) => run.files.length); + return status; +} +function sourcePathFromStatusPayload(repoRoot, value, statusPath) { + const raw = String(value || "").trim(); + if (!raw) { + return ""; + } + const resolved = resolveStatusPath(repoRoot, raw, { statusPath, rootPath: repoRoot }); + return resolved && pathIsInside3(resolved, repoRoot) ? toPosixPath(path4.relative(path4.resolve(repoRoot), resolved)) : raw.replace(/\\/g, "/"); +} + +// viewer/src/server/step/stepArtifactCompiler.mjs +import fs5 from "node:fs"; +import path6 from "node:path"; + +// viewer/src/server/step/pythonStepArtifact.mjs +import { spawn } from "node:child_process"; +import fs4 from "node:fs"; +import path5 from "node:path"; +import { fileURLToPath } from "node:url"; +var MODULE_DIR = path5.dirname(fileURLToPath(import.meta.url)); +var PACKAGE_ROOT = path5.resolve(MODULE_DIR, "../../.."); +function firstExistingFile(paths) { + return paths.find((candidate) => fs4.existsSync(candidate)) || ""; +} +function firstExistingDirectory(paths) { + return paths.find((candidate) => { + try { + return fs4.statSync(candidate).isDirectory(); + } catch { + return false; + } + }) || ""; +} +function findUpFile(relativePath) { + let current = MODULE_DIR; + for (; ; ) { + const candidate = path5.join(current, relativePath); + if (fs4.existsSync(candidate)) { + return candidate; + } + const next = path5.dirname(current); + if (next === current) { + return ""; + } + current = next; + } +} +function findUpDirectory(relativePath) { + let current = MODULE_DIR; + for (; ; ) { + const candidate = path5.join(current, relativePath); + if (firstExistingDirectory([candidate])) { + return candidate; + } + const next = path5.dirname(current); + if (next === current) { + return ""; + } + current = next; + } +} +function cadPythonExecutable(repoRoot) { + const configured = String(process.env.VIEWER_CAD_PYTHON || process.env.CAD_PYTHON || "").trim(); + if (configured) { + return configured; + } + const resolvedRepoRoot = path5.resolve(repoRoot || ""); + return firstExistingFile([ + path5.join(resolvedRepoRoot, ".venv", "bin", "python"), + path5.join(process.cwd(), ".venv", "bin", "python"), + path5.join(PACKAGE_ROOT, ".venv", "bin", "python"), + findUpFile(path5.join(".venv", "bin", "python")) + ]) || "python3"; +} +function cadPythonEnv() { + const pythonPathEntries = []; + for (const configured of [ + process.env.VIEWER_CAD_PYTHONPATH, + process.env.CAD_PYTHONPATH, + process.env.VIEWER_CADPY_PYTHONPATH + ]) { + const value = String(configured || "").trim(); + if (value) { + pythonPathEntries.push(value); + } + } + for (const discovered of [ + findUpDirectory(path5.join("scripts", "packages", "cadpy", "src")), + findUpDirectory(path5.join("scripts", "packages")), + findUpDirectory(path5.join("viewer", "packages", "cadpy", "src")), + findUpDirectory(path5.join("packages", "cadpy", "src")), + path5.join(PACKAGE_ROOT, "vendor", "python"), + findUpDirectory(path5.join("runtime", "vendor", "python")), + findUpDirectory(path5.join("vendor", "python")) + ]) { + if (discovered) { + pythonPathEntries.push(discovered); + } + } + const existingPythonPath = String(process.env.PYTHONPATH || "").trim(); + if (existingPythonPath) { + pythonPathEntries.push(existingPythonPath); + } + return { + ...process.env, + ...pythonPathEntries.length ? { PYTHONPATH: pythonPathEntries.join(path5.delimiter) } : {} + }; +} +function ensurePythonStepTopologyArtifact({ + repoRoot, + stepPath, + sourcePath = "", + force = false, + skipStepWrite = false, + writeStepAfterArtifact = false, + resolveOnFirstResult = false, + verbose = false, + meshTolerance = null, + meshAngularTolerance = null +} = {}) { + const resolvedRepoRoot = path5.resolve(repoRoot || ""); + const resolvedStepPath = path5.resolve(stepPath || ""); + const args = [ + "-m", + "cadpy.step_artifact", + "--repo-root", + resolvedRepoRoot, + "--step", + resolvedStepPath + ]; + const resolvedSourcePath = sourcePath ? path5.resolve(sourcePath) : ""; + if (resolvedSourcePath) { + args.push("--source-path", resolvedSourcePath); + } + if (force) { + args.push("--force"); + } + if (skipStepWrite) { + args.push("--skip-step-write"); + } + if (writeStepAfterArtifact) { + args.push("--write-step-after-artifact"); + } + if (meshTolerance !== null && meshTolerance !== void 0) { + args.push("--mesh-tolerance", String(meshTolerance)); + } + if (meshAngularTolerance !== null && meshAngularTolerance !== void 0) { + args.push("--mesh-angular-tolerance", String(meshAngularTolerance)); + } + if (verbose || process.env.VIEWER_STEP_ARTIFACT_VERBOSE === "1") { + args.push("--verbose"); + } + return new Promise((resolve) => { + let resolved = false; + const resolveOnce = (result) => { + if (resolved) { + return; + } + resolved = true; + resolve(result); + }; + const normalizeResult = (result) => { + if (result?.ok) { + result.stepPath = resolvedStepPath; + result.glbPath = inlineStepGlbArtifactPathForSource(resolvedStepPath); + } + return result; + }; + const maybeResolveEarly = () => { + if (!resolveOnFirstResult || resolved) { + return; + } + for (const line of stdout.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed.startsWith("{")) { + continue; + } + try { + const result = normalizeResult(JSON.parse(trimmed)); + if (result?.ok) { + resolveOnce(result); + return; + } + } catch { + } + } + }; + const child = spawn(cadPythonExecutable(resolvedRepoRoot), args, { + cwd: resolvedRepoRoot, + env: cadPythonEnv(resolvedRepoRoot), + stdio: ["ignore", "pipe", "pipe"] + }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + stdout += chunk; + maybeResolveEarly(); + }); + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + child.on("error", (error) => { + resolveOnce({ + ok: false, + stepPath: resolvedStepPath, + error: error instanceof Error ? error.message : String(error) + }); + }); + child.on("close", (code) => { + const output = stdout.trim(); + const lastJsonLine = output.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{")); + if (code === 0 && lastJsonLine) { + try { + resolveOnce(normalizeResult(JSON.parse(lastJsonLine))); + return; + } catch { + } + } + resolveOnce({ + ok: false, + stepPath: resolvedStepPath, + exitCode: code, + error: (stderr || stdout || `STEP artifact generator exited with code ${code}`).trim() + }); + }); + }); +} + +// viewer/src/server/step/stepArtifactCompiler.mjs +var STEP_SUFFIXES = /* @__PURE__ */ new Set([".step", ".stp"]); +function sameStemPythonGeneratorPath(stepPath) { + const extension = path6.extname(stepPath).toLowerCase(); + if (!STEP_SUFFIXES.has(extension)) { + return ""; + } + const candidatePath = path6.join( + path6.dirname(stepPath), + `${path6.basename(stepPath, extension)}.py` + ); + try { + return /\bgen_step\s*\(/.test(fs5.readFileSync(candidatePath, "utf-8")) ? candidatePath : ""; + } catch { + return ""; + } +} +function cadPathForStepSource2(repoRoot, sourcePath) { + const relativePath = repoRelativePath(repoRoot, sourcePath); + return relativePath.slice(0, -path6.extname(relativePath).length); +} +function canBuildStepArtifact(artifact) { + const code = String(artifact?.stepArtifact?.error?.code || ""); + return !artifact?.stepArtifact?.ok && [ + "missing_glb", + "missing_step_topology", + "missing_edge_topology", + "missing_surface_edge_attributes", + "missing_selector_topology", + "missing_source_path", + "missing_step_hash", + "stale_step_artifact", + "unsupported_step_topology" + ].includes(code); +} +async function compileStepTopologyArtifact({ + repoRoot, + stepPath, + sourcePath = "", + targetPath = inlineStepGlbArtifactPathForSource(stepPath), + force = true, + skipStepWrite = false, + writeStepAfterArtifact = false, + meshTolerance = null, + meshAngularTolerance = null +} = {}) { + if (!repoRoot) { + throw new Error("repoRoot is required"); + } + if (!stepPath) { + throw new Error("stepPath is required"); + } + const resolvedRepoRoot = path6.resolve(repoRoot); + const resolvedStepPath = path6.resolve(stepPath); + const resolvedSourcePath = sourcePath ? path6.resolve(sourcePath) : ""; + const resolvedTargetPath = path6.resolve(targetPath); + const pythonResult = await ensurePythonStepTopologyArtifact({ + repoRoot: resolvedRepoRoot, + stepPath: resolvedStepPath, + sourcePath: resolvedSourcePath, + force, + skipStepWrite, + writeStepAfterArtifact, + resolveOnFirstResult: writeStepAfterArtifact, + meshTolerance, + meshAngularTolerance + }); + if (!pythonResult?.ok) { + throw new Error(pythonResult?.error || `Failed to generate STEP topology artifact: ${resolvedStepPath}`); + } + const pythonGlbPath = path6.resolve(pythonResult.glbPath || inlineStepGlbArtifactPathForSource(resolvedStepPath)); + if (pythonGlbPath !== resolvedTargetPath) { + fs5.mkdirSync(path6.dirname(resolvedTargetPath), { recursive: true }); + fs5.copyFileSync(pythonGlbPath, resolvedTargetPath); + } + return { + ...pythonResult, + ok: true, + stepPath: resolvedStepPath, + glbPath: resolvedTargetPath + }; +} +async function ensureStepTopologyArtifact({ + repoRoot, + stepPath, + sourcePath = "", + force = false, + skipStepWrite = false, + writeStepAfterArtifact = false, + meshTolerance = null, + meshAngularTolerance = null +} = {}) { + const resolvedRepoRoot = path6.resolve(repoRoot); + const resolvedStepPath = path6.resolve(stepPath); + const resolvedSourcePath = sourcePath ? path6.resolve(sourcePath) : ""; + const cadPath = cadPathForStepSource2(resolvedRepoRoot, resolvedStepPath); + const current = validateStepTopologyArtifact({ + repoRoot: resolvedRepoRoot, + sourcePath: resolvedStepPath, + cadPath + }); + const currentArtifactError = current.stepArtifact?.error && typeof current.stepArtifact.error === "object" ? current.stepArtifact.error : {}; + const currentSourceKind = String( + current.stepArtifact?.sourceKind || currentArtifactError.sourceKind || "" + ).trim().toLowerCase(); + const currentSourcePath = String( + current.stepArtifact?.sourcePath || currentArtifactError.sourcePath || "" + ).trim(); + const stepFileExists = fs5.existsSync(resolvedStepPath); + const shouldInferPythonSource = Boolean( + resolvedSourcePath || skipStepWrite || writeStepAfterArtifact || !stepFileExists + ); + const inferredSourcePath = shouldInferPythonSource ? resolvedSourcePath || (currentSourceKind === "python" && currentSourcePath ? path6.resolve(resolvedRepoRoot, currentSourcePath) : "") || sameStemPythonGeneratorPath(resolvedStepPath) : ""; + const resolvedSkipStepWrite = Boolean( + skipStepWrite || inferredSourcePath + ); + const hasMeshOverride = meshTolerance !== null && meshTolerance !== void 0 || meshAngularTolerance !== null && meshAngularTolerance !== void 0; + if (!force && !hasMeshOverride && (current.stepArtifact?.ok || !current.stepArtifact?.ok && !canBuildStepArtifact(current))) { + return { + ok: Boolean(current.stepArtifact?.ok), + skipped: true, + reason: current.stepArtifact?.error?.code || "", + stepPath: resolvedStepPath, + glbPath: current.glbPath, + validation: current.stepArtifact + }; + } + const targetPath = inlineStepGlbArtifactPathForSource(resolvedStepPath); + const result = await compileStepTopologyArtifact({ + repoRoot: resolvedRepoRoot, + stepPath: resolvedStepPath, + sourcePath: inferredSourcePath || void 0, + targetPath, + force, + skipStepWrite: resolvedSkipStepWrite, + writeStepAfterArtifact: Boolean(writeStepAfterArtifact && resolvedSkipStepWrite), + meshTolerance, + meshAngularTolerance + }); + const next = validateStepTopologyArtifact({ + repoRoot: resolvedRepoRoot, + sourcePath: resolvedStepPath, + cadPath + }); + return { + ...result, + ok: Boolean(next.stepArtifact?.ok), + validation: next.stepArtifact + }; +} + +// viewer/packages/implicitjs/src/lib/implicitCad/export.js +import fs6 from "node:fs/promises"; +import path7 from "node:path"; +import { pathToFileURL } from "node:url"; + +// viewer/packages/implicitjs/src/common/parameters.js +var HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; +function isObject(value) { + return !!value && typeof value === "object" && !Array.isArray(value); +} +function normalizeString(value, fallback = "") { + const text = String(value ?? fallback).trim(); + return text || fallback; +} +function toFiniteNumber(value, fallback = 0) { + const numericValue = Number(value); + return Number.isFinite(numericValue) ? numericValue : fallback; +} +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} +function normalizeParameterType(value) { + const type = normalizeString(value, "number").toLowerCase(); + if (["number", "boolean", "enum", "select", "color", "string", "button"].includes(type)) { + return type === "select" ? "enum" : type; + } + return "number"; +} +function normalizeParameterOptions(value) { + return (Array.isArray(value) ? value : []).map((option) => { + if (isObject(option)) { + const valueText2 = normalizeString(option.value); + return valueText2 ? { + value: valueText2, + label: normalizeString(option.label, valueText2) + } : null; + } + const valueText = normalizeString(option); + return valueText ? { value: valueText, label: valueText } : null; + }).filter(Boolean); +} +function normalizeParameterValue(definition, value) { + const type = normalizeParameterType(definition?.type); + if (type === "boolean") { + return value === true; + } + if (type === "color") { + const color = normalizeString(value, normalizeString(definition?.defaultValue, "#ffffff")); + return HEX_COLOR_RE.test(color) ? color : "#ffffff"; + } + if (type === "enum") { + const options = Array.isArray(definition?.options) ? definition.options : []; + const valueText = normalizeString(value, options[0]?.value || ""); + return options.some((option) => option.value === valueText) ? valueText : options[0]?.value || ""; + } + if (type === "string") { + return String(value ?? ""); + } + if (type === "button") { + return Math.max(0, Math.floor(toFiniteNumber(value, 0))); + } + const min = toFiniteNumber(definition?.min, 0); + const max = Math.max(toFiniteNumber(definition?.max, min), min); + return clamp(toFiniteNumber(value, toFiniteNumber(definition?.defaultValue, min)), min, max); +} +function normalizeParameterDefinition(id, rawDefinition) { + const raw = isObject(rawDefinition) ? rawDefinition : {}; + const type = normalizeParameterType(raw.type); + const min = toFiniteNumber(raw.min, 0); + const max = Math.max(toFiniteNumber(raw.max, type === "number" ? 1 : min), min); + const options = normalizeParameterOptions(raw.options || raw.values); + const fallbackDefault = type === "boolean" ? false : type === "color" ? "#ffffff" : type === "enum" ? options[0]?.value || "" : type === "string" ? "" : 0; + return { + id, + type, + label: normalizeString(raw.label, id), + description: normalizeString(raw.description), + unit: normalizeString(raw.unit), + min, + max, + step: Math.max(toFiniteNumber(raw.step, type === "number" ? 0.01 : 1), 0), + defaultValue: normalizeParameterValue({ type, min, max, options }, raw.default ?? raw.defaultValue ?? fallbackDefault), + options + }; +} +function normalizeParameterDefinitions(value) { + const entries = Array.isArray(value) ? value.map((definition) => [definition?.id, definition]) : Object.entries(isObject(value) ? value : {}); + return entries.map(([id, rawDefinition]) => normalizeParameterDefinition(normalizeString(id), rawDefinition)).filter((definition) => definition.id); +} +function parameterMapForDefinitions(parameters) { + return Object.fromEntries( + (Array.isArray(parameters) ? parameters : []).filter((parameter) => parameter?.id).map((parameter) => [parameter.id, parameter]) + ); +} +function normalizeParameterValues(definition, values = {}) { + const parameterMap = definition?.parameterMap || {}; + return Object.fromEntries( + Object.values(parameterMap).map((parameter) => [ + parameter.id, + normalizeParameterValue(parameter, isObject(values) && Object.hasOwn(values, parameter.id) ? values[parameter.id] : parameter.defaultValue) + ]) + ); +} +function normalizeParameterAnimations(value) { + const entries = Array.isArray(value) ? value.map((animation) => [animation?.id, animation]) : Object.entries(isObject(value) ? value : {}); + return entries.map(([id, rawAnimation]) => { + const raw = isObject(rawAnimation) ? rawAnimation : {}; + const animationId = normalizeString(id); + return animationId ? { + id: animationId, + label: normalizeString(raw.label, animationId), + description: normalizeString(raw.description), + duration: Math.max(toFiniteNumber(raw.duration ?? raw.durationSeconds, 1), 1e-3), + loop: raw.loop !== false, + update: typeof raw.update === "function" ? raw.update : null, + raw + } : null; + }).filter(Boolean); +} + +// viewer/packages/implicitjs/src/lib/implicitCad/sdfEvaluator.js +function isVector(value) { + return Array.isArray(value); +} +function finiteNumber(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function truthy(value) { + if (isVector(value)) { + return value.some((component) => truthy(component)); + } + return Boolean(value); +} +function vec(args, size) { + const flat = []; + for (const arg of args) { + if (isVector(arg)) { + flat.push(...arg); + } else { + flat.push(arg); + } + } + if (flat.length === 0) { + flat.push(0); + } + if (flat.length === 1) { + return Array.from({ length: size }, () => finiteNumber(flat[0])); + } + return Array.from({ length: size }, (_, index) => finiteNumber(flat[index], 0)); +} +function vec2(x = 0, y = void 0) { + return vec(y === void 0 ? [x] : [x, y], 2); +} +function vec3(x = 0, y = void 0, z = void 0) { + return vec(y === void 0 && z === void 0 ? [x] : [x, y, z], 3); +} +function vec4(x = 0, y = void 0, z = void 0, w = void 0) { + return vec(y === void 0 && z === void 0 && w === void 0 ? [x] : [x, y, z, w], 4); +} +function mapUnary(value, fn) { + return isVector(value) ? value.map((component) => fn(component)) : fn(value); +} +function mapBinary(a, b, fn) { + if (isVector(a) && isVector(b)) { + const size = Math.max(a.length, b.length); + return Array.from({ length: size }, (_, index) => fn(a[index] ?? 0, b[index] ?? 0)); + } + if (isVector(a)) { + return a.map((component) => fn(component, b)); + } + if (isVector(b)) { + return b.map((component) => fn(a, component)); + } + return fn(a, b); +} +function add(a, b) { + return mapBinary(a, b, (x, y) => x + y); +} +function sub(a, b) { + return mapBinary(a, b, (x, y) => x - y); +} +function mul(a, b) { + return mapBinary(a, b, (x, y) => x * y); +} +function div(a, b) { + return mapBinary(a, b, (x, y) => x / y); +} +function neg(value) { + return mapUnary(value, (component) => -component); +} +function abs(value) { + return mapUnary(value, Math.abs); +} +function minValue(a, b) { + return mapBinary(a, b, Math.min); +} +function maxValue(a, b) { + return mapBinary(a, b, Math.max); +} +function clamp2(value, low, high) { + return minValue(maxValue(value, low), high); +} +function mix(a, b, t) { + return add(mul(a, sub(1, t)), mul(b, t)); +} +function smoothstep(edge0, edge1, value) { + const t = clamp2(div(sub(value, edge0), sub(edge1, edge0)), 0, 1); + return mul(mul(t, t), sub(3, mul(2, t))); +} +function mod(a, b) { + return mapBinary(a, b, (x, y) => x - y * Math.floor(x / y)); +} +function length(value) { + if (!isVector(value)) { + return Math.abs(value); + } + return Math.hypot(...value); +} +function dot(a, b) { + const va = isVector(a) ? a : [a]; + const vb = isVector(b) ? b : [b]; + const size = Math.max(va.length, vb.length); + let sum = 0; + for (let index = 0; index < size; index += 1) { + sum += (va[index] ?? 0) * (vb[index] ?? 0); + } + return sum; +} +function cross(a, b) { + const va = vec3(a); + const vb = vec3(b); + return [ + va[1] * vb[2] - va[2] * vb[1], + va[2] * vb[0] - va[0] * vb[2], + va[0] * vb[1] - va[1] * vb[0] + ]; +} +function normalize(value) { + const len = length(value); + return len > 1e-12 ? div(value, len) : mul(value, 0); +} +function swizzleIndices(swizzle) { + const lookup = { + x: 0, + y: 1, + z: 2, + w: 3, + r: 0, + g: 1, + b: 2, + a: 3, + s: 0, + t: 1, + p: 2, + q: 3 + }; + return String(swizzle || "").split("").map((component) => lookup[component]); +} +function getSwizzle(value, swizzle) { + const indices = swizzleIndices(swizzle); + if (!indices.length || indices.some((index) => index === void 0)) { + throw new Error(`Unsupported GLSL member access: .${swizzle}`); + } + const source = isVector(value) ? value : [value]; + const result = indices.map((index) => source[index] ?? 0); + return result.length === 1 ? result[0] : result; +} +function setSwizzle(target, swizzle, value) { + if (!isVector(target)) { + throw new Error(`Cannot assign .${swizzle} on a scalar value`); + } + const indices = swizzleIndices(swizzle); + const values = isVector(value) ? value : [value]; + indices.forEach((targetIndex, index) => { + if (targetIndex === void 0) { + throw new Error(`Unsupported GLSL member assignment: .${swizzle}`); + } + target[targetIndex] = finiteNumber(values[index], values[0] ?? 0); + }); + return target; +} +function implicit_linear_map(value, inMin, inMax, outMin, outMax) { + const slope = (outMax - outMin) / (inMax - inMin); + return (value - inMin) * slope + outMin; +} +function implicit_ramp(value, inMin, inMax, outMin, outMax) { + return Math.min(Math.max(implicit_linear_map(value, inMin, inMax, outMin, outMax), outMin), outMax); +} +function implicit_two_body_field(a, b) { + return (a - b) / (a + b); +} +function implicit_two_body_polar(a, b, angle) { + return a * Math.cos(angle) + b * Math.sin(angle); +} +function implicit_triangle_wave_even(value, period) { + if (isVector(value) || isVector(period)) { + return mapBinary(value, period, implicit_triangle_wave_even); + } + const halfPeriod = period * 0.5; + const quarterPeriod = period * 0.25; + const wrapped = mod(value + halfPeriod, period); + return quarterPeriod - Math.abs(wrapped - halfPeriod); +} +function implicit_triangle_wave_even_positive(value, period) { + return add(implicit_triangle_wave_even(value, period), mul(period, 0.25)); +} +function implicit_triangle_wave_odd(value, period) { + return implicit_triangle_wave_even(sub(value, mul(period, 0.5)), period); +} +function implicit_triangle_wave_odd_positive(value, period) { + return add(implicit_triangle_wave_odd(value, period), mul(period, 0.25)); +} +function implicit_repeat_centered(p, period) { + return sub(mod(add(p, mul(period, 0.5)), period), mul(period, 0.5)); +} +function implicit_intersect_round(a, b, radius) { + const k = Math.max(radius, 0); + const q = maxValue(add(vec2(a, b), k), 0); + return Math.min(-k, Math.max(a, b)) + length(q); +} +function implicit_intersect_chamfer(a, b, radius) { + return Math.max(Math.max(a, b), (a + b + radius) * 0.7071067811865476); +} +function implicit_intersect_exp(a, b, radius) { + const k = Math.max(radius, 1e-6) * 0.5; + return k * Math.log(Math.exp(a / k) + Math.exp(b / k)); +} +function implicit_intersect_lp_norm(a, b, radius, normPower) { + const k = Math.max(radius, 0); + const p = Math.max(normPower, 1e-6); + const q = maxValue(add(vec2(a, b), k), 0); + return Math.min(-k, Math.max(a, b)) + Math.pow(Math.pow(q[0], p) + Math.pow(q[1], p), 1 / p); +} +function implicit_intersect_rvachev(a, b, radius) { + const sharp = Math.max(a, b); + const k = Math.max(radius, 0); + if (k <= 0) { + return sharp; + } + const r0 = a + b - Math.sqrt(a * a + b * b); + const t = Math.min(Math.max((sharp + k) / k, 0), 1); + const s = t * t * (3 - 2 * t); + return sharp < -k ? sharp : mix(sharp, r0, s); +} +var BUILTINS = { + float: (value = 0) => finiteNumber(value), + int: (value = 0) => Math.trunc(finiteNumber(value)), + bool: (value = false) => truthy(value), + vec2, + vec3, + vec4, + abs, + min: minValue, + max: maxValue, + clamp: clamp2, + mix, + smoothstep, + mod, + length, + dot, + cross, + normalize, + sin: (value) => mapUnary(value, Math.sin), + cos: (value) => mapUnary(value, Math.cos), + tan: (value) => mapUnary(value, Math.tan), + atan: (...args) => args.length >= 2 ? Math.atan2(args[0], args[1]) : mapUnary(args[0], Math.atan), + pow: (a, b) => mapBinary(a, b, Math.pow), + sqrt: (value) => mapUnary(value, Math.sqrt), + exp: (value) => mapUnary(value, Math.exp), + log: (value) => mapUnary(value, Math.log), + floor: (value) => mapUnary(value, Math.floor), + ceil: (value) => mapUnary(value, Math.ceil), + fract: (value) => mapUnary(value, (component) => component - Math.floor(component)), + sign: (value) => mapUnary(value, Math.sign), + implicit_clamp01: (value) => clamp2(value, 0, 1), + implicit_linear_map, + implicit_ramp, + implicit_two_body_field, + implicit_two_body_polar, + implicit_triangle_wave_even, + implicit_triangle_wave_even_positive, + implicit_triangle_wave_odd, + implicit_triangle_wave_odd_positive, + implicit_repeat_centered, + implicit_intersect_sharp: (a, b) => Math.max(a, b), + implicit_union_sharp: (a, b) => Math.min(a, b), + implicit_intersect_round, + implicit_union_round: (a, b, radius) => -implicit_intersect_round(-a, -b, radius), + implicit_intersect_chamfer, + implicit_union_chamfer: (a, b, radius) => -implicit_intersect_chamfer(-a, -b, radius), + implicit_intersect_exp, + implicit_union_exp: (a, b, radius) => -implicit_intersect_exp(-a, -b, radius), + implicit_intersect_lp_norm, + implicit_union_lp_norm: (a, b, radius, normPower) => -implicit_intersect_lp_norm(-a, -b, radius, normPower), + implicit_intersect_rvachev, + implicit_union_rvachev: (a, b, radius) => -implicit_intersect_rvachev(-a, -b, radius), + implicit_plane2: (p, origin, normal) => dot(sub(p, origin), normalize(normal)), + implicit_line_segment2: (p, a, b) => { + const segment = sub(b, a); + const segmentLengthSq = dot(segment, segment); + if (segmentLengthSq < 1e-12) { + return length(sub(p, a)); + } + const t = clamp2(dot(sub(p, a), segment) / segmentLengthSq, 0, 1); + return length(sub(p, add(a, mul(t, segment)))); + }, + implicit_sphere: (p, center, radius) => length(sub(p, center)) - radius, + implicit_box_centered: (p, size, center) => { + const q = sub(abs(sub(p, center)), mul(size, 0.5)); + return length(maxValue(q, 0)) + Math.min(Math.max(q[0], Math.max(q[1], q[2])), 0); + }, + implicit_plane: (p, origin, normal) => dot(sub(p, origin), normalize(normal)), + implicit_line_segment: (p, a, b) => { + const segment = sub(b, a); + const segmentLengthSq = dot(segment, segment); + if (segmentLengthSq < 1e-12) { + return length(sub(p, a)); + } + const t = clamp2(dot(sub(p, a), segment) / segmentLengthSq, 0, 1); + return length(sub(p, add(a, mul(t, segment)))); + }, + implicit_torus: (p, majorRadius, minorRadius) => { + const q = vec2(length(getSwizzle(p, "xy")) - majorRadius, getSwizzle(p, "z")); + return length(q) - minorRadius; + }, + implicit_axis: (p, origin, direction) => { + const directionLength = length(direction); + if (directionLength < 1e-12) { + return length(sub(p, origin)); + } + const axis = div(direction, directionLength); + const toPoint = sub(p, origin); + return length(sub(toPoint, mul(dot(toPoint, axis), axis))); + }, + implicit_cylinder: (p, origin, direction, radius) => BUILTINS.implicit_axis(p, origin, direction) - radius, + implicit_cylinder_capped: (p, a, b, radius) => { + const axis = sub(b, a); + const side = BUILTINS.implicit_cylinder(p, a, axis, radius); + const capA = -BUILTINS.implicit_plane(p, a, axis); + const capB = BUILTINS.implicit_plane(p, b, axis); + return Math.max(side, Math.max(capA, capB)); + }, + implicit_capsule: (p, a, b, radius) => BUILTINS.implicit_line_segment(p, a, b) - radius, + implicit_cone_capsule: (p, a, b, radiusA, radiusB) => { + const axis = sub(b, a); + const axisLengthSq = dot(axis, axis); + if (axisLengthSq < 1e-12) { + return BUILTINS.implicit_sphere(p, a, radiusA); + } + const t = clamp2(dot(sub(p, a), axis) / axisLengthSq, 0, 1); + const radius = mix(radiusA, radiusB, t); + return length(sub(p, add(a, mul(t, axis)))) - radius; + }, + implicit_cone: (p, apex, direction, halfAngle) => { + const directionLength = length(direction); + if (directionLength < 1e-12) { + return length(sub(p, apex)); + } + const axis = div(direction, directionLength); + const toPoint = sub(p, apex); + const axial = dot(toPoint, axis); + const perpendicular = length(sub(toPoint, mul(axial, axis))); + return perpendicular - axial * Math.tan(halfAngle); + }, + implicit_cone_capped: (p, a, b, radiusA, radiusB) => { + const axis = sub(b, a); + const axisLength = length(axis); + if (axisLength < 1e-12) { + return BUILTINS.implicit_sphere(p, a, radiusA); + } + const halfAngle = Math.atan2(Math.abs(radiusB - radiusA), axisLength); + const coneDistance = radiusA < radiusB ? BUILTINS.implicit_cone(p, a, axis, halfAngle) - radiusA : BUILTINS.implicit_cone(p, b, neg(axis), halfAngle) - radiusB; + const capA = -BUILTINS.implicit_plane(p, a, axis); + const capB = BUILTINS.implicit_plane(p, b, axis); + return Math.max(coneDistance, Math.max(capA, capB)); + }, + implicit_shell: (distanceValue, thickness, bias = 0) => Math.abs(distanceValue + bias * thickness * 0.5) - thickness * 0.5, + implicit_rotate_axis: (p, origin, direction, angle) => { + const k = normalize(direction); + const local = sub(p, origin); + const c = Math.cos(angle); + const s = Math.sin(angle); + return add(add(add(origin, mul(local, c)), mul(cross(k, local), s)), mul(k, dot(k, local) * (1 - c))); + }, + implicit_remap_cylindrical: (p, circumference) => { + const radial = length(getSwizzle(p, "xy")); + const theta = Math.atan2(p[1], p[0]); + return vec3(radial, theta * (circumference / (Math.PI * 2)), p[2]); + }, + implicit_tpms_gyroid: (p, period, drop) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const field = dot(drop, mul(BUILTINS.sin(xyz), BUILTINS.cos(yzx))); + return field * (period[0] + period[1] + period[2]) / 18; + }, + implicit_tpms_schwarz: (p, period, drop, gyroidBlend) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const mixTerm = add(vec3(-(1 - gyroidBlend)), mul(BUILTINS.sin(xyz), gyroidBlend)); + const field = dot(drop, mul(BUILTINS.cos(yzx), mixTerm)); + return field * (period[0] + period[1] + period[2]) / 36; + }, + implicit_diamond: () => 0 +}; +BUILTINS.implicit_tpms_diamond = (p, period, drop, gyroidBlend) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const zxy = getSwizzle(xyz, "zxy"); + const sinXyz = BUILTINS.sin(xyz); + const cosYzx = BUILTINS.cos(yzx); + const cosZxy = BUILTINS.cos(zxy); + const blendFactor = 1 - gyroidBlend; + const term1 = blendFactor * sinXyz[0] * sinXyz[1] * sinXyz[2]; + const term2 = dot(drop, mul(mul(sinXyz, cosYzx), add(mul(cosZxy, blendFactor), vec3(gyroidBlend)))); + return (term1 + term2) * (period[0] + period[1] + period[2]) / (6 * 2.8284271247461903 * 2); +}; +BUILTINS.implicit_tpms_lidinoid = (p, period, drop, gyroidBlend) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const zxy = getSwizzle(xyz, "zxy"); + const cos2Xyz = BUILTINS.cos(mul(2, xyz)); + const cos2Yzx = getSwizzle(cos2Xyz, "yzx"); + const blendFactor = 1 - gyroidBlend; + const term1 = dot(drop, mul(mul(BUILTINS.sin(zxy), BUILTINS.cos(yzx)), add(mul(BUILTINS.sin(mul(2, xyz)), blendFactor), vec3(gyroidBlend)))); + const term2 = blendFactor * dot(cos2Xyz, cos2Yzx); + return (term1 - term2) * (period[0] + period[1] + period[2]) / 72; +}; +BUILTINS.implicit_tpms_neovius = (p, period, drop, schwarzBlend) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const cosDrop = mul(BUILTINS.cos(xyz), drop); + const term1 = -dot(cosDrop, vec3(1)); + const term2 = (1 - schwarzBlend) * (4 / 3) * cosDrop[0] * cosDrop[1] * cosDrop[2]; + return (term1 - term2) * (period[0] + period[1] + period[2]) / (6 * (26 / 3)); +}; +BUILTINS.implicit_tpms_split_p = (p, period, lidinoidBlend, gyroidOctave, schwarzOctave) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const zxy = getSwizzle(xyz, "zxy"); + const sin2Xyz = BUILTINS.sin(mul(2, xyz)); + const cos2Xyz = BUILTINS.cos(mul(2, xyz)); + const term1 = -lidinoidBlend * dot(mul(mul(sin2Xyz, BUILTINS.cos(yzx)), BUILTINS.sin(zxy)), vec3(1)); + const term2 = gyroidOctave * dot(mul(sin2Xyz, cos2Xyz), vec3(1)); + const term3 = schwarzOctave * dot(cos2Xyz, vec3(1)); + return (term1 + term2 + term3) * (period[0] + period[1] + period[2]) / 36; +}; +BUILTINS.implicit_tpms_iwp = (p, period, drop) => { + const xyz = mul(p, div(Math.PI * 2, period)); + const yzx = getSwizzle(xyz, "yzx"); + const term1 = 2 * dot(mul(mul(drop, BUILTINS.cos(xyz)), BUILTINS.cos(yzx)), vec3(1)); + const term2 = dot(BUILTINS.cos(mul(2, xyz)), vec3(1)); + return (term1 - term2) * (period[0] + period[1] + period[2]) / 48; +}; +BUILTINS.implicit_cubic_grid = (p, size) => { + const d = implicit_triangle_wave_even_positive(p, size); + return Math.min(d[0], Math.min(d[1], d[2])); +}; +BUILTINS.implicit_square_honeycomb = (p, size) => { + const d = implicit_triangle_wave_even_positive(getSwizzle(p, "xy"), size); + return Math.min(d[0], d[1]); +}; +BUILTINS.implicit_square_honeycomb_reinforced = (p, size, rotation, rotation2, hasRotation2) => { + const pxy = getSwizzle(p, "xy"); + const grid = implicit_triangle_wave_even_positive(pxy, size); + const squareGrid = Math.min(grid[0], grid[1]); + const repeated = implicit_repeat_centered(pxy, size); + const angle = Math.PI * rotation; + let diagonal = Math.abs(BUILTINS.implicit_plane2(repeated, vec2(0), vec2(Math.cos(angle), Math.sin(angle)))); + if (hasRotation2 > 0.5) { + const angle2 = Math.PI * rotation2; + const diagonal2 = Math.abs(BUILTINS.implicit_plane2(repeated, vec2(0), vec2(Math.cos(angle2), Math.sin(angle2)))); + diagonal = Math.min(diagonal, diagonal2); + } + return Math.min(squareGrid, diagonal); +}; +BUILTINS.implicit_square_diagonal_honeycomb = (p, size) => { + const period = vec2(size[0] + size[1]); + const repeated = implicit_repeat_centered(getSwizzle(p, "xy"), period); + const positive = Math.abs(BUILTINS.implicit_plane2(repeated, vec2(0), vec2(size[1], size[0]))); + const negative = Math.abs(BUILTINS.implicit_plane2(repeated, vec2(0), vec2(size[1], -size[0]))); + return Math.min(positive, negative); +}; +BUILTINS.implicit_octet_honeycomb = (p, size) => { + const pxy = getSwizzle(p, "xy"); + const square = BUILTINS.implicit_square_honeycomb(p, size); + const oddGrid = implicit_triangle_wave_odd_positive(pxy, size); + const planeGrid = Math.min(oddGrid[0], oddGrid[1]); + const diagonalPeriod = length(size) * 0.5; + const rotated = vec2((pxy[0] + pxy[1]) / 1.4142135623730951, (pxy[0] - pxy[1]) / 1.4142135623730951); + const diagonal = implicit_triangle_wave_odd_positive(rotated, vec2(diagonalPeriod)); + return Math.min(Math.min(square, planeGrid), Math.min(diagonal[0], diagonal[1])); +}; +BUILTINS.implicit_hexagonal_honeycomb = (p, size, setback) => { + const pxy = getSwizzle(p, "xy"); + const halfSize = mul(size, 0.5); + const quarterSize = mul(size, 0.25); + const starCenter = vec2(0, (1 - setback) * halfSize[1]); + const transition = vec2(halfSize[0], setback * halfSize[1]); + const folded = abs(implicit_repeat_centered(pxy, size)); + const reflected = vec2(folded[0] - halfSize[0], halfSize[1] - folded[1]); + const foldedStar = Math.min( + BUILTINS.implicit_line_segment2(folded, starCenter, vec2(0, size[1])), + Math.min( + BUILTINS.implicit_line_segment2(folded, starCenter, transition), + BUILTINS.implicit_line_segment2(folded, starCenter, vec2(-transition[0], transition[1])) + ) + ); + const reflectedStar = Math.min( + BUILTINS.implicit_line_segment2(reflected, starCenter, vec2(0, size[1])), + Math.min( + BUILTINS.implicit_line_segment2(reflected, starCenter, transition), + BUILTINS.implicit_line_segment2(reflected, starCenter, vec2(-transition[0], transition[1])) + ) + ); + return folded[0] < quarterSize[0] ? foldedStar : reflectedStar; +}; +BUILTINS.implicit_triangular_honeycomb = (p, size) => { + const folded = abs(implicit_repeat_centered(getSwizzle(p, "xy"), size)); + const halfSize = mul(size, 0.5); + const quarterSize = mul(size, 0.25); + const normalH = vec2(0, 1); + const normalP60 = normalize(vec2(size[1], size[0])); + const normalN60 = normalize(vec2(size[1], -size[0])); + const foldedStar = Math.min( + Math.abs(dot(folded, normalH)), + Math.min(Math.abs(dot(folded, normalP60)), Math.abs(dot(folded, normalN60))) + ); + const shifted = sub(folded, halfSize); + const shiftedStar = Math.min( + Math.abs(dot(shifted, normalH)), + Math.min(Math.abs(dot(shifted, normalP60)), Math.abs(dot(shifted, normalN60))) + ); + return folded[1] < quarterSize[1] ? foldedStar : shiftedStar; +}; +var TYPE_KEYWORDS = /* @__PURE__ */ new Set(["float", "int", "bool", "vec2", "vec3", "vec4"]); +var QUALIFIERS = /* @__PURE__ */ new Set(["const", "highp", "mediump", "lowp", "in", "out", "uniform", "varying"]); +var OPERATORS = ["<=", ">=", "==", "!=", "&&", "||", "+=", "-=", "*=", "/=", "++", "--"]; +function stripComments(source) { + return String(source || "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, ""); +} +function tokenize(source) { + const tokens = []; + const text = stripComments(source); + let index = 0; + while (index < text.length) { + const char = text[index]; + if (/\s/.test(char)) { + index += 1; + continue; + } + const numberMatch = text.slice(index).match(/^(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?/); + if (numberMatch) { + tokens.push({ type: "number", value: Number(numberMatch[0]) }); + index += numberMatch[0].length; + continue; + } + const identifierMatch = text.slice(index).match(/^[A-Za-z_][A-Za-z0-9_]*/); + if (identifierMatch) { + tokens.push({ type: "identifier", value: identifierMatch[0] }); + index += identifierMatch[0].length; + continue; + } + const op = OPERATORS.find((candidate) => text.startsWith(candidate, index)); + if (op) { + tokens.push({ type: "operator", value: op }); + index += op.length; + continue; + } + tokens.push({ type: "punct", value: char }); + index += 1; + } + return tokens; +} +var Parser = class { + constructor(tokens) { + this.tokens = tokens; + this.index = 0; + } + peek(offset = 0) { + return this.tokens[this.index + offset] || null; + } + consume(value = null) { + const token = this.peek(); + if (!token) { + throw new Error(`Expected ${value || "token"}, reached end of GLSL source`); + } + if (value !== null && token.value !== value) { + throw new Error(`Expected ${value}, got ${token.value}`); + } + this.index += 1; + return token; + } + match(value) { + if (this.peek()?.value === value) { + this.index += 1; + return true; + } + return false; + } + skipQualifiers() { + while (QUALIFIERS.has(this.peek()?.value)) { + this.consume(); + } + } + parseProgram() { + const globals = []; + const functions = /* @__PURE__ */ new Map(); + while (this.peek()) { + this.skipQualifiers(); + const type = this.peek()?.value; + if (!TYPE_KEYWORDS.has(type) && type !== "void") { + this.consume(); + continue; + } + this.consume(); + const name = this.consume().value; + if (this.match("(")) { + const params = []; + if (!this.match(")")) { + do { + this.skipQualifiers(); + const paramType = this.consume().value; + const paramName = this.consume().value; + params.push({ type: paramType, name: paramName }); + } while (this.match(",")); + this.consume(")"); + } + functions.set(name, { + name, + params, + returnType: type, + body: this.parseBlock() + }); + } else { + let init = { type: "literal", value: 0 }; + if (this.match("=")) { + init = this.parseExpression(); + } + this.consume(";"); + globals.push({ type: "var", varType: type, name, init }); + } + } + return { globals, functions }; + } + parseBlock() { + this.consume("{"); + const body = []; + while (this.peek() && this.peek().value !== "}") { + body.push(this.parseStatement()); + } + this.consume("}"); + return { type: "block", body }; + } + parseStatement() { + if (this.peek()?.value === "{") { + return this.parseBlock(); + } + if (this.match(";")) { + return { type: "empty" }; + } + if (this.match("return")) { + const expression2 = this.parseExpression(); + this.consume(";"); + return { type: "return", expression: expression2 }; + } + if (this.match("if")) { + this.consume("("); + const condition = this.parseExpression(); + this.consume(")"); + const consequent = this.parseStatement(); + const alternate = this.match("else") ? this.parseStatement() : null; + return { type: "if", condition, consequent, alternate }; + } + if (this.match("for")) { + this.consume("("); + const init = this.parseForPart(";"); + this.consume(";"); + const condition = this.peek()?.value === ";" ? null : this.parseExpression(); + this.consume(";"); + const update = this.peek()?.value === ")" ? null : this.parseExpression(); + this.consume(")"); + return { type: "for", init, condition, update, body: this.parseStatement() }; + } + if (TYPE_KEYWORDS.has(this.peek()?.value) || QUALIFIERS.has(this.peek()?.value) && TYPE_KEYWORDS.has(this.peek(1)?.value)) { + const statement = this.parseVariableDeclaration(); + this.consume(";"); + return statement; + } + const expression = this.parseExpression(); + this.consume(";"); + return { type: "expr", expression }; + } + parseForPart() { + if (TYPE_KEYWORDS.has(this.peek()?.value) || QUALIFIERS.has(this.peek()?.value) && TYPE_KEYWORDS.has(this.peek(1)?.value)) { + return this.parseVariableDeclaration(); + } + if (this.peek()?.value === ";") { + return { type: "empty" }; + } + return { type: "expr", expression: this.parseExpression() }; + } + parseVariableDeclaration() { + this.skipQualifiers(); + const varType = this.consume().value; + const name = this.consume().value; + let init = { type: "literal", value: defaultValueForType(varType) }; + if (this.match("=")) { + init = this.parseExpression(); + } + return { type: "var", varType, name, init }; + } + parseExpression() { + return this.parseAssignment(); + } + parseAssignment() { + const left = this.parseTernary(); + const op = this.peek()?.value; + if (["=", "+=", "-=", "*=", "/="].includes(op)) { + this.consume(); + return { type: "assign", op, left, right: this.parseAssignment() }; + } + return left; + } + parseTernary() { + const condition = this.parseLogicalOr(); + if (!this.match("?")) { + return condition; + } + const consequent = this.parseExpression(); + this.consume(":"); + return { type: "ternary", condition, consequent, alternate: this.parseExpression() }; + } + parseLogicalOr() { + let node = this.parseLogicalAnd(); + while (this.match("||")) { + node = { type: "binary", op: "||", left: node, right: this.parseLogicalAnd() }; + } + return node; + } + parseLogicalAnd() { + let node = this.parseEquality(); + while (this.match("&&")) { + node = { type: "binary", op: "&&", left: node, right: this.parseEquality() }; + } + return node; + } + parseEquality() { + let node = this.parseComparison(); + while (["==", "!="].includes(this.peek()?.value)) { + const op = this.consume().value; + node = { type: "binary", op, left: node, right: this.parseComparison() }; + } + return node; + } + parseComparison() { + let node = this.parseAdditive(); + while (["<", ">", "<=", ">="].includes(this.peek()?.value)) { + const op = this.consume().value; + node = { type: "binary", op, left: node, right: this.parseAdditive() }; + } + return node; + } + parseAdditive() { + let node = this.parseMultiplicative(); + while (["+", "-"].includes(this.peek()?.value)) { + const op = this.consume().value; + node = { type: "binary", op, left: node, right: this.parseMultiplicative() }; + } + return node; + } + parseMultiplicative() { + let node = this.parseUnary(); + while (["*", "/"].includes(this.peek()?.value)) { + const op = this.consume().value; + node = { type: "binary", op, left: node, right: this.parseUnary() }; + } + return node; + } + parseUnary() { + if (this.match("+")) { + return this.parseUnary(); + } + if (this.match("-")) { + return { type: "unary", op: "-", argument: this.parseUnary() }; + } + if (this.match("!")) { + return { type: "unary", op: "!", argument: this.parseUnary() }; + } + if (this.match("++")) { + return { type: "update", op: "++", argument: this.parseUnary(), prefix: true }; + } + if (this.match("--")) { + return { type: "update", op: "--", argument: this.parseUnary(), prefix: true }; + } + return this.parsePostfix(); + } + parsePostfix() { + let node = this.parsePrimary(); + for (; ; ) { + if (this.match(".")) { + node = { type: "member", object: node, property: this.consume().value }; + continue; + } + if (this.match("(")) { + const args = []; + if (!this.match(")")) { + do { + args.push(this.parseExpression()); + } while (this.match(",")); + this.consume(")"); + } + node = { type: "call", callee: node, args }; + continue; + } + if (this.match("++")) { + node = { type: "update", op: "++", argument: node, prefix: false }; + continue; + } + if (this.match("--")) { + node = { type: "update", op: "--", argument: node, prefix: false }; + continue; + } + break; + } + return node; + } + parsePrimary() { + const token = this.consume(); + if (token.type === "number") { + return { type: "literal", value: token.value }; + } + if (token.value === "true" || token.value === "false") { + return { type: "literal", value: token.value === "true" }; + } + if (token.value === "(") { + const expression = this.parseExpression(); + this.consume(")"); + return expression; + } + if (token.type === "identifier") { + return { type: "identifier", name: token.value }; + } + throw new Error(`Unexpected GLSL token: ${token.value}`); + } +}; +function defaultValueForType(type) { + if (type === "bool") { + return false; + } + if (type === "vec2") { + return vec2(0); + } + if (type === "vec3") { + return vec3(0); + } + if (type === "vec4") { + return vec4(0); + } + return 0; +} +var ReturnValue = class extends Error { + constructor(value) { + super("return"); + this.value = value; + } +}; +var Scope = class { + constructor(parent = null) { + this.parent = parent; + this.values = /* @__PURE__ */ new Map(); + } + has(name) { + return this.values.has(name) || Boolean(this.parent?.has(name)); + } + get(name) { + if (this.values.has(name)) { + return this.values.get(name); + } + if (this.parent) { + return this.parent.get(name); + } + throw new Error(`Unknown GLSL identifier: ${name}`); + } + set(name, value) { + if (this.values.has(name)) { + this.values.set(name, cloneValue(value)); + return; + } + if (this.parent?.has(name)) { + this.parent.set(name, value); + return; + } + this.values.set(name, cloneValue(value)); + } + define(name, value) { + this.values.set(name, cloneValue(value)); + } +}; +function cloneValue(value) { + return isVector(value) ? [...value] : value; +} +function castValue(value, type) { + if (type === "int") { + return Math.trunc(finiteNumber(value)); + } + if (type === "float") { + return finiteNumber(value); + } + if (type === "bool") { + return truthy(value); + } + if (type === "vec2") { + return vec2(value); + } + if (type === "vec3") { + return vec3(value); + } + if (type === "vec4") { + return vec4(value); + } + return value; +} +function binaryValue(op, left, right) { + switch (op) { + case "+": + return add(left, right); + case "-": + return sub(left, right); + case "*": + return mul(left, right); + case "/": + return div(left, right); + case "<": + return left < right; + case ">": + return left > right; + case "<=": + return left <= right; + case ">=": + return left >= right; + case "==": + return left === right; + case "!=": + return left !== right; + case "&&": + return truthy(left) && truthy(right); + case "||": + return truthy(left) || truthy(right); + default: + throw new Error(`Unsupported GLSL operator: ${op}`); + } +} +function evalLValue(node, scope, runtime2) { + if (node.type === "identifier") { + return { + get: () => scope.get(node.name), + set: (value) => { + scope.set(node.name, value); + return value; + } + }; + } + if (node.type === "member") { + const objectLValue = evalLValue(node.object, scope, runtime2); + return { + get: () => getSwizzle(objectLValue.get(), node.property), + set: (value) => { + const target = objectLValue.get(); + setSwizzle(target, node.property, value); + objectLValue.set(target); + return value; + } + }; + } + throw new Error("Unsupported GLSL assignment target"); +} +function evalExpression(node, scope, runtime2) { + switch (node.type) { + case "literal": + return cloneValue(node.value); + case "identifier": + return scope.get(node.name); + case "member": + return getSwizzle(evalExpression(node.object, scope, runtime2), node.property); + case "unary": { + const value = evalExpression(node.argument, scope, runtime2); + if (node.op === "-") { + return neg(value); + } + if (node.op === "!") { + return !truthy(value); + } + return value; + } + case "update": { + const lvalue = evalLValue(node.argument, scope, runtime2); + const current = lvalue.get(); + const next = node.op === "++" ? add(current, 1) : sub(current, 1); + lvalue.set(next); + return node.prefix ? next : current; + } + case "binary": + if (node.op === "&&") { + return truthy(evalExpression(node.left, scope, runtime2)) && truthy(evalExpression(node.right, scope, runtime2)); + } + if (node.op === "||") { + return truthy(evalExpression(node.left, scope, runtime2)) || truthy(evalExpression(node.right, scope, runtime2)); + } + return binaryValue(node.op, evalExpression(node.left, scope, runtime2), evalExpression(node.right, scope, runtime2)); + case "ternary": + return truthy(evalExpression(node.condition, scope, runtime2)) ? evalExpression(node.consequent, scope, runtime2) : evalExpression(node.alternate, scope, runtime2); + case "assign": { + const lvalue = evalLValue(node.left, scope, runtime2); + const right = evalExpression(node.right, scope, runtime2); + const current = lvalue.get(); + const value = node.op === "=" ? right : node.op === "+=" ? add(current, right) : node.op === "-=" ? sub(current, right) : node.op === "*=" ? mul(current, right) : div(current, right); + return lvalue.set(value); + } + case "call": { + const callee = node.callee.type === "identifier" ? node.callee.name : ""; + const args = node.args.map((arg) => evalExpression(arg, scope, runtime2)); + if (BUILTINS[callee]) { + return BUILTINS[callee](...args); + } + return runtime2.call(callee, args); + } + default: + throw new Error(`Unsupported GLSL expression node: ${node.type}`); + } +} +function executeStatement(statement, scope, runtime2) { + switch (statement.type) { + case "empty": + return; + case "block": + for (const child of statement.body) { + executeStatement(child, scope, runtime2); + } + return; + case "var": + scope.define(statement.name, castValue(evalExpression(statement.init, scope, runtime2), statement.varType)); + return; + case "expr": + evalExpression(statement.expression, scope, runtime2); + return; + case "return": + throw new ReturnValue(evalExpression(statement.expression, scope, runtime2)); + case "if": + if (truthy(evalExpression(statement.condition, scope, runtime2))) { + executeStatement(statement.consequent, scope, runtime2); + } else if (statement.alternate) { + executeStatement(statement.alternate, scope, runtime2); + } + return; + case "for": { + executeStatement(statement.init, scope, runtime2); + for (let guard = 0; guard < 1e4; guard += 1) { + if (statement.condition && !truthy(evalExpression(statement.condition, scope, runtime2))) { + return; + } + executeStatement(statement.body, scope, runtime2); + if (statement.update) { + evalExpression(statement.update, scope, runtime2); + } + } + throw new Error("GLSL for-loop exceeded exporter safety limit"); + } + default: + throw new Error(`Unsupported GLSL statement node: ${statement.type}`); + } +} +function normalizedDistanceSource(source) { + const text = String(source || "").trim(); + if (!text) { + throw new Error("Implicit CAD model has no GLSL source"); + } + return `${text} + +float implicit_distance(vec3 p) { + return sdf(p); +} +`; +} +function normalizedColorSource(source) { + const text = String(source || "").trim(); + if (!text) { + throw new Error("Implicit CAD model has no GLSL source"); + } + const hasColorFunction = /\bvec3\s+color\s*\(/.test(text); + return `${text} + +vec3 implicit_color(vec3 p, vec3 normal) { + ${hasColorFunction ? "return color(p, normal);" : "return vec3(0.831372549, 0.831372549, 0.847058824);"} +} +`; +} +function createImplicitCadProgramRuntime(model, source, functionName) { + const program = new Parser(tokenize(source)).parseProgram(); + if (!program.functions.has(functionName)) { + throw new Error(`Implicit CAD GLSL source did not define ${functionName}`); + } + const globals = new Scope(); + for (const [name, value] of Object.entries({ + PI: Math.PI, + TWO_PI: Math.PI * 2 + })) { + globals.define(name, value); + } + for (const [name, uniform] of Object.entries(model?.uniforms || {})) { + globals.define(name, castValue(uniform?.value, uniform?.type)); + } + const runtime2 = { + call(name, args) { + const fn = program.functions.get(name); + if (!fn) { + throw new Error(`Unknown GLSL function: ${name}`); + } + const scope = new Scope(globals); + fn.params.forEach((param, index) => { + scope.define(param.name, castValue(args[index], param.type)); + }); + try { + executeStatement(fn.body, scope, runtime2); + } catch (error) { + if (error instanceof ReturnValue) { + return castValue(error.value, fn.returnType); + } + throw error; + } + return defaultValueForType(fn.returnType); + } + }; + for (const global2 of program.globals) { + executeStatement(global2, globals, runtime2); + } + return runtime2; +} +function createImplicitCadSdfEvaluator(model) { + const source = normalizedDistanceSource(model?.glslSource || model?.distanceSource); + const runtime2 = createImplicitCadProgramRuntime(model, source, "implicit_distance"); + return (x, y, z) => finiteNumber(runtime2.call("implicit_distance", [vec3(x, y, z)]), 1e6); +} +function createImplicitCadColorEvaluator(model) { + const source = normalizedColorSource(model?.colorSource || model?.glslSource || model?.distanceSource); + const runtime2 = createImplicitCadProgramRuntime(model, source, "implicit_color"); + return (point, normal = [0, 0, 1]) => { + const value = runtime2.call("implicit_color", [vec3(point), vec3(normal)]); + return vec3(value).map((component) => Math.min(Math.max(finiteNumber(component, 0), 0), 1)); + }; +} + +// viewer/packages/implicitjs/src/lib/implicitCad/schema.js +var IMPLICIT_CAD_SCHEMA = "implicit.js/0.1.0"; +var IMPLICIT_CAD_KIND = "implicit"; +var IMPLICIT_CAD_EXTENSIONS2 = Object.freeze([".implicit.js", ".implicit.mjs"]); + +// viewer/packages/implicitjs/src/lib/implicitCad/model.js +var DEFAULT_BOUNDS = Object.freeze({ + min: [-50, -50, -50], + max: [50, 50, 50] +}); +var AUTO_BOUNDS_RADII = Object.freeze([4, 8, 16, 32, 64, 128, 256, 512]); +var AUTO_BOUNDS_SCAN_SAMPLES = 5; +var AUTO_BOUNDS_REFINE_SAMPLES = 9; +var AUTO_BOUNDS_CACHE_LIMIT = 32; +var DEFAULT_COLOR = "#f4f4f5"; +var HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}){1,2}$/; +var GLSL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; +var UNIFORM_TYPES = Object.freeze(/* @__PURE__ */ new Set(["float", "int", "bool", "vec2", "vec3", "vec4"])); +var PARAM_UNIFORM_TYPES = Object.freeze({ + number: "float", + boolean: "bool", + color: "vec3", + button: "int" +}); +function isObject2(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} +function finiteNumber2(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function finitePositiveNumber(value, fallback) { + const numeric = Number(value); + return Number.isFinite(numeric) && numeric > 0 ? numeric : fallback; +} +function finiteVec3(value, fallback) { + const source = Array.isArray(value) || ArrayBuffer.isView(value) ? value : []; + return [0, 1, 2].map((index) => finiteNumber2(source[index], fallback[index] || 0)); +} +function finiteVector(value, length2, fallback = []) { + const source = Array.isArray(value) || ArrayBuffer.isView(value) ? value : []; + return Array.from({ length: length2 }, (_, index) => finiteNumber2(source[index], fallback[index] || 0)); +} +function normalizeBounds(value, fallback = DEFAULT_BOUNDS) { + const radius = finitePositiveNumber(value, null); + const raw = radius ? { min: [-radius, -radius, -radius], max: [radius, radius, radius] } : Array.isArray(value) && value.length >= 2 ? { min: value[0], max: value[1] } : isObject2(value) ? value : fallback; + const min = finiteVec3(raw.min, DEFAULT_BOUNDS.min); + const max = finiteVec3(raw.max, DEFAULT_BOUNDS.max); + for (let axis = 0; axis < 3; axis += 1) { + if (max[axis] <= min[axis]) { + const center = (min[axis] + max[axis]) / 2; + min[axis] = center - 0.5; + max[axis] = center + 0.5; + } + } + return { min, max }; +} +function cloneBounds(bounds) { + return { + min: [...bounds.min], + max: [...bounds.max] + }; +} +function hasExplicitBounds(value) { + if (value === void 0 || value === null) { + return false; + } + if (String(value).trim().toLowerCase() === "auto") { + return false; + } + return !(isObject2(value) && value.auto === true); +} +function boundsSize(bounds) { + return [ + bounds.max[0] - bounds.min[0], + bounds.max[1] - bounds.min[1], + bounds.max[2] - bounds.min[2] + ]; +} +function expandBounds(min, max, margin) { + const safeMargin = Math.max(finiteNumber2(margin, 0), 0.25); + return normalizeBounds({ + min: [min[0] - safeMargin, min[1] - safeMargin, min[2] - safeMargin], + max: [max[0] + safeMargin, max[1] + safeMargin, max[2] + safeMargin] + }); +} +function sampleAutoBounds(sdf, bounds, samples) { + const size = boundsSize(bounds); + const steps = size.map((axisSize) => axisSize / Math.max(samples - 1, 1)); + const threshold = Math.max(Math.min(...steps) * 0.35, 2e-3); + const hitMin = [Infinity, Infinity, Infinity]; + const hitMax = [-Infinity, -Infinity, -Infinity]; + let hitCount = 0; + let boundaryTouch = false; + for (let iz = 0; iz < samples; iz += 1) { + const z = bounds.min[2] + steps[2] * iz; + for (let iy = 0; iy < samples; iy += 1) { + const y = bounds.min[1] + steps[1] * iy; + for (let ix = 0; ix < samples; ix += 1) { + const x = bounds.min[0] + steps[0] * ix; + const value = finiteNumber2(sdf(x, y, z), 1e6); + if (value > threshold) { + continue; + } + hitCount += 1; + hitMin[0] = Math.min(hitMin[0], x); + hitMin[1] = Math.min(hitMin[1], y); + hitMin[2] = Math.min(hitMin[2], z); + hitMax[0] = Math.max(hitMax[0], x); + hitMax[1] = Math.max(hitMax[1], y); + hitMax[2] = Math.max(hitMax[2], z); + if (value <= 0 && (ix === 0 || iy === 0 || iz === 0 || ix === samples - 1 || iy === samples - 1 || iz === samples - 1)) { + boundaryTouch = true; + } + } + } + } + return { + hitCount, + hitMin, + hitMax, + boundaryTouch, + step: steps, + threshold + }; +} +function estimateAutoBoundsStartRadius(definition, params) { + const largest = definition.parameters.filter((parameter) => parameter.type === "number").filter((parameter) => !likelyPoseParameter(parameter)).reduce((maxValue2, parameter) => { + const value = Math.abs(finiteNumber2(params[parameter.id], 0)); + return Math.max(maxValue2, value); + }, 1); + const target = Math.max(largest * 3, 4); + return AUTO_BOUNDS_RADII.find((radius) => radius >= target) || AUTO_BOUNDS_RADII[AUTO_BOUNDS_RADII.length - 1]; +} +function estimateImplicitCadAutoBounds(partialModel, definition, context) { + let sdf = null; + try { + sdf = createImplicitCadSdfEvaluator(partialModel); + } catch { + return cloneBounds(DEFAULT_BOUNDS); + } + let candidate = null; + const startRadius = estimateAutoBoundsStartRadius(definition, context.params); + for (const radius of AUTO_BOUNDS_RADII.filter((candidateRadius) => candidateRadius >= startRadius)) { + const bounds = normalizeBounds(radius); + const scan = sampleAutoBounds(sdf, bounds, AUTO_BOUNDS_SCAN_SAMPLES); + if (!scan.hitCount) { + continue; + } + const margin = Math.max(Math.max(...scan.step) * 1.5, radius * 0.06, 0.5); + candidate = expandBounds(scan.hitMin, scan.hitMax, margin); + if (!scan.boundaryTouch) { + break; + } + } + if (!candidate) { + return cloneBounds(DEFAULT_BOUNDS); + } + for (let pass = 0; pass < 1; pass += 1) { + const scan = sampleAutoBounds(sdf, candidate, AUTO_BOUNDS_REFINE_SAMPLES); + if (!scan.hitCount) { + break; + } + const margin = Math.max(Math.max(...scan.step) * 1.1, Math.max(...boundsSize(candidate)) * 0.025, 0.25); + candidate = expandBounds(scan.hitMin, scan.hitMax, margin); + } + return candidate; +} +function likelyPoseParameter(parameter) { + const unit = String(parameter?.unit || "").trim().toLowerCase(); + if (unit === "deg" || unit === "rad") { + return true; + } + const text = `${parameter?.id || ""} ${parameter?.label || ""}`.toLowerCase(); + return [ + "angle", + "phase", + "rotation", + "rotate", + "orbit", + "spin", + "yaw", + "pitch", + "roll" + ].some((token) => text.includes(token)); +} +function autoBoundsCacheKey(definition, params, glslSource) { + const values = definition.parameters.filter((parameter) => !likelyPoseParameter(parameter)).filter((parameter) => PARAM_UNIFORM_TYPES[parameter.type]).filter((parameter) => parameter.type !== "color").map((parameter) => { + const value = params[parameter.id]; + if (typeof value === "number") { + return `${parameter.id}:${Number.isFinite(value) ? value.toPrecision(8) : "0"}`; + } + if (typeof value === "boolean") { + return `${parameter.id}:${value ? "1" : "0"}`; + } + return `${parameter.id}:${String(value ?? "")}`; + }).join(";"); + return `${glslSource.length}:${values}`; +} +function readCachedAutoBounds(cache, key, animationPlaying) { + if (!cache) { + return null; + } + if (cache.map.has(key)) { + return cloneBounds(cache.map.get(key)); + } + if (animationPlaying && cache.last) { + return cloneBounds(cache.last); + } + return null; +} +function rememberAutoBounds(cache, key, bounds) { + if (!cache) { + return; + } + const cloned = cloneBounds(bounds); + cache.map.set(key, cloned); + cache.last = cloned; + while (cache.map.size > AUTO_BOUNDS_CACHE_LIMIT) { + const oldestKey = cache.map.keys().next().value; + cache.map.delete(oldestKey); + } +} +function resolveImplicitCadBounds(value, partialModel, definition, context, cache) { + if (hasExplicitBounds(value)) { + return { + bounds: normalizeBounds(value), + source: "explicit" + }; + } + const key = autoBoundsCacheKey(definition, context.params, partialModel.glslSource); + const cached = readCachedAutoBounds(cache, key, context.animationState.playing); + if (cached) { + return { + bounds: cached, + source: "auto" + }; + } + const estimated = estimateImplicitCadAutoBounds(partialModel, definition, context); + rememberAutoBounds(cache, key, estimated); + return { + bounds: estimated, + source: "auto" + }; +} +function normalizeHexColor(value, fallback = DEFAULT_COLOR) { + const color = String(value || "").trim(); + return HEX_COLOR_PATTERN.test(color) ? color : fallback; +} +function hexToRgb01(hex, fallback = DEFAULT_COLOR) { + const value = normalizeHexColor(hex, fallback); + const expanded = value.length === 4 ? `${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}` : value.slice(1); + return [ + parseInt(expanded.slice(0, 2), 16) / 255, + parseInt(expanded.slice(2, 4), 16) / 255, + parseInt(expanded.slice(4, 6), 16) / 255 + ]; +} +function evaluateValue(value, context) { + return typeof value === "function" ? value(context) : value; +} +function normalizeMaterial() { + const material = {}; + return { + color: normalizeHexColor(material.color, DEFAULT_COLOR), + roughness: Math.min(Math.max(finiteNumber2(material.roughness, 0.75), 0), 1), + metalness: Math.min(Math.max(finiteNumber2(material.metalness, 0.02), 0), 1) + }; +} +function unwrapModuleExports(moduleValue) { + if (typeof moduleValue === "function") { + return moduleValue(); + } + if (!isObject2(moduleValue)) { + return moduleValue; + } + if (moduleValue.default !== void 0) { + return typeof moduleValue.default === "function" ? moduleValue.default() : moduleValue.default; + } + if (moduleValue.model !== void 0) { + return typeof moduleValue.model === "function" ? moduleValue.model() : moduleValue.model; + } + return moduleValue; +} +function normalizeUniformValue(rawValue, context = {}) { + const raw = evaluateValue(rawValue, context); + const value = isObject2(raw) && Object.hasOwn(raw, "value") ? evaluateValue(raw.value, context) : raw; + const explicitType = isObject2(raw) ? String(raw.type || raw.kind || "").trim().toLowerCase() : ""; + const type = UNIFORM_TYPES.has(explicitType) ? explicitType : typeof value === "boolean" ? "bool" : typeof value === "number" ? "float" : typeof value === "string" && HEX_COLOR_PATTERN.test(value) ? "vec3" : Array.isArray(value) || ArrayBuffer.isView(value) ? `vec${Math.min(Math.max(value.length, 2), 4)}` : ""; + if (!UNIFORM_TYPES.has(type)) { + return null; + } + if (type === "bool") { + return { type, value: value === true }; + } + if (type === "int") { + return { type, value: Math.trunc(finiteNumber2(value, 0)) }; + } + if (type === "float") { + return { type, value: finiteNumber2(value, 0) }; + } + if (typeof value === "string" && HEX_COLOR_PATTERN.test(value)) { + return { type, value: hexToRgb01(value) }; + } + const length2 = Number(type.slice(3)); + const fallback = Array.from({ length: length2 }, () => 0); + return { + type, + value: finiteVector(value, length2, fallback) + }; +} +function uniformsFromParams(parameters = [], params = {}) { + return Object.fromEntries( + parameters.map((parameter) => { + const uniformName = String(parameter?.id || "").trim(); + if (!GLSL_IDENTIFIER_PATTERN.test(uniformName) || uniformName.startsWith("gl_")) { + throw new Error(`Implicit CAD param "${uniformName}" is not a valid GLSL uniform identifier.`); + } + const type = PARAM_UNIFORM_TYPES[parameter.type]; + if (!type) { + return null; + } + const uniform = normalizeUniformValue({ type, value: params[parameter.id] }); + return uniform ? [uniformName, uniform] : null; + }).filter(Boolean) + ); +} +function glslSourceFromModel(model, context = {}) { + return String(evaluateValue(model.glsl ?? model.glslSource, context) || "").trim(); +} +function pathIsImplicitCadSource2(value = "") { + const pathname = String(value || "").split(/[?#]/, 1)[0].toLowerCase(); + return IMPLICIT_CAD_EXTENSIONS2.some((extension) => pathname.endsWith(extension)); +} +function normalizeAnimationState(value = {}) { + const activeId = String(value?.activeId || "").trim(); + return { + activeId, + playing: value?.playing === true, + elapsedSec: Math.max(finiteNumber2(value?.elapsedSec ?? value?.elapsed, 0), 0), + speed: Math.min(Math.max(finiteNumber2(value?.speed, 1), 0.1), 5) + }; +} +function findAnimation(definition, animationId) { + const animations = Array.isArray(definition?.animations) ? definition.animations : []; + if (!animations.length) { + return null; + } + const normalizedId = String(animationId || "").trim(); + return animations.find((animation) => animation.id === normalizedId) || animations[0] || null; +} +function buildImplicitCadContext(definition, parameterValues = {}, animationState = {}) { + const params = normalizeParameterValues(definition, parameterValues); + const normalizedAnimationState = normalizeAnimationState(animationState); + const animation = findAnimation(definition, normalizedAnimationState.activeId); + const duration = Math.max(Number(animation?.duration) || 0, 1e-3); + const elapsedSec = Math.min(normalizedAnimationState.elapsedSec, duration); + const progress = animation ? Math.min(Math.max(elapsedSec / duration, 0), 1) : 0; + return { + ...params, + params, + parameterValues: params, + animation, + animationState: { + ...normalizedAnimationState, + activeId: animation?.id || normalizedAnimationState.activeId, + duration: animation?.duration || 0, + loop: animation?.loop !== false + }, + elapsed: elapsedSec, + elapsedSec, + duration: animation?.duration || 0, + progress, + cycle: duration > 0 ? elapsedSec / duration : 0, + t: elapsedSec, + time: elapsedSec + }; +} +function normalizeImplicitCadRuntimeModel(rawModel, definition, parameterValues = {}, animationState = {}, { sourceUrl = "", autoBoundsCache = null } = {}) { + const context = buildImplicitCadContext(definition, parameterValues, animationState); + const model = rawModel; + if (!isObject2(model)) { + throw new Error("Implicit CAD module must export an object, or a function returning an object."); + } + const uniforms = uniformsFromParams(definition.parameters, context.params); + const glslSource = glslSourceFromModel(model, context); + if (!glslSource) { + throw new Error("Implicit CAD module must export GLSL code in glsl."); + } + const resolvedBounds = resolveImplicitCadBounds( + evaluateValue(model.bounds, context), + { glslSource, uniforms }, + definition, + context, + autoBoundsCache + ); + const bounds = resolvedBounds.bounds; + const size = [ + bounds.max[0] - bounds.min[0], + bounds.max[1] - bounds.min[1], + bounds.max[2] - bounds.min[2] + ]; + const center = [ + (bounds.min[0] + bounds.max[0]) / 2, + (bounds.min[1] + bounds.max[1]) / 2, + (bounds.min[2] + bounds.max[2]) / 2 + ]; + const radius = Math.max(Math.hypot(size[0], size[1], size[2]) / 2, 1e-3); + const evaluatedRender = evaluateValue(model.render, context); + const render = isObject2(evaluatedRender) ? evaluatedRender : {}; + const epsilon = finitePositiveNumber(evaluateValue(render.epsilon, context), Math.max(radius * 7e-4, 2e-3)); + const normalEpsilon = finitePositiveNumber( + evaluateValue(render.normalEpsilon, context), + Math.max(epsilon * 2.5, radius * 1e-3) + ); + const stepScale = Math.min( + Math.max(finiteNumber2(evaluateValue(render.stepScale, context), 0.45), 0.05), + 1 + ); + const maxStep = finitePositiveNumber( + evaluateValue(render.maxStep, context), + Math.max(radius * 0.035, epsilon * 10) + ); + return { + schema: String(model.schema || IMPLICIT_CAD_SCHEMA), + kind: IMPLICIT_CAD_KIND, + name: String(model.name || model.title || "Implicit CAD").trim() || "Implicit CAD", + description: String(model.description || "").trim(), + units: String(model.units || "mm").trim() || "mm", + sourceUrl: String(sourceUrl || model.sourceUrl || "").trim(), + parameters: definition.parameters, + parameterMap: definition.parameterMap, + defaultParameterValues: definition.defaultParameterValues, + parameterValues: context.params, + animations: definition.animations, + animationState: context.animationState, + glslSource, + distanceSource: glslSource, + colorSource: /\bvec3\s+color\s*\(/.test(glslSource) ? glslSource : "", + uniforms, + uniformSignature: Object.entries(uniforms).map(([name, uniform]) => `${name}:${uniform.type}`).sort().join(";"), + bounds, + boundsSource: resolvedBounds.source, + center, + size, + radius, + material: normalizeMaterial(), + background: {}, + maxSteps: Math.max(16, Math.min(Math.floor(finiteNumber2(evaluateValue(render.steps, context), 192)), 768)), + maxDistance: finitePositiveNumber(evaluateValue(render.maxDistance, context), radius * 8), + stepScale, + maxStep, + epsilon, + normalEpsilon + }; +} +function normalizeImplicitCadDefinition(moduleValue, { sourceUrl = "" } = {}) { + const model = unwrapModuleExports(moduleValue); + if (!isObject2(model)) { + throw new Error("Implicit CAD module must export an object, or a function returning an object."); + } + if (model.kind === IMPLICIT_CAD_KIND && model.glslSource && Array.isArray(model.parameters)) { + return model.definition ? model : { ...model, definition: null }; + } + const parameters = normalizeParameterDefinitions(model.params); + const parameterMap = parameterMapForDefinitions(parameters); + const autoBoundsCache = { + map: /* @__PURE__ */ new Map(), + last: null + }; + const definition = { + schema: String(model.schema || IMPLICIT_CAD_SCHEMA), + kind: IMPLICIT_CAD_KIND, + name: String(model.name || model.title || "Implicit CAD").trim() || "Implicit CAD", + description: String(model.description || "").trim(), + units: String(model.units || "mm").trim() || "mm", + sourceUrl: String(sourceUrl || model.sourceUrl || "").trim(), + parameters, + parameterMap, + defaultParameterValues: {}, + animations: normalizeParameterAnimations(model.animations), + buildModel(parameterValues = {}, animationState = {}) { + return normalizeImplicitCadRuntimeModel(model, definition, parameterValues, animationState, { sourceUrl, autoBoundsCache }); + } + }; + definition.defaultParameterValues = normalizeParameterValues( + definition, + model.values || {} + ); + const defaultModel = definition.buildModel(definition.defaultParameterValues); + return { + ...defaultModel, + definition + }; +} +function normalizeImplicitCadModel(moduleValue, { sourceUrl = "" } = {}) { + return normalizeImplicitCadDefinition(moduleValue, { sourceUrl }); +} + +// viewer/packages/implicitjs/src/lib/implicitCad/animation.js +function finiteNumber3(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function clamp3(value, min, max) { + return Math.min(Math.max(value, min), max); +} +function findImplicitAnimation(definition, animationId = "") { + const animations = Array.isArray(definition?.animations) ? definition.animations : []; + if (!animations.length) { + return null; + } + const id = String(animationId || "").trim(); + return animations.find((animation) => animation.id === id) || animations[0] || null; +} +function normalizeImplicitAnimationElapsed(elapsedSec, animation) { + const duration = Math.max(finiteNumber3(animation?.duration, 1), 1e-3); + const elapsed = Math.max(finiteNumber3(elapsedSec, 0), 0); + return animation?.loop === false ? Math.min(elapsed, duration) : elapsed % duration; +} +function animatedImplicitParameterValues(definition, animationOrId, baseValues = {}, elapsedSec = 0) { + const animation = typeof animationOrId === "string" ? findImplicitAnimation(definition, animationOrId) : animationOrId; + const normalizedBase = normalizeParameterValues(definition, baseValues); + if (!definition || typeof animation?.update !== "function") { + return normalizedBase; + } + const duration = Math.max(finiteNumber3(animation.duration, 1), 1e-3); + const elapsed = normalizeImplicitAnimationElapsed(elapsedSec, animation); + const nextValues = { ...normalizedBase }; + const set = (parameterId, value) => { + const id = String(parameterId || "").trim(); + const parameter = definition.parameterMap?.[id]; + if (parameter) { + nextValues[id] = normalizeParameterValue(parameter, value); + } + }; + animation.update({ + ...normalizedBase, + elapsed, + elapsedSec: elapsed, + duration, + progress: clamp3(elapsed / duration, 0, 1), + cycle: elapsed / duration, + t: elapsed, + time: elapsed, + loop: animation.loop !== false, + params: normalizedBase, + set + }); + return nextValues; +} +function buildImplicitAnimatedModel(definition, { + animationId = "", + elapsedSec = 0, + params = null, + parameterValues = null, + playing = false, + speed = 1 +} = {}) { + if (!definition?.buildModel) { + throw new Error("Implicit animation export requires a compiled implicit definition."); + } + const animation = findImplicitAnimation(definition, animationId); + const baseValues = params || parameterValues || definition.defaultParameterValues || {}; + const animatedValues = animatedImplicitParameterValues(definition, animation, baseValues, elapsedSec); + return definition.buildModel(animatedValues, { + activeId: animation?.id || animationId, + elapsedSec: normalizeImplicitAnimationElapsed(elapsedSec, animation), + playing, + speed + }); +} + +// viewer/packages/implicitjs/src/lib/implicitCad/mesh.js +var DEFAULT_RESOLUTION = 96; +var DEFAULT_MAX_CELLS = 25e5; +var TETRAHEDRA = Object.freeze([ + [0, 5, 1, 6], + [0, 1, 2, 6], + [0, 2, 3, 6], + [0, 3, 7, 6], + [0, 7, 4, 6], + [0, 4, 5, 6] +]); +var CUBE_OFFSETS = Object.freeze([ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [0, 1, 0], + [0, 0, 1], + [1, 0, 1], + [1, 1, 1], + [0, 1, 1] +]); +function finiteNumber4(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function clamp4(value, min, max) { + return Math.min(Math.max(value, min), max); +} +function normalizeResolution(value = DEFAULT_RESOLUTION) { + return Math.max(8, Math.min(192, Math.floor(finiteNumber4(value, DEFAULT_RESOLUTION)))); +} +function normalizedGrid(model, { resolution = DEFAULT_RESOLUTION, maxCells = DEFAULT_MAX_CELLS } = {}) { + const bounds = model.bounds; + const size = model.size; + const longest = Math.max(size[0], size[1], size[2], 1e-6); + const targetResolution = normalizeResolution(resolution); + const cellSize = longest / targetResolution; + let nx = Math.max(2, Math.ceil(size[0] / cellSize)); + let ny = Math.max(2, Math.ceil(size[1] / cellSize)); + let nz = Math.max(2, Math.ceil(size[2] / cellSize)); + const maxCellCount = Math.max(4096, Math.floor(finiteNumber4(maxCells, DEFAULT_MAX_CELLS))); + const cellCount = nx * ny * nz; + if (cellCount > maxCellCount) { + const scale = Math.cbrt(maxCellCount / cellCount); + nx = Math.max(2, Math.floor(nx * scale)); + ny = Math.max(2, Math.floor(ny * scale)); + nz = Math.max(2, Math.floor(nz * scale)); + } + return { + min: bounds.min, + max: bounds.max, + size, + nx, + ny, + nz, + step: [ + size[0] / nx, + size[1] / ny, + size[2] / nz + ] + }; +} +function pointAt(grid, ix, iy, iz) { + return [ + grid.min[0] + ix * grid.step[0], + grid.min[1] + iy * grid.step[1], + grid.min[2] + iz * grid.step[2] + ]; +} +function subtract(a, b) { + return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; +} +function cross2(a, b) { + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0] + ]; +} +function dot2(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} +function normalize2(v) { + const len = Math.hypot(v[0], v[1], v[2]); + return len > 1e-12 ? [v[0] / len, v[1] / len, v[2] / len] : [0, 0, 1]; +} +function midpoint(a, b, c) { + return [ + (a[0] + b[0] + c[0]) / 3, + (a[1] + b[1] + c[1]) / 3, + (a[2] + b[2] + c[2]) / 3 + ]; +} +function estimateGradient(sdf, point, epsilon) { + const [x, y, z] = point; + return normalize2([ + sdf(x + epsilon, y, z) - sdf(x - epsilon, y, z), + sdf(x, y + epsilon, z) - sdf(x, y - epsilon, z), + sdf(x, y, z + epsilon) - sdf(x, y, z - epsilon) + ]); +} +function offsetPoint(point, direction, distance) { + return [ + point[0] + direction[0] * distance, + point[1] + direction[1] * distance, + point[2] + direction[2] * distance + ]; +} +function normalFacesOutward(sdf, point, normal, epsilon) { + const positive = finiteNumber4(sdf(...offsetPoint(point, normal, epsilon)), 1e6); + const negative = finiteNumber4(sdf(...offsetPoint(point, normal, -epsilon)), 1e6); + return positive >= negative; +} +function smoothNormalForVertex(sdf, vertex, epsilon, normalCache) { + const key = `${vertex[0].toPrecision(12)},${vertex[1].toPrecision(12)},${vertex[2].toPrecision(12)}`; + let normal = normalCache.get(key); + if (!normal) { + normal = estimateGradient(sdf, vertex, epsilon); + normalCache.set(key, normal); + } + return normal; +} +function interpolateVertex(a, b, iso = 0) { + const denominator = b.value - a.value; + const t = Math.abs(denominator) < 1e-12 ? 0.5 : clamp4((iso - a.value) / denominator, 0, 1); + return [ + a.position[0] + (b.position[0] - a.position[0]) * t, + a.position[1] + (b.position[1] - a.position[1]) * t, + a.position[2] + (b.position[2] - a.position[2]) * t + ]; +} +function pushTriangle(mesh, sdf, normalEpsilon, a, b, c, { + normalCache = null, + orientByGradient = false, + orientBySdf = true, + orientationEpsilon = normalEpsilon, + smoothNormals = false +} = {}) { + let v0 = a; + let v1 = b; + let v2 = c; + let normal = normalize2(cross2(subtract(v1, v0), subtract(v2, v0))); + const triangleMidpoint = midpoint(v0, v1, v2); + const gradient = orientByGradient ? estimateGradient(sdf, triangleMidpoint, normalEpsilon) : null; + const facesOutward = gradient ? dot2(normal, gradient) >= 0 : !orientBySdf || normalFacesOutward(sdf, triangleMidpoint, normal, orientationEpsilon); + if (!facesOutward) { + v1 = c; + v2 = b; + normal = [-normal[0], -normal[1], -normal[2]]; + } + for (const vertex of [v0, v1, v2]) { + const vertexNormal = smoothNormals && normalCache ? smoothNormalForVertex(sdf, vertex, normalEpsilon, normalCache) : normal; + const alignedNormal = dot2(vertexNormal, normal) < 0 ? [-vertexNormal[0], -vertexNormal[1], -vertexNormal[2]] : vertexNormal; + mesh.positions.push(vertex[0], vertex[1], vertex[2]); + mesh.normals.push(alignedNormal[0], alignedNormal[1], alignedNormal[2]); + } +} +function polygonizeTetra(mesh, sdf, normalEpsilon, corners, iso = 0, options = {}) { + const inside = []; + const outside = []; + for (const corner of corners) { + if (corner.value <= iso) { + inside.push(corner); + } else { + outside.push(corner); + } + } + if (inside.length === 0 || inside.length === 4) { + return; + } + const edge = (a2, b2) => interpolateVertex(a2, b2, iso); + if (inside.length === 1) { + pushTriangle(mesh, sdf, normalEpsilon, edge(inside[0], outside[0]), edge(inside[0], outside[1]), edge(inside[0], outside[2]), options); + return; + } + if (inside.length === 3) { + pushTriangle(mesh, sdf, normalEpsilon, edge(outside[0], inside[0]), edge(outside[0], inside[2]), edge(outside[0], inside[1]), options); + return; + } + const a = edge(inside[0], outside[0]); + const b = edge(inside[1], outside[0]); + const c = edge(inside[1], outside[1]); + const d = edge(inside[0], outside[1]); + pushTriangle(mesh, sdf, normalEpsilon, a, b, c, options); + pushTriangle(mesh, sdf, normalEpsilon, a, c, d, options); +} +function sampledValueIndex(grid, ix, iy, iz) { + return ix + (grid.nx + 1) * (iy + (grid.ny + 1) * iz); +} +function meshImplicitCadModel(modelValue, options = {}) { + const model = normalizeImplicitCadModel(modelValue); + const sdf = typeof options.sdf === "function" ? options.sdf : createImplicitCadSdfEvaluator(model); + const grid = normalizedGrid(model, options); + const valueCount = (grid.nx + 1) * (grid.ny + 1) * (grid.nz + 1); + const values = new Float32Array(valueCount); + for (let iz = 0; iz <= grid.nz; iz += 1) { + for (let iy = 0; iy <= grid.ny; iy += 1) { + for (let ix = 0; ix <= grid.nx; ix += 1) { + const p = pointAt(grid, ix, iy, iz); + values[sampledValueIndex(grid, ix, iy, iz)] = finiteNumber4(sdf(p[0], p[1], p[2]), 1e6); + } + } + } + const mesh = { + positions: [], + normals: [], + format: "triangle-list", + sourceModel: model, + grid: { + resolution: [grid.nx, grid.ny, grid.nz], + step: grid.step + } + }; + const normalEpsilon = Math.max(finiteNumber4(options.normalEpsilon, model.normalEpsilon), 1e-5); + const orientationEpsilon = Math.max(Math.min(...grid.step) * 0.15, 1e-5); + const normalCache = options.smoothNormals === true ? /* @__PURE__ */ new Map() : null; + for (let iz = 0; iz < grid.nz; iz += 1) { + for (let iy = 0; iy < grid.ny; iy += 1) { + for (let ix = 0; ix < grid.nx; ix += 1) { + const corners = CUBE_OFFSETS.map(([dx, dy, dz]) => { + const cx = ix + dx; + const cy = iy + dy; + const cz = iz + dz; + return { + position: pointAt(grid, cx, cy, cz), + value: values[sampledValueIndex(grid, cx, cy, cz)] + }; + }); + for (const tetra of TETRAHEDRA) { + polygonizeTetra(mesh, sdf, normalEpsilon, tetra.map((index) => corners[index]), 0, { + normalCache, + orientByGradient: options.orientByGradient === true, + orientBySdf: options.orientBySdf !== false, + orientationEpsilon, + smoothNormals: options.smoothNormals === true + }); + } + } + } + } + return { + ...mesh, + positions: new Float32Array(mesh.positions), + normals: new Float32Array(mesh.normals), + vertexCount: mesh.positions.length / 3, + triangleCount: mesh.positions.length / 9 + }; +} + +// viewer/packages/implicitjs/src/lib/implicitCad/exporters.js +var NativeBuffer = globalThis.Buffer; +var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null; +function finiteNumber5(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function clamp01(value) { + return Math.min(Math.max(finiteNumber5(value), 0), 1); +} +function bytesFromString(value, encoding = "utf-8") { + if (NativeBuffer?.from) { + return NativeBuffer.from(String(value), encoding); + } + if (encoding !== "utf-8" && encoding !== "utf8") { + const text = String(value); + const bytes2 = new Uint8Array(text.length); + for (let index = 0; index < text.length; index += 1) { + bytes2[index] = text.charCodeAt(index) & 255; + } + return bytes2; + } + return textEncoder.encode(String(value)); +} +function allocBytes(length2, fill = 0) { + if (NativeBuffer?.alloc) { + return NativeBuffer.alloc(length2, fill); + } + const bytes2 = new Uint8Array(length2); + if (fill) { + bytes2.fill(fill); + } + return bytes2; +} +function concatBytes(parts, totalLength = void 0) { + if (NativeBuffer?.concat) { + return NativeBuffer.concat(parts, totalLength); + } + const length2 = totalLength ?? parts.reduce((sum, part) => sum + part.length, 0); + const bytes2 = new Uint8Array(length2); + let offset = 0; + for (const part of parts) { + bytes2.set(part, offset); + offset += part.length; + } + return bytes2; +} +function typedArrayBytes(array) { + return new Uint8Array(array.buffer, array.byteOffset, array.byteLength); +} +function viewFor(bytes2) { + return new DataView(bytes2.buffer, bytes2.byteOffset, bytes2.byteLength); +} +function writeUInt16LE(bytes2, offset, value) { + viewFor(bytes2).setUint16(offset, value, true); +} +function writeUInt32LE(bytes2, offset, value) { + viewFor(bytes2).setUint32(offset, value, true); +} +function writeFloatLE(bytes2, offset, value) { + viewFor(bytes2).setFloat32(offset, value, true); +} +function writeAscii(bytes2, offset, text, maxLength) { + const source = String(text).slice(0, maxLength); + for (let index = 0; index < source.length; index += 1) { + bytes2[offset + index] = source.charCodeAt(index) & 127; + } +} +function align4Buffer(buffer, fill = 32) { + const padding = (4 - buffer.length % 4) % 4; + return padding ? concatBytes([buffer, allocBytes(padding, fill)]) : buffer; +} +function hexToRgb012(hex, fallback = "#d4d4d8") { + const raw = String(hex || fallback).trim(); + const value = /^#(?:[0-9a-fA-F]{3}){1,2}$/.test(raw) ? raw : fallback; + const expanded = value.length === 4 ? `${value[1]}${value[1]}${value[2]}${value[2]}${value[3]}${value[3]}` : value.slice(1); + return [ + parseInt(expanded.slice(0, 2), 16) / 255, + parseInt(expanded.slice(2, 4), 16) / 255, + parseInt(expanded.slice(4, 6), 16) / 255 + ]; +} +function hexTo3mfDisplayColor(hex, fallback = "#d4d4d8") { + const toChannel = (component) => Math.round(clamp01(component) * 255).toString(16).padStart(2, "0").toUpperCase(); + const rgb = hexToRgb012(hex, fallback); + return `#${toChannel(rgb[0])}${toChannel(rgb[1])}${toChannel(rgb[2])}FF`; +} +function triangleNormal(positions, offset) { + const ax = positions[offset]; + const ay = positions[offset + 1]; + const az = positions[offset + 2]; + const bx = positions[offset + 3]; + const by = positions[offset + 4]; + const bz = positions[offset + 5]; + const cx = positions[offset + 6]; + const cy = positions[offset + 7]; + const cz = positions[offset + 8]; + const ux = bx - ax; + const uy = by - ay; + const uz = bz - az; + const vx = cx - ax; + const vy = cy - ay; + const vz = cz - az; + const nx = uy * vz - uz * vy; + const ny = uz * vx - ux * vz; + const nz = ux * vy - uy * vx; + const length2 = Math.hypot(nx, ny, nz); + return length2 > 1e-12 ? [nx / length2, ny / length2, nz / length2] : [0, 0, 1]; +} +function sanitizeName(value, fallback = "implicit-cad") { + return String(value || fallback).trim().replace(/[\x00-\x1f<>:"/\\|?*]+/g, "-") || fallback; +} +function boundsForPositions(positions) { + const min = [Infinity, Infinity, Infinity]; + const max = [-Infinity, -Infinity, -Infinity]; + for (let index = 0; index < positions.length; index += 3) { + min[0] = Math.min(min[0], positions[index]); + min[1] = Math.min(min[1], positions[index + 1]); + min[2] = Math.min(min[2], positions[index + 2]); + max[0] = Math.max(max[0], positions[index]); + max[1] = Math.max(max[1], positions[index + 1]); + max[2] = Math.max(max[2], positions[index + 2]); + } + return { + min: min.map((value) => Number.isFinite(value) ? value : 0), + max: max.map((value) => Number.isFinite(value) ? value : 0) + }; +} +function buildGlb(gltf, binaryParts) { + const binaryChunk = align4Buffer(concatBytes(binaryParts), 0); + gltf.buffers = [{ byteLength: binaryChunk.length }]; + const jsonChunk = align4Buffer(bytesFromString(JSON.stringify(gltf)), 32); + const totalLength = 12 + 8 + jsonChunk.length + 8 + binaryChunk.length; + const header = allocBytes(12); + writeUInt32LE(header, 0, 1179937895); + writeUInt32LE(header, 4, 2); + writeUInt32LE(header, 8, totalLength); + const jsonHeader = allocBytes(8); + writeUInt32LE(jsonHeader, 0, jsonChunk.length); + writeUInt32LE(jsonHeader, 4, 1313821514); + const binHeader = allocBytes(8); + writeUInt32LE(binHeader, 0, binaryChunk.length); + writeUInt32LE(binHeader, 4, 5130562); + return concatBytes([header, jsonHeader, jsonChunk, binHeader, binaryChunk], totalLength); +} +function meshToBinaryStl(mesh, { name = "implicit-cad" } = {}) { + const positions = mesh.positions || new Float32Array(); + const triangleCount = Math.floor(positions.length / 9); + const buffer = allocBytes(84 + triangleCount * 50); + writeAscii(buffer, 0, `implicit-cad ${sanitizeName(name)}`, 80); + writeUInt32LE(buffer, 80, triangleCount); + let offset = 84; + for (let triangle = 0; triangle < triangleCount; triangle += 1) { + const positionOffset = triangle * 9; + const normal = triangleNormal(positions, positionOffset); + for (const component of normal) { + writeFloatLE(buffer, offset, component); + offset += 4; + } + for (let vertex = 0; vertex < 9; vertex += 1) { + writeFloatLE(buffer, offset, positions[positionOffset + vertex]); + offset += 4; + } + writeUInt16LE(buffer, offset, 0); + offset += 2; + } + return buffer; +} +function meshToGlb(mesh, { name = "Implicit CAD", color = "#d4d4d8" } = {}) { + const positions = mesh.positions || new Float32Array(); + const normals = mesh.normals && mesh.normals.length === positions.length ? mesh.normals : new Float32Array(positions.length); + const vertexCount = Math.floor(positions.length / 3); + const positionBuffer = typedArrayBytes(positions); + const normalBuffer = typedArrayBytes(normals); + const positionBounds = boundsForPositions(positions); + const baseColor = hexToRgb012(color); + const gltf = { + asset: { version: "2.0", generator: "implicitjs exporter" }, + scene: 0, + scenes: [{ nodes: [0] }], + nodes: [{ + mesh: 0, + name: sanitizeName(name, "Implicit CAD"), + extras: { + cadOccurrenceId: "implicit-cad:0", + cadSourceKind: "implicit-cad", + cadUnits: "mm" + } + }], + meshes: [{ + primitives: [{ + attributes: { POSITION: 0, NORMAL: 1 }, + material: 0, + mode: 4 + }] + }], + materials: [{ + name: "Implicit material", + doubleSided: true, + extras: { cadSourceColor: true }, + pbrMetallicRoughness: { + baseColorFactor: [...baseColor.map(clamp01), 1], + roughnessFactor: 0.72, + metallicFactor: 0.02 + } + }], + bufferViews: [ + { buffer: 0, byteOffset: 0, byteLength: positionBuffer.length, target: 34962 }, + { buffer: 0, byteOffset: positionBuffer.length, byteLength: normalBuffer.length, target: 34962 } + ], + accessors: [ + { + bufferView: 0, + byteOffset: 0, + componentType: 5126, + count: vertexCount, + type: "VEC3", + min: positionBounds.min, + max: positionBounds.max + }, + { + bufferView: 1, + byteOffset: 0, + componentType: 5126, + count: vertexCount, + type: "VEC3" + } + ] + }; + return buildGlb(gltf, [positionBuffer, normalBuffer]); +} +function meshToAnimatedGlb(mesh, { + name = "Implicit CAD", + color = "#d4d4d8", + duration = 4, + times = [], + targetPositions = [] +} = {}) { + const positions = mesh.positions || new Float32Array(); + const normals = mesh.normals && mesh.normals.length === positions.length ? mesh.normals : new Float32Array(positions.length); + const vertexCount = Math.floor(positions.length / 3); + const positionBuffer = typedArrayBytes(positions); + const normalBuffer = typedArrayBytes(normals); + const positionBounds = boundsForPositions(positions); + const cleanTargets = targetPositions.filter((target) => target && target.length === positions.length).map((target) => { + const delta = new Float32Array(positions.length); + for (let index = 0; index < positions.length; index += 1) { + delta[index] = finiteNumber5(target[index], positions[index]) - positions[index]; + } + return delta; + }); + if (!cleanTargets.length) { + throw new Error("Animated GLB export needs at least one compatible animation frame."); + } + const keyTimes = Array.isArray(times) && times.length >= 2 ? times.map((value) => Math.max(finiteNumber5(value, 0), 0)) : [0, Math.max(finiteNumber5(duration, 4), 1e-3)]; + const targetCount = cleanTargets.length; + const weightValues = new Float32Array(keyTimes.length * targetCount); + for (let key = 0; key < keyTimes.length; key += 1) { + const targetIndex = key - 1; + if (targetIndex >= 0 && targetIndex < targetCount) { + weightValues[key * targetCount + targetIndex] = 1; + } + } + const targetBuffers = cleanTargets.map((target) => typedArrayBytes(target)); + const timeValues = new Float32Array(keyTimes); + const timeBuffer = typedArrayBytes(timeValues); + const weightBuffer = typedArrayBytes(weightValues); + const offsets = []; + let byteOffset = 0; + for (const part of [positionBuffer, normalBuffer, ...targetBuffers, timeBuffer, weightBuffer]) { + offsets.push(byteOffset); + byteOffset += part.length; + } + const baseColor = hexToRgb012(color); + const targetAccessorStart = 2; + const timeAccessor = targetAccessorStart + targetCount; + const weightAccessor = timeAccessor + 1; + const gltf = { + asset: { version: "2.0", generator: "implicitjs animated exporter" }, + scene: 0, + scenes: [{ nodes: [0] }], + nodes: [{ + mesh: 0, + name: sanitizeName(name, "Implicit CAD"), + weights: Array.from({ length: targetCount }, () => 0), + extras: { + cadOccurrenceId: "implicit-cad:0", + cadSourceKind: "implicit-cad", + cadUnits: "mm", + implicitjsAnimated: true + } + }], + meshes: [{ + weights: Array.from({ length: targetCount }, () => 0), + primitives: [{ + attributes: { POSITION: 0, NORMAL: 1 }, + targets: cleanTargets.map((_, index) => ({ POSITION: targetAccessorStart + index })), + material: 0, + mode: 4 + }] + }], + materials: [{ + name: "Implicit material", + doubleSided: true, + extras: { cadSourceColor: true }, + pbrMetallicRoughness: { + baseColorFactor: [...baseColor.map(clamp01), 1], + roughnessFactor: 0.72, + metallicFactor: 0.02 + } + }], + animations: [{ + name: "Implicit parameter animation", + channels: [{ sampler: 0, target: { node: 0, path: "weights" } }], + samplers: [{ input: timeAccessor, output: weightAccessor, interpolation: "LINEAR" }] + }], + bufferViews: [ + { buffer: 0, byteOffset: offsets[0], byteLength: positionBuffer.length, target: 34962 }, + { buffer: 0, byteOffset: offsets[1], byteLength: normalBuffer.length, target: 34962 }, + ...targetBuffers.map((target, index) => ({ + buffer: 0, + byteOffset: offsets[2 + index], + byteLength: target.length, + target: 34962 + })), + { buffer: 0, byteOffset: offsets[2 + targetCount], byteLength: timeBuffer.length }, + { buffer: 0, byteOffset: offsets[3 + targetCount], byteLength: weightBuffer.length } + ], + accessors: [ + { + bufferView: 0, + byteOffset: 0, + componentType: 5126, + count: vertexCount, + type: "VEC3", + min: positionBounds.min, + max: positionBounds.max + }, + { + bufferView: 1, + byteOffset: 0, + componentType: 5126, + count: vertexCount, + type: "VEC3" + }, + ...cleanTargets.map((target, index) => { + const bounds = boundsForPositions(target); + return { + bufferView: targetAccessorStart + index, + byteOffset: 0, + componentType: 5126, + count: vertexCount, + type: "VEC3", + min: bounds.min, + max: bounds.max + }; + }), + { + bufferView: timeAccessor, + byteOffset: 0, + componentType: 5126, + count: timeValues.length, + type: "SCALAR", + min: [Math.min(...keyTimes)], + max: [Math.max(...keyTimes)] + }, + { + bufferView: weightAccessor, + byteOffset: 0, + componentType: 5126, + count: weightValues.length, + type: "SCALAR", + min: [0], + max: [1] + } + ] + }; + return buildGlb(gltf, [positionBuffer, normalBuffer, ...targetBuffers, timeBuffer, weightBuffer]); +} +function xmlEscape(value) { + return String(value ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); +} +function crc32(buffer) { + let table = crc32.table; + if (!table) { + table = new Uint32Array(256); + for (let index = 0; index < 256; index += 1) { + let c = index; + for (let bit = 0; bit < 8; bit += 1) { + c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1; + } + table[index] = c >>> 0; + } + crc32.table = table; + } + let crc = 4294967295; + for (const byte of buffer) { + crc = table[(crc ^ byte) & 255] ^ crc >>> 8; + } + return (crc ^ 4294967295) >>> 0; +} +function dosDateTime(date = /* @__PURE__ */ new Date()) { + const year = Math.max(date.getFullYear(), 1980); + const dosTime = date.getHours() << 11 | date.getMinutes() << 5 | Math.floor(date.getSeconds() / 2); + const dosDate = year - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate(); + return { dosTime, dosDate }; +} +function zipStore(files) { + const localParts = []; + const centralParts = []; + let offset = 0; + const { dosTime, dosDate } = dosDateTime(); + for (const file of files) { + const nameBuffer = bytesFromString(file.name); + const body = file.body instanceof Uint8Array ? file.body : bytesFromString(String(file.body || "")); + const crc = crc32(body); + const local = allocBytes(30); + writeUInt32LE(local, 0, 67324752); + writeUInt16LE(local, 4, 20); + writeUInt16LE(local, 6, 0); + writeUInt16LE(local, 8, 0); + writeUInt16LE(local, 10, dosTime); + writeUInt16LE(local, 12, dosDate); + writeUInt32LE(local, 14, crc); + writeUInt32LE(local, 18, body.length); + writeUInt32LE(local, 22, body.length); + writeUInt16LE(local, 26, nameBuffer.length); + writeUInt16LE(local, 28, 0); + localParts.push(local, nameBuffer, body); + const central = allocBytes(46); + writeUInt32LE(central, 0, 33639248); + writeUInt16LE(central, 4, 20); + writeUInt16LE(central, 6, 20); + writeUInt16LE(central, 8, 0); + writeUInt16LE(central, 10, 0); + writeUInt16LE(central, 12, dosTime); + writeUInt16LE(central, 14, dosDate); + writeUInt32LE(central, 16, crc); + writeUInt32LE(central, 20, body.length); + writeUInt32LE(central, 24, body.length); + writeUInt16LE(central, 28, nameBuffer.length); + writeUInt16LE(central, 30, 0); + writeUInt16LE(central, 32, 0); + writeUInt16LE(central, 34, 0); + writeUInt16LE(central, 36, 0); + writeUInt32LE(central, 38, 0); + writeUInt32LE(central, 42, offset); + centralParts.push(central, nameBuffer); + offset += local.length + nameBuffer.length + body.length; + } + const centralStart = offset; + const centralDirectory = concatBytes(centralParts); + const end = allocBytes(22); + writeUInt32LE(end, 0, 101010256); + writeUInt16LE(end, 4, 0); + writeUInt16LE(end, 6, 0); + writeUInt16LE(end, 8, files.length); + writeUInt16LE(end, 10, files.length); + writeUInt32LE(end, 12, centralDirectory.length); + writeUInt32LE(end, 16, centralStart); + writeUInt16LE(end, 20, 0); + return concatBytes([...localParts, centralDirectory, end]); +} +function meshTo3mf(mesh, { name = "Implicit CAD", color = "#d4d4d8" } = {}) { + const positions = mesh.positions || new Float32Array(); + const vertexCount = Math.floor(positions.length / 3); + const triangleCount = Math.floor(positions.length / 9); + const displayColor = hexTo3mfDisplayColor(color); + const vertices = []; + for (let index = 0; index < vertexCount; index += 1) { + const offset = index * 3; + vertices.push(``); + } + const triangles = []; + for (let index = 0; index < triangleCount; index += 1) { + const base = index * 3; + triangles.push(``); + } + const modelXml = ` + + ${xmlEscape(name)} + + + + + + + + ${vertices.join("\n ")} + + + ${triangles.join("\n ")} + + + + + + + +`; + const contentTypes = ` + + + +`; + const relationships = ` + + +`; + return zipStore([ + { name: "[Content_Types].xml", body: contentTypes }, + { name: "_rels/.rels", body: relationships }, + { name: "3D/3dmodel.model", body: modelXml } + ]); +} +function meshToFormat(mesh, format, options = {}) { + const normalized = String(format || "").trim().toLowerCase().replace(/^\./, ""); + if (normalized === "stl") { + return { + body: meshToBinaryStl(mesh, options), + contentType: "model/stl", + extension: ".stl" + }; + } + if (normalized === "glb" || normalized === "gltf") { + return { + body: meshToGlb(mesh, options), + contentType: "model/gltf-binary", + extension: ".glb" + }; + } + if (normalized === "3mf") { + return { + body: meshTo3mf(mesh, options), + contentType: "model/3mf", + extension: ".3mf" + }; + } + throw new Error(`Unsupported implicit CAD export format: ${format || "(missing)"}`); +} + +// viewer/packages/implicitjs/src/lib/implicitCad/exportModel.js +var IMPLICIT_CAD_EXPORT_FORMATS = Object.freeze(["glb", "stl", "3mf"]); +function isObject3(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} +function finiteNumber6(value, fallback = 0) { + const numeric = Number(value); + return Number.isFinite(numeric) ? numeric : fallback; +} +function clamp5(value, min, max) { + return Math.min(Math.max(value, min), max); +} +function normalizeImplicitExportFormat(value) { + const normalized = String(value || "").trim().toLowerCase().replace(/^\./, ""); + if (normalized === "gltf") { + return "glb"; + } + return IMPLICIT_CAD_EXPORT_FORMATS.includes(normalized) ? normalized : ""; +} +function rgb01ToHex(rgb) { + const channel = (value) => { + const numeric = Number(value); + const clamped = Math.min(Math.max(Number.isFinite(numeric) ? numeric : 0, 0), 1); + return Math.round(clamped * 255).toString(16).padStart(2, "0"); + }; + return `#${channel(rgb?.[0])}${channel(rgb?.[1])}${channel(rgb?.[2])}`; +} +function sampleImplicitCadMeshColor(model, mesh, { sampleLimit = 768 } = {}) { + if (!model?.colorSource) { + return model?.material?.color; + } + try { + const colorAt = createImplicitCadColorEvaluator(model); + const positions = mesh.positions || new Float32Array(); + const normals = mesh.normals && mesh.normals.length === positions.length ? mesh.normals : null; + const vertexCount = Math.floor(positions.length / 3); + const stride = Math.max(1, Math.floor(vertexCount / Math.max(sampleLimit, 1))); + const sum = [0, 0, 0]; + let count = 0; + for (let vertex = 0; vertex < vertexCount; vertex += stride) { + const offset = vertex * 3; + const color = colorAt( + [positions[offset], positions[offset + 1], positions[offset + 2]], + normals ? [normals[offset], normals[offset + 1], normals[offset + 2]] : [0, 0, 1] + ); + if (!color.every((component) => Number.isFinite(component))) { + continue; + } + sum[0] += color[0]; + sum[1] += color[1]; + sum[2] += color[2]; + count += 1; + } + return count ? rgb01ToHex(sum.map((component) => component / count)) : model?.material?.color; + } catch { + return model?.material?.color; + } +} +function estimateGradient2(sdf, point, epsilon) { + const [x, y, z] = point; + const gradient = [ + finiteNumber6(sdf(x + epsilon, y, z), 0) - finiteNumber6(sdf(x - epsilon, y, z), 0), + finiteNumber6(sdf(x, y + epsilon, z), 0) - finiteNumber6(sdf(x, y - epsilon, z), 0), + finiteNumber6(sdf(x, y, z + epsilon), 0) - finiteNumber6(sdf(x, y, z - epsilon), 0) + ]; + const length2 = Math.hypot(gradient[0], gradient[1], gradient[2]); + return length2 > 1e-9 ? [gradient[0] / length2, gradient[1] / length2, gradient[2] / length2] : [0, 0, 1]; +} +function projectMeshPositionsToModel(basePositions, targetModel, { + iterations = 8, + maxStep = null +} = {}) { + const sdf = createImplicitCadSdfEvaluator(targetModel); + const targetPositions = new Float32Array(basePositions.length); + const epsilon = Math.max(finiteNumber6(targetModel.normalEpsilon, targetModel.radius * 1e-3), 1e-4); + const stepLimit = Math.max( + finiteNumber6(maxStep, finiteNumber6(targetModel.maxStep, targetModel.radius * 0.08)), + epsilon + ); + for (let index = 0; index < basePositions.length; index += 3) { + const point = [ + basePositions[index], + basePositions[index + 1], + basePositions[index + 2] + ]; + for (let pass = 0; pass < iterations; pass += 1) { + const distance = finiteNumber6(sdf(point[0], point[1], point[2]), 0); + if (Math.abs(distance) <= epsilon * 0.35) { + break; + } + const normal = estimateGradient2(sdf, point, epsilon); + const step = clamp5(distance, -stepLimit, stepLimit); + point[0] -= normal[0] * step; + point[1] -= normal[1] * step; + point[2] -= normal[2] * step; + } + targetPositions[index] = point[0]; + targetPositions[index + 1] = point[1]; + targetPositions[index + 2] = point[2]; + } + return targetPositions; +} +function definitionFromModel(modelValue) { + if (modelValue?.definition?.buildModel) { + return modelValue.definition; + } + if (modelValue?.buildModel) { + return modelValue; + } + return normalizeImplicitCadModel(modelValue).definition; +} +function runtimeModelForExport(modelValue, { + params = null, + parameterValues = null, + animationState = null +} = {}) { + const model = normalizeImplicitCadModel(modelValue); + const nextParams = isObject3(params) ? params : isObject3(parameterValues) ? parameterValues : null; + if (!nextParams && !animationState) { + return model; + } + return typeof model.definition?.buildModel === "function" ? model.definition.buildModel( + nextParams || model.parameterValues || model.defaultParameterValues || {}, + isObject3(animationState) ? animationState : model.animationState || {} + ) : model; +} +function exportImplicitCadModel(modelValue, { + format = "glb", + params = null, + parameterValues = null, + animationState = null, + resolution = 96, + maxCells = void 0, + normalEpsilon = void 0, + smoothNormals = void 0 +} = {}) { + const model = runtimeModelForExport(modelValue, { + params, + parameterValues, + animationState + }); + const outputFormat = normalizeImplicitExportFormat(format); + const mesh = meshImplicitCadModel(model, { + resolution, + maxCells, + normalEpsilon, + smoothNormals: smoothNormals ?? outputFormat === "glb" + }); + if (!mesh.triangleCount) { + throw new Error("Implicit CAD export produced an empty mesh. Check bounds, parameters, and resolution."); + } + const exported = meshToFormat(mesh, format, { + name: model.name, + color: sampleImplicitCadMeshColor(model, mesh) + }); + return { + ...exported, + mesh, + model, + format: outputFormat + }; +} +function exportImplicitCadAnimatedGlb(modelOrDefinition, { + animationId = "", + params = null, + parameterValues = null, + duration = void 0, + frames = 18, + resolution = 64, + maxCells = void 0, + normalEpsilon = void 0, + smoothNormals = true, + projectionIterations = 8 +} = {}) { + const definition = definitionFromModel(modelOrDefinition); + const animation = findImplicitAnimation(definition, animationId); + if (!animation) { + throw new Error("Animated GLB export requires an animation on the implicit model."); + } + const baseValues = isObject3(params) ? params : isObject3(parameterValues) ? parameterValues : modelOrDefinition?.parameterValues || definition.defaultParameterValues || {}; + const durationSeconds = Math.max(finiteNumber6(duration, animation.duration), 1e-3); + const frameCount = Math.max(3, Math.min(Math.floor(finiteNumber6(frames, 18)), 48)); + const baseModel = buildImplicitAnimatedModel(definition, { + animationId: animation.id, + params: baseValues, + elapsedSec: 0, + playing: false + }); + const baseMesh = meshImplicitCadModel(baseModel, { + resolution, + maxCells, + normalEpsilon, + smoothNormals + }); + if (!baseMesh.triangleCount) { + throw new Error("Animated GLB export produced an empty base mesh. Check bounds, parameters, and resolution."); + } + const targetPositions = []; + const times = []; + for (let frame = 0; frame < frameCount; frame += 1) { + const elapsedSec = durationSeconds * frame / (frameCount - 1); + times.push(elapsedSec); + if (frame === 0 || animation.loop !== false && frame === frameCount - 1) { + continue; + } + const frameModel = buildImplicitAnimatedModel(definition, { + animationId: animation.id, + params: baseValues, + elapsedSec, + playing: false + }); + targetPositions.push(projectMeshPositionsToModel(baseMesh.positions, frameModel, { + iterations: projectionIterations + })); + } + const body = meshToAnimatedGlb(baseMesh, { + name: baseModel.name, + color: sampleImplicitCadMeshColor(baseModel, baseMesh), + duration: durationSeconds, + times, + targetPositions + }); + return { + body, + contentType: "model/gltf-binary", + extension: ".glb", + format: "glb", + mesh: baseMesh, + model: baseModel, + animation, + frameCount, + duration: durationSeconds + }; +} + +// viewer/packages/implicitjs/src/lib/implicitCad/export.js +function isObject4(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} +function normalizeFormat(value) { + return normalizeImplicitExportFormat(value); +} +function formatFromOutputPath(outputPath) { + return normalizeFormat(path7.extname(String(outputPath || "")).slice(1)); +} +function defaultImplicitCadExportPath(inputPath, format = "glb") { + const normalizedFormat = normalizeFormat(format) || "glb"; + const raw = String(inputPath || "").trim(); + const basename = raw.replace(/\.implicit\.(?:mjs|js)$/i, "").replace(/\.(?:mjs|js)$/i, ""); + return `${basename}.${normalizedFormat}`; +} +async function loadImplicitCadModelFromPath(inputPath, { + params = null, + parameterValues = null, + animationState = null +} = {}) { + const resolvedInput = path7.resolve(String(inputPath || "")); + const stats = await fs6.stat(resolvedInput); + if (!stats.isFile()) { + throw new Error(`Implicit CAD input is not a file: ${resolvedInput}`); + } + const inputUrl = pathToFileURL(resolvedInput); + inputUrl.searchParams.set("mtime", String(Number(stats.mtimeMs))); + const moduleValue = await import(inputUrl.href); + const defaultModel = normalizeImplicitCadModel(moduleValue, { sourceUrl: inputUrl.href }); + const nextParams = isObject4(params) ? params : isObject4(parameterValues) ? parameterValues : null; + if (nextParams || animationState) { + return defaultModel.definition.buildModel( + nextParams || defaultModel.defaultParameterValues, + isObject4(animationState) ? animationState : defaultModel.animationState + ); + } + return defaultModel; +} +async function exportImplicitCadFile({ + input, + output = "", + format = "", + params = null, + parameterValues = null, + animationState = null, + animated = false, + frames = void 0, + duration = void 0, + resolution = 96, + maxCells = void 0, + normalEpsilon = void 0, + smoothNormals = void 0 +} = {}) { + const inputPath = path7.resolve(String(input || "")); + const outputFormat = animated ? "glb" : normalizeFormat(format) || formatFromOutputPath(output) || "glb"; + const outputPath = path7.resolve(output || defaultImplicitCadExportPath(inputPath, outputFormat)); + const model = await loadImplicitCadModelFromPath(inputPath, { + params, + parameterValues, + animationState + }); + const result = animated ? exportImplicitCadAnimatedGlb(model, { + animationId: animationState?.activeId, + params: model.parameterValues, + duration, + frames, + resolution, + maxCells, + normalEpsilon, + smoothNormals + }) : exportImplicitCadModel(model, { + format: outputFormat, + resolution, + maxCells, + normalEpsilon, + smoothNormals + }); + await fs6.mkdir(path7.dirname(outputPath), { recursive: true }); + await fs6.writeFile(outputPath, result.body); + return { + ok: true, + input: inputPath, + output: outputPath, + format: result.format, + contentType: result.contentType, + bytes: result.body.length, + triangleCount: result.mesh.triangleCount, + vertexCount: result.mesh.vertexCount, + grid: result.mesh.grid, + model: { + name: result.model.name, + bounds: result.model.bounds, + units: result.model.units + } + }; +} + +// viewer/src/server/localAssetBackend.mjs +function toPosixPath2(value) { + return String(value || "").split(path8.sep).join("/"); +} +function absoluteFileRef(filePath) { + return toPosixPath2(path8.resolve(filePath)); +} +function relativeFileRef(rootPath, filePath) { + return toPosixPath2(path8.relative(path8.resolve(rootPath), path8.resolve(filePath))); +} +function pathIsInsideOrEqual(childPath, parentPath) { + const relativePath = path8.relative(path8.resolve(parentPath), path8.resolve(childPath)); + return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${path8.sep}`) && !path8.isAbsolute(relativePath); +} +function normalizedFileRef(value) { + const raw = String(value || "").trim().replace(/\\/g, "/"); + if (!raw) { + return ""; + } + if (raw.includes("\0")) { + throw new Error("File path contains an invalid null byte"); + } + return path8.isAbsolute(raw) ? absoluteFileRef(raw) : raw.replace(/^\/+/, ""); +} +function normalizedRootDir(value, baseRoot) { + const raw = String(value || "").trim(); + if (!raw) { + return ""; + } + if (raw.includes("\0")) { + throw new Error("CAD Viewer directory contains an invalid null byte"); + } + return path8.isAbsolute(raw) ? path8.resolve(raw) : path8.resolve(baseRoot, raw); +} +function requireDirectory(rootPath) { + let stats = null; + try { + stats = fs7.statSync(rootPath); + } catch { + throw new Error(`CAD Viewer directory not found: ${rootPath}`); + } + if (!stats.isDirectory()) { + throw new Error(`CAD Viewer directory is not a directory: ${rootPath}`); + } +} +function catalogEntryForFileRef(catalog, fileRef) { + const normalized = normalizedFileRef(fileRef); + if (!normalized || !Array.isArray(catalog?.entries)) { + return null; + } + return catalog.entries.find((entry) => normalizedFileRef(entry?.file) === normalized || normalizedFileRef(entry?.rootRelativeFile) === normalized) || null; +} +function ensurePathInsideRoot(filePath, resolvedRoot) { + if (!(filePath === resolvedRoot.rootPath || pathIsInside(filePath, resolvedRoot.rootPath))) { + throw new Error("Requested file is outside the active CAD Viewer root"); + } +} +function normalizedFileAssetKind(value) { + const asset = String(value || "output").trim().toLowerCase(); + if (asset === "asset") { + return "artifact"; + } + if (asset === "output" || asset === "source" || asset === "artifact") { + return asset; + } + throw new Error(`Unsupported file asset: ${asset || "(missing)"}`); +} +function normalizedImplicitExportFormat(value) { + const format = String(value || "").trim().toLowerCase().replace(/^\./, ""); + if (IMPLICIT_CAD_EXPORT_FORMATS.includes(format)) { + return format; + } + throw new Error(`Unsupported implicit CAD export format: ${format || "(missing)"}`); +} +function fileHasGenStep2(filePath) { + try { + return /\bgen_step\s*\(/.test(fs7.readFileSync(filePath, "utf-8")); + } catch { + return false; + } +} +function sameStemPythonGeneratorPath2(stepPath) { + const extension = path8.extname(stepPath).toLowerCase(); + if (extension !== ".step" && extension !== ".stp") { + return ""; + } + const candidate = path8.join(path8.dirname(stepPath), `${path8.basename(stepPath, extension)}.py`); + return fileHasGenStep2(candidate) ? candidate : ""; +} +function stepArtifactGenerationError(result) { + const directError = String(result?.error || "").trim(); + if (directError) { + return directError; + } + const validationError = result?.validation?.error; + const validationMessage = String(validationError?.message || "").trim(); + if (validationMessage) { + return validationMessage; + } + const reason = String(result?.reason || "").trim(); + if (reason) { + return `STEP artifact was not generated: ${reason}`; + } + return "STEP artifact generation failed."; +} +function contentTypeForPath(filePath) { + const extension = path8.extname(filePath).toLowerCase(); + if (extension === ".js" || extension === ".mjs") { + return "text/javascript; charset=utf-8"; + } + if (extension === ".json") { + return "application/json; charset=utf-8"; + } + if (extension === ".wasm") { + return "application/wasm"; + } + if (extension === ".glb") { + return "model/gltf-binary"; + } + if (extension === ".stl") { + return "model/stl"; + } + if (extension === ".3mf") { + return "model/3mf"; + } + if (extension === ".step" || extension === ".stp") { + return "application/step"; + } + if (extension === ".dxf") { + return "application/dxf"; + } + if (extension === ".gcode" || extension === ".py") { + return "text/plain; charset=utf-8"; + } + if (extension === ".urdf" || extension === ".srdf" || extension === ".sdf") { + return "application/xml; charset=utf-8"; + } + if (extension === ".svg") { + return "image/svg+xml"; + } + if (extension === ".png") { + return "image/png"; + } + return "application/octet-stream"; +} +function defaultSourceFileOpener(filePath) { + let command = ""; + let args = []; + if (process.platform === "darwin") { + command = "open"; + args = ["-R", filePath]; + } else if (process.platform === "win32") { + command = "explorer.exe"; + args = [`/select,${filePath}`]; + } else { + command = "xdg-open"; + args = [path8.dirname(filePath)]; + } + const child = spawn2(command, args, { + detached: true, + stdio: "ignore" + }); + child.unref(); + return { + command + }; +} +function emptyCatalog() { + return { + schemaVersion: CAD_CATALOG_SCHEMA_VERSION, + entries: [] + }; +} +function normalizeCatalog(catalog) { + return { + schemaVersion: CAD_CATALOG_SCHEMA_VERSION, + entries: Array.isArray(catalog?.entries) ? catalog.entries : [] + }; +} +function queryValueFromAssetUrl(rawUrl, name) { + try { + return new URL(String(rawUrl || ""), "http://cad.local").searchParams.get(name) || ""; + } catch { + return ""; + } +} +function assetPathFromCatalogUrl(scanRepoRoot, rawUrl) { + const text = String(rawUrl || "").trim(); + if (!text) { + return ""; + } + try { + const url = new URL(text, "http://cad.local"); + const explicitFile = url.searchParams.get("file"); + if (explicitFile) { + return path8.resolve(explicitFile); + } + return path8.resolve(scanRepoRoot, decodeURIComponent(url.pathname).replace(/^\/+/, "")); + } catch { + return path8.resolve(scanRepoRoot, text.replace(/[?#].*$/, "").replace(/^\/+/, "")); + } +} +function localAssetUrlForPath(filePath, rawUrl = "", { rootDir = "" } = {}) { + const url = new URL("/__cad/asset", "http://cad.local"); + url.searchParams.set("file", absoluteFileRef(filePath)); + const normalizedRootDir2 = String(rootDir || "").trim(); + if (normalizedRootDir2) { + url.searchParams.set("dir", normalizedRootDir2); + } + const version = queryValueFromAssetUrl(rawUrl, "v"); + if (version) { + url.searchParams.set("v", version); + } + return `${url.pathname}${url.search}`; +} +function absolutePathFromCatalogValue(scanRepoRoot, value) { + const text = String(value || "").trim(); + if (!text) { + return ""; + } + if (path8.isAbsolute(text)) { + return path8.resolve(text); + } + return path8.resolve(scanRepoRoot, text); +} +function absolutizeArtifact(artifact, scanRepoRoot) { + if (!artifact || typeof artifact !== "object") { + return artifact; + } + const next = { ...artifact }; + for (const key of ["stepPath", "glbPath", "sourcePath", "cadPath"]) { + if (next[key]) { + next[key] = absoluteFileRef(absolutePathFromCatalogValue(scanRepoRoot, next[key])); + } + } + return next; +} +function absolutizeSource(source, scanRepoRoot) { + if (!source || typeof source !== "object") { + return source; + } + const next = { ...source }; + for (const key of ["file", "path", "sourcePath"]) { + if (next[key]) { + next[key] = absoluteFileRef(absolutePathFromCatalogValue(scanRepoRoot, next[key])); + } + } + return next; +} +function absolutizeSourceStatus(sourceStatus, scanRepoRoot) { + if (!sourceStatus || typeof sourceStatus !== "object") { + return sourceStatus; + } + const next = { ...sourceStatus }; + for (const key of ["sourcePath", "stepPath", "glbPath"]) { + if (next[key]) { + next[key] = absoluteFileRef(absolutePathFromCatalogValue(scanRepoRoot, next[key])); + } + } + return next; +} +function absolutizeCatalogEntry(entry, { rootPath, scanRepoRoot, rootDir = "" }) { + if (!entry || typeof entry !== "object") { + return entry; + } + const outputPath = path8.resolve(rootPath, String(entry.file || "")); + const next = { + ...entry, + file: absoluteFileRef(outputPath), + rootRelativeFile: relativeFileRef(rootPath, outputPath) + }; + if (entry.url) { + const assetPath = assetPathFromCatalogUrl(scanRepoRoot, entry.url); + next.url = localAssetUrlForPath(assetPath, entry.url, { rootDir }); + next.assetFile = absoluteFileRef(assetPath); + } + if (entry.moduleUrl) { + const modulePath = assetPathFromCatalogUrl(scanRepoRoot, entry.moduleUrl); + next.moduleUrl = localAssetUrlForPath(modulePath, entry.moduleUrl, { rootDir }); + next.moduleFile = absoluteFileRef(modulePath); + } + if (entry.source) { + next.source = absolutizeSource(entry.source, scanRepoRoot); + } + if (entry.sourceStatus) { + next.sourceStatus = absolutizeSourceStatus(entry.sourceStatus, scanRepoRoot); + } + if (entry.artifact) { + next.artifact = absolutizeArtifact(entry.artifact, scanRepoRoot); + } + if (entry.relations && typeof entry.relations === "object") { + next.relations = { ...entry.relations }; + for (const [key, relation] of Object.entries(entry.relations)) { + if (!relation || typeof relation !== "object") { + continue; + } + const relationFilePath = path8.resolve(rootPath, String(relation.file || "")); + const nextRelation = { + ...relation, + file: absoluteFileRef(relationFilePath), + rootRelativeFile: relativeFileRef(rootPath, relationFilePath) + }; + if (relation.url) { + const relationAssetPath = assetPathFromCatalogUrl(scanRepoRoot, relation.url); + nextRelation.url = localAssetUrlForPath(relationAssetPath, relation.url, { rootDir }); + nextRelation.assetFile = absoluteFileRef(relationAssetPath); + } + next.relations[key] = nextRelation; + } + } + return next; +} +function absolutizeCatalog(catalog, context) { + return normalizeCatalog({ + ...catalog, + entries: (Array.isArray(catalog?.entries) ? catalog.entries : []).map((entry) => absolutizeCatalogEntry(entry, context)).filter(Boolean) + }); +} +function absolutizeGenerationStatus(status, rootPath) { + const files = {}; + for (const [file, value] of Object.entries(status?.files || {})) { + const absolute = absoluteFileRef(path8.resolve(rootPath, String(file || ""))); + files[absolute] = { + ...value, + file: absolute, + rootRelativeFile: relativeFileRef(rootPath, absolute) + }; + } + return { + schemaVersion: 1, + runs: (Array.isArray(status?.runs) ? status.runs : []).map((run) => ({ + ...run, + files: (Array.isArray(run?.files) ? run.files : []).map((file) => absoluteFileRef(path8.resolve(rootPath, String(file || "")))).filter(Boolean) + })), + files + }; +} +function createLocalAssetBackend({ + directoryRoot: directoryRoot2 = process.cwd(), + rootDir = "", + defaultFile = "", + githubUrl = "", + stepArtifactGenerator = ensureStepTopologyArtifact, + sourceFileOpener = defaultSourceFileOpener +} = {}) { + const baseDirectoryRoot = path8.resolve(directoryRoot2 || process.cwd()); + const defaultRootDir = rootDir ? absoluteFileRef(normalizedRootDir(rootDir, baseDirectoryRoot)) : absoluteFileRef(baseDirectoryRoot); + const catalogCache = /* @__PURE__ */ new Map(); + function effectiveRootDirForRequest(rootDir2 = "") { + return rootDir2 || defaultRootDir; + } + function resolveRoot(rootDir2 = defaultRootDir) { + const rootPath = normalizedRootDir(rootDir2 || defaultRootDir, baseDirectoryRoot); + if (!rootPath) { + throw new Error("CAD Viewer local filesystem requests must include a ?dir= path"); + } + requireDirectory(rootPath); + return { + dir: absoluteFileRef(rootPath), + rootPath, + rootName: path8.basename(rootPath) + }; + } + function resolveRequestRoot({ rootDir: rootDir2 = defaultRootDir } = {}) { + return resolveRoot(effectiveRootDirForRequest(rootDir2)); + } + function scanContextForRoot(resolvedRoot) { + const rootPath = path8.resolve(resolvedRoot.rootPath); + const scanRepoRoot = pathIsInsideOrEqual(rootPath, baseDirectoryRoot) ? baseDirectoryRoot : rootPath; + const scanRootDir = scanRepoRoot === rootPath ? "" : toPosixPath2(path8.relative(scanRepoRoot, rootPath)); + return { + rootDir: resolvedRoot.dir, + rootPath, + scanRepoRoot, + scanRootDir + }; + } + function readCatalog({ rootDir: nextRootDir = defaultRootDir, fileRef = "" } = {}) { + const effectiveRootDir = effectiveRootDirForRequest(nextRootDir); + const normalizedDir = absoluteFileRef(normalizedRootDir(effectiveRootDir, baseDirectoryRoot)); + const normalizedFile = normalizedFileRef(fileRef); + const cacheKey = `dir:${normalizedDir}`; + if (!catalogCache.has(cacheKey)) { + return refreshCatalog({ rootDir: normalizedDir, fileRef: normalizedFile }); + } + if (normalizedDir && normalizedFile) { + const resolvedRoot = resolveRoot(normalizedDir); + const requestedPath = filePathFromRef(normalizedFile, resolvedRoot); + if (requestedPath === resolvedRoot.rootPath || pathIsInside(requestedPath, resolvedRoot.rootPath)) { + return refreshCatalogForPath({ rootDir: resolvedRoot.dir, filePath: requestedPath }); + } + } + return catalogCache.get(cacheKey); + } + function readCatalogSafe({ rootDir: nextRootDir = defaultRootDir, fileRef = "" } = {}) { + try { + return readCatalog({ rootDir: nextRootDir, fileRef }); + } catch { + return emptyCatalog(); + } + } + function refreshCatalog({ rootDir: nextRootDir = defaultRootDir, fileRef = "" } = {}) { + const effectiveRootDir = effectiveRootDirForRequest(nextRootDir); + const resolvedRoot = resolveRoot(effectiveRootDir); + const context = scanContextForRoot(resolvedRoot); + const rawCatalog = scanCadDirectory({ + repoRoot: context.scanRepoRoot, + rootDir: context.scanRootDir, + includeArtifactStatus: false + }); + const catalog = absolutizeCatalog(rawCatalog, context); + catalogCache.set(`dir:${resolvedRoot.dir}`, catalog); + return catalog; + } + function replaceCatalogEntry(catalog, fileRef, nextEntry) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef) { + return normalizeCatalog(catalog); + } + const previousEntries = Array.isArray(catalog?.entries) ? catalog.entries : []; + const entries = previousEntries.filter((entry) => normalizedFileRef(entry?.file) !== normalizedRef); + if (nextEntry) { + entries.push(nextEntry); + } + return normalizeCatalog({ + ...catalog, + entries: sortCatalogEntries(entries) + }); + } + function refreshCatalogEntryForFile({ rootDir: nextRootDir = defaultRootDir, filePath } = {}) { + const resolvedRoot = resolveRoot(nextRootDir); + const context = scanContextForRoot(resolvedRoot); + const currentCatalog = readCatalog({ rootDir: resolvedRoot.dir }); + const rawEntry = scanCadFile({ + repoRoot: context.scanRepoRoot, + rootDir: context.scanRootDir, + filePath, + includeArtifactStatus: false + }); + const nextEntry = rawEntry ? absolutizeCatalogEntry(rawEntry, context) : null; + const rawFileRef = rawEntry?.file || catalogFileRefForPath({ + repoRoot: context.scanRepoRoot, + rootDir: context.scanRootDir, + filePath + }); + const fileRef = nextEntry?.file || (rawFileRef ? absoluteFileRef(path8.resolve(resolvedRoot.rootPath, rawFileRef)) : absoluteFileRef(filePath)); + const nextCatalog = replaceCatalogEntry(currentCatalog, fileRef, nextEntry); + catalogCache.set(`dir:${resolvedRoot.dir}`, nextCatalog); + return nextCatalog; + } + function refreshCatalogForPythonSource({ rootDir: nextRootDir = defaultRootDir, filePath } = {}) { + const resolvedRoot = resolveRoot(nextRootDir); + const resolvedFilePath = path8.resolve(filePath); + const sourcePath = absoluteFileRef(resolvedFilePath); + const currentCatalog = readCatalog({ rootDir: resolvedRoot.dir }); + const matchingFileRefs = new Set( + currentCatalog.entries.filter((entry) => normalizedFileRef(entry?.source?.sourcePath || entry?.source?.file) === sourcePath).map((entry) => normalizedFileRef(entry.file)).filter(Boolean) + ); + const sameStemStepPath = path8.join(path8.dirname(resolvedFilePath), `${path8.basename(resolvedFilePath, ".py")}.step`); + if (sameStemStepPath === resolvedRoot.rootPath || pathIsInside(sameStemStepPath, resolvedRoot.rootPath)) { + const context2 = scanContextForRoot(resolvedRoot); + const rawSameStemEntry = scanCadFile({ + repoRoot: context2.scanRepoRoot, + rootDir: context2.scanRootDir, + filePath: sameStemStepPath, + includeArtifactStatus: false + }); + const sameStemEntry = rawSameStemEntry ? absolutizeCatalogEntry(rawSameStemEntry, context2) : null; + const sameStemFileRef = sameStemEntry?.file || absoluteFileRef(sameStemStepPath); + if (sameStemEntry || catalogEntryForFileRef(currentCatalog, sameStemFileRef)) { + matchingFileRefs.add(sameStemFileRef); + } + } + if (!matchingFileRefs.size) { + return refreshCatalog({ rootDir: resolvedRoot.dir }); + } + let nextCatalog = currentCatalog; + const context = scanContextForRoot(resolvedRoot); + for (const fileRef of matchingFileRefs) { + const outputPath = path8.resolve(fileRef); + const rawEntry = scanCadFile({ + repoRoot: context.scanRepoRoot, + rootDir: context.scanRootDir, + filePath: outputPath, + includeArtifactStatus: false + }); + nextCatalog = replaceCatalogEntry( + nextCatalog, + fileRef, + rawEntry ? absolutizeCatalogEntry(rawEntry, context) : null + ); + } + catalogCache.set(`dir:${resolvedRoot.dir}`, nextCatalog); + return nextCatalog; + } + function refreshCatalogForPath({ rootDir: nextRootDir = defaultRootDir, filePath } = {}) { + const extension = path8.extname(String(filePath || "")).toLowerCase(); + if (extension === ".py") { + return refreshCatalogForPythonSource({ rootDir: nextRootDir, filePath }); + } + return refreshCatalogEntryForFile({ rootDir: nextRootDir, filePath }); + } + function filePathFromRef(fileRef, resolvedRoot) { + const normalized = normalizedFileRef(fileRef); + if (!normalized) { + return ""; + } + return path8.isAbsolute(normalized) ? path8.resolve(normalized) : path8.resolve(resolvedRoot.rootPath, normalized); + } + function resolveStepSource(fileRef, { resolvedRoot = resolveRequestRoot({ fileRef }), catalog = null } = {}) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef) { + throw new Error("Missing STEP file"); + } + const candidates = path8.isAbsolute(normalizedRef) ? [ + path8.resolve(normalizedRef), + path8.resolve(resolvedRoot.rootPath, normalizedRef.replace(/^\/+/, "")) + ] : [ + path8.resolve(resolvedRoot.rootPath, normalizedRef) + ]; + for (const candidatePath2 of [...new Set(candidates)]) { + if ((candidatePath2 === resolvedRoot.rootPath || pathIsInside(candidatePath2, resolvedRoot.rootPath)) && fs7.existsSync(candidatePath2)) { + const extension = path8.extname(candidatePath2).toLowerCase(); + if (extension === ".py") { + if (!fileHasGenStep2(candidatePath2)) { + throw new Error(`Python generator is not a gen_step() source: ${normalizedRef}`); + } + return { + stepPath: path8.join(path8.dirname(candidatePath2), `${path8.basename(candidatePath2, extension)}.step`), + sourcePath: candidatePath2, + skipStepWrite: true + }; + } + if (extension !== ".step" && extension !== ".stp") { + throw new Error("Only STEP/STP sources or same-stem Python generators can generate STEP topology artifacts"); + } + const generatorPath = sameStemPythonGeneratorPath2(candidatePath2); + return { + stepPath: candidatePath2, + sourcePath: generatorPath, + skipStepWrite: Boolean(generatorPath) + }; + } + } + const candidatePath = candidates.find((candidate) => candidate === resolvedRoot.rootPath || pathIsInside(candidate, resolvedRoot.rootPath)); + if (candidatePath) { + const extension = path8.extname(candidatePath).toLowerCase(); + const generatorPath = sameStemPythonGeneratorPath2(candidatePath); + if ((extension === ".step" || extension === ".stp") && generatorPath) { + return { stepPath: candidatePath, sourcePath: generatorPath, skipStepWrite: true }; + } + throw new Error(`STEP file not found: ${normalizedRef}`); + } + throw new Error("Requested STEP file is outside the active CAD Viewer root"); + } + function resolveStepSourceStatus(fileRef, { resolvedRoot = resolveRequestRoot({ fileRef }), catalog = null } = {}) { + try { + return resolveStepSource(fileRef, { resolvedRoot, catalog }); + } catch (error) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef) { + throw error; + } + const candidatePath = filePathFromRef(normalizedRef, resolvedRoot); + if (!(candidatePath === resolvedRoot.rootPath || pathIsInside(candidatePath, resolvedRoot.rootPath))) { + throw error; + } + const extension = path8.extname(candidatePath).toLowerCase(); + if (extension !== ".step" && extension !== ".stp") { + throw error; + } + const generatorPath = sameStemPythonGeneratorPath2(candidatePath); + return { + stepPath: candidatePath, + sourcePath: generatorPath, + skipStepWrite: Boolean(generatorPath) + }; + } + } + function requireCatalogEntryForFileRef(fileRef, { + resolvedRoot = resolveRequestRoot({ fileRef }), + rootDir: nextRootDir = defaultRootDir, + catalog = null + } = {}) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef) { + throw new Error("Missing file"); + } + const currentCatalog = catalog || readCatalogSafe({ rootDir: nextRootDir, fileRef: normalizedRef }); + const entry = catalogEntryForFileRef(currentCatalog, normalizedRef); + if (!entry) { + throw new Error(`CAD catalog entry not found: ${normalizedRef}`); + } + return { entry, relativeFileRef: normalizedRef, currentCatalog, resolvedRoot }; + } + function resolveOutputFilePath(fileRef, options = {}) { + const { entry, relativeFileRef: relativeFileRef2, resolvedRoot } = requireCatalogEntryForFileRef(fileRef, options); + const outputRef = normalizedFileRef(entry?.file || relativeFileRef2); + const outputPath = filePathFromRef(outputRef, resolvedRoot); + ensurePathInsideRoot(outputPath, resolvedRoot); + if (!fs7.existsSync(outputPath) || !fs7.statSync(outputPath).isFile()) { + throw new Error(`Output file not found: ${outputRef || relativeFileRef2}`); + } + return outputPath; + } + function artifactFileRefFromEntry2(entry) { + const explicitAssetFile = normalizedFileRef(entry?.assetFile || entry?.asset?.file || entry?.artifactFile || entry?.artifact?.file); + if (explicitAssetFile) { + return explicitAssetFile; + } + const rawUrl = String(entry?.url || "").trim(); + if (!rawUrl) { + throw new Error("Artifact asset is not available for this file"); + } + const assetPath = assetPathFromCatalogUrl("/", rawUrl); + return absoluteFileRef(assetPath); + } + function resolveImplicitCadFilePath(fileRef, options = {}) { + const { entry, relativeFileRef: relativeFileRef2, resolvedRoot } = requireCatalogEntryForFileRef(fileRef, options); + const outputRef = normalizedFileRef(entry?.file || relativeFileRef2); + const outputPath = filePathFromRef(outputRef, resolvedRoot); + ensurePathInsideRoot(outputPath, resolvedRoot); + if (!pathIsImplicitCadSource2(outputPath) || String(entry?.kind || "").trim().toLowerCase() !== "implicit") { + throw new Error(`File is not an implicit CAD source: ${outputRef || relativeFileRef2}`); + } + if (!fs7.existsSync(outputPath) || !fs7.statSync(outputPath).isFile()) { + throw new Error(`Implicit CAD file not found: ${outputRef || relativeFileRef2}`); + } + return outputPath; + } + function resolveArtifactFilePath(fileRef, options = {}) { + const { entry, relativeFileRef: relativeFileRef2, resolvedRoot } = requireCatalogEntryForFileRef(fileRef, options); + const artifactRef = artifactFileRefFromEntry2(entry); + if (!artifactRef) { + throw new Error(`Artifact asset is not available for ${relativeFileRef2}`); + } + const artifactPath = filePathFromRef(artifactRef, resolvedRoot); + ensurePathInsideRoot(artifactPath, resolvedRoot); + if (!fs7.existsSync(artifactPath) || !fs7.statSync(artifactPath).isFile()) { + throw new Error(`Artifact file not found: ${artifactRef}`); + } + return artifactPath; + } + function resolveSourceCodeFilePath(fileRef, options = {}) { + const { entry, relativeFileRef: relativeFileRef2, currentCatalog, resolvedRoot } = requireCatalogEntryForFileRef(fileRef, options); + const explicitSourceRef = normalizedFileRef(entry?.source?.file || entry?.sourceFile || ""); + if (explicitSourceRef) { + const sourceCandidates = [ + filePathFromRef(explicitSourceRef, resolvedRoot), + path8.resolve(baseDirectoryRoot, explicitSourceRef) + ]; + for (const sourcePath of [...new Set(sourceCandidates)]) { + if ((sourcePath === resolvedRoot.rootPath || pathIsInside(sourcePath, resolvedRoot.rootPath)) && fs7.existsSync(sourcePath) && fs7.statSync(sourcePath).isFile()) { + return sourcePath; + } + } + } + const extension = path8.extname(relativeFileRef2).toLowerCase(); + if (extension === ".step" || extension === ".stp") { + const { stepPath, sourcePath } = resolveStepSourceStatus(relativeFileRef2, { resolvedRoot, catalog: currentCatalog }); + if (sourcePath && fs7.existsSync(sourcePath) && fs7.statSync(sourcePath).isFile()) { + ensurePathInsideRoot(sourcePath, resolvedRoot); + return sourcePath; + } + ensurePathInsideRoot(stepPath, resolvedRoot); + } + throw new Error(`Source code is not available for ${relativeFileRef2}`); + } + function resolveFileAssetAccess({ + fileRef, + asset = "output", + resolvedRoot = resolveRequestRoot({ fileRef }), + rootDir: nextRootDir = defaultRootDir, + catalog = null + } = {}) { + const assetKind = normalizedFileAssetKind(asset); + const filePath = assetKind === "source" ? resolveSourceCodeFilePath(fileRef, { resolvedRoot, rootDir: nextRootDir, catalog }) : assetKind === "artifact" ? resolveArtifactFilePath(fileRef, { resolvedRoot, rootDir: nextRootDir, catalog }) : resolveOutputFilePath(fileRef, { resolvedRoot, rootDir: nextRootDir, catalog }); + return { + asset: assetKind, + file: absoluteFileRef(filePath), + rootRelativeFile: relativeFileRef(resolvedRoot.rootPath, filePath), + path: filePath, + filename: path8.basename(filePath), + contentType: contentTypeForPath(filePath) + }; + } + async function openFileAsset(request = {}) { + const access = resolveFileAssetAccess(request); + await sourceFileOpener(access.path); + return { + asset: access.asset, + file: access.file, + filename: access.filename, + opened: true + }; + } + function resolveSourceFileAccess(request = {}) { + return resolveFileAssetAccess({ ...request, asset: "source" }); + } + async function openSourceFile(request = {}) { + return openFileAsset({ ...request, asset: "source" }); + } + async function generateStepArtifact({ fileRef, force = false, resolvedRoot = resolveRequestRoot({ fileRef }), catalog = null } = {}) { + const { stepPath } = resolveStepSource(fileRef, { resolvedRoot, catalog }); + const extension = path8.extname(stepPath).toLowerCase(); + let hasStepFile = false; + try { + hasStepFile = (extension === ".step" || extension === ".stp") && fs7.statSync(stepPath).isFile(); + } catch { + hasStepFile = false; + } + if (!hasStepFile) { + throw new Error("CAD Viewer only regenerates GLB artifacts for existing STEP/STP files."); + } + const context = scanContextForRoot(resolvedRoot); + const result = await stepArtifactGenerator({ + repoRoot: context.scanRepoRoot, + stepPath, + sourcePath: "", + force, + skipStepWrite: false, + writeStepAfterArtifact: false + }); + return { + ok: Boolean(result?.ok), + error: result?.ok ? "" : stepArtifactGenerationError(result), + result, + stepPath + }; + } + async function generateImplicitExport({ + fileRef, + format = "glb", + parameterValues = null, + animationState = null, + resolution = 96, + maxCells = void 0, + resolvedRoot = resolveRoot(), + rootDir: nextRootDir = defaultRootDir, + catalog = null + } = {}) { + const exportFormat = normalizedImplicitExportFormat(format); + const inputPath = resolveImplicitCadFilePath(fileRef, { + resolvedRoot, + rootDir: nextRootDir, + catalog + }); + const inputFilename = path8.basename(inputPath); + const outputFilename = inputFilename.replace(/\.implicit\.(?:mjs|js)$/i, `.${exportFormat}`).replace(/\.(?:mjs|js)$/i, `.${exportFormat}`); + const outputPath = path8.join(path8.dirname(inputPath), outputFilename); + ensurePathInsideRoot(outputPath, resolvedRoot); + const result = await exportImplicitCadFile({ + input: inputPath, + output: outputPath, + format: exportFormat, + params: parameterValues, + animationState, + resolution, + maxCells + }); + const nextCatalog = refreshCatalogForPath({ rootDir: nextRootDir, filePath: outputPath }); + const outputFileRef = path8.relative(resolvedRoot.rootPath, outputPath).split(path8.sep).join("/"); + return { + ...result, + outputFileRef, + filename: path8.basename(outputPath), + catalog: nextCatalog, + entry: catalogEntryForFileRef(nextCatalog, outputFileRef) + }; + } + function readStepSourceStatusForFile({ fileRef, resolvedRoot = resolveRequestRoot({ fileRef }), catalog = null } = {}) { + const { stepPath, sourcePath } = resolveStepSourceStatus(fileRef, { resolvedRoot, catalog }); + const context = scanContextForRoot(resolvedRoot); + const status = readStepSourceStatus({ + repoRoot: context.scanRepoRoot, + stepPath, + pythonSourcePath: sourcePath + }); + return absolutizeSourceStatus({ + ...status, + ...status?.artifact ? { artifact: absolutizeArtifact(status.artifact, context.scanRepoRoot) } : {} + }, context.scanRepoRoot); + } + function readGeneratorStatus({ rootDir: nextRootDir = defaultRootDir } = {}) { + const resolvedRoot = resolveRoot(effectiveRootDirForRequest(nextRootDir)); + const context = scanContextForRoot(resolvedRoot); + return absolutizeGenerationStatus(readGenerationStatus({ + repoRoot: context.scanRepoRoot, + rootDir: context.scanRootDir + }), resolvedRoot.rootPath); + } + function generationStatusDir2(rootDir2 = defaultRootDir) { + const resolvedRoot = resolveRoot(effectiveRootDirForRequest(rootDir2)); + const context = scanContextForRoot(resolvedRoot); + return generationStatusDir(context.scanRepoRoot, context.scanRootDir); + } + function isGenerationStatusPath(filePath, rootDir2 = defaultRootDir) { + const resolvedRoot = resolveRoot(effectiveRootDirForRequest(rootDir2)); + const resolvedPath = path8.resolve(filePath); + const name = path8.basename(resolvedPath); + return (resolvedPath === resolvedRoot.rootPath || pathIsInside(resolvedPath, resolvedRoot.rootPath)) && name.startsWith(".") && name.endsWith(".generation.lock.json"); + } + function entryForSourcePath(catalog, resolvedRoot, sourcePath) { + const fileRef = absoluteFileRef(sourcePath); + return Array.isArray(catalog?.entries) ? catalog.entries.find((entry) => normalizedFileRef(entry?.file) === fileRef) || null : null; + } + function assetPathForFileRef(fileRef, { resolvedRoot = null, rootDir: rootDir2 = "" } = {}) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef || !path8.isAbsolute(normalizedRef)) { + return null; + } + const candidatePath = path8.resolve(normalizedRef); + if (!isServedCadAsset(candidatePath)) { + return null; + } + const activeRoot = resolvedRoot || (rootDir2 ? resolveRoot(rootDir2) : null); + if (activeRoot && !(candidatePath === activeRoot.rootPath || pathIsInside(candidatePath, activeRoot.rootPath))) { + const error = new Error("Forbidden"); + error.statusCode = 403; + throw error; + } + return candidatePath; + } + async function writeAsset({ fileRef, body, resolvedRoot = resolveRequestRoot({ fileRef }) } = {}) { + const normalizedRef = normalizedFileRef(fileRef); + if (!normalizedRef) { + throw new Error("Missing asset path"); + } + const filePath = filePathFromRef(normalizedRef, resolvedRoot); + if (!(filePath === resolvedRoot.rootPath || pathIsInside(filePath, resolvedRoot.rootPath))) { + throw new Error("Asset writes must stay inside the active CAD Viewer root"); + } + if (!isServedCadAsset(filePath)) { + throw new Error(`Unsupported CAD Viewer asset write: ${normalizedRef}`); + } + const bytes2 = Buffer.isBuffer(body) ? body : Buffer.from(body || ""); + fs7.mkdirSync(path8.dirname(filePath), { recursive: true }); + fs7.writeFileSync(filePath, bytes2); + return { + path: filePath, + bytes: bytes2.length, + contentType: contentTypeForPath(filePath) + }; + } + return { + kind: "local-fs", + canGenerateStepArtifacts: true, + repoRoot: baseDirectoryRoot, + rootDir: "", + defaultFile, + githubUrl, + resolveRoot, + resolveRequestRoot, + readCatalog, + readCatalogSafe, + refreshCatalog, + refreshCatalogForPath, + resolveStepSource, + readStepSourceStatus: readStepSourceStatusForFile, + resolveFileAssetAccess, + openFileAsset, + resolveSourceFileAccess, + openSourceFile, + readGenerationStatus: readGeneratorStatus, + generationStatusDir: generationStatusDir2, + isGenerationStatusPath, + generateStepArtifact, + generateImplicitExport, + entryForSourcePath, + assetPathForFileRef, + writeAsset, + contentTypeForPath + }; +} + +// viewer/src/server/httpHandlers.mjs +import fs8 from "node:fs"; +import path9 from "node:path"; +var STATIC_CONTENT_TYPES = /* @__PURE__ */ new Map([ + [".css", "text/css; charset=utf-8"], + [".html", "text/html; charset=utf-8"], + [".ico", "image/x-icon"], + [".js", "text/javascript; charset=utf-8"], + [".json", "application/json; charset=utf-8"], + [".map", "application/json; charset=utf-8"], + [".svg", "image/svg+xml"], + [".wasm", "application/wasm"] +]); +function contentTypeForStaticAsset(filePath) { + return STATIC_CONTENT_TYPES.get(path9.extname(String(filePath || "")).toLowerCase()) || ""; +} +function sendJson(res, statusCode, payload, { cacheControl = "no-store" } = {}) { + res.statusCode = statusCode; + res.setHeader("content-type", "application/json; charset=utf-8"); + res.setHeader("cache-control", cacheControl || "no-store"); + res.end(JSON.stringify(payload)); +} +function downloadFilename(value) { + const rawFilename = path9.basename(String(value || "").replace(/\\/g, "/")) || "download"; + return rawFilename.replace(/[\x00-\x1f"\\]/g, "_"); +} +function encodeContentDispositionFilename(value) { + return encodeURIComponent(value).replace(/['()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`); +} +function attachmentContentDisposition(filename) { + const safeFilename = downloadFilename(filename); + const quotedFilename = safeFilename.replace(/[^\x20-\x7e]/g, "_"); + return `attachment; filename="${quotedFilename}"; filename*=UTF-8''${encodeContentDispositionFilename(safeFilename)}`; +} +function errorMessage(error) { + return error instanceof Error ? error.message : String(error); +} +function requestRootDir(requestUrl) { + return String(requestUrl?.searchParams?.get("dir") || "").trim(); +} +function requestFileRef(requestUrl) { + return String(requestUrl?.searchParams?.get("file") || "").trim(); +} +function requestHeader(req, name) { + const headers = req?.headers || {}; + const value = headers[String(name || "").toLowerCase()]; + return Array.isArray(value) ? value[0] : String(value || ""); +} +function requestRefererUrl(req) { + const value = requestHeader(req, "referer") || requestHeader(req, "referrer"); + if (!value) { + return null; + } + try { + return new URL(value, "http://localhost"); + } catch { + return null; + } +} +function siblingFileRef(sourceFileRef, relativeFileRef2) { + const source = String(sourceFileRef || "").replace(/\\/g, "/"); + const relative = String(relativeFileRef2 || "").replace(/\\/g, "/").replace(/^\/+/g, ""); + if (!source || !relative) { + return ""; + } + if (path9.isAbsolute(source)) { + return path9.resolve(path9.dirname(source), relative); + } + const sourceDir = path9.posix.dirname(source); + return path9.posix.normalize(path9.posix.join(sourceDir === "." ? "" : sourceDir, relative)); +} +function legacyCadAssetFileRef(requestUrl, req) { + if (!requestUrl.pathname.startsWith("/__cad/") || requestUrl.pathname === "/__cad/asset") { + return ""; + } + const relativePath = decodeURIComponent(requestUrl.pathname.slice("/__cad/".length)); + if (!relativePath || !path9.extname(relativePath)) { + return ""; + } + const refererUrl = requestRefererUrl(req); + return siblingFileRef(requestFileRef(refererUrl), relativePath); +} +function readJsonBody(req, { limitBytes = 256 * 1024 } = {}) { + return new Promise((resolve, reject) => { + let body = ""; + req.setEncoding?.("utf8"); + req.on("data", (chunk) => { + body += chunk; + if (Buffer.byteLength(body) > limitBytes) { + reject(new Error("Request body is too large")); + req.destroy?.(); + } + }); + req.on("error", reject); + req.on("end", () => { + const text = body.trim(); + if (!text) { + resolve({}); + return; + } + try { + resolve(JSON.parse(text)); + } catch { + reject(new Error("Request body must be valid JSON")); + } + }); + }); +} +function fileAssetRequest(backend2, requestUrl, { + rootDir, + catalog +} = {}) { + const fileRef = requestFileRef(requestUrl); + const request = { + fileRef, + asset: requestUrl.searchParams.get("asset") || "output", + rootDir, + catalog + }; + if (typeof backend2.resolveRequestRoot === "function") { + request.resolvedRoot = backend2.resolveRequestRoot({ rootDir, fileRef }); + } else if (typeof backend2.resolveRoot === "function" && rootDir) { + request.resolvedRoot = backend2.resolveRoot(rootDir); + } + return request; +} +function sendBufferDownload(res, { + body, + filename, + contentType +} = {}) { + const bytes2 = Buffer.isBuffer(body) ? body : Buffer.from(body || ""); + res.statusCode = 200; + res.setHeader("content-type", contentType || "application/octet-stream"); + res.setHeader("cache-control", "no-store"); + res.setHeader("content-disposition", attachmentContentDisposition(filename)); + res.setHeader("content-length", String(bytes2.length)); + res.end(bytes2); +} +function serveStaticFile(filePath, req, res, next, { contentType, headers = {} } = {}) { + fs8.stat(filePath, (error, stats) => { + if (res.destroyed) { + return; + } + if (error || !stats.isFile()) { + next(); + return; + } + if (contentType) { + res.setHeader("content-type", contentType); + } + for (const [name, value] of Object.entries(headers)) { + if (value !== void 0 && value !== null && value !== "") { + res.setHeader(name, value); + } + } + res.setHeader("cache-control", "no-store"); + res.setHeader("content-length", String(stats.size)); + const stream = fs8.createReadStream(filePath); + res.on("close", () => { + if (!res.writableEnded) { + stream.destroy(); + } + }); + stream.on("error", () => { + if (!res.headersSent) { + next(); + } else { + res.destroy(); + } + }); + stream.pipe(res); + }); +} +function createCadViewerApiMiddleware({ + backend: backend2, + serverInfo = () => ({}), + enableStepArtifactBackend = false, + claimDisabledStepArtifactRoute = false, + preferFileDownloadRedirects = false, + onCatalogChanged = () => { + }, + onCatalogActivated = () => { + }, + onDirectoryActivated = () => { + }, + rootDir, + catalogCacheControl = "" +} = {}) { + if (!backend2) { + throw new Error("createCadViewerApiMiddleware requires backend"); + } + return async function cadViewerApiMiddleware(req, res, next) { + const requestUrl = new URL(req.url || "/", "http://localhost"); + const activeRootDir = requestRootDir(requestUrl) || rootDir || ""; + const activeFileRef = requestFileRef(requestUrl); + if (requestUrl.pathname === "/__cad/server") { + sendJson(res, 200, serverInfo({ rootDir: activeRootDir, fileRef: activeFileRef })); + return; + } + if (requestUrl.pathname === "/__cad/directory/activate") { + const method = String(req.method || "GET").toUpperCase(); + if (method !== "POST") { + res.setHeader("allow", "POST"); + sendJson(res, 405, { + error: "Use POST to activate a CAD Viewer directory" + }); + return; + } + if (typeof backend2.resolveRequestRoot !== "function" && typeof backend2.resolveRoot !== "function") { + sendJson(res, 501, { + error: "Directory activation requires a local filesystem CAD Viewer backend" + }); + return; + } + try { + const resolvedRoot = typeof backend2.resolveRequestRoot === "function" ? backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }) : backend2.resolveRoot(activeRootDir); + onDirectoryActivated(resolvedRoot, { rootDir: activeRootDir, fileRef: activeFileRef }); + sendJson(res, 200, { + ok: true, + directory: { + dir: String(resolvedRoot?.dir || activeRootDir || ""), + rootPath: String(resolvedRoot?.rootPath || ""), + rootName: String(resolvedRoot?.rootName || "") + }, + server: serverInfo({ rootDir: String(resolvedRoot?.dir || activeRootDir || ""), fileRef: activeFileRef }) + }); + } catch (error) { + sendJson(res, 400, { + ok: false, + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/catalog") { + try { + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + if (typeof backend2.resolveRequestRoot === "function" && (activeRootDir || activeFileRef)) { + onCatalogActivated( + backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }), + { rootDir: activeRootDir, fileRef: activeFileRef } + ); + } else if (activeRootDir && typeof backend2.resolveRoot === "function") { + onCatalogActivated(backend2.resolveRoot(activeRootDir), { rootDir: activeRootDir, fileRef: activeFileRef }); + } + sendJson(res, 200, catalog, { cacheControl: catalogCacheControl }); + } catch (error) { + sendJson(res, 400, { + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/generation-status") { + if (typeof backend2.readGenerationStatus !== "function") { + sendJson(res, 501, { + error: "Generation status is not available for this CAD Viewer backend" + }); + return; + } + try { + sendJson(res, 200, await backend2.readGenerationStatus({ rootDir: activeRootDir })); + } catch (error) { + sendJson(res, 400, { + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/download") { + const method = String(req.method || "GET").toUpperCase(); + if (method !== "GET") { + res.setHeader("allow", "GET"); + sendJson(res, 405, { + error: "Use GET to download a file asset" + }); + return; + } + try { + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + const request = fileAssetRequest(backend2, requestUrl, { rootDir: activeRootDir, catalog }); + if (preferFileDownloadRedirects && typeof backend2.resolveFileAssetAccess === "function") { + const access2 = await backend2.resolveFileAssetAccess(request); + if (access2?.url) { + res.statusCode = 302; + res.setHeader("location", access2.url); + res.setHeader("cache-control", "no-store"); + res.end(""); + return; + } + } + if (typeof backend2.readFileAsset === "function") { + const result = await backend2.readFileAsset(request); + sendBufferDownload(res, result); + return; + } + if (typeof backend2.resolveFileAssetAccess !== "function") { + sendJson(res, 501, { + error: "File downloads are not available for this CAD Viewer backend" + }); + return; + } + const access = await backend2.resolveFileAssetAccess(request); + if (access?.path) { + serveStaticFile(access.path, req, res, () => { + sendJson(res, 404, { + error: "File asset not found" + }); + }, { + contentType: access.contentType || backend2.contentTypeForPath?.(access.path) || "application/octet-stream", + headers: { + "content-disposition": attachmentContentDisposition(access.filename || access.file || access.path) + } + }); + return; + } + if (access?.url) { + res.statusCode = 302; + res.setHeader("location", access.url); + res.setHeader("cache-control", "no-store"); + res.end(""); + return; + } + sendJson(res, 404, { + error: "File asset not found" + }); + } catch (error) { + sendJson(res, 400, { + ok: false, + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/asset") { + const method = String(req.method || "GET").toUpperCase(); + if (method !== "GET") { + res.setHeader("allow", "GET"); + sendJson(res, 405, { + error: "Use GET to read a CAD Viewer asset" + }); + return; + } + try { + if (typeof backend2.assetPathForFileRef !== "function") { + sendJson(res, 501, { + error: "Direct CAD Viewer assets are not available for this backend" + }); + return; + } + const assetPath = backend2.assetPathForFileRef(activeFileRef, { + rootDir: activeRootDir, + ...typeof backend2.resolveRequestRoot === "function" && (activeRootDir || activeFileRef) ? { resolvedRoot: backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }) } : {} + }); + if (!assetPath) { + sendJson(res, 404, { + error: "CAD Viewer asset not found" + }); + return; + } + serveStaticFile(assetPath, req, res, () => { + sendJson(res, 404, { + error: "CAD Viewer asset not found" + }); + }, { + contentType: backend2.contentTypeForPath?.(assetPath) || "application/octet-stream" + }); + } catch (error) { + if (Number(error?.statusCode) === 403) { + sendJson(res, 403, { + error: "Forbidden" + }); + return; + } + sendJson(res, 400, { + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/reveal") { + const method = String(req.method || "GET").toUpperCase(); + if (method !== "POST") { + res.setHeader("allow", "POST"); + sendJson(res, 405, { + error: "Use POST to reveal a file asset" + }); + return; + } + try { + if (typeof backend2.openFileAsset !== "function") { + sendJson(res, 405, { + error: "Revealing files is only available for the local filesystem backend" + }); + return; + } + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + const request = fileAssetRequest(backend2, requestUrl, { rootDir: activeRootDir, catalog }); + const result = await backend2.openFileAsset(request); + sendJson(res, 200, { + ok: true, + ...result + }); + } catch (error) { + sendJson(res, 400, { + ok: false, + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/implicit-export") { + const method = String(req.method || "GET").toUpperCase(); + if (method !== "POST") { + res.setHeader("allow", "POST"); + sendJson(res, 405, { + error: "Use POST to export implicit CAD files" + }); + return; + } + if (backend2.kind !== "local-fs" || typeof backend2.generateImplicitExport !== "function" || typeof backend2.resolveRoot !== "function") { + sendJson(res, 405, { + error: "Implicit CAD export is only available for the local filesystem backend" + }); + return; + } + try { + const body = await readJsonBody(req); + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + const resolvedRoot = typeof backend2.resolveRequestRoot === "function" ? backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }) : backend2.resolveRoot(activeRootDir); + const format = requestUrl.searchParams.get("format") || body.format || "glb"; + const result = await backend2.generateImplicitExport({ + fileRef: activeFileRef || body.file, + format, + parameterValues: body.parameterValues || body.params || null, + animationState: body.animationState || body.implicitAnimationState || null, + resolution: body.resolution, + maxCells: body.maxCells, + resolvedRoot, + rootDir: activeRootDir, + catalog + }); + onCatalogChanged(resolvedRoot); + sendJson(res, 200, { + ok: true, + result, + entry: result.entry || null, + catalog: result.catalog || (typeof backend2.refreshCatalog === "function" ? await backend2.refreshCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }) : await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef })), + downloadUrl: `/__cad/download?dir=${encodeURIComponent(activeRootDir)}&file=${encodeURIComponent(result.outputFileRef)}&asset=output`, + filename: result.filename + }); + } catch (error) { + sendJson(res, 400, { + ok: false, + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/step-source-status") { + if (typeof backend2.readStepSourceStatus !== "function") { + sendJson(res, 501, { + error: "STEP source status is not available for this CAD Viewer backend" + }); + return; + } + try { + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + const request = { + fileRef: activeFileRef, + rootDir: activeRootDir, + catalog + }; + if (typeof backend2.resolveRequestRoot === "function") { + request.resolvedRoot = backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }); + } else if (typeof backend2.resolveRoot === "function" && activeRootDir) { + request.resolvedRoot = backend2.resolveRoot(activeRootDir); + } + sendJson(res, 200, await backend2.readStepSourceStatus(request)); + } catch (error) { + sendJson(res, 400, { + error: errorMessage(error) + }); + } + return; + } + if (requestUrl.pathname === "/__cad/step-artifact") { + if (!enableStepArtifactBackend) { + if (claimDisabledStepArtifactRoute) { + sendJson(res, 501, { + error: "STEP artifact generation is not enabled for this CAD Viewer backend" + }); + return; + } + next(); + return; + } + if (req.method !== "POST") { + sendJson(res, 405, { + error: "Use POST to generate a STEP artifact" + }); + return; + } + if (typeof backend2.resolveRoot !== "function") { + sendJson(res, 501, { + error: "STEP artifact generation requires a local filesystem CAD Viewer backend" + }); + return; + } + try { + const catalog = await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + const resolvedRoot = typeof backend2.resolveRequestRoot === "function" ? backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }) : backend2.resolveRoot(activeRootDir); + const result = await backend2.generateStepArtifact({ + fileRef: activeFileRef, + force: requestUrl.searchParams.get("force") === "1", + resolvedRoot, + catalog + }); + const nextCatalog = typeof backend2.refreshCatalog === "function" ? await backend2.refreshCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }) : await backend2.readCatalog({ rootDir: activeRootDir, fileRef: activeFileRef }); + onCatalogChanged(resolvedRoot); + sendJson(res, result.ok ? 200 : 500, { + ok: result.ok, + error: result.error, + result: result.result, + entry: backend2.entryForSourcePath(nextCatalog, resolvedRoot, result.stepPath), + catalog: nextCatalog + }); + } catch (error) { + sendJson(res, 400, { + error: errorMessage(error) + }); + } + return; + } + next(); + }; +} +function createLocalAssetMiddleware({ backend: backend2, rootDir } = {}) { + if (!backend2) { + throw new Error("createLocalAssetMiddleware requires backend"); + } + return function localAssetMiddleware(req, res, next) { + const requestUrl = new URL(req.url || "/", "http://localhost"); + const fallbackFileRef = legacyCadAssetFileRef(requestUrl, req); + if (requestUrl.pathname !== "/__cad/asset" && !fallbackFileRef || typeof backend2.assetPathForFileRef !== "function") { + next(); + return; + } + let assetPath = null; + try { + const refererUrl = requestRefererUrl(req); + const activeRootDir = requestRootDir(requestUrl) || requestRootDir(refererUrl) || rootDir || ""; + const activeFileRef = requestFileRef(requestUrl) || fallbackFileRef; + assetPath = backend2.assetPathForFileRef(activeFileRef, { + rootDir: activeRootDir, + ...typeof backend2.resolveRequestRoot === "function" && (activeRootDir || activeFileRef) ? { resolvedRoot: backend2.resolveRequestRoot({ rootDir: activeRootDir, fileRef: activeFileRef }) } : {} + }); + } catch (error) { + if (Number(error?.statusCode) === 403) { + res.statusCode = 403; + res.end("Forbidden"); + return; + } + next(); + return; + } + if (!assetPath) { + next(); + return; + } + serveStaticFile(assetPath, req, res, next, { + contentType: backend2.contentTypeForPath?.(assetPath) || void 0 + }); + }; +} +function serveDistAsset({ distRoot: distRoot2, indexHtmlPath = path9.join(distRoot2, "index.html") } = {}) { + return function distAssetMiddleware(req, res, next) { + const requestUrl = new URL(req.url || "/", "http://localhost"); + const requestPath = requestUrl.pathname === "/" ? "/index.html" : requestUrl.pathname; + let filePath = ""; + try { + filePath = path9.resolve(distRoot2, decodeURIComponent(requestPath).replace(/^\/+/, "")); + } catch { + res.statusCode = 400; + res.end("Bad request"); + return; + } + if (!(filePath === distRoot2 || filePath.startsWith(`${distRoot2}${path9.sep}`))) { + res.statusCode = 403; + res.end("Forbidden"); + return; + } + const fileExists = fs8.existsSync(filePath); + const isStaticAssetRequest = requestPath.startsWith("/assets/") || path9.extname(requestPath); + if (!fileExists && isStaticAssetRequest) { + res.statusCode = 404; + res.setHeader("content-type", "text/plain; charset=utf-8"); + res.setHeader("cache-control", "no-store"); + res.end("Not found"); + return; + } + const fallbackPath = fileExists ? filePath : indexHtmlPath; + serveStaticFile(fallbackPath, req, res, next, { + contentType: contentTypeForStaticAsset(fallbackPath) || void 0 + }); + }; +} + +// viewer/src/server/vercelBlobAssetBackend.mjs +import path10 from "node:path"; +var CATALOG_BLOB_CACHE_CONTROL_MAX_AGE_SECONDS = 60; +function normalizePrefix(value) { + const rawValue = String(value || "").trim(); + if (!rawValue) { + return ""; + } + try { + return new URL(rawValue).pathname.replace(/^\/+|\/+$/g, ""); + } catch { + return rawValue.replace(/^\/+|\/+$/g, ""); + } +} +function joinBlobPath(prefix, fileRef) { + const normalizedRef = normalizeFileRef(fileRef); + return [normalizePrefix(prefix), normalizedRef].filter(Boolean).join("/"); +} +function publicBlobUrlForRef(prefix, fileRef) { + const rawPrefix = String(prefix || "").trim(); + if (!rawPrefix) { + return ""; + } + try { + const url = new URL(rawPrefix); + const prefixPath = url.pathname.replace(/^\/+|\/+$/g, ""); + const normalizedRef = normalizeFileRef(fileRef); + if (!normalizedRef) { + return ""; + } + url.pathname = `/${[prefixPath, normalizedRef].filter(Boolean).join("/")}`; + url.search = ""; + url.hash = ""; + return url.toString(); + } catch { + return ""; + } +} +function cacheBypassedCatalogUrl(value) { + try { + const url = new URL(String(value || "")); + url.searchParams.set("_tcad_catalog", "1"); + return url.toString(); + } catch { + return value; + } +} +function normalizeFileRef(value) { + const normalized = path10.posix.normalize(String(value || "").trim().replace(/\\/g, "/").replace(/^\/+/, "")); + return normalized && normalized !== "." && !normalized.startsWith("../") ? normalized : ""; +} +function catalogEntryForFileRef2(catalog, fileRef) { + const normalized = normalizeFileRef(fileRef); + if (!normalized || !Array.isArray(catalog?.entries)) { + return null; + } + return catalog.entries.find((entry) => normalizeFileRef(entry?.file) === normalized) || null; +} +function normalizeString2(value) { + return String(value || "").trim(); +} +function sourceKindIsPython(value) { + return normalizeString2(value).toLowerCase() === "python"; +} +function artifactErrorCode(artifact) { + const rawError = artifact?.error; + if (rawError && typeof rawError === "object" && !Array.isArray(rawError)) { + return normalizeString2(rawError.code); + } + return normalizeString2(rawError || artifact?.code); +} +function shouldSuppressHostedPythonArtifactWarning(entry) { + const artifact = entry?.artifact; + if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) { + return false; + } + if (artifactErrorCode(artifact) !== "missing_source_path") { + return false; + } + return sourceKindIsPython(entry?.sourceKind) || sourceKindIsPython(entry?.stepSourceKind) || sourceKindIsPython(artifact?.sourceKind); +} +function normalizeVercelBlobCatalogEntry(entry) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) { + return entry; + } + if (!shouldSuppressHostedPythonArtifactWarning(entry)) { + return entry; + } + const { artifact, ...nextEntry } = entry; + return nextEntry; +} +function normalizeVercelBlobCatalog(catalog) { + if (!catalog || typeof catalog !== "object" || Array.isArray(catalog) || !Array.isArray(catalog.entries)) { + return catalog; + } + let changed = false; + const entries = catalog.entries.map((entry) => { + const nextEntry = normalizeVercelBlobCatalogEntry(entry); + if (nextEntry !== entry) { + changed = true; + } + return nextEntry; + }); + return changed ? { ...catalog, entries } : catalog; +} +function sourceUrlFromEntry(entry) { + return normalizeString2(entry?.sourceUrl || entry?.source?.url); +} +function stepUrlFromEntry(entry) { + const explicitStepUrl = normalizeString2(entry?.stepUrl || entry?.step?.url); + if (explicitStepUrl) { + return explicitStepUrl; + } + const sourceKind = String(entry?.sourceKind || entry?.stepSourceKind || "").trim().toLowerCase(); + return sourceKind === "python" ? "" : sourceUrlFromEntry(entry); +} +function outputUrlFromEntry(entry, fileRef) { + const extension = path10.posix.extname(normalizeFileRef(fileRef)).toLowerCase(); + if (extension === ".step" || extension === ".stp") { + return stepUrlFromEntry(entry); + } + return normalizeString2(entry?.outputUrl || entry?.output?.url || entry?.url); +} +function sourceFileRefFromEntry(entry) { + return normalizeFileRef(entry?.sourceFile || entry?.source?.file || entry?.source?.path) || normalizeFileRef(filenameFromUrl(sourceUrlFromEntry(entry))); +} +function stepFileRefFromEntry(entry, fallback = "") { + return normalizeFileRef(entry?.stepFile || entry?.step?.file || entry?.step?.path) || normalizeFileRef(fallback); +} +function filenameFromUrl(url) { + try { + return path10.posix.basename(new URL(url).pathname); + } catch { + return ""; + } +} +function artifactFileRefFromEntry(entry) { + return normalizeFileRef(entry?.assetFile || entry?.asset?.file || entry?.artifactFile || entry?.artifact?.file) || normalizeFileRef(filenameFromUrl(entry?.url)); +} +function normalizedFileAssetKind2(value) { + const asset = String(value || "output").trim().toLowerCase(); + if (asset === "asset") { + return "artifact"; + } + if (asset === "output" || asset === "source" || asset === "artifact") { + return asset; + } + throw new Error(`Unsupported file asset: ${asset || "(missing)"}`); +} +function contentTypeForFileRef(fileRef, fallback = "") { + const extension = path10.posix.extname(normalizeFileRef(fileRef)).toLowerCase(); + if (fallback) { + return fallback; + } + if (extension === ".glb") { + return "model/gltf-binary"; + } + if (extension === ".stl") { + return "model/stl"; + } + if (extension === ".3mf") { + return "model/3mf"; + } + if (extension === ".step" || extension === ".stp") { + return "application/step"; + } + if (extension === ".dxf") { + return "application/dxf"; + } + if (extension === ".gcode") { + return "text/plain; charset=utf-8"; + } + if (extension === ".js" || extension === ".mjs") { + return "text/javascript; charset=utf-8"; + } + if (extension === ".json") { + return "application/json; charset=utf-8"; + } + if (extension === ".urdf" || extension === ".srdf" || extension === ".sdf") { + return "application/xml; charset=utf-8"; + } + return "application/octet-stream"; +} +async function loadBlobClient(client) { + if (client) { + return client; + } + try { + return await Promise.resolve().then(() => (init_dist(), dist_exports)); + } catch (error) { + throw new Error( + `Vercel Blob backend requires @vercel/blob to be installed: ${error instanceof Error ? error.message : String(error)}` + ); + } +} +async function blobErrorDetail(response) { + const requestId = normalizeString2(response.headers?.get?.("x-vercel-id")); + let body = ""; + try { + body = normalizeString2(await response.text()).slice(0, 200); + } catch { + body = ""; + } + return [ + requestId ? `request ${requestId}` : "", + body + ].filter(Boolean).join(": "); +} +async function readJsonFromUrl(url, { fetchImpl = globalThis.fetch } = {}) { + if (!fetchImpl) { + throw new Error("Vercel Blob backend requires fetch to read catalog URLs"); + } + const response = await fetchImpl(cacheBypassedCatalogUrl(url)); + if (!response.ok) { + const detail = await blobErrorDetail(response); + throw new Error( + `Failed to read Vercel Blob catalog: ${response.status} ${response.statusText}${detail ? ` (${detail})` : ""}` + ); + } + return response.json(); +} +async function readJsonFromBlobGetResult(result, pathname) { + if (!result) { + throw new Error(`Vercel Blob catalog not found: ${pathname}`); + } + if (!result.stream) { + throw new Error(`Vercel Blob catalog response had no body: ${pathname}`); + } + return new Response(result.stream).json(); +} +function hasBlobSdkReadCredentials(token) { + return Boolean( + normalizeString2(token) || normalizeString2(process.env.BLOB_READ_WRITE_TOKEN) || normalizeString2(process.env.VERCEL_OIDC_TOKEN) && normalizeString2(process.env.BLOB_STORE_ID) + ); +} +function createVercelBlobAssetBackend({ + prefix = "", + catalogPath = "catalog.json", + catalogUrl = "", + client = null, + fetchImpl = globalThis.fetch, + token = process.env.VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN || process.env.BLOB_READ_WRITE_TOKEN, + readOnly = false, + catalogCacheTtlMs = 0 +} = {}) { + const normalizedPrefix = normalizePrefix(prefix); + const normalizedCatalogPath = joinBlobPath(normalizedPrefix, catalogPath || "catalog.json"); + const resolvedCatalogUrl = catalogUrl || publicBlobUrlForRef(prefix, catalogPath || "catalog.json"); + let cachedCatalog = null; + let cachedCatalogAt = 0; + let catalogFetchInFlight = null; + async function blobClient() { + return loadBlobClient(client); + } + async function fetchCatalog() { + if (resolvedCatalogUrl) { + return normalizeVercelBlobCatalog(await readJsonFromUrl(resolvedCatalogUrl, { fetchImpl })); + } + if (hasBlobSdkReadCredentials(token)) { + const blob2 = await blobClient(); + if (typeof blob2.get === "function") { + const getOptions = { access: "public" }; + if (token) { + getOptions.token = token; + } + return normalizeVercelBlobCatalog(await readJsonFromBlobGetResult( + await blob2.get(normalizedCatalogPath, getOptions), + normalizedCatalogPath + )); + } + } + const blob = await blobClient(); + const listing = await blob.list({ prefix: normalizedCatalogPath, token }); + const catalogBlob = Array.isArray(listing?.blobs) ? listing.blobs.find((entry) => entry.pathname === normalizedCatalogPath) || listing.blobs[0] : null; + if (!catalogBlob?.url) { + throw new Error(`Vercel Blob catalog not found: ${normalizedCatalogPath}`); + } + return normalizeVercelBlobCatalog(await readJsonFromUrl(catalogBlob.url, { fetchImpl })); + } + async function fetchCatalogCached({ force = false } = {}) { + if (!(catalogCacheTtlMs > 0)) { + return fetchCatalog(); + } + if (!force && cachedCatalog && Date.now() - cachedCatalogAt < catalogCacheTtlMs) { + return cachedCatalog; + } + if (!catalogFetchInFlight) { + catalogFetchInFlight = fetchCatalog().finally(() => { + catalogFetchInFlight = null; + }); + } + try { + const catalog = await catalogFetchInFlight; + cachedCatalog = catalog; + cachedCatalogAt = Date.now(); + return catalog; + } catch (error) { + if (cachedCatalog) { + console.warn( + `Serving cached Vercel Blob catalog after read failure: ${error instanceof Error ? error.message : String(error)}` + ); + return cachedCatalog; + } + throw error; + } + } + async function readCatalog() { + return fetchCatalogCached(); + } + async function writeAsset({ + fileRef, + body, + contentType = "application/octet-stream", + cacheControlMaxAge + } = {}) { + const pathname = joinBlobPath(normalizedPrefix, fileRef); + if (!pathname) { + throw new Error("Missing Vercel Blob asset path"); + } + const blob = await blobClient(); + return blob.put(pathname, body, { + access: "public", + addRandomSuffix: false, + allowOverwrite: true, + contentType, + ...cacheControlMaxAge !== void 0 ? { cacheControlMaxAge } : {}, + token + }); + } + async function writeCatalog(catalog) { + const normalizedCatalog = normalizeVercelBlobCatalog(catalog); + return writeAsset({ + fileRef: catalogPath || "catalog.json", + body: JSON.stringify({ + schemaVersion: 4, + entries: Array.isArray(normalizedCatalog?.entries) ? normalizedCatalog.entries : [] + }, null, 2), + contentType: "application/json; charset=utf-8", + cacheControlMaxAge: CATALOG_BLOB_CACHE_CONTROL_MAX_AGE_SECONDS + }); + } + async function refreshCatalog() { + return fetchCatalogCached({ force: true }); + } + async function urlForBlobRef(fileRef) { + const pathname = joinBlobPath(normalizedPrefix, fileRef); + if (!pathname) { + return ""; + } + const blob = await blobClient(); + const listing = await blob.list({ prefix: pathname, token }); + const match = Array.isArray(listing?.blobs) ? listing.blobs.find((entry) => entry.pathname === pathname) : null; + return match?.url || ""; + } + async function resolveFileAssetAccess({ fileRef, asset = "output", catalog = null } = {}) { + const assetKind = normalizedFileAssetKind2(asset); + const requestedFileRef = normalizeFileRef(fileRef); + if (assetKind === "source") { + throw new Error( + `Source code is not available in Vercel Blob deployments for ${requestedFileRef || "(missing)"}` + ); + } + const currentCatalog = catalog || await readCatalog(); + const entry = catalogEntryForFileRef2(currentCatalog, requestedFileRef); + if (!entry) { + throw new Error(`CAD catalog entry not found: ${requestedFileRef || "(missing)"}`); + } + const outputRef = normalizeFileRef(entry.file || requestedFileRef); + const outputExtension = path10.posix.extname(outputRef).toLowerCase(); + const explicitSourceUrl = sourceUrlFromEntry(entry); + const explicitSourceRef = sourceFileRefFromEntry(entry); + const explicitStepUrl = stepUrlFromEntry(entry); + const explicitStepRef = stepFileRefFromEntry(entry, outputRef); + const explicitArtifactUrl = normalizeString2(entry?.url); + const explicitArtifactRef = artifactFileRefFromEntry(entry); + const fileRefForAsset = assetKind === "source" ? explicitSourceRef : assetKind === "artifact" ? explicitArtifactRef : outputExtension === ".step" || outputExtension === ".stp" ? explicitStepRef : outputRef; + const fallbackUrl = assetKind === "source" ? explicitSourceUrl : assetKind === "artifact" ? explicitArtifactUrl : outputUrlFromEntry(entry, fileRefForAsset || outputRef || requestedFileRef); + const blobUrl = fallbackUrl || !fileRefForAsset ? "" : await urlForBlobRef(fileRefForAsset); + const url = fallbackUrl || blobUrl; + if (!fileRefForAsset || !url) { + throw new Error( + assetKind === "source" ? `Source code is not available in Vercel Blob for ${requestedFileRef || "(missing)"}` : assetKind === "artifact" ? `Artifact file is not available in Vercel Blob for ${requestedFileRef || "(missing)"}` : `Output file is not available in Vercel Blob for ${requestedFileRef || "(missing)"}` + ); + } + return { + asset: assetKind, + file: fileRefForAsset, + url, + filename: path10.posix.basename(fileRefForAsset) || filenameFromUrl(url), + contentType: contentTypeForFileRef(fileRefForAsset) + }; + } + async function readFileAsset(request = {}) { + if (!fetchImpl) { + throw new Error("Vercel Blob backend requires fetch to download file assets"); + } + const access = await resolveFileAssetAccess(request); + const response = await fetchImpl(access.url); + if (!response.ok) { + throw new Error(`Failed to download file asset from Blob: ${response.status} ${response.statusText}`); + } + const contentType = response.headers?.get?.("content-type") || access.contentType; + return { + ...access, + body: Buffer.from(await response.arrayBuffer()), + contentType: contentTypeForFileRef(access.file, contentType) + }; + } + async function resolveSourceFileAccess(request = {}) { + return resolveFileAssetAccess({ ...request, asset: "source" }); + } + async function readSourceFile(request = {}) { + return readFileAsset({ ...request, asset: "source" }); + } + async function readStepSourceStatus2({ fileRef, catalog = null } = {}) { + const requestedFileRef = normalizeFileRef(fileRef); + const currentCatalog = catalog || await readCatalog(); + const entry = catalogEntryForFileRef2(currentCatalog, requestedFileRef); + if (!entry) { + throw new Error(`STEP catalog entry not found: ${requestedFileRef || "(missing)"}`); + } + const repoStepRef = stepFileRefFromEntry(entry, entry.file || requestedFileRef); + const sourceUrl = stepUrlFromEntry(entry) || (readOnly ? "" : await urlForBlobRef(repoStepRef)); + return { + ok: Boolean(sourceUrl), + file: repoStepRef, + stepPath: repoStepRef, + sourceKind: "step", + step: sourceUrl ? { + ok: true, + status: "current", + missing: false, + stale: false + } : { + ok: false, + status: "missing", + missing: true, + stale: false, + message: "STEP file is missing." + } + }; + } + const backend2 = { + kind: "vercel-blob", + readOnly: Boolean(readOnly), + canGenerateStepArtifacts: false, + prefix: normalizedPrefix, + catalogPath: normalizedCatalogPath, + readCatalog, + readStepSourceStatus: readStepSourceStatus2, + resolveFileAssetAccess, + readFileAsset, + resolveSourceFileAccess, + readSourceFile, + refreshCatalog, + urlForBlobRef + }; + if (!readOnly) { + return { + ...backend2, + writeAsset, + writeCatalog + }; + } + return backend2; +} + +// viewer/src/server/viewerEnv.mjs +var VIEWER_ASSET_BACKENDS = Object.freeze({ + LOCAL_FS: "local-fs", + VERCEL_BLOB: "vercel-blob" +}); +var DEPRECATED_LOCAL_ROOT_ENV_VARS = Object.freeze([ + "VIEWER_LOCAL_ROOT_DIR", + "VIEWER_LOCAL_WORKSPACE_ROOT" +]); +var VALID_VIEWER_ASSET_BACKENDS = new Set(Object.values(VIEWER_ASSET_BACKENDS)); +function envValue(env, name, fallback = "") { + return String(env?.[name] ?? fallback).trim(); +} +function normalizeViewerAssetBackend(value, fallback = VIEWER_ASSET_BACKENDS.LOCAL_FS) { + const normalized = String(value || fallback || "").trim().toLowerCase(); + if (VALID_VIEWER_ASSET_BACKENDS.has(normalized)) { + return normalized; + } + throw new Error( + `Unsupported VIEWER_ASSET_BACKEND: ${normalized || "(missing)"}. Expected one of: ${[...VALID_VIEWER_ASSET_BACKENDS].join(", ")}.` + ); +} +function assertNoDeprecatedLocalRootEnv(env = process.env) { + const configured = DEPRECATED_LOCAL_ROOT_ENV_VARS.filter((name) => String(env?.[name] || "").trim()); + if (configured.length) { + throw new Error( + `${configured.join(", ")} ${configured.length === 1 ? "is" : "are"} no longer supported. Pass ?dir= in the Viewer URL instead.` + ); + } +} +function vercelBlobCatalogUrlFromPrefix(prefix, catalogPath = "catalog.json") { + const rawPrefix = envValue({ prefix }, "prefix"); + if (!rawPrefix) { + return ""; + } + const normalizedCatalogPath = String(catalogPath || "catalog.json").trim().replace(/^\/+/, ""); + if (!normalizedCatalogPath) { + return ""; + } + try { + const url = new URL(rawPrefix); + const pathname = url.pathname.replace(/^\/+|\/+$/g, ""); + url.pathname = `/${[pathname, normalizedCatalogPath].filter(Boolean).join("/")}`; + url.search = ""; + url.hash = ""; + return url.toString(); + } catch { + return ""; + } +} +function normalizeVercelBlobStoreId(value) { + const normalized = String(value || "").trim().replace(/^store_/i, ""); + return /^[a-z0-9]+$/i.test(normalized) ? normalized.toLowerCase() : ""; +} +function vercelBlobStoreIdFromToken(value) { + const token = String(value || "").trim(); + const parts = token.split("_"); + if (parts[0] !== "vercel" || parts[1] !== "blob") { + return ""; + } + const tokenKindLength = parts[2] === "read" && parts[3] === "write" ? 2 : 1; + return normalizeVercelBlobStoreId(parts[2 + tokenKindLength]); +} +function vercelBlobStoreIdFromEnv(env = process.env) { + return normalizeVercelBlobStoreId(envValue(env, "VIEWER_VERCEL_BLOB_STORE_ID")) || normalizeVercelBlobStoreId(envValue(env, "BLOB_STORE_ID")) || vercelBlobStoreIdFromToken(envValue(env, "VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN")) || vercelBlobStoreIdFromToken(envValue(env, "BLOB_READ_WRITE_TOKEN")); +} +function vercelBlobPrefixFromEnv(env = process.env) { + const prefix = envValue(env, "VIEWER_VERCEL_BLOB_PREFIX"); + const storeId = vercelBlobStoreIdFromEnv(env); + if (!prefix || !storeId) { + return prefix; + } + try { + const url = new URL(prefix); + const hostname = url.hostname.toLowerCase(); + if (hostname.endsWith(".public.blob.vercel-storage.com") || hostname === "blob.vercel-storage.com") { + url.hostname = `${storeId}.public.blob.vercel-storage.com`; + return url.toString(); + } + } catch { + const pathname = prefix.replace(/^\/+|\/+$/g, ""); + return `https://${storeId}.public.blob.vercel-storage.com${pathname ? `/${pathname}` : ""}`; + } + return prefix; +} +function vercelBlobConfigFromEnv(env = process.env) { + const prefix = vercelBlobPrefixFromEnv(env); + const catalogPath = envValue(env, "VIEWER_VERCEL_BLOB_CATALOG_PATH", "catalog.json") || "catalog.json"; + return { + prefix, + catalogPath, + catalogUrl: vercelBlobCatalogUrlFromPrefix(prefix, catalogPath), + token: envValue(env, "VIEWER_VERCEL_BLOB_READ_WRITE_TOKEN") || envValue(env, "BLOB_READ_WRITE_TOKEN") || void 0 + }; +} + +// viewer/src/server/vercelApi.mjs +function vercelUrlFromHost(value) { + const rawValue = String(value || "").trim(); + if (!rawValue) { + return ""; + } + try { + const url = new URL(rawValue.includes("://") ? rawValue : `https://${rawValue}`); + const pathname = url.pathname.replace(/\/+$/g, ""); + return `${url.protocol}//${url.host}${pathname === "/" ? "" : pathname}`; + } catch { + return ""; + } +} +function hostedViewerPublicUrlFromEnv(env = process.env) { + const candidates = [ + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_URL", + "VERCEL_BRANCH_URL" + ]; + for (const name of candidates) { + const url = vercelUrlFromHost(envValue(env, name)); + if (url) { + return url; + } + } + return ""; +} +function buildHostedViewerServerInfo({ + backend: backend2, + env = process.env, + rootDir = "" +} = {}) { + return { + schemaVersion: 1, + app: "cad-viewer", + backend: backend2?.kind || "vercel-blob", + rootDir, + catalogPath: backend2?.catalogPath || vercelBlobConfigFromEnv(env).catalogPath, + stepArtifactGenerationAvailable: false, + url: hostedViewerPublicUrlFromEnv(env) + }; +} + +// viewer/src/server/viewerServerInfo.mjs +import path11 from "node:path"; +var VIEWER_SERVER_INFO_SCHEMA_VERSION = 1; +var VIEWER_SERVER_API_VERSION = 2; +var VIEWER_SERVER_APP_ID = "cad-viewer"; +var DEFAULT_VIEWER_HOST = "127.0.0.1"; +var DEFAULT_VIEWER_PORT = 4178; +function normalizeViewerPort(value, fallback = DEFAULT_VIEWER_PORT) { + const parsed = Number.parseInt(String(value ?? ""), 10); + if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) { + return parsed; + } + return fallback; +} +function normalizeViewerActiveDirectory(value, directoryRoot2) { + if (!value || typeof value !== "object") { + return null; + } + const rawDir = String(value.dir || "").trim(); + const rawRootPath = String(value.rootPath || "").trim(); + if (!rawDir && !rawRootPath) { + return null; + } + const resolvedRawRootPath = rawRootPath ? path11.resolve(path11.isAbsolute(rawRootPath) ? rawRootPath : path11.join(directoryRoot2, rawRootPath)) : ""; + const resolvedRoot = rawRootPath ? { + dir: rawDir, + rootPath: resolvedRawRootPath, + rootName: path11.basename(resolvedRawRootPath) + } : path11.isAbsolute(rawDir) ? { + dir: path11.resolve(rawDir), + rootPath: path11.resolve(rawDir), + rootName: path11.basename(path11.resolve(rawDir)) + } : resolveViewerRoot(directoryRoot2, normalizeViewerRootDir(rawDir)); + const dir = rawDir || resolvedRoot.dir || ""; + const rootPath = resolvedRoot.rootPath || ""; + if (!rootPath) { + return null; + } + return { + dir, + rootPath, + rootName: String(value.rootName || resolvedRoot.rootName || path11.basename(rootPath) || dir || "Directory") + }; +} +function normalizeViewerActiveDirectories(activeDirectories2, directoryRoot2) { + if (!Array.isArray(activeDirectories2) || !directoryRoot2) { + return []; + } + const seen = /* @__PURE__ */ new Set(); + const normalized = []; + for (const value of activeDirectories2) { + const directory = normalizeViewerActiveDirectory(value, directoryRoot2); + if (!directory || !directory.dir || seen.has(directory.rootPath)) { + continue; + } + seen.add(directory.rootPath); + normalized.push(directory); + } + return normalized; +} +function buildViewerServerInfo({ + directoryRoot: directoryRoot2, + rootDir = DEFAULT_VIEWER_ROOT_DIR, + port: port2 = DEFAULT_VIEWER_PORT, + pid = process.pid, + host: host2 = DEFAULT_VIEWER_HOST, + backend: backend2 = "local-fs", + dynamicRoot = false, + stepArtifactGenerationAvailable = true, + viewerVersion: viewerVersion2 = "", + serverMode = "", + git = "", + serverFeatures = [], + activeDirectories: activeDirectories2 = [] +} = {}) { + if (!directoryRoot2) { + throw new Error("directoryRoot is required"); + } + const resolvedDirectoryRoot = path11.resolve(directoryRoot2); + const rawRootDir = String(rootDir || "").trim(); + const resolvedViewerRoot = rawRootDir ? path11.isAbsolute(rawRootDir) ? { + dir: path11.resolve(rawRootDir), + rootPath: path11.resolve(rawRootDir), + rootName: path11.basename(path11.resolve(rawRootDir)) + } : resolveViewerRoot(resolvedDirectoryRoot, normalizeViewerRootDir(rawRootDir)) : { + dir: DEFAULT_VIEWER_ROOT_DIR, + rootPath: "", + rootName: "" + }; + const normalizedPort = normalizeViewerPort(port2); + const normalizedServerMode = String(serverMode || "").trim(); + const normalizedGit = String(git || "").trim(); + const normalizedActiveDirectories = normalizeViewerActiveDirectories(activeDirectories2, resolvedDirectoryRoot); + return { + schemaVersion: VIEWER_SERVER_INFO_SCHEMA_VERSION, + serverApiVersion: VIEWER_SERVER_API_VERSION, + app: VIEWER_SERVER_APP_ID, + viewerVersion: String(viewerVersion2 || ""), + ...normalizedServerMode ? { serverMode: normalizedServerMode } : {}, + ...normalizedGit ? { git: normalizedGit } : {}, + serverFeatures: Array.isArray(serverFeatures) ? serverFeatures.map((feature) => String(feature || "").trim()).filter(Boolean) : [], + backend: backend2, + dynamicRoot: Boolean(dynamicRoot), + directoryRoot: resolvedDirectoryRoot, + rootDir: resolvedViewerRoot.dir, + rootPath: resolvedViewerRoot.rootPath, + rootName: resolvedViewerRoot.rootName, + activeDirectories: normalizedActiveDirectories, + port: normalizedPort, + pid: Number.isInteger(pid) ? pid : process.pid, + stepArtifactGenerationAvailable: stepArtifactGenerationAvailable !== false, + url: `http://${host2}:${normalizedPort}` + }; +} + +// viewer/src/shared/viewerConfig.mjs +var DEFAULT_VIEWER_GITHUB_URL = "https://github.com/earthtojake/text-to-cad"; +function normalizeViewerDefaultFile(value = "") { + const rawValue = String(value ?? "").trim(); + return rawValue.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, ""); +} +function normalizeViewerGithubUrl(value = "", fallback = DEFAULT_VIEWER_GITHUB_URL) { + return normalizeHttpUrlCandidate(value) || normalizeHttpUrlCandidate(fallback); +} +function normalizeHttpUrlCandidate(value = "") { + const rawValue = String(value ?? "").trim(); + if (!rawValue) { + return ""; + } + const urlValue = /^[a-z][a-z\d+.-]*:\/\//i.test(rawValue) ? rawValue : `https://${rawValue.replace(/^\/+/, "")}`; + try { + const url = new URL(urlValue); + return ["http:", "https:"].includes(url.protocol) ? url.href : ""; + } catch { + return ""; + } +} + +// viewer/src/server/directoryRoot.mjs +import path12 from "node:path"; +function resolveDirectoryRoot({ + directoryRoot: directoryRoot2 = "", + env = process.env, + cwd = process.cwd(), + appRoot = "", + defaultDirectoryRoot: defaultDirectoryRoot2 = "" +} = {}) { + const explicitRoot = directoryRoot2 || ""; + if (explicitRoot) { + return path12.resolve(cwd, explicitRoot); + } + const resolvedAppRoot = appRoot ? path12.resolve(appRoot) : ""; + for (const candidate of [env.INIT_CWD, cwd]) { + if (!candidate) { + continue; + } + const resolvedCandidate = path12.resolve(candidate); + if (!resolvedAppRoot || resolvedCandidate !== resolvedAppRoot && !pathIsInside(resolvedCandidate, resolvedAppRoot)) { + return resolvedCandidate; + } + } + return defaultDirectoryRoot2 ? path12.resolve(defaultDirectoryRoot2) : path12.resolve(cwd); +} + +// viewer/src/server/serverLifetime.mjs +var MAX_SERVER_LIFETIME_MS = 2147483647; +function parseServerLifetimeMs(value, flag = "--shutdown-after") { + const rawValue = String(value ?? "").trim().toLowerCase(); + const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(rawValue); + if (!match) { + throw new Error(`${flag} must be a positive duration such as 30m, 2h, or 43200000.`); + } + const amount = Number.parseFloat(match[1]); + const unit = match[2] || "ms"; + const multiplier = { + ms: 1, + s: 1e3, + m: 60 * 1e3, + h: 60 * 60 * 1e3 + }[unit]; + const parsed = Math.round(amount * multiplier); + if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SERVER_LIFETIME_MS) { + throw new Error(`${flag} must be between 1ms and ${MAX_SERVER_LIFETIME_MS}ms.`); + } + return parsed; +} +function normalizeServerLifetimeMs(value, defaultMs = null) { + const rawValue = String(value ?? "").trim(); + if (!rawValue) { + return defaultMs; + } + const parsed = Number.parseInt(rawValue, 10); + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > MAX_SERVER_LIFETIME_MS) { + return defaultMs; + } + return parsed; +} +function formatServerLifetime(ms) { + if (ms % (60 * 60 * 1e3) === 0) { + return `${ms / (60 * 60 * 1e3)}h`; + } + if (ms % (60 * 1e3) === 0) { + return `${ms / (60 * 1e3)}m`; + } + if (ms % 1e3 === 0) { + return `${ms / 1e3}s`; + } + return `${ms}ms`; +} +function closeHttpServer(server2) { + return new Promise((resolve, reject) => { + server2.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +} +function scheduleProcessShutdown({ + lifetimeMs, + label = "CAD Viewer server", + close = async () => { + } +} = {}) { + const normalizedLifetimeMs = normalizeServerLifetimeMs(lifetimeMs); + if (normalizedLifetimeMs === null) { + return null; + } + const timer = setTimeout(() => { + console.log(`${label} reached ${formatServerLifetime(normalizedLifetimeMs)} lifetime; shutting down.`); + const forceExit = setTimeout(() => process.exit(0), 5e3); + forceExit.unref?.(); + Promise.resolve().then(() => close()).then( + () => process.exit(0), + (error) => { + console.error(`${label} shutdown failed: ${error instanceof Error ? error.message : String(error)}`); + process.exit(1); + } + ); + }, normalizedLifetimeMs); + timer.unref?.(); + return timer; +} + +// viewer/src/server/serverArgs.mjs +function requiredValue(argv, index, flag) { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`${flag} requires a value`); + } + return value; +} +function parsePort(value, flag) { + const parsed = Number.parseInt(String(value ?? ""), 10); + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) { + throw new Error(`${flag} must be a TCP port from 1 to 65535`); + } + return parsed; +} +function parseServerArgs(argv = []) { + const options = { + port: null, + host: "", + rootDir: "", + shutdownAfterMs: null, + help: false + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + options.help = true; + continue; + } + if (arg.startsWith("--root-dir=")) { + throw new Error("--root-dir has been removed; pass ?dir= in the Viewer URL."); + } + if (arg === "--root-dir") { + throw new Error("--root-dir has been removed; pass ?dir= in the Viewer URL."); + } + if (arg.startsWith("--dir=")) { + options.rootDir = arg.slice("--dir=".length).trim(); + continue; + } + if (arg === "--dir") { + options.rootDir = requiredValue(argv, index, arg).trim(); + index += 1; + continue; + } + if (arg.startsWith("--port=")) { + options.port = parsePort(arg.slice("--port=".length), "--port"); + continue; + } + if (arg === "--port") { + options.port = parsePort(requiredValue(argv, index, arg), arg); + index += 1; + continue; + } + if (arg.startsWith("--host=")) { + options.host = arg.slice("--host=".length).trim(); + continue; + } + if (arg === "--host") { + options.host = requiredValue(argv, index, arg).trim(); + index += 1; + continue; + } + if (arg.startsWith("--shutdown-after=")) { + options.shutdownAfterMs = parseServerLifetimeMs(arg.slice("--shutdown-after=".length), "--shutdown-after"); + continue; + } + if (arg === "--shutdown-after") { + options.shutdownAfterMs = parseServerLifetimeMs(requiredValue(argv, index, arg), arg); + index += 1; + continue; + } + throw new Error(`Unknown argument: ${arg}`); + } + return options; +} +function serverHelpText() { + return `Usage: node backend/server.mjs [options] + +Options: + --port Port to bind. Defaults to 4178. + --host Host to bind. Defaults to 127.0.0.1. + --dir Default local directory root. Defaults to startup directory. + --shutdown-after