velxio/frontend/src/components/simulator/Oscilloscope.css

332 lines
5.8 KiB
CSS
Raw Normal View History

/* ── Oscilloscope panel ─────────────────────────────────────────────────── */
.osc-container {
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
border-top: 1px solid #333;
font-family: 'Consolas', 'Menlo', monospace;
font-size: 12px;
color: #ccc;
overflow: hidden;
}
/* ── Header ── */
.osc-header {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
background: #252526;
border-bottom: 1px solid #333;
flex-shrink: 0;
flex-wrap: wrap;
min-height: 32px;
}
.osc-title {
color: #cccccc;
font-weight: 600;
font-size: 12px;
white-space: nowrap;
margin-right: 4px;
}
.osc-btn {
background: transparent;
border: 1px solid #555;
color: #ccc;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
white-space: nowrap;
transition:
border-color 0.15s,
background 0.15s;
}
.osc-btn:hover {
border-color: var(--color-action-primary);
color: #fff;
}
.osc-btn-active {
background: #0e3a5a;
border-color: var(--color-action-primary);
color: var(--color-action-primary);
}
.osc-btn-danger {
border-color: #7a0000;
color: #ff6b6b;
}
.osc-btn-danger:hover {
background: #3a0000;
border-color: #ff6b6b;
}
.osc-label {
color: #888;
font-size: 11px;
white-space: nowrap;
}
.osc-select {
background: #1e1e1e;
border: 1px solid #444;
color: #ccc;
padding: 2px 4px;
border-radius: 3px;
font-size: 11px;
outline: none;
cursor: pointer;
}
.osc-select:focus {
border-color: var(--color-action-primary);
}
/* ── Channel picker dropdown ── */
.osc-picker-wrap {
position: relative;
}
.osc-picker-dropdown {
/* position/left/bottom/z-index set via inline style (portal) */
background: #252526;
border: 1px solid #555;
border-radius: 4px;
padding: 4px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 3px;
min-width: 200px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.6);
}
/* Multi-board picker layout */
.osc-picker-multiboard {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px;
min-width: 260px;
max-height: 320px;
overflow: hidden;
}
.osc-picker-board-tabs {
display: flex;
gap: 4px;
flex-wrap: wrap;
border-bottom: 1px solid #3a3a3a;
padding-bottom: 6px;
}
.osc-picker-board-tab {
background: #2d2d2d;
border: 1px solid #444;
color: #999;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
transition:
background 0.1s,
color 0.1s;
}
.osc-picker-board-tab:hover {
background: #383838;
color: #ccc;
}
.osc-picker-board-tab.active {
background: #0e3a5a;
border-color: var(--color-action-primary);
color: var(--color-action-primary);
}
.osc-picker-board-label {
font-size: 10px;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.osc-picker-pins {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 3px;
overflow-y: auto;
flex: 1;
}
.osc-pin-btn {
background: #2d2d2d;
border: 1px solid #444;
color: #ccc;
padding: 3px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
text-align: center;
transition:
background 0.1s,
border-color 0.1s;
}
.osc-pin-btn:hover {
background: #0e3a5a;
border-color: var(--color-action-primary);
color: #fff;
}
.osc-pin-btn-active {
background: #1a3a1a;
border-color: #00ff41;
color: #00ff41;
cursor: default;
}
/* ── Waveform area ── */
.osc-waveforms {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
background: #0a0a0a;
}
.osc-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #555;
font-size: 12px;
flex-direction: column;
gap: 8px;
}
.osc-channel-row {
display: flex;
align-items: stretch;
border-bottom: 1px solid #1a1a1a;
height: 60px;
}
.osc-channel-label {
width: 72px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4px 6px;
background: #141414;
border-right: 1px solid #222;
gap: 2px;
}
.osc-channel-board {
font-size: 9px;
color: #666;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
}
.osc-channel-name {
font-size: 11px;
font-weight: 700;
}
.osc-channel-remove {
background: transparent;
border: none;
color: #555;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0;
transition: color 0.1s;
}
.osc-channel-remove:hover {
color: #ff6b6b;
}
.osc-channel-canvas-wrap {
flex: 1;
position: relative;
overflow: hidden;
}
.osc-channel-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* ── Time ruler ── */
.osc-ruler {
height: 20px;
flex-shrink: 0;
background: #141414;
border-top: 1px solid #222;
position: relative;
}
.osc-ruler-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
feat(scope/trigger): add Auto / Normal / Single-shot trigger modes Real digital storage scopes have a trigger that pins the visible window around a detected edge — without it, sparse activity (UART bytes once per loop, an interrupt firing every few seconds) scrolls off the screen faster than the eye can catch. Velxio's scope was free-running only, which made the recent UART TX waveform work effectively invisible at fine time/div settings: the byte burst was 87 µs but the window only showed the most recent 1 ms. Three trigger modes, matching what you'd find on a Rigol / Tektronix: * Auto — current free-running behaviour, window's right edge tracks the most recent sample. Default. * Normal — window pins around each triggering edge so the event lands at `triggerPosition * windowMs` from the left (default centred at 0.5). Keeps re-pinning on every new triggering edge. * Single — arms once, freezes the trace on the first triggering edge by flipping `running = false`. User clicks "Re-arm" to capture again. Three knobs configurable per mode: - source: which channel produces the trigger event - edge: rising (↑) / falling (↓) / either (⇅) - position: trigger lands at this fraction of the window (UI hard-codes centre 0.5 for now; the store field accepts any value if we want a draggable handle later) UI additions in the scope header (only shown when mode != auto): - source / edge dropdowns - status badge (Armed / Triggered / Captured) with pulse animation on Armed so the user knows the scope is waiting for an event - Re-arm button in Single mode after capture Canvas changes: - Dashed orange "T" marker drawn at the trigger position when an edge is latched and within the visible window. Store changes: - pushSample peeks at the trigger channel's previous state, detects a matching edge, sets triggeredAtMs (and stops `running` for Single mode). matchesTriggerEdge() exported for unit testing. - clearSamples / setTriggerMode / setTriggerChannel / setTriggerEdge all re-arm the trigger; rearmTrigger() explicitly resets and resumes capture (used by the Re-arm button after a single-shot). Covered by 11 new vitest cases (oscilloscope-trigger.test.ts) plus the existing 1892 tests still pass. Closes the "I set 0.1 ms/div on a Serial.print sketch and see a flat line" UX trap reported on the Discord follow-up — at 0.1 ms/div the window is 1 ms but bytes fire every 2 s, so without a trigger the chance of catching the burst is < 0.05 %. With Normal trigger on rising D1 the burst pins in the middle of the window and the user can zoom down to bit level (8.68 µs each) without losing it.
2026-05-23 02:11:03 +07:00
/* ── Trigger status badge ───────────────────────────────────────────────── */
.osc-trigger-status {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border: 1px solid currentColor;
}
.osc-trigger-status-idle {
color: #6b7280;
}
.osc-trigger-status-armed {
color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
animation: osc-pulse-armed 1.5s ease-in-out infinite;
}
.osc-trigger-status-triggered {
color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.osc-trigger-status-captured {
color: #ff9800;
background: rgba(255, 152, 0, 0.15);
border-color: #ff9800;
}
@keyframes osc-pulse-armed {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}