Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/renderer/src/app/app-route-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@ const routeRegistry: ReadonlyArray<RouteRegistryEntry> = [
),
}),
},
{
path: 'scene-lab',
label: 'Scene Lab',
icon: 'view_in_ar',
lab: true,
nav: true,
toRoute: () => ({
path: 'scene-lab',
loadComponent: () =>
import('./features/scene-lab/scene-lab-page').then(
(m) => m.SceneLabPage,
),
}),
},
{
path: 'canvas-lab',
label: 'Canvas Lab',
Expand Down
62 changes: 62 additions & 0 deletions apps/renderer/src/app/features/scene-lab/scene-lab-page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
.scene-lab {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 1080px;
margin: 0 auto;
}

.intro-card p {
margin: 0;
color: var(--mat-sys-on-surface-variant, #9aa4b2);
}

.scene-frame {
height: 420px;
border-radius: 8px;
overflow: hidden;
background: #080b12;
}

.scene-canvas {
display: block;
width: 100%;
height: 100%;
}

.control-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
flex-wrap: wrap;
}

.control-row label {
min-width: 90px;
font-size: 13px;
color: var(--mat-sys-on-surface-variant, #9aa4b2);
}

.control-row input[type='range'] {
flex: 1;
accent-color: var(--mat-sys-primary, #38bdf8);
}

.diag-grid {
display: grid;
grid-template-columns: max-content 1fr;
gap: 6px 20px;
margin: 0;
}

.diag-grid dt {
font-size: 13px;
font-weight: 600;
color: var(--mat-sys-on-surface-variant, #9aa4b2);
}

.diag-grid dd {
margin: 0;
font-size: 13px;
}
68 changes: 68 additions & 0 deletions apps/renderer/src/app/features/scene-lab/scene-lab-page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<section class="scene-lab">
<mat-card class="intro-card">
<mat-card-header>
<mat-icon matCardAvatar>view_in_ar</mat-icon>
<mat-card-title>Scene Lab</mat-card-title>
<mat-card-subtitle>
three.js WebGL scene — drag to orbit, scroll to zoom
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p>
A wireframe torus knot inside a star field, rendered by a lazy-loaded
three.js chunk. The render loop runs outside Angular change detection;
the controls below update signals read every frame.
</p>
</mat-card-content>
</mat-card>

<mat-card class="stage-card">
<mat-card-content>
<div class="scene-frame">
<canvas
#sceneCanvas
class="scene-canvas"
aria-label="Interactive 3D torus knot scene"
></canvas>
</div>
</mat-card-content>
</mat-card>

<mat-card class="controls-card">
<mat-card-header>
<mat-card-title>Controls</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="control-row">
<label for="rotation">Spin: {{ rotationSpeed() / 10 }}x</label>
<input
id="rotation"
type="range"
min="0"
max="40"
step="1"
[value]="rotationSpeed()"
(input)="setRotationSpeed($any($event.target).valueAsNumber)"
/>
</div>

<div class="control-row">
<button mat-stroked-button type="button" (click)="toggleWireframe()">
<mat-icon>grid_4x4</mat-icon>
{{ wireframe() ? "Wireframe: on" : "Wireframe: off" }}
</button>
<button mat-stroked-button type="button" (click)="toggleRunning()">
<mat-icon>{{ running() ? "pause" : "play_arrow" }}</mat-icon>
{{ running() ? "Pause" : "Resume" }}
</button>
</div>

<dl class="diag-grid">
<dt>FPS</dt>
<dd>{{ fps() }}</dd>
<dt>Status</dt>
<dd>{{ webglReady() ? "Rendering" : "Initializing…" }}</dd>
</dl>
</mat-card-content>
</mat-card>
</section>
22 changes: 22 additions & 0 deletions apps/renderer/src/app/features/scene-lab/scene-lab-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TestBed } from '@angular/core/testing';
import { SceneLabPage } from './scene-lab-page';

describe('SceneLabPage', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SceneLabPage],
}).compileComponents();
});

it('should create the page', () => {
const fixture = TestBed.createComponent(SceneLabPage);
expect(fixture.componentInstance).toBeTruthy();
});

it('should tear down without errors after render', async () => {
const fixture = TestBed.createComponent(SceneLabPage);
fixture.detectChanges();
await fixture.whenStable();
expect(() => fixture.destroy()).not.toThrow();
});
});
Loading
Loading