手順を導き出す「Logic Editor 3D」
・ダウンロードされる方はこちら。↓
Logic Editor 3D - 操作説明書
このツールは、**「やりたいこと(ゴール)」を指定すると、
手持ちの手段(関数)の中から最適な組み合わせを選び出し、
「手順(スケジュール)」**を自動生成するツールです。
1. 基本的な考え方
このツールでは、データを以下の2種類に分けて管理します。
TYPES (データ型): 「材料」や「状態」のこと。
例:「野菜」「水」「カレー」「報告書」「未処理データ」
FUNCTIONS (関数): 「処理」や「作業」のこと。
例:「野菜を切る(野菜 → カット野菜)」「集計する(データ → 報告書)」
ユーザーが行うのは「材料」と「処理」の登録だけです。
「どの順番でやるか」はAIが自動で考えます。
2. 画面構成
左パネル (Editor): データや関数の登録・削除を行う場所です。
中央パネル (3D View): 自動生成された手順が、左から右へと流れる3Dフローチャートとして表示されます。
右パネル (Result): 実行すべき手順がリスト形式で表示されます。
3. 使い方ステップバイステップ
Step 1: TYPES(データ型)を登録する
左パネルのタブ 「1. TYPES」 をクリックします。
入力欄に、作業に登場するモノの名前を入力します。(例:Raw Data)
Add Type ボタンを押します。これを必要な分だけ繰り返します。
Step 2: FUNCTIONS(関数)を登録する
タブ 「2. FUNCTIONS」 をクリックします。
Function Name: 作業の名前を入力します。(例:Clean Data)
Inputs: その作業に必要な材料を選びます。
ヒント: Ctrlキー (MacはCommandキー) を押しながらクリックすると、複数の材料を選択できます。
Output: その作業の結果、何ができるかを選びます。
Cost: 作業の大変さを数字で入れます。
AIの挙動: 数字が小さい(楽な)作業を優先して選びます。
Register Function ボタンを押して登録します。
Step 3: RUN(実行)する
タブ 「RUN」 をクリックします。
Goal: 最終的に欲しい結果を選びます。(例:Monthly Report)
Find Best Schedule ボタンを押します。
右パネルに「やるべきリスト」が表示され、中央画面に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>

コメント