fix: sửa lỗi zoom in full màn hình
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
|
||||
|
||||
# Technical Specification: Grid Snapping System (Grid Snapping Specification)
|
||||
|
||||
This document defines the graphical user interface design and coordinate/signal processing algorithms required to build a synchronized grid snapping feature across both the Web Frontend and the Dockerized Python Desktop Backend.
|
||||
|
||||
---
|
||||
|
||||
## 1. Toolbar UI Design Upgrade
|
||||
|
||||
The toolbar layout has been expanded to double its physical vertical height. This increase in interactive space allows for larger navigation buttons and the integration of a dedicated Snap controller.
|
||||
|
||||
```text
|
||||
+───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────+
|
||||
| [Pro Toolbar - Height: 64px] |
|
||||
| +──────────+ +──────────+ +──────────+ +──────────+ +───────────────────────────────────────────────────────────────+ |
|
||||
| | Cut | | Copy | | Paste | | Snap: | | Transport Monitor | |
|
||||
| | [Ctrl+X]| | [Ctrl+C]| | [Ctrl+V]| | [1/4 ▼] | | [Tempo: 120 BPM] [Time Signature: 4/4] [Bar:Beat 1.3.00] | |
|
||||
| +──────────+ +──────────+ +──────────+ +──────────+ +───────────────────────────────────────────────────────────────+ |
|
||||
+───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────+
|
||||
|
||||
```
|
||||
|
||||
### 1.1. Snap Dropdown Configuration
|
||||
|
||||
* **Placement:** Located immediately following the *Paste* button on the primary toolbar row.
|
||||
* **UI Syntax:** `Snap: <Dropdown_Widget>`
|
||||
* **Dropdown Option Matrix:**
|
||||
* `free`: Disables snapping; allows unrestricted pixel-by-pixel dragging.
|
||||
* `1`: Snaps to the beginning of each complete measure (Whole Bar / 1/1).
|
||||
* `1/2`: Divides the bar into 2 subdivisions (Half Note).
|
||||
* `1/4`: Divides the bar into 4 subdivisions (Quarter Note / 1 Beat).
|
||||
* `1/8`: Divides the bar into 8 subdivisions (Eighth Note).
|
||||
* `1/16`: Divides the bar into 16 subdivisions (Sixteenth Note).
|
||||
* `1/32`: Divides the bar into 32 subdivisions (Thirty-second Note).
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 2. DSP Grid Math: Grid Subdivision & Time Interval Calculations
|
||||
|
||||
To calculate the absolute temporal duration between individual grid lanes, the time metrics must be derived dynamically from the project's master tempo.
|
||||
|
||||
Let:
|
||||
|
||||
* $B$ be the project tempo (Beats Per Minute, e.g., $120\text{ BPM}$).
|
||||
* $T_{\text{beat}}$ be the duration of a single beat (seconds).
|
||||
* $T_{\text{bar}}$ be the duration of a full measure/bar (seconds)—assuming a standard $4/4$ time signature (4 beats per bar).
|
||||
|
||||
The foundational constants are established as follows:
|
||||
|
||||
|
||||
$$T_{\text{beat}} = \frac{60}{B} \quad (\text{seconds})$$
|
||||
|
||||
$$T_{\text{bar}} = 4 \times T_{\text{beat}} = \frac{240}{B} \quad (\text{seconds})$$
|
||||
|
||||
*Example:* At a tempo of $120\text{ BPM}$, a single $1\text{ Bar}$ measure spans exactly $2.0\text{ seconds}$.
|
||||
|
||||
### 2.1. Determining Grid Time Interval Modifiers ($\Delta t$)
|
||||
|
||||
Based on the user's active choice inside the snap dropdown selection ($S \in \{\text{free}, 1, 1/2, 1/4, 1/8, 1/16, 1/32\}$), the exact grid time step $\Delta t$ (seconds) is mapped as follows:
|
||||
|
||||
$$\Delta t = \begin{cases} 0 & S = \text{free} \\ T_{\text{bar}} & S = 1 \\ \frac{T_{\text{bar}}}{2} & S = 1/2 \\ \frac{T_{\text{bar}}}{4} = T_{\text{beat}} & S = 1/4 \\ \frac{T_{\text{bar}}}{8} & S = 1/8 \\ \frac{T_{\text{bar}}}{16} & S = 1/16 \\ \frac{T_{\text{bar}}}{32} & S = 1/32 \end{cases}$$
|
||||
|
||||
---
|
||||
|
||||
## 3. Snapping Coordinate Calculation
|
||||
|
||||
When a user executes a drag-and-drop event on an audio clip, or updates a timeline marker position, the system continuously converts raw cursor values into aligned coordinates.
|
||||
|
||||
```text
|
||||
Grid Line 1 (k * dt) Grid Line 2 ((k+1) * dt)
|
||||
│ │
|
||||
├───────────────○─────────────┤
|
||||
▲
|
||||
│ [ Cursor dragging action ]
|
||||
Raw Time (t_raw)
|
||||
│
|
||||
▼ [ Apply Snap round() function ]
|
||||
────────────────┼─────────────►
|
||||
Snapped Time (t_snapped)
|
||||
|
||||
```
|
||||
|
||||
### 3.1. Pixel to Snap-Time Translation Workflow
|
||||
|
||||
1. Intercept the actual client-side horizontal cursor position $X_{\text{raw}}$ (pixels).
|
||||
2. Convert it into a raw timeline duration metric $t_{\text{raw}}$ (seconds) utilizing the current scaling zoom factor $Z$ (pixels/second):
|
||||
|
||||
$$t_{\text{raw}} = \frac{X_{\text{raw}}}{Z}$$
|
||||
|
||||
|
||||
3. Apply the rounding constraint formula to lock the raw timestamp to the absolute nearest grid marker:
|
||||
|
||||
$$t_{\text{snapped}} = \begin{cases} t_{\text{raw}} & S = \text{free} \\ \text{round}\left( \frac{t_{\text{raw}}}{\Delta t} \right) \times \Delta t & S \neq \text{free} \end{cases}$$
|
||||
|
||||
|
||||
4. Map the snapped timeline index $t_{\text{snapped}}$ back to the layout canvas system coordinates to paint the element at its snapped visual boundary:
|
||||
|
||||
$$X_{\text{snapped}} = t_{\text{snapped}} \times Z$$
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. Python Porting Blueprint
|
||||
|
||||
When translating this architectural logic into a desktop Python core environment using frameworks like PyQt6, the snapping evaluations are tied directly into the tracking loop inside the `mouseMoveEvent` handler.
|
||||
|
||||
```python
|
||||
# [PYTHON PORTING BLUEPRINT] - Integrating the Snap algorithm into Python UI layer
|
||||
import numpy as np
|
||||
|
||||
class AudioSnapEngine:
|
||||
def __init__(self, bpm: float = 120.0):
|
||||
self.bpm = bpm
|
||||
|
||||
def calculate_grid_step(self, snap_option: str) -> float:
|
||||
"""
|
||||
Calculates the grid's target duration step (seconds) based on Tempo and Snap selection.
|
||||
"""
|
||||
if snap_option == "free":
|
||||
return 0.0
|
||||
|
||||
# 1 Bar in a standard 4/4 signature equals 240 / BPM seconds
|
||||
t_bar = 240.0 / self.bpm
|
||||
|
||||
fraction_map = {
|
||||
"1": 1.0,
|
||||
"1/2": 2.0,
|
||||
"1/4": 4.0,
|
||||
"1/8": 8.0,
|
||||
"1/16": 16.0,
|
||||
"1/32": 32.0
|
||||
}
|
||||
|
||||
division = fraction_map.get(snap_option, 4.0)
|
||||
return float(t_bar / division)
|
||||
|
||||
def snap_time(self, raw_time_seconds: float, snap_option: str) -> float:
|
||||
"""
|
||||
Hard-clamps a raw timestamp to the nearest grid milestone. Prevents negative index overflows.
|
||||
"""
|
||||
dt = self.calculate_grid_step(snap_option)
|
||||
if dt == 0.0:
|
||||
return max(0.0, raw_time_seconds)
|
||||
|
||||
# Find the nearest integer index k of the target grid lane: raw_time / dt
|
||||
k = round(raw_time_seconds / dt)
|
||||
snapped_time = k * dt
|
||||
|
||||
return max(0.0, snapped_time)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Visual Grid Alignment
|
||||
|
||||
To maintain an intuitive environment for multi-channel editing, whenever a snapping constraint value other than `free` is engaged:
|
||||
|
||||
* The rendering engine overlays thin, low-contrast vertical grid lines (`rgba(255, 255, 255, 0.05)`) over the background profile of every active Waveform Lane.
|
||||
* These marker lines are projected onto every timeline axis point that satisfies a whole multiple increment of $\Delta t$.
|
||||
* Displaying these alignment indicators ensures that users can visually anticipate bounding snapping positions before releasing their mouse track buttons.
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
|
||||
|
||||
# Technical Specification: Multi-Channel Layout Synchronization & Scroll Management (Unified DAW Layout & Sync Scroll)
|
||||
|
||||
This document analyzes and defines the structural hierarchy of the graphical user interface based on the real-world interface analysis. This specification serves to guide Frontend interface programming and porting to a Python Desktop application running inside a Docker container.
|
||||
|
||||
---
|
||||
|
||||
## 1. Structural Wireframe
|
||||
|
||||
Based on the visual analysis, the layout composition is split into vertically static and dynamic zones:
|
||||
|
||||
```text
|
||||
+───────────────────────────────────────────────────────────────────────────────+
|
||||
| [ZONE A - STATIC] HEADER ZONE (Sticky - Permanently fixed when scrolling down)|
|
||||
| +────────────────────+──────────────────────────────────────────────────────+ |
|
||||
| | Channels & Tools | Time Ruler Scale | |
|
||||
| |--------------------|------------------------------------------------------| |
|
||||
| | Tempo Track Header | Tempo Grid Lane (120 BPM) | |
|
||||
| +────────────────────+──────────────────────────────────────────────────────+ |
|
||||
+───────────────────────────────────────────────────────────────────────────────+
|
||||
| [ZONE B - DYNAMIC] TRACKS SCROLL WORKSPACE (Synchronized vertical scroll) |
|
||||
| +────────────────────+──────────────────────────────────────────────────────+ |
|
||||
| | TCP - Track 01 | Waveform Lane - Track 01 | |
|
||||
| | TCP - Track 02 | Waveform Lane - Track 02 | |
|
||||
| | TCP - Track 03 | Waveform Lane - Track 03 | |
|
||||
| | ... | ... | |
|
||||
| +────────────────────+──────────────────────────────────────────────────────+ |
|
||||
+───────────────────────────────────────────────────────────────────────────────+ ▲
|
||||
│ [Vertical Scrollbar]
|
||||
│ (Single unified scroll)
|
||||
▼
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Layout Specifications
|
||||
|
||||
### 2.1. Fixed Header Zone (Green Border Area - Sticky Header)
|
||||
|
||||
* **Visual Scope:** Encompasses the toolbar, the time ruler scale, and the Tempo Track Lane (indicated by the green bounding border in `image_fbbd4e.png`).
|
||||
* **Graphical Sticky Behavior:**
|
||||
* When a user adds dozens of tracks and scrolls downward, this entire zone must remain anchored to the top of the screen and is not permitted to slide out of view.
|
||||
* This ensures that users can continuously track the Ruler Seconds and the master project tempo (Tempo BPM) while editing tracks located deeper down the timeline.
|
||||
|
||||
|
||||
|
||||
### 2.2. Absolute Horizontal Row Alignment (Red Border Area - Row Alignment)
|
||||
|
||||
* **Interaction Scope:** The exact matching pair consisting of the left Track Control Panel (TCP) and the right Waveform Lane of the same track (e.g., Track 4 inside the red border of `image_fbbd4e.png`).
|
||||
* **Row Alignment Rules:**
|
||||
* The corresponding TCP and Waveform Lane must have identical heights ($H = 96\text{ px}$).
|
||||
* These two elements must be wrapped within a single parent row container (`Flex Row` or `Grid Row`) to guarantee that during vertical scrolling, both move simultaneously along the exact same vertical axis coordinate ($Y$).
|
||||
* Row misalignment must be strictly avoided (e.g., situations where the Track 4 TCP sits higher or lower than the Track 4 Waveform lane).
|
||||
|
||||
|
||||
|
||||
### 2.3. Single Vertical Scrollbar Mandate
|
||||
|
||||
* **Issue to Avoid:** Separating the TCP into an independent scrollable column and the Timeline into another independent scrollable column. Doing so leads to scroll-position desynchronization errors when a user drags the scrollbar.
|
||||
* **Design Standard:**
|
||||
* Only a single unified Vertical Scrollbar is permitted to appear on the absolute far right of the application window (as directed by the two red arrows in `image_fbbd4e.png`).
|
||||
* This vertical scrollbar moves the entire dynamic wrapper (**Tracks Scroll Workspace**), scrolling both TCPs and Waveform Lanes up or down in sync.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation Guide
|
||||
|
||||
### 3.1. Web Frontend Integration (HTML / Tailwind CSS)
|
||||
|
||||
To group everything into one scrollbar while keeping the Tempo Track anchored at the top, use `position: sticky` and wrap the dynamic track list inside a single container:
|
||||
|
||||
```html
|
||||
<!-- Main Container (Entire Editor Wrapper) -->
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
|
||||
<!-- [ZONE A] Top Anchored Sticky Header Zone -->
|
||||
<div class="sticky top-0 z-40 bg-[#242424] border-b border-[#141414] shrink-0">
|
||||
<!-- Toolbar & Time Ruler -->
|
||||
<div class="h-8 flex">
|
||||
<div class="w-[300px] border-r border-zinc-900 px-4 flex items-center">CHANNELS</div>
|
||||
<div class="flex-1 relative h-full">...Ruler Numbers...</div>
|
||||
</div>
|
||||
<!-- Tempo Track (Green Border Area) -->
|
||||
<div class="h-[44px] flex border-t border-zinc-800 bg-[#212121]">
|
||||
<div class="w-[300px] border-r border-zinc-900 px-4 flex items-center justify-between">
|
||||
<span class="font-bold text-zinc-400">Tempo Track</span>
|
||||
<span class="bg-zinc-800 px-1.5 py-0.5 rounded text-[10px]">120 BPM</span>
|
||||
</div>
|
||||
<div class="flex-1">...Tempo Grid Lines...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- [ZONE B] Dynamic Track Workspace (Single global vertical scrollbar on the far right) -->
|
||||
<div class="flex-1 overflow-y-auto bg-[#1a1a1a]">
|
||||
<div class="flex flex-col divide-y divide-[#141414]">
|
||||
|
||||
<!-- Track Row Container (Absolute Horizontal Row Alignment) -->
|
||||
<div class="h-[96px] flex hover:bg-zinc-800/20 transition-colors">
|
||||
<!-- Left: TCP -->
|
||||
<div class="w-[300px] border-r border-zinc-900 p-2.5 flex-shrink-0">
|
||||
...Controls (Mute, Solo, Volume, File Name)...
|
||||
</div>
|
||||
<!-- Right: Waveform Lane -->
|
||||
<div class="flex-1 relative overflow-hidden">
|
||||
...Waveform Canvas...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add more track rows repeating the structure above... -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
```
|
||||
|
||||
### 3.2. Desktop App Integration (Python PyQt6)
|
||||
|
||||
When engineering this user interface using the Qt framework in Python, utilize a `QScrollArea` to encapsulate a `QWidget` managed by a layout of rows to control the single scrollbar behavior:
|
||||
|
||||
```python
|
||||
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QLabel
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
class MasterDAWWidget(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.main_layout.setSpacing(0)
|
||||
|
||||
# 1. Initialize Fixed Header (Toolbar, Ruler, Tempo)
|
||||
self.header_widget = QWidget()
|
||||
self.header_widget.setFixedHeight(76) # 32px Ruler + 44px Tempo
|
||||
self.setup_header_ui()
|
||||
self.main_layout.addWidget(self.header_widget)
|
||||
|
||||
# 2. Initialize Scroll Area for dynamic track rows
|
||||
self.scroll_area = QScrollArea()
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
# Force a single vertical scrollbar on the far right
|
||||
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
|
||||
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
# Widget container hosting the track list inside the Scroll Area
|
||||
self.tracks_container = QWidget()
|
||||
self.tracks_layout = QVBoxLayout(self.tracks_container)
|
||||
self.tracks_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.tracks_layout.setSpacing(0)
|
||||
self.tracks_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.scroll_area.setWidget(self.tracks_container)
|
||||
self.main_layout.addWidget(self.scroll_area)
|
||||
|
||||
def add_track_row(self, track_id, track_name):
|
||||
"""
|
||||
Appends a new track row. Uses QHBoxLayout to lock the TCP and Waveform Lane
|
||||
into absolute horizontal sync within the row.
|
||||
"""
|
||||
row_widget = QWidget()
|
||||
row_widget.setFixedHeight(96) # Rigid constraint for the entire row
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row_layout.setSpacing(0)
|
||||
|
||||
# Left: Track Control Panel (TCP)
|
||||
tcp_widget = QWidget()
|
||||
tcp_widget.setFixedWidth(300)
|
||||
# Setup TCP UI components...
|
||||
row_layout.addWidget(tcp_widget)
|
||||
|
||||
# Right: Waveform Lane
|
||||
waveform_widget = QWidget()
|
||||
# Setup Waveform Canvas Painter...
|
||||
row_layout.addWidget(waveform_widget)
|
||||
|
||||
self.tracks_layout.addWidget(row_widget)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Layout Architecture Advantages
|
||||
|
||||
* **Fluid User Experience:** Eliminates row-stuttering or scrolling layout shifts between the control panels and audio visuals when a user scrolls through long track stacks rapidly.
|
||||
* **Flawless Python Porting Compatibility:** By wrapping the TCP and the Waveform Canvas inside a common row (`QHBoxLayout` in Qt or `Flex Row` in Web), the core widget tree hierarchy remains incredibly lean. This design removes the need to write custom coordinate bridging code to bind two separate scroll engines together.
|
||||
* **Clean Interface Aesthetics:** Safely protects the pixel rendering mapping ratios of the fixed time grids at the top, precisely matching the professional DAW interface conventions observed
|
||||
+614
-205
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user