手順を導き出す「Logic Editor 3D」



画像

・ダウンロードされる方はこちら。↓

Logic Editor 3D - 操作説明書


このツールは、**「やりたいこと(ゴール)」を指定すると、
手持ちの手段(関数)の中から最適な組み合わせを選び出し、
「手順(スケジュール)」**を自動生成するツールです。

1. 基本的な考え方

このツールでは、データを以下の2種類に分けて管理します。

  1. TYPES (データ型): 「材料」や「状態」のこと。

    • 例:「野菜」「水」「カレー」「報告書」「未処理データ」

  2. FUNCTIONS (関数): 「処理」や「作業」のこと。

    • 例:「野菜を切る(野菜 → カット野菜)」「集計する(データ → 報告書)」

ユーザーが行うのは「材料」と「処理」の登録だけです。
「どの順番でやるか」はAIが自動で考えます。

2. 画面構成

  • 左パネル (Editor): データや関数の登録・削除を行う場所です。

  • 中央パネル (3D View): 自動生成された手順が、左から右へと流れる3Dフローチャートとして表示されます。

  • 右パネル (Result): 実行すべき手順がリスト形式で表示されます。

3. 使い方ステップバイステップ

Step 1: TYPES(データ型)を登録する

  1. 左パネルのタブ 「1. TYPES」 をクリックします。

  2. 入力欄に、作業に登場するモノの名前を入力します。(例:Raw Data)

  3. Add Type ボタンを押します。これを必要な分だけ繰り返します。

Step 2: FUNCTIONS(関数)を登録する

  1. タブ 「2. FUNCTIONS」 をクリックします。

  2. Function Name: 作業の名前を入力します。(例:Clean Data)

  3. Inputs: その作業に必要な材料を選びます。

    • ヒント: Ctrlキー (MacはCommandキー) を押しながらクリックすると、複数の材料を選択できます。

  4. Output: その作業の結果、何ができるかを選びます。

  5. Cost: 作業の大変さを数字で入れます。

    • AIの挙動: 数字が小さい(楽な)作業を優先して選びます。

  6. Register Function ボタンを押して登録します。

Step 3: RUN(実行)する

  1. タブ 「RUN」 をクリックします。

  2. Goal: 最終的に欲しい結果を選びます。(例:Monthly Report)

  3. Find Best Schedule ボタンを押します。

  4. 右パネルに「やるべきリスト」が表示され、中央画面に3D図解が表示されます。

4. ファイル操作について

画面上部のメニューバーから、データの保存や読み込みができます。

  • New: 現在のデータを全て消去し、新規作成します。

  • Open: パソコンに保存した .json ファイルを読み込みます。

  • Save: 現在編集中のファイル名で上書き保存(ダウンロード)します。

  • Save As: 名前を付けてファイルを保存(ダウンロード)します。

