Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.
This commit is contained in:
+256
-64
@@ -1,108 +1,300 @@
|
||||
/**
|
||||
* ParagraphLayout Module
|
||||
* Interfaces with the Knuth-Plass line breaking algorithm to calculate optimal line breaks.
|
||||
* Paragraph Layout Module
|
||||
* Implements the Knuth and Plass line breaking algorithm for optimal typography
|
||||
* and connects it to the text rendering pipeline.
|
||||
*/
|
||||
import { BaseModule } from './base-module.js';
|
||||
import { moduleRegistry } from './module-registry.js';
|
||||
|
||||
class ParagraphLayoutModule extends BaseModule {
|
||||
/**
|
||||
* Create a new ParagraphLayout
|
||||
*/
|
||||
constructor() {
|
||||
super('paragraph-layout', 'Paragraph Layout');
|
||||
this.kapAlgorithm = null;
|
||||
this.measureText = null;
|
||||
|
||||
// Module dependencies
|
||||
this.dependencies = ['text-processor'];
|
||||
|
||||
// Caching canvas context for text measurements
|
||||
this.textMeasureCtx = null;
|
||||
|
||||
// Configuration - use parent's config system
|
||||
this.updateConfig({
|
||||
maxLineWidth: 600,
|
||||
hyphenationEnabled: true,
|
||||
defaultFontSize: '1rem',
|
||||
defaultFontFamily: "'EB Garamond', serif",
|
||||
defaultLineHeight: 1.5,
|
||||
debugMode: false
|
||||
});
|
||||
|
||||
// Bind methods using parent's bindMethods utility
|
||||
this.bindMethods([
|
||||
'calculateLayout',
|
||||
'measureText',
|
||||
'setDebugMode',
|
||||
'updateFont',
|
||||
'processTextForLayout',
|
||||
'initializeTextMeasurement',
|
||||
'setupEventListeners',
|
||||
'loadLayoutDependencies'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load module dependencies
|
||||
* @returns {Promise} - Resolves when dependencies are loaded
|
||||
*/
|
||||
async loadDependencies() {
|
||||
async initialize() {
|
||||
try {
|
||||
// First load linebreak.js if needed
|
||||
if (!window.linebreak) {
|
||||
await this.loadScript('/js/linebreak.js');
|
||||
this.reportProgress(40, "Linebreak algorithm loaded");
|
||||
this.reportProgress(20, "Initializing paragraph layout");
|
||||
|
||||
// Get text processor using parent's getModule method
|
||||
this.textProcessor = this.getModule('text-processor');
|
||||
|
||||
if (!this.textProcessor) {
|
||||
console.warn("Paragraph Layout: Text Processor not found, will use fallback processing");
|
||||
}
|
||||
|
||||
// Then load knuth-and-plass.js if needed
|
||||
if (!window.kap) {
|
||||
await this.loadScript('/js/knuth-and-plass.js');
|
||||
this.reportProgress(60, "KAP algorithm loaded");
|
||||
}
|
||||
// Load required dependencies
|
||||
await this.loadLayoutDependencies();
|
||||
|
||||
this.kapAlgorithm = window.kap;
|
||||
// Create off-screen canvas for text measurements
|
||||
this.initializeTextMeasurement();
|
||||
|
||||
// Set up event listeners for config changes
|
||||
this.setupEventListeners();
|
||||
|
||||
this.reportProgress(100, "Paragraph layout ready");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error loading paragraph layout dependencies:", error);
|
||||
console.error("Error initializing Paragraph Layout:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a script dynamically
|
||||
* @param {string} src - Script source URL
|
||||
* @returns {Promise} - Resolves when script is loaded
|
||||
* Load required dependencies for layout calculations
|
||||
*/
|
||||
loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
|
||||
document.head.appendChild(script);
|
||||
async loadLayoutDependencies() {
|
||||
try {
|
||||
this.reportProgress(30, "Loading layout dependencies");
|
||||
|
||||
// Load LinkedList.js first as it's required by linebreak.js
|
||||
await this.loadScript('/js/linked-list.js');
|
||||
|
||||
// Load linebreak.js which is required by knuth-and-plass.js
|
||||
await this.loadScript('/js/linebreak.js');
|
||||
|
||||
// Load knuth-and-plass.js which contains the kap function
|
||||
await this.loadScript('/js/knuth-and-plass.js');
|
||||
|
||||
this.reportProgress(50, "Layout dependencies loaded");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error loading layout dependencies:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize text measurement canvas
|
||||
*/
|
||||
initializeTextMeasurement() {
|
||||
// Create off-screen canvas for text measurements
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 2000;
|
||||
canvas.height = 100;
|
||||
this.textMeasureCtx = canvas.getContext('2d');
|
||||
|
||||
// Set default font
|
||||
this.updateFont(this.config.defaultFontSize, this.config.defaultFontFamily);
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Use parent's addEventListener for automatic cleanup
|
||||
this.addEventListener(document, 'ui:font:change', (event) => {
|
||||
if (event.detail) {
|
||||
const { fontSize, fontFamily } = event.detail;
|
||||
if (fontSize || fontFamily) {
|
||||
this.updateFont(
|
||||
fontSize || this.config.defaultFontSize,
|
||||
fontFamily || this.config.defaultFontFamily
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Use parent's addEventListener for automatic cleanup
|
||||
this.addEventListener(document, 'ui:typography:hyphenation', (event) => {
|
||||
if (event.detail && typeof event.detail.enabled === 'boolean') {
|
||||
// Use parent's updateConfig method
|
||||
this.updateConfig({ hyphenationEnabled: event.detail.enabled });
|
||||
console.log(`Paragraph Layout: Hyphenation ${this.config.hyphenationEnabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the module
|
||||
* @returns {Promise<boolean>} - Resolves with success status
|
||||
* Update the font for text measurements
|
||||
* @param {string} fontSize - Font size (with units)
|
||||
* @param {string} fontFamily - Font family
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
// The measureText function will be provided by the game controller later
|
||||
this.reportProgress(100, "Paragraph layout initialized");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error initializing paragraph layout:", error);
|
||||
return false;
|
||||
updateFont(fontSize, fontFamily) {
|
||||
if (!this.textMeasureCtx) return;
|
||||
|
||||
// Store the font settings
|
||||
this.config.defaultFontSize = fontSize;
|
||||
this.config.defaultFontFamily = fontFamily;
|
||||
|
||||
// Set the font on the canvas context
|
||||
const fontString = `${fontSize} ${fontFamily}`;
|
||||
this.textMeasureCtx.font = fontString;
|
||||
|
||||
if (this.config.debugMode) {
|
||||
console.log(`Paragraph Layout: Font updated to ${fontString}`);
|
||||
|
||||
// Test measurement
|
||||
const testText = "The quick brown fox jumps over the lazy dog";
|
||||
const width = this.measureText(testText);
|
||||
console.log(`Paragraph Layout: Test text width: ${width}px`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate layout for a paragraph
|
||||
* @param {string} processedText - The pre-processed text (with SmartyPants and hyphenation)
|
||||
* @param {Array<number>} measures - Array of line width measurements
|
||||
* @param {boolean} hyphenate - Whether to enable hyphenation
|
||||
* @param {Function} [measureFunc] - Optional specific measurement function for this call
|
||||
* @returns {Object} Layout data with nodes and breaks
|
||||
* Measure text width using canvas context
|
||||
* @param {string} text - Text to measure
|
||||
* @returns {number} - Text width in pixels
|
||||
*/
|
||||
calculateLayout(processedText, measures, hyphenate = true, measureFunc = null) {
|
||||
const measure = measureFunc || this.measureText; // Use provided func or fallback to instance default
|
||||
if (typeof measure !== 'function') {
|
||||
throw new Error('No text measurement function available');
|
||||
measureText(text) {
|
||||
if (!this.textMeasureCtx) {
|
||||
this.initializeTextMeasurement();
|
||||
}
|
||||
|
||||
return this.kapAlgorithm(processedText, measure, measures, hyphenate);
|
||||
if (!text) return 0;
|
||||
|
||||
return this.textMeasureCtx.measureText(text).width;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a new text measurement function
|
||||
* @param {Function} measureFunc - The new measurement function
|
||||
* Process text for layout (apply hyphenation and smartypants)
|
||||
* @param {string} text - Text to process
|
||||
* @returns {string} - Processed text
|
||||
*/
|
||||
setMeasureFunction(measureFunc) {
|
||||
this.measureText = measureFunc;
|
||||
processTextForLayout(text) {
|
||||
if (!text) return '';
|
||||
|
||||
// Remove extra whitespace
|
||||
text = text.trim().replace(/\s+/g, ' ');
|
||||
|
||||
try {
|
||||
// Apply text processor transformations if available
|
||||
if (this.textProcessor) {
|
||||
// Apply smartypants for typography improvements
|
||||
if (this.textProcessor.applySmartypants) {
|
||||
text = this.textProcessor.applySmartypants(text);
|
||||
}
|
||||
|
||||
// Apply hyphenation if enabled
|
||||
if (this.config.hyphenationEnabled && this.textProcessor.hyphenateText) {
|
||||
text = this.textProcessor.hyphenateText(text);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (error) {
|
||||
console.error("Error processing text for layout:", error);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a new Knuth and Plass algorithm implementation
|
||||
* @param {Function} kapFunc - The new KAP algorithm function
|
||||
* Calculate layout for a paragraph using Knuth and Plass algorithm
|
||||
* @param {string} text - Text to layout
|
||||
* @param {Object} options - Layout options
|
||||
* @returns {Object} - Layout data with line breaks
|
||||
*/
|
||||
setKapAlgorithm(kapFunc) {
|
||||
this.kapAlgorithm = kapFunc;
|
||||
calculateLayout(text, options = {}) {
|
||||
if (!text) return null;
|
||||
|
||||
try {
|
||||
// Check if the kap function is available
|
||||
if (typeof window.kap !== 'function') {
|
||||
console.error("Paragraph Layout: kap function not available. Make sure knuth-and-plass.js is loaded.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Process text for layout (hyphenation, etc)
|
||||
const processedText = this.processTextForLayout(text);
|
||||
|
||||
// Prepare options by merging with defaults
|
||||
const layoutOptions = {
|
||||
width: options.width || this.config.maxLineWidth,
|
||||
fontSize: options.fontSize || this.config.defaultFontSize,
|
||||
fontFamily: options.fontFamily || this.config.defaultFontFamily,
|
||||
lineHeight: options.lineHeight || this.config.defaultLineHeight,
|
||||
tolerance: options.tolerance || 3, // Tolerance for line breaking algorithm
|
||||
demerits: options.demerits || {
|
||||
line: 10, // Demerits for each line break
|
||||
flagged: 100, // Demerits for flagged break points (like hyphens)
|
||||
fitness: 3000 // Demerits for consecutive lines with very different looseness/tightness
|
||||
}
|
||||
};
|
||||
|
||||
// Update font for measurement
|
||||
this.updateFont(layoutOptions.fontSize, layoutOptions.fontFamily);
|
||||
|
||||
// Create measure array - this is crucial for proper line breaking
|
||||
// The first value is the full width, subsequent values can be for indented lines
|
||||
const measure = [layoutOptions.width];
|
||||
|
||||
if (this.config.debugMode) {
|
||||
console.log("Paragraph Layout: Calculating layout for text", {
|
||||
text: processedText,
|
||||
measure,
|
||||
options: layoutOptions
|
||||
});
|
||||
}
|
||||
|
||||
// Use the global Knuth and Plass algorithm function with proper parameters
|
||||
const layout = window.kap(
|
||||
processedText,
|
||||
this.measureText.bind(this),
|
||||
measure,
|
||||
this.config.hyphenationEnabled,
|
||||
layoutOptions.tolerance,
|
||||
layoutOptions.demerits
|
||||
);
|
||||
|
||||
// If layout failed, return null
|
||||
if (!layout || !layout.breaks || !layout.nodes) {
|
||||
console.warn("Paragraph Layout: Failed to calculate layout for text");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.config.debugMode) {
|
||||
console.log("Paragraph Layout: Layout calculated", {
|
||||
breaks: layout.breaks.length,
|
||||
nodes: layout.nodes.length
|
||||
});
|
||||
}
|
||||
|
||||
// Return layout data with original text for reference
|
||||
return {
|
||||
...layout,
|
||||
originalText: text,
|
||||
processedText: processedText,
|
||||
width: layoutOptions.width,
|
||||
lineHeight: layoutOptions.lineHeight
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error calculating layout:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set debug mode
|
||||
* @param {boolean} enabled - Whether debug mode should be enabled
|
||||
*/
|
||||
setDebugMode(enabled) {
|
||||
// Use parent's updateConfig method
|
||||
this.updateConfig({ debugMode: enabled });
|
||||
console.log(`Paragraph Layout: Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user