Files
2026-01-10 16:17:00 +08:00

595 lines
22 KiB
JavaScript

// 修正:使用绝对路径引用,解决 "Failed to fetch" 问题
import { app } from "/scripts/app.js";
const directions = [
"front", "front-right quarter", "right side", "back-right quarter",
"back", "back-left quarter", "left side", "front-left quarter"
];
const angles = ["low-angle", "eye-level", "elevated", "high-angle"];
const shots = ["close-up", "medium shot", "wide shot"];
app.registerExtension({
name: "Rabbit.Prompt.Generator",
async beforeRegisterNodeDef(nodeDef, nodeData, app) {
if (nodeData.name === "RabbitCameraSelector") {
const onNodeCreated = nodeDef.prototype.onNodeCreated;
nodeDef.prototype.onNodeCreated = function () {
onNodeCreated?.apply(this, arguments);
// 节点设置为左右分栏布局 - 缩小整体高度
this.setSize([750, 380]);
const widgetInput = this.widgets[0];
// 大幅缩小文本框高度
widgetInput.computeSize = function() { return [this.width, 35]; };
if (widgetInput.element) {
widgetInput.element.style.height = "35px";
widgetInput.element.style.minHeight = "35px";
widgetInput.element.style.maxHeight = "35px";
widgetInput.element.style.fontSize = "11px";
widgetInput.element.style.padding = "6px 8px";
widgetInput.element.style.lineHeight = "1.2";
widgetInput.element.style.resize = "none";
}
widgetInput.options = widgetInput.options || {};
widgetInput.options.height = 35;
// === 主容器:左右分栏 ===
const mainContainer = document.createElement("div");
mainContainer.style.cssText = `
display:flex; gap:8px; padding:6px 6px 20px 6px; width:100%; height:calc(100% - 10px);
box-sizing:border-box; overflow:hidden; min-height:0;
margin-top:20px;
`;
// === 左侧:控制面板 ===
const leftPanel = document.createElement("div");
leftPanel.style.cssText = `
flex:0 1 45%; display:flex; flex-direction:column; gap:8px;
min-width:150px; max-width:45%; overflow:hidden;
`;
// 当前选择状态
let selectedDir = 0;
let selectedAngle = 1;
let selectedShot = 1;
// 创建下拉按钮
function createDropdown(label, options, initialIdx, onSelect) {
const wrapper = document.createElement("div");
wrapper.style.cssText = "position:relative; width:100%; min-width:0; flex-shrink:0;";
const btn = document.createElement("button");
btn.innerText = `${label}: ${options[initialIdx]}`;
btn.style.cssText = `
width:100%; padding:8px 10px; background:#2a2a2a; color:#fff;
border:1px solid #555; border-radius:4px; cursor:pointer;
font-size:12px; text-align:left; transition:all 0.2s;
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
box-sizing:border-box; min-height:32px;
`;
btn.onmouseenter = () => btn.style.background = "#3a3a3a";
btn.onmouseleave = () => btn.style.background = "#2a2a2a";
const dropdown = document.createElement("div");
dropdown.style.cssText = `
display:none; position:absolute; top:100%; left:0; right:0;
background:#2a2a2a; border:1px solid #555; border-radius:4px;
margin-top:2px; max-height:180px; overflow-y:auto; z-index:1000;
box-shadow:0 4px 12px rgba(0,0,0,0.6);
`;
options.forEach((opt, idx) => {
const item = document.createElement("div");
item.innerText = opt;
item.style.cssText = `
padding:8px 10px; cursor:pointer; font-size:11px;
color:#ddd; transition:background 0.15s;
`;
item.onmouseenter = () => item.style.background = "#444";
item.onmouseleave = () => item.style.background = "transparent";
item.onclick = () => {
btn.innerText = `${label}: ${opt}`;
dropdown.style.display = "none";
onSelect(idx);
};
dropdown.appendChild(item);
});
btn.onclick = (e) => {
e.stopPropagation();
const isOpen = dropdown.style.display === "block";
document.querySelectorAll('.rabbit-dropdown').forEach(d => d.style.display = "none");
dropdown.style.display = isOpen ? "none" : "block";
};
dropdown.className = "rabbit-dropdown";
wrapper.appendChild(btn);
wrapper.appendChild(dropdown);
return { wrapper, updateText: (text) => { btn.innerText = `${label}: ${text}`; } };
}
// 关闭所有下拉菜单
document.addEventListener("click", () => {
document.querySelectorAll('.rabbit-dropdown').forEach(d => d.style.display = "none");
});
// 三个按钮及其更新函数
const dirDropdown = createDropdown("Direction", directions, selectedDir, idx => {
selectedDir = idx;
updateRabbitView();
updatePromptFromSelections();
});
const angleDropdown = createDropdown("Angle", angles, selectedAngle, idx => {
selectedAngle = idx;
updateRabbitView();
updatePromptFromSelections();
});
const shotDropdown = createDropdown("Shot", shots, selectedShot, idx => {
selectedShot = idx;
updateRabbitView();
updatePromptFromSelections();
});
leftPanel.appendChild(dirDropdown.wrapper);
leftPanel.appendChild(angleDropdown.wrapper);
leftPanel.appendChild(shotDropdown.wrapper);
// 状态提示
const statusHint = document.createElement("div");
statusHint.style.cssText = `
margin-top:auto; padding:8px; background:#1a1a1a;
border:1px solid #333; border-radius:4px;
color:#888; font-size:10px; text-align:center; min-height:20px;
word-break:break-word; overflow:hidden; flex-shrink:0;
`;
statusHint.innerText = "Drag 3D rabbit →";
leftPanel.appendChild(statusHint);
// === 右侧:3D 视图 ===
const rightPanel = document.createElement("div");
rightPanel.style.cssText = `
flex:1 1 55%; background:#1a1a1a; border:1px solid #333;
border-radius:4px; position:relative; overflow:hidden;
min-width:0; min-height:0;
`;
const canvas = document.createElement("canvas");
canvas.style.cssText = "width:100%; height:100%; cursor:grab; display:block;";
rightPanel.appendChild(canvas);
const hint = document.createElement("div");
hint.style.cssText = `
position:absolute; bottom:6px; left:6px; right:6px;
text-align:center; color:#888; font-size:10px;
pointer-events:none; background:rgba(0,0,0,0.85);
padding:4px; border-radius:3px;
`;
hint.innerText = "Drag | Wheel to Zoom";
rightPanel.appendChild(hint);
// 组装主容器
mainContainer.appendChild(leftPanel);
mainContainer.appendChild(rightPanel);
this.addDOMWidget("rabbit_main", "HTML", mainContainer);
// 更新canvas尺寸以适应容器
function updateCanvasSize() {
const rect = rightPanel.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
canvas.width = rect.width;
canvas.height = rect.height;
}
}
// 初始化canvas尺寸
setTimeout(() => updateCanvasSize(), 0);
// 监听窗口resize
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize();
});
resizeObserver.observe(rightPanel);
// 更新兔子视角(从按钮选择)
let updateRabbitFromButtons = null;
function updateRabbitView() {
if (updateRabbitFromButtons) {
updateRabbitFromButtons(selectedDir, selectedAngle, selectedShot);
}
}
// 更新提示词
function updatePromptFromSelections() {
const text = `<sks> ${directions[selectedDir]} view ${angles[selectedAngle]} shot ${shots[selectedShot]}`;
widgetInput.value = text;
navigator.clipboard.writeText(text).then(() => {
statusHint.innerText = "✓ COPIED: " + text;
statusHint.style.color = "#4ade80";
setTimeout(() => {
statusHint.style.color = "#888";
statusHint.innerText = "Drag 3D rabbit →";
}, 2000);
}).catch(() => {
statusHint.innerText = "✓ " + text;
statusHint.style.color = "#4ade80";
setTimeout(() => {
statusHint.style.color = "#888";
statusHint.innerText = "Drag 3D rabbit →";
}, 2000);
});
}
// 启动 3D 引擎
try {
updateRabbitFromButtons = initImproved3D(canvas, hint, (dir, ang, sht) => {
selectedDir = dir;
selectedAngle = ang;
selectedShot = sht;
dirDropdown.updateText(directions[dir]);
angleDropdown.updateText(angles[ang]);
shotDropdown.updateText(shots[sht]);
updatePromptFromSelections();
});
} catch (e) {
console.error("Rabbit Engine Error:", e);
hint.innerText = "Error starting engine";
hint.style.color = "#ef4444";
}
};
}
}
});
// === 改进的 3D 引擎 ===
function initImproved3D(canvas, hint, onUpdate) {
const ctx = canvas.getContext("2d");
// 状态 - 初始朝向front (yaw=0)
let yaw = 0;
let pitch = 0.3;
let dist = 450;
let targetYaw = 0;
let targetPitch = 0.3;
let targetDist = 450;
// 交互
let isDown = false;
let lastX = 0, lastY = 0;
canvas.addEventListener('mousedown', e => {
isDown = true;
canvas.style.cursor = "grabbing";
const rect = canvas.getBoundingClientRect();
lastX = (e.clientX - rect.left) * (canvas.width / rect.width);
lastY = (e.clientY - rect.top) * (canvas.height / rect.height);
});
window.addEventListener('mouseup', () => {
if (isDown) {
isDown = false;
canvas.style.cursor = "grab";
onUpdate(...calcParams());
}
});
canvas.addEventListener('mousemove', e => {
if (!isDown) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (canvas.width / rect.width);
const y = (e.clientY - rect.top) * (canvas.height / rect.height);
// 修正:反转拖拽方向,使其跟随鼠标
targetYaw += (x - lastX) * 0.008; // 改为 +=
targetPitch -= (y - lastY) * 0.008;
targetPitch = Math.max(-0.8, Math.min(1.2, targetPitch));
lastX = x;
lastY = y;
});
canvas.addEventListener('wheel', e => {
e.preventDefault();
targetDist += e.deltaY * 0.3;
targetDist = Math.max(250, Math.min(800, targetDist));
onUpdate(...calcParams());
});
// 计算参数 - 修正方向逻辑
function calcParams() {
let normYaw = yaw % (Math.PI * 2);
if (normYaw < 0) normYaw += Math.PI * 2;
// yaw角度到方向的映射
// 0 = back (互换后)
// π/2 = right (互换后)
// π = front (互换后)
// 3π/2 = left (互换后)
let segment = Math.round(normYaw / (Math.PI / 4)) % 8;
// 映射: 前后互换,左右互换
// 0=back, 1=back-right, 2=right, 3=front-right, 4=front, 5=front-left, 6=left, 7=back-left
const dirMap = [4, 3, 2, 1, 0, 7, 6, 5];
let dIdx = dirMap[segment];
let aIdx = 1;
if (pitch < -0.2) aIdx = 0;
else if (pitch < 0.3) aIdx = 1;
else if (pitch < 0.7) aIdx = 2;
else aIdx = 3;
let sIdx = 1;
if (dist < 350) sIdx = 0;
else if (dist < 550) sIdx = 1;
else sIdx = 2;
return [dIdx, aIdx, sIdx];
}
// 从参数设置视角
function setViewFromParams(dirIdx, angIdx, shotIdx) {
// 方向到yaw的反向映射 (前后左右互换)
const dirToSegment = [4, 3, 2, 1, 0, 7, 6, 5];
const segment = dirToSegment[dirIdx];
targetYaw = (segment * Math.PI / 4);
// 角度映射到 pitch
const angleToPitch = [-0.4, 0.1, 0.5, 0.9];
targetPitch = angleToPitch[angIdx];
// 景别映射到 dist
const shotToDist = [300, 450, 650];
targetDist = shotToDist[shotIdx];
}
// 创建圆润的兔子模型
function createRabbit() {
const parts = [];
// 身体 (椭圆体)
parts.push({
type: 'ellipsoid',
center: {x: 0, y: 30, z: 0},
radius: {x: 45, y: 60, z: 35},
color: '#e8e8e8',
segments: 16
});
// 头部 (球体)
parts.push({
type: 'sphere',
center: {x: 0, y: -30, z: 5},
radius: 32,
color: '#f5f5f5',
segments: 16
});
// 左耳
parts.push({
type: 'ellipsoid',
center: {x: -15, y: -68, z: 8},
radius: {x: 8, y: 35, z: 6},
color: '#ffccd5',
segments: 12
});
// 右耳
parts.push({
type: 'ellipsoid',
center: {x: 15, y: -68, z: 8},
radius: {x: 8, y: 35, z: 6},
color: '#ffccd5',
segments: 12
});
// 鼻子 (红色指示器 - 指向front方向)
parts.push({
type: 'sphere',
center: {x: 0, y: -32, z: 35},
radius: 6,
color: '#ff6b6b',
segments: 8
});
// 左眼
parts.push({
type: 'sphere',
center: {x: -12, y: -38, z: 28},
radius: 4,
color: '#2a2a2a',
segments: 8
});
// 右眼
parts.push({
type: 'sphere',
center: {x: 12, y: -38, z: 28},
radius: 4,
color: '#2a2a2a',
segments: 8
});
// 尾巴
parts.push({
type: 'sphere',
center: {x: 0, y: 60, z: -30},
radius: 15,
color: '#f0f0f0',
segments: 12
});
return parts;
}
const rabbit = createRabbit();
// 生成网格顶点
function generateMesh(part) {
const vertices = [];
const faces = [];
const {center, segments} = part;
if (part.type === 'sphere') {
const r = part.radius;
for (let lat = 0; lat <= segments; lat++) {
const theta = (lat * Math.PI) / segments;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let lon = 0; lon <= segments; lon++) {
const phi = (lon * 2 * Math.PI) / segments;
const x = center.x + r * sinTheta * Math.cos(phi);
const y = center.y + r * cosTheta;
const z = center.z + r * sinTheta * Math.sin(phi);
vertices.push({x, y, z});
}
}
} else if (part.type === 'ellipsoid') {
const {x: rx, y: ry, z: rz} = part.radius;
for (let lat = 0; lat <= segments; lat++) {
const theta = (lat * Math.PI) / segments;
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
for (let lon = 0; lon <= segments; lon++) {
const phi = (lon * 2 * Math.PI) / segments;
const x = center.x + rx * sinTheta * Math.cos(phi);
const y = center.y + ry * cosTheta;
const z = center.z + rz * sinTheta * Math.sin(phi);
vertices.push({x, y, z});
}
}
}
// 生成面
for (let lat = 0; lat < segments; lat++) {
for (let lon = 0; lon < segments; lon++) {
const first = lat * (segments + 1) + lon;
const second = first + segments + 1;
faces.push([first, second, second + 1, first + 1]);
}
}
return {vertices, faces, color: part.color};
}
const meshes = rabbit.map(generateMesh);
function project(p) {
// 旋转
const cosY = Math.cos(yaw), sinY = Math.sin(yaw);
const x1 = p.x * cosY - p.z * sinY;
const z1 = p.x * sinY + p.z * cosY;
const cosP = Math.cos(pitch), sinP = Math.sin(pitch);
const y2 = p.y * cosP - z1 * sinP;
const z2 = p.y * sinP + z1 * cosP;
// 投影
const w = canvas.width || 400;
const h = canvas.height || 350;
const scale = 300 / (dist + z2);
return {
x: x1 * scale + w / 2,
y: y2 * scale + h / 2,
z: z2
};
}
function draw() {
const w = canvas.width || 400;
const h = canvas.height || 350;
ctx.clearRect(0, 0, w, h);
// 绘制网格地面
ctx.strokeStyle = "#2a2a2a";
ctx.lineWidth = 1;
for (let i = -5; i <= 5; i++) {
const p1 = project({x: i * 50, y: 100, z: -250});
const p2 = project({x: i * 50, y: 100, z: 250});
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
const p3 = project({x: -250, y: 100, z: i * 50});
const p4 = project({x: 250, y: 100, z: i * 50});
ctx.beginPath();
ctx.moveTo(p3.x, p3.y);
ctx.lineTo(p4.x, p4.y);
ctx.stroke();
}
// 收集所有面
const allFaces = [];
meshes.forEach(mesh => {
const projVerts = mesh.vertices.map(project);
mesh.faces.forEach(face => {
const pts = face.map(i => projVerts[i]);
const avgZ = pts.reduce((sum, p) => sum + p.z, 0) / pts.length;
// 背面剔除
const v1 = {x: pts[1].x - pts[0].x, y: pts[1].y - pts[0].y};
const v2 = {x: pts[2].x - pts[1].x, y: pts[2].y - pts[1].y};
const cross = v1.x * v2.y - v1.y * v2.x;
if (cross > 0) {
allFaces.push({pts, z: avgZ, color: mesh.color});
}
});
});
// 按深度排序
allFaces.sort((a, b) => b.z - a.z);
// 渲染
allFaces.forEach(face => {
ctx.beginPath();
ctx.moveTo(face.pts[0].x, face.pts[0].y);
for (let i = 1; i < face.pts.length; i++) {
ctx.lineTo(face.pts[i].x, face.pts[i].y);
}
ctx.closePath();
// 光照效果
const brightness = Math.max(0.6, 1 - (face.z / 500));
const rgb = hexToRgb(face.color);
ctx.fillStyle = `rgba(${rgb.r * brightness}, ${rgb.g * brightness}, ${rgb.b * brightness}, 1)`;
ctx.fill();
ctx.strokeStyle = "rgba(80, 80, 80, 0.3)";
ctx.lineWidth = 0.5;
ctx.stroke();
});
// 方向指示器
const noseTip = project({x: 0, y: -32, z: 45});
ctx.fillStyle = "#ff6b6b";
ctx.beginPath();
ctx.arc(noseTip.x, noseTip.y, 4, 0, Math.PI * 2);
ctx.fill();
}
function hexToRgb(hex) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return {r, g, b};
}
// 动画循环(平滑插值)
function animate() {
yaw += (targetYaw - yaw) * 0.15;
pitch += (targetPitch - pitch) * 0.15;
dist += (targetDist - dist) * 0.15;
draw();
requestAnimationFrame(animate);
}
animate();
// 返回控制函数供按钮调用
return setViewFromParams;
}