※ ブラウザの制約上、「上書き保存」も
「ダウンロード」として動作しますが、ファイル名は引き継がれます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Logic Editor 3D 1.0</title>
    <style>
        /* --- Professional Theme (Dark/Cyberpunk) --- */
        :root {
            --bg-dark: #121212;
            --bg-panel: #1e1e1e;
            --border: #333;
            --accent: #00e676; /* Green for Time/Success */
            --accent-sec: #2979ff; /* Blue for Logic */
            --danger: #ff5252;
            --text-main: #e0e0e0;
            --text-dim: #888;
        }
        body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; }
        
        /* Layout Grid */
        #app { display: grid; grid-template-rows: 45px 1fr 30px; height: 100%; }
        
        /* 1. Menubar */
        #menubar { background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; user-select: none; }
        .brand { font-weight: 800; color: var(--accent); margin-right: 25px; font-size: 1.1em; letter-spacing: 1px; display:flex; align-items:center; gap:10px; }
        .menu-divider { width: 1px; height: 20px; background: var(--border); margin: 0 10px; }
        .btn { background: #2c2c2c; border: 1px solid #444; color: #ccc; padding: 6px 12px; cursor: pointer; border-radius: 4px; font-size: 0.85em; transition: 0.2s; display:inline-flex; align-items:center; gap:5px; }
        .btn:hover { background: #444; border-color: #666; color: #fff; }
        .btn-primary { background: var(--accent); color: #000; border:none; font-weight:bold; }
        .btn-primary:hover { background: #00c853; }
        
        /* 2. Main Workspace */
        #workspace { display: grid; grid-template-columns: 320px 1fr 320px; overflow: hidden; }

        /* Left Panel: Editor */
        .panel { background: var(--bg-panel); display: flex; flex-direction: column; border-right: 1px solid var(--border); z-index: 10; }
        .panel-right { border-right: none; border-left: 1px solid var(--border); }
        
        .panel-header { padding: 0; display: flex; background: #252525; border-bottom: 1px solid var(--border); }
        .tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; color: var(--text-dim); font-size: 0.8em; font-weight: 600; transition: 0.2s; border-bottom: 2px solid transparent; }
        .tab:hover { color: #fff; background: #2a2a2a; }
        .tab.active { color: var(--accent); border-bottom: 2px solid var(--accent); background: #1e1e1e; }

        .scroll-area { flex: 1; overflow-y: auto; padding: 15px; }
        .scroll-area::-webkit-scrollbar { width: 6px; }
        .scroll-area::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }

        /* Forms */
        .form-group { margin-bottom: 20px; background: #252525; padding: 15px; border-radius: 6px; border: 1px solid var(--border); }
        .label { display: block; font-size: 0.7em; color: var(--text-dim); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-weight:bold; }
        .input-row { display: flex; gap: 10px; }
        input, select { width: 100%; background: #121212; border: 1px solid #333; color: #fff; padding: 8px; border-radius: 4px; font-size: 0.9em; box-sizing: border-box; transition: 0.2s; }
        input:focus, select:focus { border-color: var(--accent); outline: none; }
        
        /* List Items */
        .list-item { background: #252525; border: 1px solid var(--border); padding: 10px; margin-bottom: 5px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; transition: 0.1s; }
        .list-item:hover { border-color: #555; }
        .tag { background: #333; color: #aaa; padding: 2px 6px; border-radius: 3px; font-size: 0.7em; margin-right: 5px; }
        .del-icon { color: var(--danger); cursor: pointer; padding: 4px; opacity: 0.7; font-weight:bold; }
        .del-icon:hover { opacity: 1; background: rgba(255,82,82,0.1); border-radius: 4px; }

        /* Center Canvas */
        #canvas-wrapper { position: relative; background: radial-gradient(circle at center, #1a1a1a 0%, #000 100%); overflow: hidden; }
        #canvas-overlay { position: absolute; top: 15px; left: 15px; pointer-events: none; }
        .badge { background: rgba(0,0,0,0.7); color: var(--accent); padding: 5px 10px; border-radius: 20px; font-size: 0.8em; border: 1px solid var(--accent); backdrop-filter: blur(4px); }

        /* Right Panel: Dashboard */
        .metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; }
        .metric-card { background: #252525; padding: 10px; border-radius: 4px; text-align: center; border: 1px solid var(--border); }
        .metric-val { font-size: 1.4em; font-weight: bold; color: #fff; display:block; }
        .metric-label { font-size: 0.7em; color: var(--text-dim); text-transform: uppercase; }

        .schedule-step { border-left: 2px solid var(--border); padding-left: 15px; margin-bottom: 20px; position: relative; }
        .schedule-step::before { content: ''; position: absolute; left: -6px; top: 0; width: 10px; height: 10px; background: #333; border-radius: 50%; }
        .schedule-step.critical::before { background: var(--accent); box-shadow: 0 0 5px var(--accent); }
        .step-header { font-weight: bold; color: #fff; margin-bottom: 4px; }
        .step-meta { font-size: 0.8em; color: #888; display: flex; gap: 10px; }
        
        /* 3. Status Bar */
        #statusbar { background: var(--bg-panel); border-top: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; font-size: 0.8em; color: var(--text-dim); justify-content: space-between; }

        /* Toast Notification */
        #toast-container { position: fixed; bottom: 40px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
        .toast { background: #333; color: #fff; padding: 10px 20px; border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); border-left: 4px solid var(--accent); font-size: 0.9em; transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); pointer-events: auto; }
        .toast.show { transform: translateX(0); }
        .toast.error { border-left-color: var(--danger); }

        .hidden { display: none !important; }
    </style>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
</head>
<body>

<div id="app">
    <div id="menubar">
        <div class="brand">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20M2 12l4-4m-4 4l4 4M22 4v16"/></svg>
            LOGIC ARCHITECT <span style="font-weight:normal; font-size:0.8em; color:#666;">PRO</span>
        </div>
        
        <div class="menu-divider"></div>
        <button class="btn" onclick="app.newProject()">New</button>
        <button class="btn" onclick="app.openProject()">Open</button>
        <button class="btn" onclick="app.saveProject()">Save</button>
        <div class="menu-divider"></div>
        <div style="font-size:0.85em; color:#888;">File: <span id="filename-display" style="color:#fff;">Untitled</span></div>
    </div>

    <div id="workspace">
        
        <div class="panel">
            <div class="panel-header">
                <div class="tab active" onclick="UIManager.switchTab('run')">DASHBOARD</div>
                <div class="tab" onclick="UIManager.switchTab('types')">DB: TYPES</div>
                <div class="tab" onclick="UIManager.switchTab('funcs')">DB: FUNCS</div>
            </div>

            <div id="tab-run" class="scroll-area">
                <div class="form-group">
                    <div class="label">Strategy Optimization</div>
                    <div style="margin-bottom:10px; font-size:0.85em; color:#888;">Select the output you need and the optimization priority.</div>
                    
                    <div class="label">Target Goal</div>
                    <select id="run-goal"></select>
                    
                    <div class="label" style="margin-top:10px;">Priority Mode</div>
                    <select id="run-mode">
                        <option value="time">Minimize Time (Fastest)</option>
                        <option value="cost">Minimize Budget (Cheapest)</option>
                        <option value="balanced">Balanced (Score)</option>
                    </select>

                    <button class="btn btn-primary" style="width:100%; margin-top:15px;" onclick="app.solve()">GENERATE SCHEDULE</button>
                </div>

                <div id="diagnostics-box" class="form-group hidden" style="border-color:var(--danger);">
                    <div class="label" style="color:var(--danger);">Analysis Failed</div>
                    <div id="diagnostics-msg" style="font-size:0.85em; color:#ccc;"></div>
                </div>
            </div>

            <div id="tab-types" class="scroll-area hidden">
                <div class="form-group">
                    <div class="label">Define Resource Type</div>
                    <div class="input-row">
                        <input type="text" id="type-name" placeholder="e.g. Raw Data, HTML">
                        <button class="btn" onclick="app.addType()">Add</button>
                    </div>
                </div>
                <div id="type-list"></div>
            </div>

            <div id="tab-funcs" class="scroll-area hidden">
                <div class="form-group">
                    <div class="label">Define Logic / Task</div>
                    <input type="text" id="func-name" placeholder="Task Name (e.g. Analyze)" style="margin-bottom:10px;">
                    
                    <div class="label">Required Inputs (Ctrl+Click)</div>
                    <select id="func-inputs" multiple style="height:80px; margin-bottom:10px;"></select>
                    
                    <div class="label">Produces Output</div>
                    <select id="func-output" style="margin-bottom:10px;"></select>

                    <div class="input-row">
                        <div style="flex:1">
                            <div class="label">Time (h)</div>
                            <input type="number" id="func-time" value="1" min="0" step="0.5">
                        </div>
                        <div style="flex:1">
                            <div class="label">Cost ($)</div>
                            <input type="number" id="func-cost" value="0" min="0" step="10">
                        </div>
                    </div>
                    <button class="btn" style="width:100%; margin-top:10px;" onclick="app.addFunction()">Register Function</button>
                </div>
                <div id="func-list"></div>
            </div>
        </div>

        <div id="canvas-wrapper">
            <div id="canvas-overlay">
                <span class="badge" id="view-mode-badge">3D GRAPH VIEW</span>
            </div>
        </div>

        <div class="panel panel-right">
            <div class="panel-header">
                <div class="tab active">PROJECT METRICS</div>
            </div>
            <div id="metrics-panel" class="scroll-area">
                
                <div class="metric-grid">
                    <div class="metric-card">
                        <span class="metric-val" id="metric-time">0h</span>
                        <span class="metric-label">Total Time</span>
                    </div>
                    <div class="metric-card">
                        <span class="metric-val" id="metric-cost">$0</span>
                        <span class="metric-label">Est. Budget</span>
                    </div>
                </div>

                <div class="label" style="border-bottom:1px solid #333; padding-bottom:5px; margin-bottom:15px;">Critical Path</div>
                <div id="schedule-list">
                    <div style="text-align:center; color:#555; padding:20px;">
                        No active schedule.<br>Run the optimizer.
                    </div>
                </div>

            </div>
        </div>

    </div>

    <div id="statusbar">
        <span id="status-msg">Ready</span>
        <span style="font-family:monospace; color:#555;">v3.2.0 FIXED</span>
    </div>
</div>

<input type="file" id="file-uploader" style="display:none" accept=".json">
<div id="toast-container"></div>

<script>
    /**
     * Logic Architect Pro v3.2 Core (Initialization Fix)
     */

    // --- Toast System ---
    const Toast = {
        show(msg, type = 'info') {
            const container = document.getElementById('toast-container');
            const el = document.createElement('div');
            el.className = `toast ${type}`;
            el.innerText = msg;
            container.appendChild(el);
            requestAnimationFrame(() => el.classList.add('show'));
            setTimeout(() => {
                el.classList.remove('show');
                setTimeout(() => el.remove(), 300);
            }, 3000);
        }
    };

    // --- Main Application Class ---
    class App {
        constructor() {
            this.types = [];
            this.functions = [];
            this.currentProjectName = "Untitled Project";
            
            // Modules
            this.solver = new LogicSolver(this);
            this.viz = new Visualizer('canvas-wrapper');
            
            // Init Steps
            this.setupDragDrop();
            this.loadLocalStorage();
            this.viz.init();
            
            // Note: UIManager.refreshAll() is moved to initUI() to be called after construction
        }

        // Explicit Init Method to ensure 'app' variable is assigned
        initUI() {
            UIManager.refreshAll();
        }

        // --- Data & State ---
        newProject() {
            if(confirm("Create new project? Unsaved changes will be lost.")) {
                this.types = [];
                this.functions = [];
                this.currentProjectName = "Untitled Project";
                this.saveLocalStorage();
                UIManager.refreshAll();
                this.viz.clearScene();
                Toast.show("New project created");
            }
        }

        saveProject() {
            const data = JSON.stringify({
                version: "3.2",
                name: this.currentProjectName,
                types: this.types,
                functions: this.functions
            }, null, 2);
            const blob = new Blob([data], {type: "application/json"});
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = this.currentProjectName.replace(/\s+/g, '_') + ".json";
            a.click();
            URL.revokeObjectURL(url);
            Toast.show("Project saved successfully");
        }

        openProject() {
            document.getElementById('file-uploader').click();
        }

        loadFromFile(file) {
            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const data = JSON.parse(e.target.result);
                    this.types = data.types || [];
                    this.functions = data.functions || [];
                    this.currentProjectName = data.name || file.name.replace('.json','');
                    this.saveLocalStorage();
                    UIManager.refreshAll();
                    this.viz.clearScene();
                    Toast.show("Project loaded");
                } catch(err) {
                    Toast.show("Invalid file format", "error");
                }
            };
            reader.readAsText(file);
        }

        setupDragDrop() {
            const d = document.body;
            d.ondragover = (e) => { e.preventDefault(); d.style.opacity = 0.5; };
            d.ondragleave = (e) => { e.preventDefault(); d.style.opacity = 1; };
            d.ondrop = (e) => {
                e.preventDefault();
                d.style.opacity = 1;
                if(e.dataTransfer.files.length > 0) this.loadFromFile(e.dataTransfer.files[0]);
            };
            document.getElementById('file-uploader').onchange = (e) => {
                if(e.target.files.length > 0) this.loadFromFile(e.target.files[0]);
            };
        }

        // --- Storage ---
        loadLocalStorage() {
            const saved = localStorage.getItem('logic_pro_db');
            if(saved) {
                try {
                    const data = JSON.parse(saved);
                    this.types = data.types || [];
                    this.functions = data.functions || [];
                } catch(e) {}
            } else {
                this.loadSampleData();
            }
        }

        saveLocalStorage() {
            localStorage.setItem('logic_pro_db', JSON.stringify({
                types: this.types,
                functions: this.functions
            }));
            UIManager.updateFilename(this.currentProjectName);
        }

        loadSampleData() {
            this.types = ["Raw Footage", "Script", "Voiceover", "Rough Cut", "Final Edit", "Thumbnail", "Published Video"];
            this.functions = [
                { id: "f1", name: "Write Script", inputs: [], output: "Script", time: 4, cost: 0 },
                { id: "f2", name: "Record Voice", inputs: ["Script"], output: "Voiceover", time: 2, cost: 50 },
                { id: "f3", name: "Edit Rough Cut", inputs: ["Raw Footage", "Voiceover"], output: "Rough Cut", time: 6, cost: 100 },
                { id: "f4", name: "Final Polish", inputs: ["Rough Cut"], output: "Final Edit", time: 3, cost: 100 },
                { id: "f5", name: "Hire Editor (Fast)", inputs: ["Raw Footage", "Voiceover"], output: "Final Edit", time: 2, cost: 500 },
                { id: "f6", name: "Create Thumb", inputs: ["Final Edit"], output: "Thumbnail", time: 1, cost: 20 },
                { id: "f7", name: "Upload", inputs: ["Final Edit", "Thumbnail"], output: "Published Video", time: 0.5, cost: 0 }
            ];
            this.saveLocalStorage();
        }

        // --- Logic ---
        addType() {
            const name = document.getElementById('type-name').value.trim();
            if(!name) return Toast.show("Name required", "error");
            if(this.types.includes(name)) return Toast.show("Exists already", "error");
            this.types.push(name);
            document.getElementById('type-name').value = '';
            this.saveLocalStorage();
            UIManager.renderTypeList();
            UIManager.updateSelects();
        }

        deleteType(name) {
            if(this.functions.some(f => f.output === name || f.inputs.includes(name))) return Toast.show("In use by function", "error");
            this.types = this.types.filter(t => t !== name);
            this.saveLocalStorage();
            UIManager.refreshAll();
        }

        addFunction() {
            const name = document.getElementById('func-name').value.trim();
            const inputs = Array.from(document.getElementById('func-inputs').selectedOptions).map(o=>o.value);
            const output = document.getElementById('func-output').value;
            const time = parseFloat(document.getElementById('func-time').value) || 0;
            const cost = parseFloat(document.getElementById('func-cost').value) || 0;

            if(!name || !output) return Toast.show("Invalid function definition", "error");

            this.functions.push({
                id: 'func_'+Date.now(),
                name, inputs, output, time, cost
            });
            this.saveLocalStorage();
            UIManager.renderFuncList();
            Toast.show("Function registered");
        }

        deleteFunction(id) {
            this.functions = this.functions.filter(f => f.id !== id);
            this.saveLocalStorage();
            UIManager.renderFuncList();
        }

        solve() {
            const goal = document.getElementById('run-goal').value;
            const mode = document.getElementById('run-mode').value;
            
            document.getElementById('diagnostics-box').classList.add('hidden');
            
            const result = this.solver.solve(goal, mode);
            
            if(!result.success) {
                const box = document.getElementById('diagnostics-box');
                const msg = document.getElementById('diagnostics-msg');
                box.classList.remove('hidden');
                msg.innerHTML = `Unable to create <b>${goal}</b>.<br>Missing inputs: ` + 
                                (result.missing.length ? result.missing.join(', ') : "Logic gap detected.");
                UIManager.renderMetrics(0, 0);
                UIManager.clearSchedule();
                this.viz.clearScene();
                Toast.show("Optimization failed", "error");
                return;
            }

            const schedule = this.solver.flatten(result.tree);
            
            // Calculate Totals
            let totalTime = 0;
            let totalCost = 0;
            schedule.forEach(node => {
                totalTime += node.time;
                totalCost += node.cost;
            });

            UIManager.renderMetrics(totalTime, totalCost);
            UIManager.renderSchedule(schedule);
            this.viz.renderGraph(schedule);
            Toast.show("Schedule optimized!");
        }
    }

    /**
     * Logic Solver with Multi-Weight Support
     */
    class LogicSolver {
        constructor(app) { this.app = app; }

        solve(target, mode) {
            this.missingTypes = new Set();
            const res = this.findPath(target, mode, new Set());
            
            if(!res) {
                return { success: false, missing: Array.from(this.missingTypes) };
            }
            return { success: true, tree: res };
        }

        findPath(target, mode, visited) {
            const candidates = this.app.functions.filter(f => f.output === target);

            // Leaf Node check
            if (candidates.length === 0) {
                if (this.app.types.includes(target)) {
                    // It's a type but no function makes it. Must be raw input.
                    return { type: 'INPUT', name: target, inputs: [], totalScore: 0, time:0, cost:0 };
                }
                this.missingTypes.add(target);
                return null;
            }

            let bestOption = null;
            let bestScore = Infinity;

            for (const func of candidates) {
                if (visited.has(func.id)) continue;
                
                const newVisited = new Set(visited).add(func.id);
                const inputNodes = [];
                let currentScore = 0;
                let valid = true;
                
                // Weight Calculation
                let selfScore = 0;
                if(mode === 'time') selfScore = func.time;
                else if(mode === 'cost') selfScore = func.cost;
                else selfScore = func.time * 10 + func.cost; 

                currentScore += selfScore;

                for (const inp of func.inputs) {
                    const sub = this.findPath(inp, mode, newVisited);
                    if (!sub) { valid = false; break; }
                    inputNodes.push(sub);
                    currentScore += sub.totalScore;
                }

                if (valid) {
                    if (currentScore < bestScore) {
                        bestScore = currentScore;
                        bestOption = {
                            func: func,
                            inputs: inputNodes,
                            totalScore: currentScore,
                            time: func.time + inputNodes.reduce((s,n)=>s+n.time, 0),
                            cost: func.cost + inputNodes.reduce((s,n)=>s+n.cost, 0)
                        };
                    }
                }
            }
            return bestOption;
        }

        flatten(node, list = []) {
            if (!node || node.type === 'INPUT') return list;
            node.inputs.forEach(n => this.flatten(n, list));
            if (!list.find(i => i.id === node.func.id)) list.push(node.func);
            return list;
        }
    }

    /**
     * UIManager Class
     */
    class UIManager {
        static switchTab(id) {
            ['run','types','funcs'].forEach(t => document.getElementById('tab-'+t).classList.add('hidden'));
            document.querySelectorAll('.tab').forEach(e => e.classList.remove('active'));
            document.getElementById('tab-'+id).classList.remove('hidden');
            const idx = ['run','types','funcs'].indexOf(id);
            document.querySelectorAll('.tab')[idx].classList.add('active');
        }

        static updateFilename(name) {
            document.getElementById('filename-display').innerText = name;
        }

        static refreshAll() {
            this.renderTypeList();
            this.renderFuncList();
            this.updateSelects();
        }

        static renderTypeList() {
            const c = document.getElementById('type-list');
            c.innerHTML = '';
            if (app.types) {
                app.types.forEach(t => {
                    const el = document.createElement('div');
                    el.className = 'list-item';
                    el.innerHTML = `<span>${t}</span> <span class="del-icon" onclick="app.deleteType('${t}')">×</span>`;
                    c.appendChild(el);
                });
            }
        }

        static renderFuncList() {
            const c = document.getElementById('func-list');
            c.innerHTML = '';
            if (app.functions) {
                app.functions.forEach(f => {
                    const el = document.createElement('div');
                    el.className = 'list-item';
                    el.innerHTML = `
                        <div style="flex:1">
                            <div style="color:#fff; font-weight:bold;">${f.name}</div>
                            <div style="font-size:0.75em; color:#888;">${f.inputs.length?f.inputs.join('+'):'Input'} <span style="color:var(--accent)">➜</span> ${f.output}</div>
                            <div style="font-size:0.7em; margin-top:2px;">
                                <span class="tag">⏱ ${f.time}h</span>
                                <span class="tag">💲 ${f.cost}</span>
                            </div>
                        </div>
                        <span class="del-icon" onclick="app.deleteFunction('${f.id}')">×</span>
                    `;
                    c.appendChild(el);
                });
            }
        }

        static updateSelects() {
            const els = [document.getElementById('run-goal'), document.getElementById('func-output')];
            const inEl = document.getElementById('func-inputs');
            
            els.forEach(e => e.innerHTML = '');
            inEl.innerHTML = '';

            if (app.types) {
                app.types.forEach(t => {
                    els.forEach(e => {
                        const o = document.createElement('option');
                        o.value = t; o.text = t;
                        e.appendChild(o);
                    });
                    const o2 = document.createElement('option');
                    o2.value = t; o2.text = t;
                    inEl.appendChild(o2);
                });
            }
        }

        static renderMetrics(time, cost) {
            document.getElementById('metric-time').innerText = time.toFixed(1) + 'h';
            document.getElementById('metric-cost').innerText = '$' + cost.toFixed(0);
        }

        static renderSchedule(list) {
            const c = document.getElementById('schedule-list');
            c.innerHTML = '';
            list.forEach((item, i) => {
                const el = document.createElement('div');
                el.className = 'schedule-step ' + (item.time > 0 ? 'critical' : '');
                el.innerHTML = `
                    <div class="step-header">${i+1}. ${item.name}</div>
                    <div class="step-meta">
                        <span>Output: ${item.output}</span>
                        <span>${item.time}h / $${item.cost}</span>
                    </div>
                `;
                c.appendChild(el);
            });
        }
        
        static clearSchedule() {
            document.getElementById('schedule-list').innerHTML = '<div style="text-align:center;color:#555;padding:20px;">Optimization Failed</div>';
        }
    }

    /**
     * 3D Visualizer
     */
    class Visualizer {
        constructor(id) {
            this.container = document.getElementById(id);
            this.meshMap = new Map();
        }

        init() {
            this.scene = new THREE.Scene();
            this.scene.background = new THREE.Color(0x0e0e0e);
            
            const w = this.container.clientWidth;
            const h = this.container.clientHeight;
            this.camera = new THREE.PerspectiveCamera(50, w/h, 0.1, 1000);
            this.camera.position.set(0, 20, 40);

            this.renderer = new THREE.WebGLRenderer({antialias:true});
            this.renderer.setSize(w, h);
            this.container.appendChild(this.renderer.domElement);
            
            this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
            this.controls.enableDamping = true;

            const amb = new THREE.AmbientLight(0xffffff, 0.5);
            const dir = new THREE.DirectionalLight(0xffffff, 1);
            dir.position.set(10, 30, 10);
            this.scene.add(amb);
            this.scene.add(dir);

            const grid = new THREE.GridHelper(100, 20, 0x222222, 0x111111);
            this.scene.add(grid);

            this.raycaster = new THREE.Raycaster();
            this.mouse = new THREE.Vector2();
            this.renderer.domElement.addEventListener('click', (e) => this.onClick(e));
            window.addEventListener('resize', () => this.onResize());
            
            this.animate();
        }

        renderGraph(schedule) {
            this.clearScene();
            if(!schedule || schedule.length === 0) return;

            const levels = new Map();
            schedule.forEach((node, i) => {
                levels.set(node.id, i * 10);
            });

            schedule.forEach((node, i) => {
                const geo = new THREE.BoxGeometry(4, 1, 2);
                const mat = new THREE.MeshLambertMaterial({color: 0x00e676});
                const mesh = new THREE.Mesh(geo, mat);
                
                mesh.position.set((i * 8) - (schedule.length * 4), 1, (i % 2 === 0 ? 3 : -3));
                mesh.userData = { id: node.id, info: node };
                
                this.scene.add(mesh);
                this.meshMap.set(node.id, mesh);
                this.addLabel(node.name, mesh.position);

                node.inputs.forEach(inpName => {
                    const provider = schedule.find(p => p.output === inpName);
                    if(provider) {
                        const pm = this.meshMap.get(provider.id);
                        if(pm) this.drawCurve(pm.position, mesh.position);
                    } else {
                        const start = mesh.position.clone().add(new THREE.Vector3(-3, -3, 0));
                        this.drawCurve(start, mesh.position, 0xff9100);
                        this.addLabel(inpName, start, 0.6);
                    }
                });
            });
            this.controls.reset();
        }

        drawCurve(p1, p2, color=0x2979ff) {
            const mid = p1.clone().add(p2).multiplyScalar(0.5);
            mid.y += 2; 
            const curve = new THREE.QuadraticBezierCurve3(p1, mid, p2);
            const pts = curve.getPoints(20);
            const geo = new THREE.BufferGeometry().setFromPoints(pts);
            const mat = new THREE.LineBasicMaterial({color: color, linewidth: 2});
            this.scene.add(new THREE.Line(geo, mat));
        }

        addLabel(text, pos, scale=1) {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            canvas.width = 256; canvas.height = 64;
            ctx.font = "Bold 32px Arial";
            ctx.fillStyle = "white";
            ctx.textAlign = "center";
            ctx.fillText(text, 128, 42);
            const tex = new THREE.CanvasTexture(canvas);
            const sp = new THREE.Sprite(new THREE.SpriteMaterial({map:tex, transparent:true}));
            sp.position.copy(pos);
            sp.position.y += 2;
            sp.scale.set(5*scale, 1.25*scale, 1);
            this.scene.add(sp);
        }

        clearScene() {
            this.meshMap.clear();
            this.scene.children = this.scene.children.filter(c => c.type === "GridHelper" || c.type === "AmbientLight" || c.type === "DirectionalLight");
        }

        onClick(e) {
            const rect = this.renderer.domElement.getBoundingClientRect();
            this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
            this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
            
            this.raycaster.setFromCamera(this.mouse, this.camera);
            const intersects = this.raycaster.intersectObjects(this.scene.children);
            
            if(intersects.length > 0) {
                const obj = intersects[0].object;
                if(obj.userData && obj.userData.info) {
                    const info = obj.userData.info;
                    Toast.show(`${info.name} (Time: ${info.time}h / Cost: $${info.cost})`);
                }
            }
        }

        onResize() {
            const w = this.container.clientWidth;
            const h = this.container.clientHeight;
            this.camera.aspect = w/h;
            this.camera.updateProjectionMatrix();
            this.renderer.setSize(w, h);
        }

        animate() {
            requestAnimationFrame(()=>this.animate());
            this.controls.update();
            this.renderer.render(this.scene, this.camera);
        }
    }

    var app;
    window.onload = () => {
        app = new App();
        app.initUI();
    };
</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
手順を導き出す「Logic Editor 3D」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1