"""
Galvanized Mesh-Cage Utility Trailer — Blender procedural model

使用方法：
1. 打开 Blender > Scripting > New。
2. 粘贴或打开本文件，点击 Run Script。
3. 模型会生成在 TRAILER_MODEL 集合中。

当前比例依据单张参考图目测。收到实测数据后，可直接修改下方参数。
"""

import bpy
import math
from mathutils import Vector


# -----------------------------------------------------------------------------
# Editable proportion parameters
# -----------------------------------------------------------------------------
MODEL_SCALE = 1.0
BED_LENGTH = 3.15 * MODEL_SCALE
BED_WIDTH = 1.62 * MODEL_SCALE
FLOOR_HEIGHT = 0.72 * MODEL_SCALE
LOWER_PANEL_HEIGHT = 0.53 * MODEL_SCALE
CAGE_HEIGHT = 0.79 * MODEL_SCALE
WHEEL_RADIUS = 0.39 * MODEL_SCALE
WHEEL_WIDTH = 0.20 * MODEL_SCALE
AXLE_X = 0.52 * MODEL_SCALE
HITCH_X = -3.15 * MODEL_SCALE


def clear_scene():
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=False)
    for datablocks in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.cameras, bpy.data.lights):
        for block in list(datablocks):
            if block.users == 0:
                datablocks.remove(block)


def make_material(name, base_color, metallic=0.0, roughness=0.5, emission=None):
    material = bpy.data.materials.new(name)
    material.diffuse_color = (*base_color, 1.0)
    material.use_nodes = True
    bsdf = next(node for node in material.node_tree.nodes if node.type == 'BSDF_PRINCIPLED')
    bsdf.inputs['Base Color'].default_value = (*base_color, 1.0)
    bsdf.inputs['Metallic'].default_value = metallic
    bsdf.inputs['Roughness'].default_value = roughness
    if emission:
        bsdf.inputs['Emission Color'].default_value = (*emission, 1.0)
        bsdf.inputs['Emission Strength'].default_value = 1.2
    return material


def move_to_collection(obj, collection):
    for source in list(obj.users_collection):
        source.objects.unlink(obj)
    collection.objects.link(obj)


def box(name, size, location, material, collection, bevel=0.012):
    bpy.ops.mesh.primitive_cube_add(location=location)
    obj = bpy.context.object
    obj.name = name
    obj.dimensions = size
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if material:
        obj.data.materials.append(material)
    if bevel > 0:
        modifier = obj.modifiers.new('Soft galvanized edges', 'BEVEL')
        modifier.width = bevel * MODEL_SCALE
        modifier.segments = 2
    move_to_collection(obj, collection)
    return obj


def cylinder(name, radius, depth, location, rotation, material, collection, vertices=32):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=location, rotation=rotation)
    obj = bpy.context.object
    obj.name = name
    obj.data.materials.append(material)
    move_to_collection(obj, collection)
    for poly in obj.data.polygons:
        poly.use_smooth = True
    bevel = obj.modifiers.new('Edge bevel', 'BEVEL')
    bevel.width = 0.008 * MODEL_SCALE
    bevel.segments = 2
    return obj


def beam_between(name, start, end, thickness, material, collection):
    start = Vector(start)
    end = Vector(end)
    direction = end - start
    obj = box(name, (direction.length, thickness, thickness), (start + end) / 2, material, collection, bevel=0.01)
    obj.rotation_mode = 'QUATERNION'
    obj.rotation_quaternion = direction.to_track_quat('X', 'Z')
    return obj


def torus(name, major_radius, minor_radius, location, rotation, material, collection):
    bpy.ops.mesh.primitive_torus_add(
        major_radius=major_radius,
        minor_radius=minor_radius,
        major_segments=36,
        minor_segments=10,
        location=location,
        rotation=rotation,
    )
    obj = bpy.context.object
    obj.name = name
    obj.data.materials.append(material)
    move_to_collection(obj, collection)
    for poly in obj.data.polygons:
        poly.use_smooth = True
    return obj


