import csv import math from pathlib import Path # ----------------------- # CONFIG # ----------------------- CSV_PATH = "conveyors.csv" OUT_TSCN = "generated_conveyors.tscn" SCALE = 0.0254 FIXED_Y = 2.5 STRAIGHT_BELT_ID = "3_38ygf" CURVED_BELT_ID = "1_ef28r" # Spur / curve tuning SPUR_TANGENT_NUDGE = 0.0 MIN_CURVE_DEG = 1.0 # Geometry (must match asset) CURVE_INNER_RADIUS = 1.815 BELT_WIDTH = 1.524 # ----------------------- # HELPERS # ----------------------- def transform_from_points(x1, y1, x2, y2): dx = x2 - x1 dy = y2 - y1 length = math.hypot(dx, dy) * SCALE mid_x = (x1 + x2) / 2 * SCALE mid_z = -(y1 + y2) / 2 * SCALE # AutoCAD Y → Godot -Z rot_y = math.atan2(-dy, dx) return { "length": length, "pos": (mid_x, FIXED_Y, mid_z), "rot_y": rot_y, } def transform3d(rot_y, x, y, z): c = math.cos(rot_y) s = math.sin(rot_y) return ( f"Transform3D({c}, 0, {-s}, " f"0, 1, 0, " f"{s}, 0, {c}, " f"{x}, {y}, {z})" ) def parse_key(key): p = key.split("_") return p[0], int(p[1]) def unit_fwd(rot_y): return math.cos(rot_y), math.sin(rot_y) def unit_right(rot_y): return -math.sin(rot_y), math.cos(rot_y) def end_point(conv): dx, dz = unit_fwd(conv["rot_y"]) half = conv["length"] / 2 return ( conv["pos"][0] + dx * half, conv["pos"][2] + dz * half ) def start_point(conv): dx, dz = unit_fwd(conv["rot_y"]) half = conv["length"] / 2 return ( conv["pos"][0] - dx * half, conv["pos"][2] - dz * half ) def add_straight_node(lines, name, rot_y, cx, cz, length): t = transform3d(rot_y, cx, FIXED_Y, cz) lines.append(f'[node name="{name}" parent="." instance=ExtResource("{STRAIGHT_BELT_ID}")]') lines.append(f"transform = {t}") lines.append("right_side_guards_enabled = false") lines.append("left_side_guards_enabled = false") lines.append("head_end_leg_enabled = false") lines.append("tail_end_leg_enabled = false") lines.append("enable_comms = true") lines.append(f'speed_tag_name = "{name}_OIP"') lines.append(f"size = Vector3({length:.6f}, 0.5, 1.524)") lines.append("") def add_curved_node(lines, name, rot_y, px, pz, angle_deg): t = transform3d(rot_y, px, FIXED_Y, pz) lines.append(f'[node name="{name}" parent="." instance=ExtResource("{CURVED_BELT_ID}")]') lines.append(f"transform = {t}") lines.append(f"inner_radius = {CURVE_INNER_RADIUS}") lines.append(f"conveyor_angle = {angle_deg:.3f}") lines.append("enable_comms = true") lines.append(f'speed_tag_name = "{name}_OIP"') lines.append("") # ----------------------- # READ CSV # ----------------------- straight = {} vfd_only = [] # included=0 candidates (some will become spurs, others will be chain-continued) with open(CSV_PATH, newline="") as f: reader = csv.DictReader(f) for row in reader: key = row["conveyor_key"].strip() included = row["included"].strip() prefix, sec = parse_key(key) if included == "1": if not all(row[c].strip() for c in ("start_x", "start_y", "end_x", "end_y")): continue conv = transform_from_points( float(row["start_x"]), float(row["start_y"]), float(row["end_x"]), float(row["end_y"]) ) conv["name"] = key conv["prefix"] = prefix conv["sec"] = sec straight[key] = conv else: vfd_only.append({"name": key, "prefix": prefix, "sec": sec}) # Precompute endpoints & directions for geometry straights for c in straight.values(): c["start"] = start_point(c) c["end"] = end_point(c) c["fwd"] = unit_fwd(c["rot_y"]) # ----------------------- # WRITE TSCN # ----------------------- lines = [] lines.append('[gd_scene load_steps=3 format=3]') lines.append('') lines.append('[ext_resource type="PackedScene" path="res://parts/assemblies/BeltConveyorAssembly.tscn" id="3_38ygf"]') lines.append('[ext_resource type="PackedScene" path="res://parts/assemblies/CurvedBeltConveyorAssembly.tscn" id="1_ef28r"]') lines.append('') lines.append('[node name="GeneratedConveyors" type="Node3D"]') lines.append('') # ----------------------- # 1) Geometry straight conveyors (included=1) # ----------------------- for c in straight.values(): cx, _, cz = c["pos"] add_straight_node(lines, c["name"], c["rot_y"], cx, cz, c["length"]) # ----------------------- # 2) Spur conveyors (use vfd_only list, but mark those we actually place) # ----------------------- placed_spurs = set() for spur in vfd_only: prefix = spur["prefix"] sec = spur["sec"] prev_key = f"{prefix}_{sec-1}" next_key = f"{prefix}_{sec+1}" # spur needs both neighbors with geometry (or already-added straights later, but at this stage it's fine) if prev_key not in straight or next_key not in straight: continue prev = straight[prev_key] nxt = straight[next_key] pfx, pfz = prev["fwd"] nfx, nfz = nxt["fwd"] cross = pfx * nfz - pfz * nfx dot = pfx * nfx + pfz * nfz delta = math.atan2(cross, dot) angle_deg = abs(delta) * 180.0 / math.pi if angle_deg < MIN_CURVE_DEG: continue turn_sign = 1.0 if delta > 0 else -1.0 rx, rz = unit_right(prev["rot_y"]) end_x, end_z = prev["end"] # place along side edge then inward by inner radius (your current spur logic) edge_x = end_x + rx * (BELT_WIDTH / 2) * turn_sign edge_z = end_z + rz * (BELT_WIDTH / 2) * turn_sign inward_x = -rx * turn_sign inward_z = -rz * turn_sign place_x = edge_x + inward_x * CURVE_INNER_RADIUS + pfx * SPUR_TANGENT_NUDGE place_z = edge_z + inward_z * CURVE_INNER_RADIUS + pfz * SPUR_TANGENT_NUDGE add_curved_node(lines, spur["name"], prev["rot_y"], place_x, place_z, angle_deg) placed_spurs.add(spur["name"]) # ----------------------- # 3) VFD-only straight conveyors (CHAIN CONTINUATION) # Run AFTER spurs. Only place leftovers (not in placed_spurs). # Also: after we place one, add it to "straight" so multiple missing sections can chain. # ----------------------- # Build quick lookup for vfd_only rows by key vfd_by_key = {s["name"]: s for s in vfd_only} placed_chain = set() made_progress = True while made_progress: made_progress = False for spur in vfd_only: name = spur["name"] if name in placed_spurs or name in placed_chain: continue if name in straight: continue prefix = spur["prefix"] sec = spur["sec"] prev_key = f"{prefix}_{sec-1}" next_key = f"{prefix}_{sec+1}" # --- Case A: place AFTER previous (if previous exists, next does NOT exist/placed) --- if prev_key in straight and next_key not in straight: ref = straight[prev_key] dx, dz = ref["fwd"] length = ref["length"] # start at previous end, same direction, same length sx, sz = ref["end"] ex, ez = (sx + dx * length, sz + dz * length) cx, cz = (sx + dx * (length / 2), sz + dz * (length / 2)) add_straight_node(lines, name, ref["rot_y"], cx, cz, length) # register as a new straight so the chain can continue newc = { "name": name, "prefix": prefix, "sec": sec, "length": length, "pos": (cx, FIXED_Y, cz), "rot_y": ref["rot_y"], } newc["start"] = (sx, sz) newc["end"] = (ex, ez) newc["fwd"] = (dx, dz) straight[name] = newc placed_chain.add(name) made_progress = True continue # --- Case B: place BEFORE next (if next exists, prev does NOT exist/placed) --- if next_key in straight and prev_key not in straight: ref = straight[next_key] dx, dz = ref["fwd"] length = ref["length"] # end at next start, same direction, same length (going backwards) ex, ez = ref["start"] sx, sz = (ex - dx * length, ez - dz * length) cx, cz = (sx + dx * (length / 2), sz + dz * (length / 2)) add_straight_node(lines, name, ref["rot_y"], cx, cz, length) newc = { "name": name, "prefix": prefix, "sec": sec, "length": length, "pos": (cx, FIXED_Y, cz), "rot_y": ref["rot_y"], } newc["start"] = (sx, sz) newc["end"] = (ex, ez) newc["fwd"] = (dx, dz) straight[name] = newc placed_chain.add(name) made_progress = True continue # ----------------------- # WRITE FILE # ----------------------- Path(OUT_TSCN).write_text("\n".join(lines), encoding="utf-8") leftovers = [s["name"] for s in vfd_only if s["name"] not in placed_spurs and s["name"] not in placed_chain] print( f"Generated {len([k for k in straight.keys() if k not in vfd_by_key])} geometry straights, " f"{len(placed_spurs)} spurs, " f"{len(placed_chain)} chain-continued VFD-only straights. " f"Leftovers not placed: {len(leftovers)}" )