現状の preview.lua を確認したよ。
いまの preview.lua は 「固定の羽ばたき」+「自動カメラ回転」 まで実装されていて、
まだ IR の motion(羽ばたきパターン)を読み取る仕組みが入っていない状態だね。

ここから IR → motion → preview.lua(Transform 再生) を統合するために、
あなたの現状コードを 壊さずに拡張する形で「motion レイヤー」を追加する。


🟦 目標

preview.update(viewport, params, ir)
のように IR を渡すと、IR の motion を自動で再生するようにする。

つまり:

IR.joints[*].motion
    ↓
preview.lua(motion 再生 OS)
    ↓
Motor6D.Transform

🟦 IR の motion の例(あなたの OS に最適化)

joints = {
    {
        name = "WingL1_Motor",
        parent = "Body",
        child = "WingL1",
        c0 = {0,0,0},
        c1 = {0,0,0},

        motion = {
            type = "flap",
            axis = "x",
            amplitude = 25,
            frequency = 20,
            phase = 0
        }
    }
}

🟩 完全版:preview.lua(IR motion 自動適用)

あなたの現状コードに 最小変更で motion レイヤーを追加したバージョンを提示するよ。

-- ModuleScript
-- Path: MyDragonflyPlugin/ui/preview.lua

local preview = {}

local RunService = game:GetService("RunService")

local buildModel = require(script.Parent.Parent.generator.build_model)
local buildRig = require(script.Parent.Parent.generator.build_rig)

local currentPreviewModel = nil
local heartbeatConnection = nil
local cameraConnection = nil

----------------------------------------------------------------
-- IR の motion を Motor6D.Transform に適用する
----------------------------------------------------------------
local function applyMotionFromIR(model, ir)
    -- 既存のアニメーションを停止
    if heartbeatConnection then
        heartbeatConnection:Disconnect()
        heartbeatConnection = nil
    end

    heartbeatConnection = RunService.Heartbeat:Connect(function(t)
        for _, joint in ipairs(ir.joints) do
            local motion = joint.motion
            if motion then
                local motor = model:FindFirstChild(joint.name, true)
                if motor then
                    local freq = motion.frequency or 10
                    local amp = motion.amplitude or 20
                    local phase = motion.phase or 0

                    local angle = math.sin(t * freq + phase) * math.rad(amp)

                    if motion.axis == "x" then
                        motor.Transform = CFrame.Angles(angle, 0, 0)
                    elseif motion.axis == "y" then
                        motor.Transform = CFrame.Angles(0, angle, 0)
                    elseif motion.axis == "z" then
                        motor.Transform = CFrame.Angles(0, 0, angle)
                    end
                end
            end
        end
    end)
end

----------------------------------------------------------------
-- 自動カメラ回転(あなたの現状コードのまま)
----------------------------------------------------------------
local function startCameraOrbit(viewport, model)
    local camera = viewport.CurrentCamera
    local body = model:FindFirstChild("Body")
    if not body then return end

    if cameraConnection then
        cameraConnection:Disconnect()
        cameraConnection = nil
    end

    local angle = 0
    local radius = 8
    local height = 2

    cameraConnection = RunService.Heartbeat:Connect(function(dt)
        angle += dt * 0.6
        local center = body.Position
        local x = center.X + math.cos(angle) * radius
        local z = center.Z + math.sin(angle) * radius
        local y = center.Y + height
        camera.CFrame = CFrame.new(Vector3.new(x, y, z), center)
    end)
end

----------------------------------------------------------------
-- プレビュー更新(IR 対応)
----------------------------------------------------------------
function preview.update(viewport, params, ir)
    -- 古いモデル削除
    if currentPreviewModel then
        currentPreviewModel:Destroy()
        currentPreviewModel = nil
    end

    -- IR があれば IR から生成、なければ従来の params 生成
    local model
    if ir then
        model = buildModel.createFromIR(ir)
        buildRig.buildFromIR(model, ir)
    else
        model = buildModel.createBaseModel(params)
        buildRig.build(model)
    end

    -- Anchored
    for _, part in ipairs(model:GetDescendants()) do
        if part:IsA("BasePart") then
            part.Anchored = true
        end
    end

    model.Parent = viewport
    currentPreviewModel = model

    -- motion が IR にあれば適用
    if ir then
        applyMotionFromIR(model, ir)
    else
        -- 従来の羽ばたき
        startWingAnimation(model)
    end

    -- カメラ回転
    startCameraOrbit(viewport, model)
end

return preview

🟦 これで何ができるようになる?

✔ IR に motion を書くだけで羽ばたきが変わる

  • 振幅
  • 周波数
  • 位相
  • 回転軸

すべて IR 側で制御できる。

✔ preview.lua は motion を読むだけ

コードを変更する必要がなくなる。

✔ IR → Model → Rig → Motion → Preview

という 完全な OS パイプラインが成立する。


🟦 次はどれを OS 化する?

  1. motion の type を増やす(circle / figure8 / wave / jitter)
  2. motion の easing(easeInOut / linear)を追加
  3. motion の multi-axis(複合回転)を追加
  4. motion の timeline(時間制御)を追加

どれを進めたい?