def curved_fender(name, center, side, material, collection):
    curve_data = bpy.data.curves.new(name, 'CURVE')
    curve_data.dimensions = '3D'
    curve_data.bevel_depth = 0.06 * MODEL_SCALE
    curve_data.bevel_resolution = 3
    spline = curve_data.splines.new('POLY')
    segments = 28
    spline.points.add(segments)
    for index in range(segments + 1):
        angle = math.pi * index / segments
        x = center[0] + math.cos(angle) * 0.46 * MODEL_SCALE
        z = center[2] + math.sin(angle) * 0.46 * MODEL_SCALE
        spline.points[index].co = (x, center[1] + side * 0.01, z, 1)
    obj = bpy.data.objects.new(name, curve_data)
    curve_data.materials.append(material)
    collection.objects.link(obj)
    return obj


def chain_link(name, location, rotation, material, collection):
    obj = torus(name, 0.052 * MODEL_SCALE, 0.012 * MODEL_SCALE, location, rotation, material, collection)
    obj.scale.x = 1.25
    obj.scale.z = 0.72
    return obj


def build_trailer():
    clear_scene()
    model_collection = bpy.data.collections.new('TRAILER_MODEL')
    preview_collection = bpy.data.collections.new('_PREVIEW')
    bpy.context.scene.collection.children.link(model_collection)
    bpy.context.scene.collection.children.link(preview_collection)

    galvanized = make_material('Galvanized Steel', (0.48, 0.55, 0.56), metallic=0.78, roughness=0.30)
    galvanized_light = make_material('Galvanized Light', (0.67, 0.72, 0.72), metallic=0.82, roughness=0.25)
    galvanized_dark = make_material('Galvanized Dark', (0.30, 0.36, 0.37), metallic=0.72, roughness=0.38)
    wire_material = make_material('Cage Wire', (0.55, 0.62, 0.62), metallic=0.84, roughness=0.26)
    tire_material = make_material('Tire Rubber', (0.018, 0.025, 0.026), metallic=0.0, roughness=0.88)
    tread_material = make_material('Tire Tread', (0.06, 0.075, 0.078), metallic=0.0, roughness=0.98)
    red_material = make_material('Tail Light Red', (0.50, 0.02, 0.025), metallic=0.0, roughness=0.24, emission=(0.34, 0.005, 0.005))
    black_material = make_material('Handle Black', (0.03, 0.04, 0.04), metallic=0.15, roughness=0.74)

    half_length = BED_LENGTH / 2
    half_width = BED_WIDTH / 2

    # Floor and underframe
    box('Tray Floor', (BED_LENGTH, BED_WIDTH, 0.10 * MODEL_SCALE), (0, 0, FLOOR_HEIGHT), galvanized_dark, model_collection)
    for y in (-half_width + 0.06, half_width - 0.06):
        box('Longitudinal Chassis Rail', (BED_LENGTH + 0.10, 0.10, 0.12), (0, y, FLOOR_HEIGHT - 0.12), galvanized_light, model_collection)
    for x in (-1.25, -0.62, 0, 0.62, 1.25):
        box('Crossmember', (0.09, BED_WIDTH - 0.10, 0.09), (x * MODEL_SCALE, 0, FLOOR_HEIGHT - 0.14), galvanized_dark, model_collection)

    # Lower side panels
    panel_z = FLOOR_HEIGHT + LOWER_PANEL_HEIGHT / 2
    for y, label in ((-half_width, 'Left'), (half_width, 'Right')):
        box(f'{label} Lower Side Panel', (BED_LENGTH + 0.06, 0.075, LOWER_PANEL_HEIGHT), (0, y, panel_z), galvanized, model_collection)
        outer_y = y + math.copysign(0.042, y)
        for x in (-1.15, -0.58, 0, 0.58, 1.15):
            box('Pressed Side Rib', (0.035, 0.025, 0.37), (x * MODEL_SCALE, outer_y, panel_z), galvanized_dark, model_collection, bevel=0.004)
        for z in (panel_z - 0.11, panel_z + 0.12):
            box('Pressed Horizontal Rib', (BED_LENGTH - 0.20, 0.035, 0.04), (0, outer_y, z), galvanized_dark, model_collection, bevel=0.004)

    for x, label in ((-half_length, 'Front'), (half_length, 'Rear')):
        box(f'{label} Lower Panel', (0.075, BED_WIDTH, LOWER_PANEL_HEIGHT), (x, 0, panel_z), galvanized, model_collection)
    for y in (-0.56, -0.28, 0, 0.28, 0.56):
        box('Rear Pressed Rib', (0.035, 0.03, 0.38), (half_length + 0.04, y * MODEL_SCALE, panel_z), galvanized_dark, model_collection, bevel=0.004)

    # Cage frame and wire grid
    cage_bottom = FLOOR_HEIGHT + LOWER_PANEL_HEIGHT + 0.04
    cage_top = cage_bottom + CAGE_HEIGHT
    cage_mid = (cage_bottom + cage_top) / 2
    for y in (-half_width, half_width):
        box('Cage Bottom Rail', (BED_LENGTH + 0.08, 0.055, 0.055), (0, y, cage_bottom), galvanized_dark, model_collection)
        box('Cage Top Rail', (BED_LENGTH + 0.08, 0.065, 0.065), (0, y, cage_top), galvanized_dark, model_collection)
        for x in (-half_length, -0.78, 0, 0.78, half_length):
            box('Cage Upright', (0.065, 0.065, CAGE_HEIGHT), (x, y, cage_mid), galvanized_dark, model_collection)
        x = -1.43 * MODEL_SCALE
        while x <= 1.44 * MODEL_SCALE:
            box('Vertical Cage Wire', (0.014, 0.014, CAGE_HEIGHT - 0.08), (x, y, cage_mid), wire_material, model_collection, bevel=0.002)
            x += 0.18 * MODEL_SCALE
        z = cage_bottom + 0.13 * MODEL_SCALE
        while z < cage_top - 0.05 * MODEL_SCALE:
            box('Horizontal Cage Wire', (BED_LENGTH - 0.08, 0.014, 0.014), (0, y, z), wire_material, model_collection, bevel=0.002)
            z += 0.14 * MODEL_SCALE

    for x, label in ((-half_length, 'Front'), (half_length, 'Rear')):
        box(f'{label} Cage Bottom Rail', (0.06, BED_WIDTH + 0.02, 0.055), (x, 0, cage_bottom), galvanized_dark, model_collection)
        box(f'{label} Cage Top Rail', (0.06, BED_WIDTH + 0.02, 0.065), (x, 0, cage_top), galvanized_dark, model_collection)
        for y in (-half_width, -0.40, 0, 0.40, half_width):
            box(f'{label} Cage Upright', (0.06, 0.06, CAGE_HEIGHT), (x, y, cage_mid), galvanized_dark, model_collection)
        y = -0.68 * MODEL_SCALE
        while y <= 0.69 * MODEL_SCALE:
            box('End Vertical Cage Wire', (0.014, 0.014, CAGE_HEIGHT - 0.08), (x, y, cage_mid), wire_material, model_collection, bevel=0.002)
            y += 0.17 * MODEL_SCALE
        z = cage_bottom + 0.13 * MODEL_SCALE
        while z < cage_top - 0.05 * MODEL_SCALE:
            box('End Horizontal Cage Wire', (0.014, BED_WIDTH - 0.08, 0.014), (x, 0, z), wire_material, model_collection, bevel=0.002)
            z += 0.14 * MODEL_SCALE

    # Axle and wheels. Blender cylinder axis is rotated from Z to Y.
    cylinder('Axle', 0.055 * MODEL_SCALE, 2.0 * MODEL_SCALE, (AXLE_X, 0, 0.49 * MODEL_SCALE), (math.pi / 2, 0, 0), galvanized_dark, model_collection, 20)
    for side in (-1, 1):
        wheel_y = side * 0.96 * MODEL_SCALE
        tire = cylinder('Wheel Tire', WHEEL_RADIUS, WHEEL_WIDTH, (AXLE_X, wheel_y, 0.47 * MODEL_SCALE), (math.pi / 2, 0, 0), tire_material, model_collection, 48)
        cylinder('Wheel Hub', 0.21 * MODEL_SCALE, 0.215 * MODEL_SCALE, (AXLE_X, wheel_y, 0.47 * MODEL_SCALE), (math.pi / 2, 0, 0), galvanized_light, model_collection, 28)
        cylinder('Hub Cap', 0.075 * MODEL_SCALE, 0.225 * MODEL_SCALE, (AXLE_X, wheel_y, 0.47 * MODEL_SCALE), (math.pi / 2, 0, 0), galvanized_dark, model_collection, 20)
        for index in range(12):
            angle = index * math.tau / 12
            tread = box(
                'Tire Tread Block',
                (0.04, WHEEL_WIDTH + 0.025, 0.035),
                (AXLE_X + math.cos(angle) * WHEEL_RADIUS * 0.95, wheel_y, 0.47 * MODEL_SCALE + math.sin(angle) * WHEEL_RADIUS * 0.95),
                tread_material,
                model_collection,
                bevel=0.004,
            )
            tread.rotation_euler.y = -angle
        curved_fender('Curved Wheel Fender', (AXLE_X, wheel_y, 0.48 * MODEL_SCALE), side, galvanized_light, model_collection)
        box('Fender Flat', (0.94 * MODEL_SCALE, 0.18 * MODEL_SCALE, 0.08 * MODEL_SCALE), (AXLE_X, wheel_y, 0.53 * MODEL_SCALE), galvanized_light, model_collection)

    # A-frame drawbar
    hitch_point = (HITCH_X, 0, 0.56 * MODEL_SCALE)
    beam_between('A-frame Left', (-half_length, -0.68 * MODEL_SCALE, 0.62 * MODEL_SCALE), hitch_point, 0.105 * MODEL_SCALE, galvanized_light, model_collection)
    beam_between('A-frame Right', (-half_length, 0.68 * MODEL_SCALE, 0.62 * MODEL_SCALE), hitch_point, 0.105 * MODEL_SCALE, galvanized_light, model_collection)
    beam_between('Centre Drawbar', (-half_length, 0, 0.62 * MODEL_SCALE), (-3.03 * MODEL_SCALE, 0, 0.62 * MODEL_SCALE), 0.11 * MODEL_SCALE, galvanized_dark, model_collection)
    box('Coupler Body', (0.31, 0.19, 0.14), (-3.19 * MODEL_SCALE, 0, 0.57 * MODEL_SCALE), galvanized_dark, model_collection)
    bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=12, location=(-3.37 * MODEL_SCALE, 0, 0.58 * MODEL_SCALE))
    coupler = bpy.context.object
    coupler.name = 'Coupler Head'
    coupler.scale = (0.144 * MODEL_SCALE, 0.102 * MODEL_SCALE, 0.078 * MODEL_SCALE)
    coupler.data.materials.append(galvanized_light)
    move_to_collection(coupler, model_collection)

    # Jockey wheel and winding handle
    box('Jockey Post', (0.08, 0.08, 0.70), (-2.55 * MODEL_SCALE, -0.32 * MODEL_SCALE, 0.50 * MODEL_SCALE), galvanized_dark, model_collection)
    box('Jockey Clamp', (0.18, 0.13, 0.11), (-2.55 * MODEL_SCALE, -0.32 * MODEL_SCALE, 0.67 * MODEL_SCALE), galvanized_light, model_collection)
    cylinder('Jockey Wheel', 0.15 * MODEL_SCALE, 0.07 * MODEL_SCALE, (-2.55 * MODEL_SCALE, -0.32 * MODEL_SCALE, 0.15 * MODEL_SCALE), (math.pi / 2, 0, 0), tire_material, model_collection, 28)
    box('Jockey Fork', (0.18, 0.08, 0.12), (-2.55 * MODEL_SCALE, -0.32 * MODEL_SCALE, 0.28 * MODEL_SCALE), galvanized_dark, model_collection)
    box('Winding Handle', (0.07, 0.30, 0.08), (-2.55 * MODEL_SCALE, -0.32 * MODEL_SCALE, 0.84 * MODEL_SCALE), black_material, model_collection)

    # Safety chains
    for side in (-1, 1):
        for index in range(8):
            progress = index / 7
            location = (
                (-2.56 - progress * 0.64) * MODEL_SCALE,
                side * (0.13 - progress * 0.08) * MODEL_SCALE,
                (0.48 - math.sin(progress * math.pi) * 0.20) * MODEL_SCALE,
            )
            rotation = (math.pi / 2 if index % 2 else 0, 0, 0)
            chain_link('Safety Chain Link', location, rotation, galvanized_dark, model_collection)

    # Tail lamps and latch details
    for side in (-1, 1):
        y = side * 0.66 * MODEL_SCALE
        box('Rear Red Lamp', (0.06, 0.11, 0.18), (half_length + 0.075, y, 0.90 * MODEL_SCALE), red_material, model_collection, bevel=0.015)
        box('Rear Corner Plate', (0.09, 0.06, 0.24), (half_length + 0.06, y + side * 0.12, 1.12 * MODEL_SCALE), galvanized_dark, model_collection)
    box('Tailgate Latch', (0.06, 0.08, 0.31), (half_length + 0.07, 0.72 * MODEL_SCALE, 1.06 * MODEL_SCALE), galvanized_dark, model_collection)
    box('Tailgate Handle', (0.12, 0.24, 0.05), (half_length + 0.12, 0.72 * MODEL_SCALE, 1.18 * MODEL_SCALE), galvanized_light, model_collection)

    # Separate preview collection: floor, camera and studio lights.
    floor_material = make_material('Preview Floor', (0.59, 0.64, 0.64), metallic=0.0, roughness=0.94)
    floor = cylinder('Preview Floor', 5.8 * MODEL_SCALE, 0.025 * MODEL_SCALE, (0, 0, -0.02), (0, 0, 0), floor_material, preview_collection, 96)
    floor.display_type = 'SOLID'

    world = bpy.context.scene.world or bpy.data.worlds.new('Trailer Studio World')
    bpy.context.scene.world = world
    world.use_nodes = True
    world.node_tree.nodes['Background'].inputs['Color'].default_value = (0.72, 0.77, 0.78, 1)
    world.node_tree.nodes['Background'].inputs['Strength'].default_value = 0.7

    def add_area(name, location, energy, size, color):
        data = bpy.data.lights.new(name, 'AREA')
        data.energy = energy
        data.shape = 'DISK'
        data.size = size
        data.color = color
        obj = bpy.data.objects.new(name, data)
        obj.location = location
        obj.rotation_euler = ((Vector((0, 0, 0.9)) - obj.location).to_track_quat('-Z', 'Y')).to_euler()
        preview_collection.objects.link(obj)

    add_area('Key Light', (-3.8, -4.2, 7.2), 1200, 4.5, (1.0, 0.98, 0.94))
    add_area('Fill Light', (4.2, 2.6, 4.2), 750, 3.5, (0.75, 0.88, 1.0))

    camera_data = bpy.data.cameras.new('Trailer Camera')
    camera = bpy.data.objects.new('Trailer Camera', camera_data)
    camera.location = (-5.1 * MODEL_SCALE, -5.1 * MODEL_SCALE, 3.2 * MODEL_SCALE)
    camera.rotation_euler = ((Vector((-0.15, 0, 1.0)) - camera.location).to_track_quat('-Z', 'Y')).to_euler()
    camera_data.lens = 52
    preview_collection.objects.link(camera)
    bpy.context.scene.camera = camera

    scene = bpy.context.scene
    scene.render.engine = 'BLENDER_EEVEE'
    scene.render.resolution_x = 1600
    scene.render.resolution_y = 1000
    scene.render.resolution_percentage = 60
    scene.render.image_settings.file_format = 'PNG'
    scene.view_settings.look = 'AgX - Medium High Contrast'

    # Select the complete model collection for immediate inspection.
    bpy.ops.object.select_all(action='DESELECT')
    for obj in model_collection.objects:
        obj.select_set(True)
    if model_collection.objects:
        bpy.context.view_layer.objects.active = model_collection.objects[0]

    print('Trailer model generated in TRAILER_MODEL collection.')
    print('Edit the parameters at the top of the script when measured dimensions are available.')


if __name__ == '__main__':
    build_trailer()
