Unrestrict 3D layer view camera — full x/y/z orbit

- Add rotateY axis (drag left/right rotates Y, up/down rotates X)
- Shift+drag for Z-axis roll
- Remove 10-80° clamp on rotateX — full ±180° range
- Remove backface-visibility:hidden so layers visible from all angles
- Fix overflow:hidden → overflow:visible for proper 3D perspective
- Increase layer spacing 80→120px for more dramatic depth
- Increase viewport height 340→420px, perspective origin centered

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-16 13:49:13 -07:00
parent d62a5e9b15
commit bf28e96ae6
1 changed files with 20 additions and 13 deletions

View File

@ -101,6 +101,7 @@ export class RStackTabBar extends HTMLElement {
#flowDialogTargetId = ""; #flowDialogTargetId = "";
// 3D scene state // 3D scene state
#sceneRotX = 55; #sceneRotX = 55;
#sceneRotY = 0;
#sceneRotZ = -15; #sceneRotZ = -15;
#scenePerspective = 1200; #scenePerspective = 1200;
#orbitDragging = false; #orbitDragging = false;
@ -555,7 +556,7 @@ export class RStackTabBar extends HTMLElement {
const layerCount = this.#layers.length; const layerCount = this.#layers.length;
if (layerCount === 0) return ""; if (layerCount === 0) return "";
const layerSpacing = 80; const layerSpacing = 120;
const animDuration = 2 / this.#simSpeed; const animDuration = 2 / this.#simSpeed;
// Build layer planes // Build layer planes
@ -676,7 +677,7 @@ export class RStackTabBar extends HTMLElement {
<div class="stack-view-3d" id="stack-3d" <div class="stack-view-3d" id="stack-3d"
style="perspective:${this.#scenePerspective}px;"> style="perspective:${this.#scenePerspective}px;">
<div class="stack-scene" id="stack-scene" <div class="stack-scene" id="stack-scene"
style="transform: rotateX(${this.#sceneRotX}deg) rotateZ(${this.#sceneRotZ}deg);"> style="transform: rotateX(${this.#sceneRotX}deg) rotateY(${this.#sceneRotY}deg) rotateZ(${this.#sceneRotZ}deg);">
${layersHtml} ${layersHtml}
${tubesHtml} ${tubesHtml}
${particlesHtml} ${particlesHtml}
@ -684,7 +685,7 @@ export class RStackTabBar extends HTMLElement {
</div> </div>
<div class="stack-legend">${legendHtml}</div> <div class="stack-legend">${legendHtml}</div>
${scrubberHtml} ${scrubberHtml}
${this.#layers.length >= 2 ? `<div class="stack-hint">Click an output port to wire, or drag between layers · Drag empty space to orbit</div>` : ""} ${this.#layers.length >= 2 ? `<div class="stack-hint">Drag to orbit · Shift+drag to roll · Scroll to zoom · Click output ports to wire</div>` : ""}
${this.#flowDialogOpen ? this.#renderFlowDialog() : ""} ${this.#flowDialogOpen ? this.#renderFlowDialog() : ""}
</div> </div>
`; `;
@ -777,7 +778,7 @@ export class RStackTabBar extends HTMLElement {
scene.querySelectorAll(".flow-tube, .flow-particle").forEach(el => el.remove()); scene.querySelectorAll(".flow-tube, .flow-particle").forEach(el => el.remove());
const layerZMap = new Map<string, number>(); const layerZMap = new Map<string, number>();
const layerSpacing = 80; const layerSpacing = 120;
this.#layers.forEach((layer, i) => layerZMap.set(layer.id, i * layerSpacing)); this.#layers.forEach((layer, i) => layerZMap.set(layer.id, i * layerSpacing));
const animDuration = 2 / this.#simSpeed; const animDuration = 2 / this.#simSpeed;
@ -1123,12 +1124,18 @@ export class RStackTabBar extends HTMLElement {
if (this.#orbitDragging) { if (this.#orbitDragging) {
const dx = e.clientX - this.#orbitLastX; const dx = e.clientX - this.#orbitLastX;
const dy = e.clientY - this.#orbitLastY; const dy = e.clientY - this.#orbitLastY;
this.#sceneRotZ += dx * 0.3; if (e.shiftKey) {
this.#sceneRotX = Math.max(10, Math.min(80, this.#sceneRotX - dy * 0.3)); // Shift+drag: rotate around Z axis
this.#sceneRotZ += dx * 0.3;
} else {
// Normal drag: rotate around X and Y axes
this.#sceneRotY += dx * 0.3;
this.#sceneRotX = Math.max(-180, Math.min(180, this.#sceneRotX - dy * 0.3));
}
this.#orbitLastX = e.clientX; this.#orbitLastX = e.clientX;
this.#orbitLastY = e.clientY; this.#orbitLastY = e.clientY;
const scene = this.#shadow.getElementById("stack-scene"); const scene = this.#shadow.getElementById("stack-scene");
if (scene) scene.style.transform = `rotateX(${this.#sceneRotX}deg) rotateZ(${this.#sceneRotZ}deg)`; if (scene) scene.style.transform = `rotateX(${this.#sceneRotX}deg) rotateY(${this.#sceneRotY}deg) rotateZ(${this.#sceneRotZ}deg)`;
} }
// Drag-to-connect: track target via bounding rects (robust for 3D) // Drag-to-connect: track target via bounding rects (robust for 3D)
@ -1781,8 +1788,8 @@ const STYLES = `
.stack-view { .stack-view {
padding: 12px; padding: 12px;
overflow: hidden; overflow: visible;
max-height: 60vh; max-height: 80vh;
transition: max-height 0.3s ease; transition: max-height 0.3s ease;
position: relative; position: relative;
background: color-mix(in srgb, var(--rs-bg-surface) 50%, transparent); background: color-mix(in srgb, var(--rs-bg-surface) 50%, transparent);
@ -1790,14 +1797,15 @@ const STYLES = `
} }
.stack-view-3d { .stack-view-3d {
perspective-origin: 50% 40%; perspective-origin: 50% 50%;
width: 100%; width: 100%;
height: 340px; height: 420px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: grab; cursor: grab;
user-select: none; user-select: none;
overflow: visible;
} }
.stack-scene { .stack-scene {
@ -1819,7 +1827,6 @@ const STYLES = `
cursor: pointer; cursor: pointer;
transition: border-color 0.2s, box-shadow 0.2s; transition: border-color 0.2s, box-shadow 0.2s;
transform-style: preserve-3d; transform-style: preserve-3d;
backface-visibility: hidden;
background: color-mix(in srgb, var(--rs-bg-surface) 65%, transparent); background: color-mix(in srgb, var(--rs-bg-surface) 65%, transparent);
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
@ -2297,7 +2304,7 @@ const STYLES = `
.tab-label { display: none; } .tab-label { display: none; }
.tab { padding: 4px 8px; } .tab { padding: 4px 8px; }
.stack-view { max-height: 40vh; } .stack-view { max-height: 40vh; }
.stack-view-3d { height: 260px; } .stack-view-3d { height: 320px; }
.stack-scene { width: 240px; } .stack-scene { width: 240px; }
.layer-plane { width: 240px; min-height: 40px; padding: 8px 10px; } .layer-plane { width: 240px; min-height: 40px; padding: 8px 10px; }
.io-chip { font-size: 0.5rem; padding: 1px 5px; max-width: 100px; } .io-chip { font-size: 0.5rem; padding: 1px 5px; max-width: 100px; }