Skip to content

Latest commit

Β 

History

History
911 lines (716 loc) Β· 25.3 KB

File metadata and controls

911 lines (716 loc) Β· 25.3 KB

RPUT System Changes: Old vs New Implementation

Overview

This document details the comprehensive changes made to the RPUT (LaTeX positioning) system in LaTeX2JS to fix race conditions, positioning issues, and improve reliability.

Table of Contents

  1. Core RPUT Function Changes
  2. Coordinate Transformation Improvements
  3. Cleanup and Re-render Pipeline
  4. Error Handling and Validation
  5. Performance and Timing Improvements
  6. Debug Infrastructure
  7. CSS and Styling Changes
  8. Migration Guide

Core RPUT Function Changes

πŸ“ Location: /packages/pstricks/src/lib/psgraph.ts:444-584

Old Implementation

rput(el: any): void {
  const div = document.createElement('div');

  const x = this.x;
  const y = this.y;

  div.innerHTML = this.text;
  div.className = 'math';
  div.style.visibility = 'hidden';
  div.style.position = 'absolute';
  div.style.top = `${y}px`;
  div.style.left = `${x}px`;

  el.appendChild(div);

  const done = () => {
    const rct = div.getBoundingClientRect();
    const w = rct.width / 2;
    const h = rct.height / 2;
    div.style.visibility = 'visible';
    div.style.top = `${y - h}px`;
    div.style.left = `${x - w}px`;
  };

  const mathJax = (window as any).MathJax;
  if (mathJax && mathJax.typesetPromise) {
    mathJax.typesetPromise([div]).then(done).catch((err: any) => {
      console.error('MathJax typesetting failed:', err);
      done();
    });
  } else {
    done();
  }
}

New Implementation

rput(el: any): void {
  // Import debug utilities
  const startTime = Date.now();
  
  // Validate coordinates
  const x = this.x;
  const y = this.y;
  
  if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
    console.warn('RPUT: Invalid coordinates detected', { x, y, text: this.text });
    return;
  }

  // Validate parent container
  if (!el || !el.appendChild) {
    console.warn('RPUT: Invalid parent container provided');
    return;
  }

  // Validate content
  if (!this.text || typeof this.text !== 'string') {
    console.warn('RPUT: Invalid text content', { text: this.text });
    return;
  }

  const div = document.createElement('div');
  
  // Set up element with improved styling for better measurement
  div.className = 'math';
  div.style.position = 'absolute';
  div.style.visibility = 'hidden';
  div.style.whiteSpace = 'nowrap'; // Prevent text wrapping during measurement
  div.style.top = `${y}px`;
  div.style.left = `${x}px`;
  div.style.pointerEvents = 'none'; // Prevent interference during positioning
  
  // Add data attributes for debugging
  div.setAttribute('data-rput-x', x.toString());
  div.setAttribute('data-rput-y', y.toString());
  div.setAttribute('data-rput-text', this.text);

  // Enhanced positioning function with better measurement
  const positionElement = () => {
    return new Promise<void>((resolve) => {
      // Use requestAnimationFrame to ensure DOM has been updated
      requestAnimationFrame(() => {
        try {
          // Get accurate bounding box
          const rect = div.getBoundingClientRect();
          
          // Validate measurements
          if (rect.width === 0 || rect.height === 0) {
            console.warn('RPUT: Element has zero dimensions, retrying...', { 
              text: this.text, 
              rect: { width: rect.width, height: rect.height } 
            });
            
            // Retry measurement after a short delay
            setTimeout(() => {
              const retryRect = div.getBoundingClientRect();
              const w = retryRect.width / 2;
              const h = retryRect.height / 2;
              
              // Apply centering with fallback for zero dimensions
              div.style.top = `${y - (h || 10)}px`;
              div.style.left = `${x - (w || 20)}px`;
              div.style.visibility = 'visible';
              div.style.pointerEvents = 'auto';
              resolve();
            }, 10);
            return;
          }

          // Calculate center offsets
          const centerX = rect.width / 2;
          const centerY = rect.height / 2;

          // Apply precise centering
          div.style.top = `${y - centerY}px`;
          div.style.left = `${x - centerX}px`;
          div.style.visibility = 'visible';
          div.style.pointerEvents = 'auto';
          
          resolve();
        } catch (error) {
          console.error('RPUT: Error during positioning', error);
          // Fallback positioning
          div.style.top = `${y}px`;
          div.style.left = `${x}px`;
          div.style.visibility = 'visible';
          div.style.pointerEvents = 'auto';
          resolve();
        }
      });
    });
  };

  // Enhanced MathJax processing with better async handling
  const processContent = async () => {
    const mathJax = (window as any).MathJax;
    
    if (mathJax && mathJax.typesetPromise) {
      try {
        // Set content before MathJax processing
        div.innerHTML = this.text;
        
        // Process with MathJax
        await mathJax.typesetPromise([div]);
        
        // Wait for MathJax to complete rendering
        await new Promise(resolve => setTimeout(resolve, 0));
        
        // Position element after MathJax is complete
        await positionElement();
        
      } catch (error) {
        console.error('MathJax typesetting failed:', error);
        // Fallback to plain HTML
        div.innerHTML = this.text;
        await positionElement();
      }
    } else {
      // No MathJax available, use plain HTML
      div.innerHTML = this.text;
      await positionElement();
    }
  };

  // Ensure parent is ready before appending
  if (el.isConnected === false) {
    console.warn('RPUT: Parent container not connected to DOM');
  }
  
  // Append to DOM
  el.appendChild(div);
  
  // Process content asynchronously
  processContent().catch((error) => {
    console.error('RPUT: Failed to process content', error);
    // Emergency fallback
    div.style.visibility = 'visible';
    div.style.pointerEvents = 'auto';
  });
}

