手順を導き出す「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="ja">
<head>
<meta charset="UTF-8">
<title>Logic Editor 3D 1.0</title>
<style>
/* --- Styles --- */
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background-color: #121212; color: #e0e0e0; font-family: 'Segoe UI', Roboto, sans-serif; }
/* Layout */
#app { display: flex; flex-direction: column; height: 100%; }
/* Menubar */
#menubar { height: 40px; background: #1e1e1e; border-bottom: 1px solid #333; display: flex; align-items: center; padding: 0 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.5); }
.brand { font-weight: bold; color: #00e676; margin-right: 20px; letter-spacing: 1px; font-size:1.1em; }
.menu-group { display: flex; gap: 5px; border-right: 1px solid #333; padding-right: 15px; margin-right: 15px; }
.menu-btn { background: #2d2d2d; border: 1px solid #444; color: #ccc; padding: 5px 12px; cursor: pointer; border-radius: 3px; font-size: 0.85em; transition: 0.2s; }
.menu-btn:hover { background: #444; color: #fff; border-color: #666; }
.file-info { font-size: 0.85em; color: #888; }
.file-name { color: #00e676; font-weight: bold; margin-left: 5px; }
#main-area { display: flex; flex: 1; overflow: hidden; }
/* Left Panel: Editor */
#editor-panel { width: 340px; background: #181818; border-right: 1px solid #333; display: flex; flex-direction: column; z-index: 10; }
.tab-header { display: flex; border-bottom: 1px solid #333; }
.tab { flex: 1; padding: 12px; text-align: center; cursor: pointer; background: #222; color: #888; font-size: 0.85em; font-weight: bold; border-bottom: 2px solid transparent; }
.tab:hover { color: #ddd; }
.tab.active { background: #181818; color: #00e676; border-bottom: 2px solid #00e676; }
.panel-content { flex: 1; overflow-y: auto; padding: 15px; }
.form-group { margin-bottom: 20px; background: #222; padding: 12px; border-radius: 6px; border: 1px solid #333; }
.form-label { display: block; font-size: 0.75em; color: #aaa; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
input, select { width: 100%; background: #333; border: 1px solid #444; color: #eee; padding: 8px; box-sizing: border-box; border-radius: 4px; margin-bottom: 8px; font-size:0.95em; }
input:focus, select:focus { border-color: #00e676; outline: none; }
/* List Items */
.item-list { margin-top: 10px; }
.db-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #2a2a2a; font-size: 0.9em; background:#1f1f1f; margin-bottom:2px; border-radius:3px;}
.db-item:hover { background: #2a2a2a; }
.del-btn { color: #ff5252; cursor: pointer; font-weight: bold; padding: 2px 8px; border-radius:4px; }
.del-btn:hover { background: rgba(255, 82, 82, 0.1); }
button.primary { width: 100%; background: #00e676; color: #000; font-weight: bold; border: none; padding: 10px; cursor: pointer; border-radius: 4px; margin-top: 5px; transition:0.2s; }
button.primary:hover { background: #00c853; }
/* Center: 3D */
#canvas-container { flex: 1; position: relative; background: radial-gradient(circle at center, #1a1a1a 0%, #000 100%); overflow: hidden; }
#overlay { position: absolute; top: 10px; left: 10px; pointer-events: none; }
.overlay-tag { background: rgba(0,0,0,0.6); color: #00e676; padding: 5px 10px; border-radius: 15px; font-size: 0.8em; border: 1px solid #00e676; }
/* Right: Results */
#result-panel { width: 300px; background: #181818; border-left: 1px solid #333; display: flex; flex-direction: column; overflow-y: auto; }
.step-card { padding: 15px; border-bottom: 1px solid #333; position: relative; background: #1e1e1e; margin-bottom:1px; }
.step-idx { position: absolute; top: 15px; right: 15px; font-size: 1.5em; color: #333; font-weight: bold; }
.step-name { font-weight: bold; color: #fff; margin-bottom: 5px; font-size: 1.1em; }
.step-detail { font-size: 0.85em; color: #aaa; margin-bottom: 5px; }
.missing-warning { background: rgba(255, 145, 0, 0.1); color: #ff9100; font-size: 0.8em; margin-top: 5px; border: 1px solid #ff9100; padding: 4px 8px; border-radius: 4px; display: inline-block; }
.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">LOGIC EDITOR</div>
<div class="menu-group">
<button class="menu-btn" onclick="app.newFile()">New</button>
<button class="menu-btn" onclick="app.openFile()">Open...</button>
<button class="menu-btn" onclick="app.saveFile()">Save</button>
<button class="menu-btn" onclick="app.saveFileAs()">Save As...</button>
</div>
<div class="file-info">Editing: <span id="current-filename" class="file-name">Untitled.json</span></div>
<div style="flex:1"></div>
<div style="font-size:0.8em; color:#666;">Auto-saved to LocalStorage</div>
</div>
<div id="main-area">
<div id="editor-panel">
<div class="tab-header">
<div class="tab active" onclick="app.ui.switchTab('run')">RUN</div>
<div class="tab" onclick="app.ui.switchTab('types')">1. TYPES</div>
<div class="tab" onclick="app.ui.switchTab('funcs')">2. FUNCTIONS</div>
</div>
<div id="tab-run" class="panel-content">
<div style="margin-bottom:20px; padding:10px; background:#222; border-left:3px solid #00e676; font-size:0.9em; color:#ccc;">
<strong>Process:</strong> Select your desired output. The AI will calculate the most efficient workflow based on your defined functions.
</div>
<div class="form-group">
<label class="form-label">Goal (Desired Output)</label>
<select id="run-goal"></select>
<button class="primary" onclick="app.solve()">Find Best Schedule</button>
</div>
</div>
<div id="tab-types" class="panel-content hidden">
<div class="form-group">
<label class="form-label">Define Data/Object Type</label>
<input type="text" id="type-name" placeholder="e.g. Raw Data, Monthly Report">
<button class="primary" onclick="app.addType()">Add Type</button>
</div>
<label class="form-label">Registered Types</label>
<div id="type-list" class="item-list"></div>
</div>
<div id="tab-funcs" class="panel-content hidden">
<div class="form-group">
<label class="form-label">Function Name</label>
<input type="text" id="func-name" placeholder="e.g. Analyze Data">
<label class="form-label">Inputs (Ctrl+Click for multiple)</label>
<select id="func-inputs" multiple style="height:100px;"></select>
<label class="form-label">Output (Result)</label>
<select id="func-output"></select>
<label class="form-label">Cost (Effort/Time)</label>
<input type="number" id="func-cost" value="1" min="1">
<button class="primary" onclick="app.addFunction()">Register Function</button>
</div>
<label class="form-label">Registered Functions</label>
<div id="func-list" class="item-list"></div>
</div>
</div>
<div id="canvas-container">
<div id="overlay">
<span class="overlay-tag" id="status-tag">Ready</span>
</div>
</div>
<div id="result-panel">
<div style="padding:12px; font-weight:bold; border-bottom:1px solid #333; background:#222; color:#00e676;">OPTIMIZED SCHEDULE</div>
<div id="schedule-container">
<div style="padding:30px; color:#555; text-align:center;">
Select a goal in the RUN tab to generate a schedule.
</div>
</div>
</div>
</div>
</div>
<input type="file" id="file-uploader" style="display:none" accept=".json">
<script>
/**
* Logic Editor Core
*/
class App {
constructor() {
this.types = [];
this.functions = [];
this.currentFileName = "Untitled.json";
this.solver = new LogicSolver(this);
this.viz = new Visualizer('canvas-container');
this.ui = new UIManager(this);
// Load initial data (LocalStorage or Default)
this.loadLocalStorage();
this.viz.init();
}
// --- File Operations ---
newFile() {
if(confirm("Create new file? Unsaved changes will be lost.")) {
this.types = [];
this.functions = [];
this.currentFileName = "Untitled.json";
this.ui.refreshAll();
this.viz.clearScene();
this.ui.updateFilename();
}
}
openFile() {
document.getElementById('file-uploader').click();
}
handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
this.types = data.types || [];
this.functions = data.functions || [];
this.currentFileName = file.name;
this.ui.refreshAll();
this.viz.clearScene();
this.ui.updateFilename();
alert("File loaded successfully.");
} catch (err) {
alert("Error loading file: Invalid format.");
}
};
reader.readAsText(file);
event.target.value = ''; // Reset input
}
saveFile() {
// "Save" behaves like download, but uses current filename
this.downloadJSON(this.currentFileName);
}
saveFileAs() {
const name = prompt("Enter file name:", this.currentFileName);
if (name) {
this.currentFileName = name.endsWith('.json') ? name : name + '.json';
this.ui.updateFilename();
this.downloadJSON(this.currentFileName);
}
}
downloadJSON(filename) {
const data = JSON.stringify({
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 = filename;
a.click();
URL.revokeObjectURL(url);
// Also update local storage
this.saveLocalStorage();
}
// --- Local Storage (Auto-save) ---
loadLocalStorage() {
const saved = localStorage.getItem('logic_editor_db');
if (saved) {
try {
const data = JSON.parse(saved);
this.types = data.types || [];
this.functions = data.functions || [];
} catch(e) { /* Ignore */ }
}
this.ui.refreshAll();
}
saveLocalStorage() {
localStorage.setItem('logic_editor_db', JSON.stringify({
types: this.types,
functions: this.functions
}));
}
// --- CRUD Operations ---
addType() {
const name = document.getElementById('type-name').value.trim();
if (!name) return alert("Enter a name.");
if (this.types.includes(name)) return alert("Type already exists.");
this.types.push(name);
document.getElementById('type-name').value = '';
this.saveLocalStorage();
this.ui.refreshAll();
}
deleteType(name) {
const used = this.functions.some(f => f.output === name || f.inputs.includes(name));
if (used) return alert("Cannot delete: Type is in use.");
this.types = this.types.filter(t => t !== name);
this.saveLocalStorage();
this.ui.refreshAll();
}
addFunction() {
const name = document.getElementById('func-name').value.trim();
const inputsEl = document.getElementById('func-inputs');
const output = document.getElementById('func-output').value;
const cost = parseInt(document.getElementById('func-cost').value);
const inputs = Array.from(inputsEl.selectedOptions).map(opt => opt.value);
if (!name || !output) return alert("Name and Output are required.");
this.functions.push({
id: 'func_' + Date.now(),
name: name,
inputs: inputs,
output: output,
cost: cost || 1
});
document.getElementById('func-name').value = '';
this.saveLocalStorage();
this.ui.refreshAll();
}
deleteFunction(id) {
this.functions = this.functions.filter(f => f.id !== id);
this.saveLocalStorage();
this.ui.refreshAll();
}
solve() {
const goal = document.getElementById('run-goal').value;
if(!goal) return alert("Select a goal first.");
const solution = this.solver.solve(goal);
if (!solution) return alert("Impossible to reach this goal.");
const schedule = this.solver.flatten(solution);
this.ui.renderSchedule(schedule);
this.viz.renderGraph(schedule);
}
}
/**
* Logic Solver
*/
class LogicSolver {
constructor(app) { this.app = app; }
solve(targetType, visited = new Set()) {
const candidates = this.app.functions.filter(f => f.output === targetType);
if (candidates.length === 0) {
// Leaf node (Raw Input)
return this.app.types.includes(targetType) ? null : undefined;
}
let bestPath = null;
let minCost = Infinity;
for (const func of candidates) {
if (visited.has(func.id)) continue;
const newVisited = new Set(visited).add(func.id);
const inputNodes = [];
let currentCost = func.cost;
let possible = true;
for (const inputType of func.inputs) {
const subPath = this.solve(inputType, newVisited);
if (subPath === undefined) { possible = false; break; }
if (subPath) {
currentCost += subPath.totalCost;
inputNodes.push(subPath);
} else {
inputNodes.push({ type: 'INPUT', name: inputType, inputs:[], totalCost:0 });
}
}
if (possible && currentCost < minCost) {
minCost = currentCost;
bestPath = { func: func, inputs: inputNodes, totalCost: currentCost };
}
}
return bestPath;
}
flatten(node, list = []) {
if (!node || node.type === 'INPUT') return list;
node.inputs.forEach(n => this.flatten(n, list));
if (!list.find(item => item.id === node.func.id)) list.push(node.func);
return list;
}
}
/**
* UI Manager
*/
class UIManager {
constructor(app) {
this.app = app;
document.getElementById('file-uploader').addEventListener('change', (e) => app.handleFileSelect(e));
}
switchTab(name) {
['run', 'types', 'funcs'].forEach(t => document.getElementById(`tab-${t}`).classList.add('hidden'));
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
document.getElementById(`tab-${name}`).classList.remove('hidden');
// Highlight active tab logic
const idx = ['run', 'types', 'funcs'].indexOf(name);
document.querySelectorAll('.tab')[idx].classList.add('active');
}
updateFilename() {
document.getElementById('current-filename').innerText = this.app.currentFileName;
}
refreshAll() {
this.renderTypeList();
this.renderFuncList();
this.updateSelects();
}
renderTypeList() {
const el = document.getElementById('type-list');
el.innerHTML = '';
this.app.types.forEach(t => {
const div = document.createElement('div');
div.className = 'db-item';
div.innerHTML = `<span>${t}</span> <span class="del-btn" onclick="app.deleteType('${t}')">×</span>`;
el.appendChild(div);
});
}
renderFuncList() {
const el = document.getElementById('func-list');
el.innerHTML = '';
this.app.functions.forEach(f => {
const div = document.createElement('div');
div.className = 'db-item';
div.innerHTML = `
<div style="flex:1">
<div style="font-weight:bold; color:#fff;">${f.name}</div>
<div style="font-size:0.75em; color:#888;">IN: [${f.inputs.join(', ')}] → OUT: ${f.output} (Cost:${f.cost})</div>
</div>
<span class="del-btn" onclick="app.deleteFunction('${f.id}')">×</span>
`;
el.appendChild(div);
});
}
updateSelects() {
const goalSel = document.getElementById('run-goal');
const inSel = document.getElementById('func-inputs');
const outSel = document.getElementById('func-output');
[goalSel, inSel, outSel].forEach(s => s.innerHTML = '');
this.app.types.forEach(t => {
[goalSel, outSel].forEach(s => {
const opt = document.createElement('option');
opt.value = t; opt.text = t;
s.appendChild(opt);
});
const opt = document.createElement('option');
opt.value = t; opt.text = t;
inSel.appendChild(opt);
});
}
renderSchedule(list) {
const container = document.getElementById('schedule-container');
container.innerHTML = '';
if(list.length === 0) {
container.innerHTML = '<div style="padding:20px; text-align:center;">Goal is already achieved (Raw Input).</div>';
return;
}
list.forEach((step, i) => {
const div = document.createElement('div');
div.className = 'step-card';
let missingHtml = '';
const requiredInputs = step.inputs.filter(inp => !list.find(prev => prev.output === inp));
requiredInputs.forEach(req => missingHtml += `<div class="missing-warning">Input Needed: ${req}</div> `);
div.innerHTML = `
<div class="step-idx">${i+1}</div>
<div class="step-name">${step.name}</div>
<div class="step-detail">Output: ${step.output}</div>
${missingHtml}
`;
container.appendChild(div);
});
}
}
/**
* 3D Visualizer
*/
class Visualizer {
constructor(domId) {
this.domId = domId;
this.scene = null;
this.meshMap = new Map();
}
init() {
const container = document.getElementById(this.domId);
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x0a0a0a);
this.camera = new THREE.PerspectiveCamera(50, container.clientWidth/container.clientHeight, 0.1, 1000);
this.camera.position.set(0, 15, 30);
this.renderer = new THREE.WebGLRenderer({antialias:true});
this.renderer.setSize(container.clientWidth, container.clientHeight);
container.appendChild(this.renderer.domElement);
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
const grid = new THREE.GridHelper(100, 20, 0x222222, 0x111111);
this.scene.add(grid);
this.scene.add(new THREE.AmbientLight(0x404040));
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(10,20,10);
this.scene.add(light);
window.addEventListener('resize', () => {
this.camera.aspect = container.clientWidth/container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(container.clientWidth, container.clientHeight);
});
this.animate();
}
clearScene() {
this.meshMap.forEach(m => this.scene.remove(m));
this.meshMap.clear();
this.scene.children.filter(c => c.type==="Line" || c.type==="Sprite").forEach(c => this.scene.remove(c));
}
renderGraph(schedule) {
this.clearScene();
if(!schedule || schedule.length === 0) return;
const spacingX = 8;
schedule.forEach((node, i) => {
const geo = new THREE.BoxGeometry(3, 1, 2);
const mat = new THREE.MeshLambertMaterial({color: 0x00e676});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set((i * spacingX) - (schedule.length * spacingX / 2), 1, (i%2 === 0 ? 2 : -2));
this.scene.add(mesh);
this.meshMap.set(node.id, mesh);
this.addLabel(node.name, mesh.position);
node.inputs.forEach(inpName => {
const provider = schedule.find(prev => prev.output === inpName);
if (provider) {
const providerMesh = this.meshMap.get(provider.id);
if(providerMesh) this.drawLine(providerMesh.position, mesh.position);
} else {
const inputPos = mesh.position.clone().add(new THREE.Vector3(-2, -2, 0));
this.drawLine(inputPos, mesh.position, 0xff9100);
this.addLabel(inpName, inputPos, 0.5);
}
});
});
this.controls.reset();
}
drawLine(p1, p2, color=0x4db8ff) {
const mat = new THREE.LineBasicMaterial({color: color});
const geo = new THREE.BufferGeometry().setFromPoints([p1, p2]);
this.scene.add(new THREE.Line(geo, mat));
}
addLabel(text, pos, scale=1.0) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 256; canvas.height = 64;
ctx.font = "Bold 36px Arial";
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.fillText(text, 128, 45);
const tex = new THREE.CanvasTexture(canvas);
const sp = new THREE.Sprite(new THREE.SpriteMaterial({map:tex}));
sp.position.copy(pos);
sp.position.y += 1.5;
sp.scale.set(4*scale, 1*scale, 1);
this.scene.add(sp);
}
animate() {
requestAnimationFrame(() => this.animate());
this.controls.update();
this.renderer.render(this.scene, this.camera);
}
}
var app;
window.onload = () => { app = new App(); };
</script>
</body>
</html>

コメント