mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-21 20:52:12 -03:00
Switch mask preview from chunked to canvas rendering
Replaced chunked rendering approach with direct canvas drawing for mask preview, then applying to main canvas. Added "Mask Opacity" slider.
This commit is contained in:
@@ -131,6 +131,11 @@ export class CanvasInteractions {
|
||||
this.canvas.viewport.x = worldCoords.x - (mouseBufferX / this.canvas.viewport.zoom);
|
||||
this.canvas.viewport.y = worldCoords.y - (mouseBufferY / this.canvas.viewport.zoom);
|
||||
|
||||
// Update stroke overlay if mask tool is drawing during zoom
|
||||
if (this.canvas.maskTool.isDrawing) {
|
||||
this.canvas.maskTool.handleViewportChange();
|
||||
}
|
||||
|
||||
this.canvas.onViewportChange?.();
|
||||
}
|
||||
|
||||
@@ -243,7 +248,7 @@ export class CanvasInteractions {
|
||||
|
||||
if (this.interaction.mode === 'drawingMask') {
|
||||
this.canvas.maskTool.handleMouseDown(coords.world, coords.view);
|
||||
this.canvas.render();
|
||||
// Don't render here - mask tool will handle its own drawing
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -327,10 +332,7 @@ export class CanvasInteractions {
|
||||
switch (this.interaction.mode) {
|
||||
case 'drawingMask':
|
||||
this.canvas.maskTool.handleMouseMove(coords.world, coords.view);
|
||||
// Only render if actually drawing, not just moving cursor
|
||||
if (this.canvas.maskTool.isDrawing) {
|
||||
this.canvas.render();
|
||||
}
|
||||
// Don't render during mask drawing - it's handled by mask tool internally
|
||||
break;
|
||||
case 'panning':
|
||||
this.panViewport(e);
|
||||
@@ -370,6 +372,7 @@ export class CanvasInteractions {
|
||||
|
||||
if (this.interaction.mode === 'drawingMask') {
|
||||
this.canvas.maskTool.handleMouseUp(coords.view);
|
||||
// Render only once after drawing is complete
|
||||
this.canvas.render();
|
||||
return;
|
||||
}
|
||||
@@ -840,6 +843,12 @@ export class CanvasInteractions {
|
||||
this.canvas.viewport.x -= dx / this.canvas.viewport.zoom;
|
||||
this.canvas.viewport.y -= dy / this.canvas.viewport.zoom;
|
||||
this.interaction.panStart = {x: e.clientX, y: e.clientY};
|
||||
|
||||
// Update stroke overlay if mask tool is drawing during pan
|
||||
if (this.canvas.maskTool.isDrawing) {
|
||||
this.canvas.maskTool.handleViewportChange();
|
||||
}
|
||||
|
||||
this.canvas.render();
|
||||
this.canvas.onViewportChange?.();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ export class CanvasRenderer {
|
||||
lastRenderTime: any;
|
||||
renderAnimationFrame: any;
|
||||
renderInterval: any;
|
||||
// Overlay used to preview in-progress mask strokes (separate from cursor overlay)
|
||||
strokeOverlayCanvas!: HTMLCanvasElement;
|
||||
strokeOverlayCtx!: CanvasRenderingContext2D;
|
||||
constructor(canvas: any) {
|
||||
this.canvas = canvas;
|
||||
this.renderAnimationFrame = null;
|
||||
@@ -15,8 +18,9 @@ export class CanvasRenderer {
|
||||
this.renderInterval = 1000 / 60;
|
||||
this.isDirty = false;
|
||||
|
||||
// Initialize overlay canvas
|
||||
// Initialize overlay canvases
|
||||
this.initOverlay();
|
||||
this.initStrokeOverlay();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,9 +148,11 @@ export class CanvasRenderer {
|
||||
ctx.save();
|
||||
|
||||
if (this.canvas.maskTool.isActive) {
|
||||
// In draw mask mode, use the previewOpacity value from the slider
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 0.5;
|
||||
ctx.globalAlpha = this.canvas.maskTool.previewOpacity;
|
||||
} else {
|
||||
// When not in draw mask mode, show mask at full opacity
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
ctx.globalAlpha = 1.0;
|
||||
}
|
||||
@@ -208,9 +214,11 @@ export class CanvasRenderer {
|
||||
}
|
||||
this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);
|
||||
|
||||
// Ensure overlay canvas is in DOM and properly sized
|
||||
// Ensure overlay canvases are in DOM and properly sized
|
||||
this.addOverlayToDOM();
|
||||
this.updateOverlaySize();
|
||||
this.addStrokeOverlayToDOM();
|
||||
this.updateStrokeOverlaySize();
|
||||
|
||||
// Update Batch Preview UI positions
|
||||
if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
|
||||
@@ -769,6 +777,141 @@ export class CanvasRenderer {
|
||||
this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a dedicated overlay for real-time mask stroke preview
|
||||
*/
|
||||
initStrokeOverlay(): void {
|
||||
// Create canvas if not created yet
|
||||
if (!this.strokeOverlayCanvas) {
|
||||
this.strokeOverlayCanvas = document.createElement('canvas');
|
||||
const ctx = this.strokeOverlayCanvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get 2D context for stroke overlay canvas');
|
||||
}
|
||||
this.strokeOverlayCtx = ctx;
|
||||
}
|
||||
|
||||
// Size match main canvas
|
||||
this.updateStrokeOverlaySize();
|
||||
|
||||
// Position above main canvas but below cursor overlay
|
||||
this.strokeOverlayCanvas.style.position = 'absolute';
|
||||
this.strokeOverlayCanvas.style.left = '1px';
|
||||
this.strokeOverlayCanvas.style.top = '1px';
|
||||
this.strokeOverlayCanvas.style.pointerEvents = 'none';
|
||||
this.strokeOverlayCanvas.style.zIndex = '19'; // Below cursor overlay (20)
|
||||
// Opacity is now controlled by MaskTool.previewOpacity
|
||||
this.strokeOverlayCanvas.style.opacity = String(this.canvas.maskTool.previewOpacity || 0.5);
|
||||
|
||||
// Add to DOM
|
||||
this.addStrokeOverlayToDOM();
|
||||
log.debug('Stroke overlay canvas initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add stroke overlay canvas to DOM if needed
|
||||
*/
|
||||
addStrokeOverlayToDOM(): void {
|
||||
if (this.canvas.canvas.parentElement && !this.strokeOverlayCanvas.parentElement) {
|
||||
this.canvas.canvas.parentElement.appendChild(this.strokeOverlayCanvas);
|
||||
log.debug('Stroke overlay canvas added to DOM');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure stroke overlay size matches main canvas
|
||||
*/
|
||||
updateStrokeOverlaySize(): void {
|
||||
const w = Math.max(1, this.canvas.canvas.clientWidth);
|
||||
const h = Math.max(1, this.canvas.canvas.clientHeight);
|
||||
if (this.strokeOverlayCanvas.width !== w || this.strokeOverlayCanvas.height !== h) {
|
||||
this.strokeOverlayCanvas.width = w;
|
||||
this.strokeOverlayCanvas.height = h;
|
||||
log.debug(`Stroke overlay resized to ${w}x${h}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the stroke overlay
|
||||
*/
|
||||
clearMaskStrokeOverlay(): void {
|
||||
if (!this.strokeOverlayCtx) return;
|
||||
this.strokeOverlayCtx.clearRect(0, 0, this.strokeOverlayCanvas.width, this.strokeOverlayCanvas.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a preview stroke segment onto the stroke overlay in screen space
|
||||
* Uses line drawing with gradient to match MaskTool's drawLineOnChunk exactly
|
||||
*/
|
||||
drawMaskStrokeSegment(startWorld: { x: number; y: number }, endWorld: { x: number; y: number }): void {
|
||||
// Ensure overlay is present and sized
|
||||
this.updateStrokeOverlaySize();
|
||||
|
||||
const zoom = this.canvas.viewport.zoom;
|
||||
const toScreen = (p: { x: number; y: number }) => ({
|
||||
x: (p.x - this.canvas.viewport.x) * zoom,
|
||||
y: (p.y - this.canvas.viewport.y) * zoom
|
||||
});
|
||||
|
||||
const startScreen = toScreen(startWorld);
|
||||
const endScreen = toScreen(endWorld);
|
||||
|
||||
const brushRadius = (this.canvas.maskTool.brushSize / 2) * zoom;
|
||||
const hardness = this.canvas.maskTool.brushHardness;
|
||||
const strength = this.canvas.maskTool.brushStrength;
|
||||
|
||||
// If strength is 0, don't draw anything
|
||||
if (strength <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.strokeOverlayCtx.save();
|
||||
|
||||
// Draw line segment exactly as MaskTool does
|
||||
this.strokeOverlayCtx.beginPath();
|
||||
this.strokeOverlayCtx.moveTo(startScreen.x, startScreen.y);
|
||||
this.strokeOverlayCtx.lineTo(endScreen.x, endScreen.y);
|
||||
|
||||
// Match the gradient setup from MaskTool's drawLineOnChunk
|
||||
if (hardness === 1) {
|
||||
this.strokeOverlayCtx.strokeStyle = `rgba(255, 255, 255, ${strength})`;
|
||||
} else {
|
||||
const innerRadius = brushRadius * hardness;
|
||||
const gradient = this.strokeOverlayCtx.createRadialGradient(
|
||||
endScreen.x, endScreen.y, innerRadius,
|
||||
endScreen.x, endScreen.y, brushRadius
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${strength})`);
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||
this.strokeOverlayCtx.strokeStyle = gradient;
|
||||
}
|
||||
|
||||
// Match line properties from MaskTool
|
||||
this.strokeOverlayCtx.lineWidth = this.canvas.maskTool.brushSize * zoom;
|
||||
this.strokeOverlayCtx.lineCap = 'round';
|
||||
this.strokeOverlayCtx.lineJoin = 'round';
|
||||
this.strokeOverlayCtx.globalCompositeOperation = 'source-over';
|
||||
this.strokeOverlayCtx.stroke();
|
||||
|
||||
this.strokeOverlayCtx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the entire stroke overlay from world coordinates
|
||||
* Used when viewport changes during drawing to maintain visual consistency
|
||||
*/
|
||||
redrawMaskStrokeOverlay(strokePoints: { x: number; y: number }[]): void {
|
||||
if (strokePoints.length < 2) return;
|
||||
|
||||
// Clear the overlay first
|
||||
this.clearMaskStrokeOverlay();
|
||||
|
||||
// Redraw all segments with current viewport
|
||||
for (let i = 1; i < strokePoints.length; i++) {
|
||||
this.drawMaskStrokeSegment(strokePoints[i - 1], strokePoints[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness
|
||||
* @param worldPoint World coordinates of cursor
|
||||
@@ -797,27 +940,29 @@ export class CanvasRenderer {
|
||||
// Save context state
|
||||
this.canvas.overlayCtx.save();
|
||||
|
||||
// 1. Draw inner fill to visualize STRENGTH (opacity)
|
||||
// Higher strength = more opaque fill
|
||||
this.canvas.overlayCtx.beginPath();
|
||||
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||
this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${brushStrength * 0.2})`; // Max 20% opacity for visibility
|
||||
this.canvas.overlayCtx.fill();
|
||||
|
||||
// 2. Draw gradient edge to visualize HARDNESS
|
||||
// Hard brush = sharp edge, Soft brush = gradient edge
|
||||
if (brushHardness < 1) {
|
||||
// Create radial gradient for soft brushes
|
||||
const innerRadius = brushRadius * brushHardness;
|
||||
// If strength is 0, just draw outline
|
||||
if (brushStrength > 0) {
|
||||
// Draw inner fill to visualize brush effect - matches actual brush rendering
|
||||
const gradient = this.canvas.overlayCtx.createRadialGradient(
|
||||
screenX, screenY, innerRadius,
|
||||
screenX, screenY, 0,
|
||||
screenX, screenY, brushRadius
|
||||
);
|
||||
|
||||
// Inner part is solid
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + brushStrength * 0.3})`);
|
||||
// Outer part fades based on hardness
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, ${0.05})`);
|
||||
// Preview alpha - subtle to not obscure content
|
||||
const previewAlpha = brushStrength * 0.15; // Very subtle preview (max 15% opacity)
|
||||
|
||||
if (brushHardness === 1) {
|
||||
// Hard brush - uniform fill within radius
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${previewAlpha})`);
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, ${previewAlpha})`);
|
||||
} else {
|
||||
// Soft brush - gradient fade matching actual brush
|
||||
gradient.addColorStop(0, `rgba(255, 255, 255, ${previewAlpha})`);
|
||||
if (brushHardness > 0) {
|
||||
gradient.addColorStop(brushHardness, `rgba(255, 255, 255, ${previewAlpha})`);
|
||||
}
|
||||
gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
|
||||
}
|
||||
|
||||
this.canvas.overlayCtx.beginPath();
|
||||
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||
@@ -825,28 +970,28 @@ export class CanvasRenderer {
|
||||
this.canvas.overlayCtx.fill();
|
||||
}
|
||||
|
||||
// 3. Draw outer circle (SIZE indicator)
|
||||
// Draw outer circle (SIZE indicator)
|
||||
this.canvas.overlayCtx.beginPath();
|
||||
this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
|
||||
|
||||
// Make the stroke opacity also reflect strength slightly
|
||||
const strokeOpacity = 0.4 + brushStrength * 0.4; // Range from 0.4 to 0.8
|
||||
// Stroke opacity based on strength (dimmer when strength is 0)
|
||||
const strokeOpacity = brushStrength > 0 ? (0.4 + brushStrength * 0.4) : 0.3;
|
||||
this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
|
||||
this.canvas.overlayCtx.lineWidth = 1.5;
|
||||
|
||||
// Use solid line for hard brushes, dashed for soft brushes
|
||||
// Visual feedback for hardness
|
||||
if (brushHardness > 0.8) {
|
||||
// Hard brush - solid line
|
||||
this.canvas.overlayCtx.setLineDash([]);
|
||||
} else {
|
||||
// Soft brush - dashed line, dash length based on hardness
|
||||
const dashLength = 2 + (1 - brushHardness) * 4; // Longer dashes for softer brushes
|
||||
// Soft brush - dashed line
|
||||
const dashLength = 2 + (1 - brushHardness) * 4;
|
||||
this.canvas.overlayCtx.setLineDash([dashLength, dashLength]);
|
||||
}
|
||||
|
||||
this.canvas.overlayCtx.stroke();
|
||||
|
||||
// 4. Optional: Draw center dot for very precise brushes
|
||||
// Center dot for small brushes
|
||||
if (brushRadius < 5) {
|
||||
this.canvas.overlayCtx.beginPath();
|
||||
this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI);
|
||||
|
||||
@@ -640,6 +640,24 @@ $el("label.clipboard-switch.mask-switch", {
|
||||
setTimeout(() => canvas.render(), 0);
|
||||
}
|
||||
}),
|
||||
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
||||
$el("label", {for: "preview-opacity-slider", textContent: "Mask Opacity:"}),
|
||||
$el("input", {
|
||||
id: "preview-opacity-slider",
|
||||
type: "range",
|
||||
min: "0",
|
||||
max: "1",
|
||||
step: "0.05",
|
||||
value: "0.5",
|
||||
oninput: (e: Event) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
canvas.maskTool.setPreviewOpacity(parseFloat(value));
|
||||
const valueEl = document.getElementById('preview-opacity-value');
|
||||
if (valueEl) valueEl.textContent = `${Math.round(parseFloat(value) * 100)}%`;
|
||||
}
|
||||
}),
|
||||
$el("div.slider-value", {id: "preview-opacity-value"}, ["50%"])
|
||||
]),
|
||||
$el("div.painter-slider-container.mask-control", {style: {display: 'none'}}, [
|
||||
$el("label", {for: "brush-size-slider", textContent: "Size:"}),
|
||||
$el("input", {
|
||||
|
||||
174
src/MaskTool.ts
174
src/MaskTool.ts
@@ -24,6 +24,7 @@ export class MaskTool {
|
||||
private _brushHardness: number;
|
||||
public brushSize: number;
|
||||
private _brushStrength: number;
|
||||
private _previewOpacity: number;
|
||||
private canvasInstance: Canvas & { canvasState: CanvasState, width: number, height: number };
|
||||
public isActive: boolean;
|
||||
public isDrawing: boolean;
|
||||
@@ -31,6 +32,9 @@ export class MaskTool {
|
||||
private lastPosition: Point | null;
|
||||
private mainCanvas: HTMLCanvasElement;
|
||||
|
||||
// Track strokes during drawing for efficient overlay updates
|
||||
private currentStrokePoints: Point[] = [];
|
||||
|
||||
// Chunked mask system
|
||||
private maskChunks: Map<string, MaskChunk>; // Key: "x,y" (chunk coordinates)
|
||||
private chunkSize: number;
|
||||
@@ -72,6 +76,9 @@ export class MaskTool {
|
||||
this.mainCanvas = canvasInstance.canvas;
|
||||
this.onStateChange = callbacks.onStateChange || null;
|
||||
|
||||
// Initialize stroke tracking for overlay drawing
|
||||
this.currentStrokePoints = [];
|
||||
|
||||
// Initialize chunked mask system
|
||||
this.maskChunks = new Map();
|
||||
this.chunkSize = 512;
|
||||
@@ -98,6 +105,7 @@ export class MaskTool {
|
||||
this.brushSize = 20;
|
||||
this._brushStrength = 0.5;
|
||||
this._brushHardness = 0.5;
|
||||
this._previewOpacity = 0.5; // Default 50% opacity for preview
|
||||
this.isDrawing = false;
|
||||
this.lastPosition = null;
|
||||
|
||||
@@ -165,10 +173,24 @@ export class MaskTool {
|
||||
return this._brushHardness;
|
||||
}
|
||||
|
||||
get previewOpacity(): number {
|
||||
return this._previewOpacity;
|
||||
}
|
||||
|
||||
setBrushHardness(hardness: number): void {
|
||||
this._brushHardness = Math.max(0, Math.min(1, hardness));
|
||||
}
|
||||
|
||||
setPreviewOpacity(opacity: number): void {
|
||||
this._previewOpacity = Math.max(0, Math.min(1, opacity));
|
||||
// Update the stroke overlay canvas opacity when preview opacity changes
|
||||
if (this.canvasInstance.canvasRenderer && this.canvasInstance.canvasRenderer.strokeOverlayCanvas) {
|
||||
this.canvasInstance.canvasRenderer.strokeOverlayCanvas.style.opacity = String(this._previewOpacity);
|
||||
}
|
||||
// Trigger canvas render to update mask display opacity
|
||||
this.canvasInstance.render();
|
||||
}
|
||||
|
||||
initMaskCanvas(): void {
|
||||
// Initialize chunked system
|
||||
this.chunkSize = 512;
|
||||
@@ -884,10 +906,12 @@ export class MaskTool {
|
||||
this.isDrawing = true;
|
||||
this.lastPosition = worldCoords;
|
||||
|
||||
// Activate chunks around the drawing position for performance
|
||||
this.updateActiveChunksForDrawing(worldCoords);
|
||||
// Initialize stroke tracking for live preview
|
||||
this.currentStrokePoints = [worldCoords];
|
||||
|
||||
// Clear any previous stroke overlay
|
||||
this.canvasInstance.canvasRenderer.clearMaskStrokeOverlay();
|
||||
|
||||
this.draw(worldCoords);
|
||||
this.clearPreview();
|
||||
}
|
||||
|
||||
@@ -897,18 +921,83 @@ export class MaskTool {
|
||||
}
|
||||
if (!this.isActive || !this.isDrawing) return;
|
||||
|
||||
// Dynamically update active chunks as user moves while drawing
|
||||
this.updateActiveChunksForDrawing(worldCoords);
|
||||
// Add point to stroke tracking
|
||||
this.currentStrokePoints.push(worldCoords);
|
||||
|
||||
// Draw interpolated segments for smooth strokes without gaps
|
||||
if (this.lastPosition) {
|
||||
// Calculate distance between last and current position
|
||||
const dx = worldCoords.x - this.lastPosition.x;
|
||||
const dy = worldCoords.y - this.lastPosition.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// If distance is small, just draw a single segment
|
||||
if (distance < this.brushSize / 4) {
|
||||
this.canvasInstance.canvasRenderer.drawMaskStrokeSegment(this.lastPosition, worldCoords);
|
||||
} else {
|
||||
// Interpolate points for smooth drawing without gaps
|
||||
const interpolatedPoints = this.interpolatePoints(this.lastPosition, worldCoords, distance);
|
||||
|
||||
// Draw all interpolated segments
|
||||
for (let i = 0; i < interpolatedPoints.length - 1; i++) {
|
||||
this.canvasInstance.canvasRenderer.drawMaskStrokeSegment(
|
||||
interpolatedPoints[i],
|
||||
interpolatedPoints[i + 1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.draw(worldCoords);
|
||||
this.lastPosition = worldCoords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates points between two positions to create smooth strokes without gaps
|
||||
* Based on the BrushTool's approach for eliminating dotted lines during fast drawing
|
||||
*/
|
||||
private interpolatePoints(start: Point, end: Point, distance: number): Point[] {
|
||||
const points: Point[] = [];
|
||||
|
||||
// Calculate number of interpolated points based on brush size
|
||||
// More points = smoother line
|
||||
const stepSize = Math.max(1, this.brushSize / 6); // Adjust divisor for smoothness
|
||||
const numSteps = Math.ceil(distance / stepSize);
|
||||
|
||||
// Always include start point
|
||||
points.push(start);
|
||||
|
||||
// Interpolate intermediate points
|
||||
for (let i = 1; i < numSteps; i++) {
|
||||
const t = i / numSteps;
|
||||
points.push({
|
||||
x: start.x + (end.x - start.x) * t,
|
||||
y: start.y + (end.y - start.y) * t
|
||||
});
|
||||
}
|
||||
|
||||
// Always include end point
|
||||
points.push(end);
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when viewport changes during drawing to update stroke overlay
|
||||
* This ensures the stroke preview scales correctly with zoom changes
|
||||
*/
|
||||
handleViewportChange(): void {
|
||||
if (this.isDrawing && this.currentStrokePoints.length > 1) {
|
||||
// Redraw the entire stroke overlay with new viewport settings
|
||||
this.canvasInstance.canvasRenderer.redrawMaskStrokeOverlay(this.currentStrokePoints);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseLeave(): void {
|
||||
this.previewVisible = false;
|
||||
this.clearPreview();
|
||||
// Clear overlay canvas when mouse leaves
|
||||
// Clear overlay canvases when mouse leaves
|
||||
this.canvasInstance.canvasRenderer.clearOverlay();
|
||||
this.canvasInstance.canvasRenderer.clearMaskStrokeOverlay();
|
||||
}
|
||||
|
||||
handleMouseEnter(): void {
|
||||
@@ -919,11 +1008,18 @@ export class MaskTool {
|
||||
if (!this.isActive) return;
|
||||
if (this.isDrawing) {
|
||||
this.isDrawing = false;
|
||||
|
||||
// Commit the stroke from overlay to actual mask chunks
|
||||
this.commitStrokeToChunks();
|
||||
|
||||
// Clear stroke overlay and reset state
|
||||
this.canvasInstance.canvasRenderer.clearMaskStrokeOverlay();
|
||||
this.currentStrokePoints = [];
|
||||
this.lastPosition = null;
|
||||
this.currentDrawingChunk = null;
|
||||
|
||||
// After drawing is complete, update active canvas to show all chunks
|
||||
this.updateActiveMaskCanvas(true); // forceShowAll = true
|
||||
this.updateActiveMaskCanvas(true); // Force full update
|
||||
|
||||
this.completeMaskOperation();
|
||||
this.drawBrushPreview(viewCoords);
|
||||
@@ -943,6 +1039,44 @@ export class MaskTool {
|
||||
this.updateActiveCanvasIfNeeded(this.lastPosition, worldCoords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current stroke from overlay to actual mask chunks
|
||||
* This replays the entire stroke path with interpolation to ensure pixel-perfect accuracy
|
||||
*/
|
||||
private commitStrokeToChunks(): void {
|
||||
if (this.currentStrokePoints.length < 2) {
|
||||
return; // Need at least 2 points for a stroke
|
||||
}
|
||||
|
||||
log.debug(`Committing stroke with ${this.currentStrokePoints.length} points to chunks`);
|
||||
|
||||
// Replay the entire stroke path with interpolation for smooth, accurate lines
|
||||
for (let i = 1; i < this.currentStrokePoints.length; i++) {
|
||||
const startPoint = this.currentStrokePoints[i - 1];
|
||||
const endPoint = this.currentStrokePoints[i];
|
||||
|
||||
// Calculate distance between points
|
||||
const dx = endPoint.x - startPoint.x;
|
||||
const dy = endPoint.y - startPoint.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < this.brushSize / 4) {
|
||||
// Small distance - draw single segment
|
||||
this.drawOnChunks(startPoint, endPoint);
|
||||
} else {
|
||||
// Large distance - interpolate for smooth line without gaps
|
||||
const interpolatedPoints = this.interpolatePoints(startPoint, endPoint, distance);
|
||||
|
||||
// Draw all interpolated segments
|
||||
for (let j = 0; j < interpolatedPoints.length - 1; j++) {
|
||||
this.drawOnChunks(interpolatedPoints[j], interpolatedPoints[j + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Stroke committed to chunks successfully with interpolation");
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a line between two world coordinates on the appropriate chunks
|
||||
*/
|
||||
@@ -1040,29 +1174,17 @@ export class MaskTool {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates active canvas when drawing affects chunks with throttling to prevent lag
|
||||
* During drawing, only updates the affected active chunks for performance
|
||||
* Updates active canvas when drawing affects chunks
|
||||
* Since we now use overlay during drawing, this is only called after drawing is complete
|
||||
*/
|
||||
private updateActiveCanvasIfNeeded(startWorld: Point, endWorld: Point): void {
|
||||
// Calculate which chunks were affected by this drawing operation
|
||||
const minX = Math.min(startWorld.x, endWorld.x) - this.brushSize;
|
||||
const maxX = Math.max(startWorld.x, endWorld.x) + this.brushSize;
|
||||
const minY = Math.min(startWorld.y, endWorld.y) - this.brushSize;
|
||||
const maxY = Math.max(startWorld.y, endWorld.y) + this.brushSize;
|
||||
|
||||
const affectedChunkMinX = Math.floor(minX / this.chunkSize);
|
||||
const affectedChunkMinY = Math.floor(minY / this.chunkSize);
|
||||
const affectedChunkMaxX = Math.floor(maxX / this.chunkSize);
|
||||
const affectedChunkMaxY = Math.floor(maxY / this.chunkSize);
|
||||
|
||||
// During drawing, only update affected chunks that are active for performance
|
||||
if (this.isDrawing) {
|
||||
// Use throttled partial update for active chunks only
|
||||
this.scheduleThrottledActiveMaskUpdate(affectedChunkMinX, affectedChunkMinY, affectedChunkMaxX, affectedChunkMaxY);
|
||||
} else {
|
||||
// This method is now simplified - we only update after drawing is complete
|
||||
// The overlay handles all live preview, so we don't need complex chunk activation
|
||||
if (!this.isDrawing) {
|
||||
// Not drawing - do full update to show all chunks
|
||||
this.updateActiveMaskCanvas(true);
|
||||
}
|
||||
// During drawing, we don't update chunks at all - overlay handles preview
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user