Key Changes Summary

Aspect Old New
Validation None Comprehensive coordinate, container, and content validation
Content Setting Before MathJax After DOM insertion, before MathJax
Positioning Synchronous Async with requestAnimationFrame
Error Handling Basic catch Multiple fallback layers
Measurement Single attempt Retry logic for zero dimensions
Debug Support None Data attributes and logging
CSS Properties Basic Enhanced with performance hints

Coordinate Transformation Improvements

πŸ“ Location: /packages/utils/src/index.ts:132-209

Old X Function

export const X = function (this: any, v: number | string) {
  if (isNaN(this.w) || isNaN(this.x1) || isNaN(this.xunit)) {
    console.warn('X function: NaN detected in context properties', { w: this.w, x1: this.x1, xunit: this.xunit });
    return 0;
  }
  return (this.w - (this.x1 - Number(v))) * this.xunit;
};

New X Function

export const X = function (this: any, v: number | string) {
  // Enhanced validation for coordinate transformation
  const numV = typeof v === 'string' ? parseFloat(v) : v;
  
  if (isNaN(numV)) {
    console.warn('X function: Invalid input value', { input: v, parsed: numV });
    return 0;
  }
  
  if (isNaN(this.w) || isNaN(this.x1) || isNaN(this.xunit)) {
    console.warn('X function: NaN detected in context properties', { w: this.w, x1: this.x1, xunit: this.xunit });
    return 0;
  }
  
  // Validate context properties are reasonable
  if (this.xunit <= 0) {
    console.warn('X function: Invalid xunit value', { xunit: this.xunit });
    return 0;
  }
  
  // Use more precise calculation with proper parentheses
  const result = (this.w - (this.x1 - numV)) * this.xunit;
  
  // Validate result is finite
  if (!isFinite(result)) {
    console.warn('X function: Non-finite result', { 
      input: numV, 
      w: this.w, 
      x1: this.x1, 
      xunit: this.xunit, 
      result 
    });
    return 0;
  }
  
  return Math.round(result * 100) / 100; // Round to 2 decimal places for pixel precision
};

Y Function Changes

Similar improvements were made to the Y function with:

  • Better input parsing (parseFloat vs Number)
  • Unit validation (yunit > 0)
  • Finite result checking
  • Pixel-level precision rounding

Coordinate Transformation Improvements

Issue Old Behavior New Behavior
String Parsing Number() conversion parseFloat() for better handling
Unit Validation No validation Checks for positive unit values
Precision Raw floating point Rounded to 2 decimal places
Error Handling Basic NaN check Comprehensive validation with detailed logging
Result Validation No validation isFinite() check for mathematical errors

Cleanup and Re-render Pipeline

πŸ“ Location: /packages/pstricks/src/lib/psgraph.ts:685-782

Old RPUT Processing

// In pspicture method
// Clear existing rput elements to prevent duplication
const existingRputElements = el.querySelectorAll('.math');
existingRputElements.forEach((element: any) => element.remove());

this.plot.rput.forEach((rput: any) => {
  psgraph.rput.call(rput.data, el);
});

New RPUT Processing Pipeline

// In pspicture method
// Enhanced cleanup and RPUT processing
psgraph.processRputElements.call(this, el);

// New method: processRputElements
processRputElements(el: any): void {
  // Validate container
  if (!el || typeof el.querySelectorAll !== 'function') {
    console.warn('RPUT: Invalid container for RPUT processing');
    return;
  }

  // Validate RPUT data
  if (!this.plot || !Array.isArray(this.plot.rput)) {
    console.warn('RPUT: No RPUT data to process');
    return;
  }

  // Enhanced cleanup with better error handling
  try {
    // Remove existing RPUT elements
    const existingElements = el.querySelectorAll('.math[data-rput-x]');
    let cleanupCount = 0;
    
    existingElements.forEach((element: HTMLElement) => {
      try {
        // Clean up any pending async operations
        element.style.visibility = 'hidden';
        element.remove();
        cleanupCount++;
      } catch (error) {
        console.warn('RPUT: Error removing existing element', error);
      }
    });

    if (cleanupCount > 0) {
      console.log(`RPUT: Cleaned up ${cleanupCount} existing elements`);
    }

    // Wait for DOM to settle after cleanup
    requestAnimationFrame(() => {
      psgraph.renderRputElements.call(this, el);
    });

  } catch (error) {
    console.error('RPUT: Error during cleanup', error);
    // Fallback to immediate rendering
    psgraph.renderRputElements.call(this, el);
  }
},

// New method: renderRputElements
renderRputElements(el: any): void {
  if (!this.plot?.rput || this.plot.rput.length === 0) {
    return;
  }

  // Track rendering for debugging
  console.log(`RPUT: Rendering ${this.plot.rput.length} elements`);
  
  // Process RPUT elements with better error isolation
  const renderPromises: Promise<void>[] = [];
  
  this.plot.rput.forEach((rput: any, index: number) => {
    try {
      // Validate RPUT data
      if (!rput || !rput.data) {
        console.warn(`RPUT: Invalid RPUT data at index ${index}`, rput);
        return;
      }

      // Add global context
      rput.data.global = this.env;
      
      // Create a promise for this RPUT element
      const renderPromise = new Promise<void>((resolve) => {
        try {
          // Use setTimeout to prevent blocking the main thread
          setTimeout(() => {
            psgraph.rput.call(rput.data, el);
            resolve();
          }, index * 10); // Stagger rendering slightly
        } catch (error) {
          console.error(`RPUT: Error rendering element ${index}`, error);
          resolve();
        }
      });
      
      renderPromises.push(renderPromise);
      
    } catch (error) {
      console.error(`RPUT: Error processing element ${index}`, error);
    }
  });

  // Wait for all RPUT elements to be processed
  Promise.all(renderPromises)
    .then(() => {
      console.log('RPUT: All elements rendered successfully');
    })
    .catch((error) => {
      console.error('RPUT: Error in batch rendering', error);
    });
}

Pipeline Improvements

Aspect Old New
Cleanup Strategy Simple querySelectorAll('.math') Targeted querySelectorAll('.math[data-rput-x]')
Error Isolation Single failure breaks all Individual element error handling
Timing Synchronous processing Async with staggered rendering
Validation No validation Container and data validation
Performance All at once (blocking) Staggered (10ms intervals)
Debugging Silent operation Comprehensive logging

Error Handling and Validation

Old Error Handling

// Minimal error handling
const mathJax = (window as any).MathJax;
if (mathJax && mathJax.typesetPromise) {
  mathJax.typesetPromise([div]).then(done).catch((err: any) => {
    console.error('MathJax typesetting failed:', err);
    done();
  });
} else {
  done();
}

New Error Handling

// Comprehensive validation and error handling

// 1. Input Validation
if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) {
  console.warn('RPUT: Invalid coordinates detected', { x, y, text: this.text });
  return;
}

// 2. Container Validation
if (!el || !el.appendChild) {
  console.warn('RPUT: Invalid parent container provided');
  return;
}

// 3. Content Validation
if (!this.text || typeof this.text !== 'string') {
  console.warn('RPUT: Invalid text content', { text: this.text });
  return;
}

// 4. DOM Connection Check
if (el.isConnected === false) {
  console.warn('RPUT: Parent container not connected to DOM');
}

// 5. MathJax Error Handling with Fallbacks
const processContent = async () => {
  const mathJax = (window as any).MathJax;
  
  if (mathJax && mathJax.typesetPromise) {
    try {
      div.innerHTML = this.text;
      await mathJax.typesetPromise([div]);
      await new Promise(resolve => setTimeout(resolve, 0));
      await positionElement();
    } catch (error) {
      console.error('MathJax typesetting failed:', error);
      // Fallback to plain HTML
      div.innerHTML = this.text;
      await positionElement();
    }
  } else {
    // No MathJax available, use plain HTML
    div.innerHTML = this.text;
    await positionElement();
  }
};

// 6. Emergency Fallback
processContent().catch((error) => {
  console.error('RPUT: Failed to process content', error);
  // Emergency fallback
  div.style.visibility = 'visible';
  div.style.pointerEvents = 'auto';
});

Error Handling Matrix

Error Type Old Response New Response
Invalid Coordinates Silent failure Early return with warning
Missing Container Runtime error Validation with graceful exit
Invalid Content Display error Validation with logging
MathJax Failure Basic catch Multiple fallback layers
DOM Issues Crash Graceful degradation
Measurement Failure Zero dimensions Retry logic with fallbacks

Performance and Timing Improvements

Old Timing Issues

  1. Race Conditions: Elements measured before MathJax completed
  2. Blocking Rendering: All elements processed synchronously
  3. No DOM Timing: No consideration for browser rendering cycles
  4. Single Point of Failure: One error affects all elements

New Performance Features

1. Async Processing with Proper Timing

// Old: Immediate measurement
const done = () => {
  const rct = div.getBoundingClientRect();
  // ...
};

// New: Delayed measurement with validation
const positionElement = () => {
  return new Promise<void>((resolve) => {
    requestAnimationFrame(() => {
      const rect = div.getBoundingClientRect();
      
      if (rect.width === 0 || rect.height === 0) {
        // Retry logic
        setTimeout(() => {
          const retryRect = div.getBoundingClientRect();
          // Process with fallback
        }, 10);
      } else {
        // Normal processing
      }
    });
  });
};

2. Staggered Rendering

// Old: All at once
this.plot.rput.forEach((rput: any) => {
  psgraph.rput.call(rput.data, el);
});

// New: Staggered with delays
this.plot.rput.forEach((rput: any, index: number) => {
  setTimeout(() => {
    psgraph.rput.call(rput.data, el);
    resolve();
  }, index * 10); // 10ms stagger
});

3. Better CSS for Performance

/* New performance-optimized CSS */
.math {
  will-change: transform; /* GPU acceleration hint */
  backface-visibility: hidden; /* Prevent subpixel issues */
  white-space: nowrap; /* Prevent reflow during measurement */
  pointer-events: none; /* Prevent interference during positioning */
}

Performance Comparison

Metric Old New Improvement
Rendering Strategy Synchronous Async + Staggered Non-blocking
Error Recovery None Retry + Fallback Robust
DOM Timing Immediate requestAnimationFrame Proper timing
GPU Utilization None CSS hints Hardware acceleration
Memory Management Basic Targeted cleanup Better resource usage

Debug Infrastructure

New Debug Features Added

1. Debug CSS (/packages/pstricks/src/lib/rput.css)

/* Debug styling (can be enabled for troubleshooting) */
.math[data-debug="true"] {
  border: 1px dashed rgba(255, 0, 0, 0.5);
  background: rgba(255, 255, 0, 0.1);
}

.math[data-debug="true"]::before {
  content: attr(data-rput-x) "," attr(data-rput-y);
  position: absolute;
  top: -20px;
  left: 0;
  font-size: 10px;
  color: red;
  background: white;
  padding: 2px;
  border-radius: 2px;
  font-family: monospace;
}

2. Debug Utilities (/packages/pstricks/src/lib/rput-debug.ts)

// Global debug functions available in browser console
window.enableRputDebug();    // Enable debug mode
window.checkRputIssues();    // Check for common problems  
window.exportRputReport();   // Export debug report
window.disableRputDebug();   // Disable debug mode

3. Debug Data Attributes

// Added to each RPUT element
div.setAttribute('data-rput-x', x.toString());
div.setAttribute('data-rput-y', y.toString());
div.setAttribute('data-rput-text', this.text);

4. Comprehensive Logging

// Before: Minimal logging
console.error('MathJax typesetting failed:', err);

// After: Detailed logging
console.warn('RPUT: Invalid coordinates detected', { x, y, text: this.text });
console.warn('RPUT: Element has zero dimensions, retrying...', { 
  text: this.text, 
  rect: { width: rect.width, height: rect.height } 
});
console.log(`RPUT: Cleaned up ${cleanupCount} existing elements`);
console.log(`RPUT: Rendering ${this.plot.rput.length} elements`);

Debug Features Matrix

Feature Old New
Visual Debug None CSS borders and coordinate display
Console Tools None Global debug functions
Data Tracking None Element data attributes
Error Logging Basic Detailed with context
Performance Tracking None Render timing and counts
Issue Detection Manual Automated checking

CSS and Styling Changes

Old CSS

/* Minimal styling applied via JavaScript */
div.className = 'math';
div.style.visibility = 'hidden';
div.style.position = 'absolute';
div.style.top = `${y}px`;
div.style.left = `${x}px`;

New CSS Infrastructure

1. Enhanced JavaScript Styling

// Improved styling for better measurement and performance
div.className = 'math';
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.whiteSpace = 'nowrap'; // Prevent text wrapping during measurement
div.style.top = `${y}px`;
div.style.left = `${x}px`;
div.style.pointerEvents = 'none'; // Prevent interference during positioning

2. New CSS File (rput.css)

.math {
  /* Essential positioning properties */
  position: absolute;
  z-index: 10; /* Ensure RPUT elements appear above SVG */
  
  /* Performance optimizations */
  will-change: transform; /* Hint browser for GPU acceleration */
  backface-visibility: hidden; /* Prevent subpixel rendering issues */
  
  /* Text rendering optimizations */
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  
  /* Prevent layout interference during positioning */
  white-space: nowrap;
  overflow: visible;
  
  /* Ensure consistent box model */
  box-sizing: border-box;
  
  /* Prevent selection during positioning */
  user-select: none;
}

/* Hidden state for measurement */
.math[data-rput-measuring] {
  visibility: hidden !important;
  pointer-events: none !important;
}

/* Container requirements */
.pspicture-container {
  position: relative;
  overflow: visible; /* Allow RPUT elements to extend beyond SVG bounds */
}

CSS Improvements Summary

Property Old New Purpose
z-index None 10 Ensure visibility above SVG
will-change None transform GPU acceleration hint
backface-visibility None hidden Prevent subpixel issues
white-space None nowrap Prevent text wrapping
text-rendering None optimizeLegibility Better text quality
user-select None none Prevent selection during positioning

Migration Guide

Breaking Changes

1. CSS Requirements

Old: No specific CSS requirements New: Requires parent container to have position: relative

/* Required for proper RPUT positioning */
.pspicture-container {
  position: relative;
}

2. Console Output

Old: Minimal logging New: Comprehensive logging (can be disabled if needed)

// To reduce logging in production
console.warn = () => {}; // Disable warnings

3. Debug Attributes

Old: No debug attributes New: Debug attributes added to elements

Elements now have data-rput-* attributes that can be used for debugging or styling.

Non-Breaking Improvements

1. Better Error Handling

Existing code will continue to work but with better error recovery.

2. Performance Improvements

Automatic performance improvements with no code changes required.

3. Debug Tools

New debug tools available but don't affect normal operation.

Recommended Updates

1. Update Parent Containers

<!-- Old -->
<div class="diagram">
  <!-- PSTricks content -->
</div>

<!-- New (Recommended) -->
<div class="diagram pspicture-container">
  <!-- PSTricks content -->
</div>

2. Include RPUT CSS

<!-- Add to your HTML head -->
<link rel="stylesheet" href="path/to/rput.css">

3. Enable Debug Mode (Development)

// In development environment
if (process.env.NODE_ENV === 'development') {
  window.enableRputDebug();
}

Testing Your Migration

1. Visual Verification

  • Check that RPUT elements appear in correct positions
  • Verify no duplicate elements
  • Ensure proper centering on coordinates

2. Console Monitoring

  • Watch for any warning messages
  • Monitor performance with debug tools
  • Check for any error messages

3. Debug Tools Usage

// Enable debug mode
window.enableRputDebug();

// Check for issues
window.checkRputIssues();

// Export report if problems found
const report = window.exportRputReport();
console.log(report);

Summary of Improvements

🎯 Core Issues Fixed

  1. Race Conditions: Proper async handling prevents measurement before rendering
  2. Positioning Accuracy: Enhanced coordinate transformation and centering
  3. Duplicate Elements: Improved cleanup prevents element duplication
  4. Error Recovery: Multiple fallback layers ensure robustness
  5. Performance: Staggered rendering prevents UI blocking

πŸ“ˆ Performance Gains

  • Non-blocking Rendering: Staggered element processing
  • GPU Acceleration: CSS hints for hardware acceleration
  • Better Cleanup: Targeted element removal
  • Retry Logic: Handles edge cases gracefully

πŸ”§ Developer Experience

  • Debug Tools: Comprehensive debugging infrastructure
  • Better Logging: Detailed error messages and warnings
  • Visual Debug: CSS-based visual debugging
  • Validation: Input validation prevents silent failures

πŸš€ Future-Proof Design

  • Modular Structure: Separated concerns for easier maintenance
  • Extensible Debug System: Easy to add new debug features
  • Error Isolation: Individual element failures don't affect others
  • Performance Monitoring: Built-in timing and tracking

The new RPUT system provides a robust, performant, and debuggable foundation for positioning mathematical content in LaTeX2JS applications.