Implement Timeline V2 Phase 2: Track hierarchy with selection and scrolling
Phase 2 Implementation: - Added TrackHierarchy class to build and manage hierarchical track structure - Track display with expand/collapse triangles for layers and groups - Hierarchical indentation for visual hierarchy - Track selection syncs with stage selection (shapes, objects, layers) - Vertical scrolling for track area when many tracks present - Horizontal scrolling in ruler area for timeline navigation Timeline Integration: - Set TimelineV2 as default timeline on app load - Timeline automatically updates when shapes added or grouped - Trigger timeline redraw in renderLayers() for efficient batching Selection System: - Clicking tracks selects corresponding objects/shapes on stage - Selected tracks highlighted in timeline - Updates context.selection and context.shapeselection arrays - Stores oldselection/oldshapeselection for undo support - Calls updateUI() and updateMenu() to sync UI state Visual Improvements: - Use predefined colors from styles.js (no hardcoded colors) - Alternating track background colors for readability - Selection highlighting with predefined highlight color - Type indicators for tracks: [L]ayer, [G]roup, [S]hape Mouse Interactions: - Click ruler area to move playhead - Click track expand/collapse triangles to show/hide children - Click track name to select object/shape - Scroll wheel in ruler area for horizontal timeline scroll - Scroll wheel in track area for vertical track list scroll - Adjusts hit testing for vertical scroll offset
This commit is contained in:
parent
6c79914ffb
commit
1936e91327
37
src/main.js
37
src/main.js
|
|
@ -467,6 +467,7 @@ let actions = {
|
||||||
undoStack.push({ name: "addShape", action: action });
|
undoStack.push({ name: "addShape", action: action });
|
||||||
actions.addShape.execute(action);
|
actions.addShape.execute(action);
|
||||||
updateMenu();
|
updateMenu();
|
||||||
|
updateLayers();
|
||||||
},
|
},
|
||||||
execute: (action) => {
|
execute: (action) => {
|
||||||
let layer = pointerList[action.layer];
|
let layer = pointerList[action.layer];
|
||||||
|
|
@ -1553,6 +1554,7 @@ let actions = {
|
||||||
undoStack.push({ name: "group", action: action });
|
undoStack.push({ name: "group", action: action });
|
||||||
actions.group.execute(action);
|
actions.group.execute(action);
|
||||||
updateMenu();
|
updateMenu();
|
||||||
|
updateLayers();
|
||||||
},
|
},
|
||||||
execute: (action) => {
|
execute: (action) => {
|
||||||
let group = new GraphicsObject(action.groupUuid);
|
let group = new GraphicsObject(action.groupUuid);
|
||||||
|
|
@ -4818,7 +4820,7 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||||
rootPane,
|
rootPane,
|
||||||
10,
|
10,
|
||||||
true,
|
true,
|
||||||
createPane(panes.timeline),
|
createPane(panes.timelineV2),
|
||||||
);
|
);
|
||||||
let [stageAndTimeline, _infopanel] = splitPane(
|
let [stageAndTimeline, _infopanel] = splitPane(
|
||||||
panel,
|
panel,
|
||||||
|
|
@ -7394,6 +7396,11 @@ function timelineV2() {
|
||||||
canvas.addEventListener("wheel", (event) => {
|
canvas.addEventListener("wheel", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Get mouse position
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mouseX = event.clientX - rect.left;
|
||||||
|
const mouseY = event.clientY - rect.top;
|
||||||
|
|
||||||
// Check if this is a pinch-zoom gesture (ctrlKey is set on trackpad pinch)
|
// Check if this is a pinch-zoom gesture (ctrlKey is set on trackpad pinch)
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
// Pinch zoom - zoom in/out based on deltaY
|
// Pinch zoom - zoom in/out based on deltaY
|
||||||
|
|
@ -7401,8 +7408,6 @@ function timelineV2() {
|
||||||
const oldPixelsPerSecond = timelineWidget.timelineState.pixelsPerSecond;
|
const oldPixelsPerSecond = timelineWidget.timelineState.pixelsPerSecond;
|
||||||
|
|
||||||
// Calculate the time under the mouse BEFORE zooming
|
// Calculate the time under the mouse BEFORE zooming
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
const mouseX = event.clientX - rect.left;
|
|
||||||
const mouseTimeBeforeZoom = timelineWidget.timelineState.pixelToTime(mouseX);
|
const mouseTimeBeforeZoom = timelineWidget.timelineState.pixelToTime(mouseX);
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
|
|
@ -7420,12 +7425,23 @@ function timelineV2() {
|
||||||
|
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
} else {
|
} else {
|
||||||
// Regular scroll - horizontal scroll for timeline
|
// Check if mouse is over the ruler area (horizontal scroll) or track area (vertical scroll)
|
||||||
|
if (mouseY <= timelineWidget.ruler.height) {
|
||||||
|
// Mouse over ruler - horizontal scroll for timeline
|
||||||
const deltaX = event.deltaX * config.scrollSpeed;
|
const deltaX = event.deltaX * config.scrollSpeed;
|
||||||
|
|
||||||
// Update viewport horizontal scroll
|
|
||||||
timelineWidget.timelineState.viewportStartTime += deltaX / timelineWidget.timelineState.pixelsPerSecond;
|
timelineWidget.timelineState.viewportStartTime += deltaX / timelineWidget.timelineState.pixelsPerSecond;
|
||||||
timelineWidget.timelineState.viewportStartTime = Math.max(0, timelineWidget.timelineState.viewportStartTime);
|
timelineWidget.timelineState.viewportStartTime = Math.max(0, timelineWidget.timelineState.viewportStartTime);
|
||||||
|
} else {
|
||||||
|
// Mouse over track area - vertical scroll for tracks
|
||||||
|
const deltaY = event.deltaY * config.scrollSpeed;
|
||||||
|
timelineWidget.trackScrollOffset -= deltaY;
|
||||||
|
|
||||||
|
// Clamp scroll offset
|
||||||
|
const trackAreaHeight = canvas.height - timelineWidget.ruler.height;
|
||||||
|
const totalTracksHeight = timelineWidget.trackHierarchy.getTotalHeight();
|
||||||
|
const maxScroll = Math.min(0, trackAreaHeight - totalTracksHeight);
|
||||||
|
timelineWidget.trackScrollOffset = Math.max(maxScroll, Math.min(0, timelineWidget.trackScrollOffset));
|
||||||
|
}
|
||||||
|
|
||||||
updateCanvasSize();
|
updateCanvasSize();
|
||||||
}
|
}
|
||||||
|
|
@ -7896,6 +7912,10 @@ function updateUI() {
|
||||||
uiDirty = true;
|
uiDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add updateUI and updateMenu to context so widgets can call them
|
||||||
|
context.updateUI = updateUI;
|
||||||
|
context.updateMenu = updateMenu;
|
||||||
|
|
||||||
function renderUI() {
|
function renderUI() {
|
||||||
for (let canvas of canvases) {
|
for (let canvas of canvases) {
|
||||||
let ctx = canvas.getContext("2d");
|
let ctx = canvas.getContext("2d");
|
||||||
|
|
@ -8016,6 +8036,11 @@ function updateLayers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLayers() {
|
function renderLayers() {
|
||||||
|
// Also trigger TimelineV2 redraw if it exists
|
||||||
|
if (context.timelineWidget?.requestRedraw) {
|
||||||
|
context.timelineWidget.requestRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
for (let canvas of document.querySelectorAll(".timeline")) {
|
for (let canvas of document.querySelectorAll(".timeline")) {
|
||||||
const width = canvas.width;
|
const width = canvas.width;
|
||||||
const height = canvas.height;
|
const height = canvas.height;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// Timeline V2 - New timeline implementation for AnimationData curve-based system
|
// Timeline V2 - New timeline implementation for AnimationData curve-based system
|
||||||
|
|
||||||
|
import { backgroundColor, foregroundColor, shadow, labelColor, scrubberColor } from "./styles.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TimelineState - Global state for timeline display and interaction
|
* TimelineState - Global state for timeline display and interaction
|
||||||
*/
|
*/
|
||||||
|
|
@ -163,7 +165,7 @@ class TimeRuler {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
// Background
|
// Background
|
||||||
ctx.fillStyle = '#2a2a2a'
|
ctx.fillStyle = backgroundColor
|
||||||
ctx.fillRect(0, 0, width, this.height)
|
ctx.fillRect(0, 0, width, this.height)
|
||||||
|
|
||||||
// Determine interval based on current zoom and format
|
// Determine interval based on current zoom and format
|
||||||
|
|
@ -200,7 +202,7 @@ class TimeRuler {
|
||||||
const startFrame = Math.floor(this.state.timeToFrame(startTime) / interval) * interval
|
const startFrame = Math.floor(this.state.timeToFrame(startTime) / interval) * interval
|
||||||
const endFrame = Math.ceil(this.state.timeToFrame(endTime) / interval) * interval
|
const endFrame = Math.ceil(this.state.timeToFrame(endTime) / interval) * interval
|
||||||
|
|
||||||
ctx.fillStyle = '#cccccc'
|
ctx.fillStyle = labelColor
|
||||||
ctx.font = '11px sans-serif'
|
ctx.font = '11px sans-serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.textBaseline = 'top'
|
ctx.textBaseline = 'top'
|
||||||
|
|
@ -212,7 +214,7 @@ class TimeRuler {
|
||||||
if (x < 0 || x > width) continue
|
if (x < 0 || x > width) continue
|
||||||
|
|
||||||
// Major tick
|
// Major tick
|
||||||
ctx.strokeStyle = '#888888'
|
ctx.strokeStyle = foregroundColor
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x, this.height - 10)
|
ctx.moveTo(x, this.height - 10)
|
||||||
|
|
@ -232,7 +234,7 @@ class TimeRuler {
|
||||||
|
|
||||||
if (minorX < 0 || minorX > width) continue
|
if (minorX < 0 || minorX > width) continue
|
||||||
|
|
||||||
ctx.strokeStyle = '#555555'
|
ctx.strokeStyle = shadow
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(minorX, this.height - 5)
|
ctx.moveTo(minorX, this.height - 5)
|
||||||
ctx.lineTo(minorX, this.height)
|
ctx.lineTo(minorX, this.height)
|
||||||
|
|
@ -249,7 +251,7 @@ class TimeRuler {
|
||||||
const startTick = Math.floor(startTime / interval) * interval
|
const startTick = Math.floor(startTime / interval) * interval
|
||||||
const endTick = Math.ceil(endTime / interval) * interval
|
const endTick = Math.ceil(endTime / interval) * interval
|
||||||
|
|
||||||
ctx.fillStyle = '#cccccc'
|
ctx.fillStyle = labelColor
|
||||||
ctx.font = '11px sans-serif'
|
ctx.font = '11px sans-serif'
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
ctx.textBaseline = 'top'
|
ctx.textBaseline = 'top'
|
||||||
|
|
@ -260,7 +262,7 @@ class TimeRuler {
|
||||||
if (x < 0 || x > width) continue
|
if (x < 0 || x > width) continue
|
||||||
|
|
||||||
// Major tick
|
// Major tick
|
||||||
ctx.strokeStyle = '#888888'
|
ctx.strokeStyle = foregroundColor
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x, this.height - 10)
|
ctx.moveTo(x, this.height - 10)
|
||||||
|
|
@ -278,7 +280,7 @@ class TimeRuler {
|
||||||
|
|
||||||
if (minorX < 0 || minorX > width) continue
|
if (minorX < 0 || minorX > width) continue
|
||||||
|
|
||||||
ctx.strokeStyle = '#555555'
|
ctx.strokeStyle = shadow
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(minorX, this.height - 5)
|
ctx.moveTo(minorX, this.height - 5)
|
||||||
ctx.lineTo(minorX, this.height)
|
ctx.lineTo(minorX, this.height)
|
||||||
|
|
@ -296,7 +298,7 @@ class TimeRuler {
|
||||||
// Only draw if playhead is visible
|
// Only draw if playhead is visible
|
||||||
if (x < 0 || x > width) return
|
if (x < 0 || x > width) return
|
||||||
|
|
||||||
ctx.strokeStyle = '#ff0000'
|
ctx.strokeStyle = scrubberColor
|
||||||
ctx.lineWidth = 2
|
ctx.lineWidth = 2
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x, 0)
|
ctx.moveTo(x, 0)
|
||||||
|
|
@ -304,7 +306,7 @@ class TimeRuler {
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
// Playhead handle (triangle at top)
|
// Playhead handle (triangle at top)
|
||||||
ctx.fillStyle = '#ff0000'
|
ctx.fillStyle = scrubberColor
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(x, 0)
|
ctx.moveTo(x, 0)
|
||||||
ctx.lineTo(x - 6, 8)
|
ctx.lineTo(x - 6, 8)
|
||||||
|
|
@ -332,7 +334,6 @@ class TimeRuler {
|
||||||
this.state.currentTime = this.state.pixelToTime(x)
|
this.state.currentTime = this.state.pixelToTime(x)
|
||||||
this.state.currentTime = Math.max(0, this.state.currentTime)
|
this.state.currentTime = Math.max(0, this.state.currentTime)
|
||||||
this.draggingPlayhead = true
|
this.draggingPlayhead = true
|
||||||
console.log("TimeRuler: Set draggingPlayhead = true, currentTime =", this.state.currentTime);
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,11 +341,9 @@ class TimeRuler {
|
||||||
* Handle mouse move - drag playhead
|
* Handle mouse move - drag playhead
|
||||||
*/
|
*/
|
||||||
mousemove(x, y) {
|
mousemove(x, y) {
|
||||||
console.log("TimeRuler.mousemove called, draggingPlayhead =", this.draggingPlayhead);
|
|
||||||
if (this.draggingPlayhead) {
|
if (this.draggingPlayhead) {
|
||||||
const newTime = this.state.pixelToTime(x);
|
const newTime = this.state.pixelToTime(x);
|
||||||
this.state.currentTime = Math.max(0, newTime);
|
this.state.currentTime = Math.max(0, newTime);
|
||||||
console.log("TimeRuler: Updated currentTime to", this.state.currentTime);
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
204
src/widgets.js
204
src/widgets.js
|
|
@ -1,6 +1,6 @@
|
||||||
import { backgroundColor, foregroundColor, frameWidth, highlight, layerHeight, shade, shadow } from "./styles.js";
|
import { backgroundColor, foregroundColor, frameWidth, highlight, layerHeight, shade, shadow, labelColor } from "./styles.js";
|
||||||
import { clamp, drawBorderedRect, drawCheckerboardBackground, hslToRgb, hsvToRgb, rgbToHex } from "./utils.js"
|
import { clamp, drawBorderedRect, drawCheckerboardBackground, hslToRgb, hsvToRgb, rgbToHex } from "./utils.js"
|
||||||
import { TimelineState, TimeRuler } from "./timeline.js"
|
import { TimelineState, TimeRuler, TrackHierarchy } from "./timeline.js"
|
||||||
|
|
||||||
function growBoundingBox(bboxa, bboxb) {
|
function growBoundingBox(bboxa, bboxb) {
|
||||||
bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min);
|
bboxa.x.min = Math.min(bboxa.x.min, bboxb.x.min);
|
||||||
|
|
@ -520,6 +520,7 @@ class TimelineWindow extends ScrollableWindow {
|
||||||
/**
|
/**
|
||||||
* TimelineWindowV2 - New timeline widget using AnimationData curve-based system
|
* TimelineWindowV2 - New timeline widget using AnimationData curve-based system
|
||||||
* Phase 1: Time ruler with zoom-adaptive intervals and playhead
|
* Phase 1: Time ruler with zoom-adaptive intervals and playhead
|
||||||
|
* Phase 2: Track hierarchy display
|
||||||
*/
|
*/
|
||||||
class TimelineWindowV2 extends Widget {
|
class TimelineWindowV2 extends Widget {
|
||||||
constructor(x, y, context) {
|
constructor(x, y, context) {
|
||||||
|
|
@ -534,48 +535,235 @@ class TimelineWindowV2 extends Widget {
|
||||||
// Create time ruler widget
|
// Create time ruler widget
|
||||||
this.ruler = new TimeRuler(this.timelineState)
|
this.ruler = new TimeRuler(this.timelineState)
|
||||||
|
|
||||||
|
// Create track hierarchy manager
|
||||||
|
this.trackHierarchy = new TrackHierarchy()
|
||||||
|
|
||||||
// Track if we're dragging playhead
|
// Track if we're dragging playhead
|
||||||
this.draggingPlayhead = false
|
this.draggingPlayhead = false
|
||||||
|
|
||||||
|
// Vertical scroll offset for track hierarchy
|
||||||
|
this.trackScrollOffset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx) {
|
||||||
ctx.save()
|
ctx.save()
|
||||||
|
|
||||||
// Draw background
|
// Draw background
|
||||||
ctx.fillStyle = '#1e1e1e'
|
ctx.fillStyle = backgroundColor
|
||||||
ctx.fillRect(0, 0, this.width, this.height)
|
ctx.fillRect(0, 0, this.width, this.height)
|
||||||
|
|
||||||
// Draw time ruler at top
|
// Draw time ruler at top
|
||||||
this.ruler.draw(ctx, this.width)
|
this.ruler.draw(ctx, this.width)
|
||||||
|
|
||||||
// TODO Phase 2: Draw track hierarchy below ruler
|
// Phase 2: Build and draw track hierarchy
|
||||||
|
if (this.context.activeObject) {
|
||||||
|
this.trackHierarchy.buildTracks(this.context.activeObject)
|
||||||
|
this.drawTracks(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Phase 3: Draw segments
|
// TODO Phase 3: Draw segments
|
||||||
// TODO Phase 4: Draw minimized curves
|
// TODO Phase 4: Draw minimized curves
|
||||||
|
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw track hierarchy (Phase 2)
|
||||||
|
*/
|
||||||
|
drawTracks(ctx) {
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(0, this.ruler.height) // Start below ruler
|
||||||
|
|
||||||
|
// Clip to available track area
|
||||||
|
const trackAreaHeight = this.height - this.ruler.height
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(0, 0, this.width, trackAreaHeight)
|
||||||
|
ctx.clip()
|
||||||
|
|
||||||
|
// Apply vertical scroll offset
|
||||||
|
ctx.translate(0, this.trackScrollOffset)
|
||||||
|
|
||||||
|
const indentSize = 20 // Pixels per indent level
|
||||||
|
|
||||||
|
for (let i = 0; i < this.trackHierarchy.tracks.length; i++) {
|
||||||
|
const track = this.trackHierarchy.tracks[i]
|
||||||
|
const y = i * this.trackHierarchy.trackHeight
|
||||||
|
|
||||||
|
// Check if this track is selected
|
||||||
|
const isSelected = this.isTrackSelected(track)
|
||||||
|
|
||||||
|
// Draw track background (alternating colors or selected highlight)
|
||||||
|
if (isSelected) {
|
||||||
|
ctx.fillStyle = highlight // Highlighted color for selected track
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = i % 2 === 0 ? backgroundColor : shade
|
||||||
|
}
|
||||||
|
ctx.fillRect(0, y, this.width, this.trackHierarchy.trackHeight)
|
||||||
|
|
||||||
|
// Draw track border
|
||||||
|
ctx.strokeStyle = shadow
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(0, y + this.trackHierarchy.trackHeight)
|
||||||
|
ctx.lineTo(this.width, y + this.trackHierarchy.trackHeight)
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Calculate indent
|
||||||
|
const indent = track.indent * indentSize
|
||||||
|
|
||||||
|
// Draw expand/collapse indicator for layers and objects with children
|
||||||
|
if (track.type === 'layer' || (track.type === 'object' && track.object.children && track.object.children.length > 0)) {
|
||||||
|
const triangleX = indent + 8
|
||||||
|
const triangleY = y + this.trackHierarchy.trackHeight / 2
|
||||||
|
|
||||||
|
ctx.fillStyle = foregroundColor
|
||||||
|
ctx.beginPath()
|
||||||
|
if (track.collapsed) {
|
||||||
|
// Collapsed: right-pointing triangle ▶
|
||||||
|
ctx.moveTo(triangleX, triangleY - 4)
|
||||||
|
ctx.lineTo(triangleX + 6, triangleY)
|
||||||
|
ctx.lineTo(triangleX, triangleY + 4)
|
||||||
|
} else {
|
||||||
|
// Expanded: down-pointing triangle ▼
|
||||||
|
ctx.moveTo(triangleX - 4, triangleY - 2)
|
||||||
|
ctx.lineTo(triangleX + 4, triangleY - 2)
|
||||||
|
ctx.lineTo(triangleX, triangleY + 4)
|
||||||
|
}
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw track name
|
||||||
|
ctx.fillStyle = labelColor
|
||||||
|
ctx.font = '12px sans-serif'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.textBaseline = 'middle'
|
||||||
|
ctx.fillText(track.name, indent + 20, y + this.trackHierarchy.trackHeight / 2)
|
||||||
|
|
||||||
|
// Draw type indicator
|
||||||
|
ctx.fillStyle = foregroundColor
|
||||||
|
ctx.font = '10px sans-serif'
|
||||||
|
const typeText = track.type === 'layer' ? '[L]' : track.type === 'object' ? '[G]' : '[S]'
|
||||||
|
ctx.fillText(typeText, indent + 20 + ctx.measureText(track.name).width + 8, y + this.trackHierarchy.trackHeight / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
mousedown(x, y) {
|
mousedown(x, y) {
|
||||||
console.log("TimelineV2 mousedown:", x, y, "ruler height:", this.ruler.height);
|
|
||||||
// Check if clicking in ruler area
|
// Check if clicking in ruler area
|
||||||
if (y <= this.ruler.height) {
|
if (y <= this.ruler.height) {
|
||||||
// Let the ruler handle the mousedown (for playhead dragging)
|
// Let the ruler handle the mousedown (for playhead dragging)
|
||||||
const hitPlayhead = this.ruler.mousedown(x, y);
|
const hitPlayhead = this.ruler.mousedown(x, y);
|
||||||
console.log("Ruler mousedown returned:", hitPlayhead);
|
|
||||||
if (hitPlayhead) {
|
if (hitPlayhead) {
|
||||||
this.draggingPlayhead = true
|
this.draggingPlayhead = true
|
||||||
this._globalEvents.add("mousemove")
|
this._globalEvents.add("mousemove")
|
||||||
this._globalEvents.add("mouseup")
|
this._globalEvents.add("mouseup")
|
||||||
console.log("Started dragging playhead");
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if clicking in track area
|
||||||
|
const trackY = y - this.ruler.height
|
||||||
|
if (trackY >= 0) {
|
||||||
|
// Adjust for vertical scroll offset
|
||||||
|
const adjustedY = trackY - this.trackScrollOffset
|
||||||
|
const track = this.trackHierarchy.getTrackAtY(adjustedY)
|
||||||
|
if (track) {
|
||||||
|
const indentSize = 20
|
||||||
|
const indent = track.indent * indentSize
|
||||||
|
const triangleX = indent + 8
|
||||||
|
|
||||||
|
// Check if clicking on expand/collapse triangle
|
||||||
|
if (x >= triangleX - 8 && x <= triangleX + 14) {
|
||||||
|
// Toggle collapsed state
|
||||||
|
if (track.type === 'layer') {
|
||||||
|
track.object.collapsed = !track.object.collapsed
|
||||||
|
} else if (track.type === 'object') {
|
||||||
|
track.object.trackCollapsed = !track.object.trackCollapsed
|
||||||
|
}
|
||||||
|
// Rebuild tracks after collapsing/expanding
|
||||||
|
this.trackHierarchy.buildTracks(this.context.activeObject)
|
||||||
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clicking elsewhere on track selects it
|
||||||
|
this.selectTrack(track)
|
||||||
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a track is currently selected
|
||||||
|
*/
|
||||||
|
isTrackSelected(track) {
|
||||||
|
if (track.type === 'layer') {
|
||||||
|
return this.context.activeLayer === track.object
|
||||||
|
} else if (track.type === 'shape') {
|
||||||
|
return this.context.shapeselection?.includes(track.object)
|
||||||
|
} else if (track.type === 'object') {
|
||||||
|
return this.context.selection?.includes(track.object)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a track and update the stage selection
|
||||||
|
*/
|
||||||
|
selectTrack(track) {
|
||||||
|
// Store old selection before changing
|
||||||
|
this.context.oldselection = this.context.selection
|
||||||
|
this.context.oldshapeselection = this.context.shapeselection
|
||||||
|
|
||||||
|
if (track.type === 'layer') {
|
||||||
|
// Find the index of this layer in the activeObject
|
||||||
|
const layerIndex = this.context.activeObject.children.indexOf(track.object)
|
||||||
|
if (layerIndex !== -1) {
|
||||||
|
this.context.activeObject.currentLayer = layerIndex
|
||||||
|
}
|
||||||
|
// Clear selections when selecting layer
|
||||||
|
this.context.selection = []
|
||||||
|
this.context.shapeselection = []
|
||||||
|
} else if (track.type === 'shape') {
|
||||||
|
// Find the layer this shape belongs to and select it
|
||||||
|
for (let i = 0; i < this.context.activeObject.allLayers.length; i++) {
|
||||||
|
const layer = this.context.activeObject.allLayers[i]
|
||||||
|
if (layer.shapes && layer.shapes.includes(track.object)) {
|
||||||
|
// Find index in children array
|
||||||
|
const layerIndex = this.context.activeObject.children.indexOf(layer)
|
||||||
|
if (layerIndex !== -1) {
|
||||||
|
this.context.activeObject.currentLayer = layerIndex
|
||||||
|
}
|
||||||
|
// Set shape selection
|
||||||
|
this.context.shapeselection = [track.object]
|
||||||
|
this.context.selection = []
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (track.type === 'object') {
|
||||||
|
// Select the GraphicsObject
|
||||||
|
this.context.selection = [track.object]
|
||||||
|
this.context.shapeselection = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the stage UI to reflect selection changes
|
||||||
|
if (this.context.updateUI) {
|
||||||
|
this.context.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update menu to enable/disable menu items based on selection
|
||||||
|
if (this.context.updateMenu) {
|
||||||
|
this.context.updateMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mousemove(x, y) {
|
mousemove(x, y) {
|
||||||
if (this.draggingPlayhead) {
|
if (this.draggingPlayhead) {
|
||||||
console.log("TimelineV2 mousemove while dragging:", x, y);
|
|
||||||
// Let the ruler handle the mousemove
|
// Let the ruler handle the mousemove
|
||||||
this.ruler.mousemove(x, y)
|
this.ruler.mousemove(x, y)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue