/** * PANDA 3D SOFTWARE * Copyright (c) Carnegie Mellon University. All rights reserved. * * All use of this software is subject to the terms of the revised BSD * license. You should have received a copy of this license along * with this source code in a file named "LICENSE." * * @file textAssembler.cxx * @author drose * @date 2004-04-06 */ #include "textAssembler.h" #include "textGlyph.h" #include "cullFaceAttrib.h" #include "colorAttrib.h" #include "cullBinAttrib.h" #include "textureAttrib.h" #include "transparencyAttrib.h" #include "textPropertiesManager.h" #include "textEncoder.h" #include "config_text.h" #include "geomTriangles.h" #include "geomLines.h" #include "geomPoints.h" #include "geomVertexReader.h" #include "geomVertexWriter.h" #include "geomLines.h" #include "geomVertexFormat.h" #include "geomVertexData.h" #include "geom.h" #include "modelNode.h" #include #include // for sprintf // This is the factor by which CT_small scales the character down. static const PN_stdfloat small_accent_scale = 0.6f; // This is the factor by which CT_tiny scales the character down. static const PN_stdfloat tiny_accent_scale = 0.4; // This is the factor by which CT_squash scales the character in X and Y. static const PN_stdfloat squash_accent_scale_x = 0.8f; static const PN_stdfloat squash_accent_scale_y = 0.5f; // This is the factor by which CT_small_squash scales the character in X and // Y. static const PN_stdfloat small_squash_accent_scale_x = 0.6f; static const PN_stdfloat small_squash_accent_scale_y = 0.3; // This is the factor by which the advance is reduced for the first character // of a two-character ligature. static const PN_stdfloat ligature_advance_scale = 0.6f; /** * An internal function that works like isspace() but is safe to call for a * wide character. */ static INLINE bool isspacew(unsigned int ch) { return isascii(ch) && isspace(ch); } /** * An internal function, similar to isspace(), except it does not consider * newlines to be whitespace. It also includes the soft-hyphen character. */ static INLINE bool isbreakpoint(unsigned int ch) { return (ch == ' ' || ch == '\t' || ch == (unsigned int)text_soft_hyphen_key || ch == (unsigned int)text_soft_break_key); } /** * */ TextAssembler:: TextAssembler(TextEncoder *encoder) : _encoder(encoder), _usage_hint(Geom::UH_static), _max_rows(0), _dynamic_merge(text_dynamic_merge), _multiline_mode(true) { _initial_cprops = new ComputedProperties(TextProperties()); clear(); } /** * */ TextAssembler:: TextAssembler(const TextAssembler ©) : _initial_cprops(copy._initial_cprops), _text_string(copy._text_string), _text_block(copy._text_block), _ul(copy._ul), _lr(copy._lr), _next_row_ypos(copy._next_row_ypos), _encoder(copy._encoder), _usage_hint(copy._usage_hint), _max_rows(copy._max_rows), _dynamic_merge(copy._dynamic_merge), _multiline_mode(copy._multiline_mode) { } /** * */ void TextAssembler:: operator = (const TextAssembler ©) { _initial_cprops = copy._initial_cprops; _text_string = copy._text_string; _text_block = copy._text_block; _ul = copy._ul; _lr = copy._lr; _next_row_ypos = copy._next_row_ypos; _encoder = copy._encoder; _usage_hint = copy._usage_hint; _max_rows = copy._max_rows; _dynamic_merge = copy._dynamic_merge; _multiline_mode = copy._multiline_mode; } /** * */ TextAssembler:: ~TextAssembler() { } /** * Reinitializes the contents of the TextAssembler. */ void TextAssembler:: clear() { _ul.set(0.0f, 0.0f); _lr.set(0.0f, 0.0f); _next_row_ypos = 0.0f; _text_string.clear(); _text_block.clear(); } /** * Accepts a new text string and associated properties structure, and * precomputes the wordwrapping layout appropriately. After this call, * get_wordwrapped_wtext() and get_num_rows() can be called. * * The return value is true if all the text is accepted, or false if some was * truncated (see set_max_rows()). */ bool TextAssembler:: set_wtext(const wstring &wtext) { clear(); // First, expand all of the embedded TextProperties references within the // string. wstring::const_iterator si = wtext.begin(); scan_wtext(_text_string, si, wtext.end(), _initial_cprops); while (si != wtext.end()) { // If we returned without consuming the whole string, it means there was // an embedded text_pop_properties_key that didn't match the push. That's // worth a warning, and then go back and pick up the rest of the string. text_cat.warning() << "pop_properties encountered without preceding push_properties.\n"; scan_wtext(_text_string, si, wtext.end(), _initial_cprops); } // Then apply any wordwrap requirements. return wordwrap_text(); } /** * Replaces the 'count' characters from 'start' of the current text with the * indicated replacement text. If the replacement text does not have count * characters, the length of the string will be changed accordingly. * * The substring may include nested formatting characters, but they must be * self-contained and self-closed. The formatting characters are not * literally saved in the internal string; they are parsed at the time of the * set_wsubstr() call. * * The return value is true if all the text is accepted, or false if some was * truncated (see set_max_rows()). */ bool TextAssembler:: set_wsubstr(const wstring &wtext, int start, int count) { nassertr(start >= 0 && start <= (int)_text_string.size(), false); nassertr(count >= 0 && start + count <= (int)_text_string.size(), false); // Use scan_wtext to unroll the substring we wish to insert, as in // set_wtext(), above. TextString substr; wstring::const_iterator si = wtext.begin(); scan_wtext(substr, si, wtext.end(), _initial_cprops); while (si != wtext.end()) { text_cat.warning() << "pop_properties encountered without preceding push_properties.\n"; scan_wtext(substr, si, wtext.end(), _initial_cprops); } _text_string.erase(_text_string.begin() + start, _text_string.begin() + start + count); _text_string.insert(_text_string.begin() + start, substr.begin(), substr.end()); return wordwrap_text(); } /** * Returns a wstring that represents the contents of the text, without any * embedded properties characters. If there is an embedded graphic object, a * zero value is inserted in that position. * * This string has the same length as get_num_characters(), and the characters * in this string correspond one-to-one with the characters returned by * get_character(n). */ wstring TextAssembler:: get_plain_wtext() const { wstring wtext; TextString::const_iterator si; for (si = _text_string.begin(); si != _text_string.end(); ++si) { const TextCharacter &tch = (*si); if (tch._graphic == (TextGraphic *)NULL) { wtext += tch._character; } else { wtext.push_back(0); } } return wtext; } /** * Returns a wstring that represents the contents of the text, with newlines * inserted according to the wordwrapping. The string will contain no * embedded properties characters. If there is an embedded graphic object, a * zero value is inserted in that position. * * This string has the same number of newline characters as get_num_rows(), * and the characters in this string correspond one-to-one with the characters * returned by get_character(r, c). */ wstring TextAssembler:: get_wordwrapped_plain_wtext() const { wstring wtext; TextBlock::const_iterator bi; for (bi = _text_block.begin(); bi != _text_block.end(); ++bi) { const TextRow &row = (*bi); if (bi != _text_block.begin()) { wtext += '\n'; } TextString::const_iterator si; for (si = row._string.begin(); si != row._string.end(); ++si) { const TextCharacter &tch = (*si); if (tch._graphic == (TextGraphic *)NULL) { wtext += tch._character; } else { wtext.push_back(0); } } } return wtext; } /** * Returns a wstring that represents the contents of the text. * * The string will contain embedded properties characters, which may not * exactly match the embedded properties characters of the original string, * but it will encode the same way. */ wstring TextAssembler:: get_wtext() const { wstring wtext; PT(ComputedProperties) current_cprops = _initial_cprops; TextString::const_iterator si; for (si = _text_string.begin(); si != _text_string.end(); ++si) { const TextCharacter &tch = (*si); current_cprops->append_delta(wtext, tch._cprops); if (tch._graphic == (TextGraphic *)NULL) { wtext += tch._character; } else { wtext.push_back(text_embed_graphic_key); wtext += tch._graphic_wname; wtext.push_back(text_embed_graphic_key); } current_cprops = tch._cprops; } current_cprops->append_delta(wtext, _initial_cprops); return wtext; } /** * Returns a wstring that represents the contents of the text, with newlines * inserted according to the wordwrapping. * * The string will contain embedded properties characters, which may not * exactly match the embedded properties characters of the original string, * but it will encode the same way. * * Embedded properties characters will be closed before every newline, then * reopened (if necessary) on the subsequent character following the newline. * This means it will be safe to divide the text up at the newline characters * and treat each line as an independent piece. */ wstring TextAssembler:: get_wordwrapped_wtext() const { wstring wtext; PT(ComputedProperties) current_cprops = _initial_cprops; TextBlock::const_iterator bi; for (bi = _text_block.begin(); bi != _text_block.end(); ++bi) { const TextRow &row = (*bi); if (bi != _text_block.begin()) { current_cprops->append_delta(wtext, _initial_cprops); current_cprops = _initial_cprops; wtext += '\n'; } TextString::const_iterator si; for (si = row._string.begin(); si != row._string.end(); ++si) { const TextCharacter &tch = (*si); current_cprops->append_delta(wtext, tch._cprops); if (tch._graphic == (TextGraphic *)NULL) { wtext += tch._character; } else { wtext.push_back(text_embed_graphic_key); wtext += tch._graphic_wname; wtext.push_back(text_embed_graphic_key); } current_cprops = tch._cprops; } } current_cprops->append_delta(wtext, _initial_cprops); return wtext; } /** * Computes the row and column index of the nth character or graphic object in * the text. Fills r and c accordingly. * * Returns true if the nth character is valid and has a corresponding r and c * position, false otherwise (for instance, a soft-hyphen character, or a * newline character, may not have a corresponding position). In either case, * r and c will be filled in sensibly. */ bool TextAssembler:: calc_r_c(int &r, int &c, int n) const { nassertr(n >= 0 && n <= (int)_text_string.size(), false); if (n == (int)_text_string.size()) { // A special case for one past the last character. if (_text_string.empty()) { r = 0; c = 0; } else { r = _text_block.size() - 1; c = _text_block[r]._string.size(); } return true; } else if (n == 0) { // Another special case for the beginning. r = 0; c = 0; return true; } r = 0; while (r + 1 < (int)_text_block.size() && _text_block[r + 1]._row_start < n) { r += 1; } const TextRow &row = _text_block[r]; bool is_real_char = true; nassertr(n > 0, false); if (row._got_soft_hyphens) { // If there are any soft hyphen or soft break keys in the source text, we // have to scan past them to get c precisely. c = 0; int i = row._row_start; while (i < n - 1) { if (_text_string[i]._character != text_soft_hyphen_key && _text_string[i]._character != text_soft_break_key) { ++c; } ++i; } if (_text_string[n - 1]._character != text_soft_hyphen_key && _text_string[n - 1]._character != text_soft_break_key) { ++c; if (_text_string[n - 1]._character == '\n') { is_real_char = false; } } else { is_real_char = false; } } else { // If there are no soft characters, then the string maps one-to-one. c = min(n - row._row_start, (int)row._string.size()); if (_text_string[n - 1]._character == '\n') { is_real_char = false; } } return is_real_char; } /** * Computes the character index of the character at the rth row and cth column * position. This is the inverse of calc_r_c(). * * It is legal for c to exceed the index number of the last column by 1, and * it is legal for r to exceed the index number of the last row by 1, if c is * 0. */ int TextAssembler:: calc_index(int r, int c) const { nassertr(r >= 0 && r <= (int)_text_block.size(), 0); if (r == (int)_text_block.size()) { nassertr(c == 0, 0); return _text_string.size(); } else { nassertr(c >= 0 && c <= (int)_text_block[r]._string.size(), 0); const TextRow &row = _text_block[r]; if (row._got_soft_hyphens) { // If there are any soft hyphen or soft break keys in the source text, // we have to scan past them to get n precisely. int n = row._row_start; while (c > 0) { if (_text_string[n]._character != text_soft_hyphen_key && _text_string[n]._character != text_soft_break_key) { --c; } ++n; } return n; } else { // If there are no soft characters, then the string maps one-to-one. return row._row_start + c; } } } /** * Returns the x position of the origin of the character or graphic object at * the indicated position in the indicated row. * * It is legal for c to exceed the index number of the last column by 1, and * it is legal for r to exceed the index number of the last row by 1, if c is * 0. */ PN_stdfloat TextAssembler:: get_xpos(int r, int c) const { nassertr(r >= 0 && r <= (int)_text_block.size(), 0.0f); if (r == (int)_text_block.size()) { nassertr(c == 0, 0.0f); return 0.0f; } else { nassertr(c >= 0 && c <= (int)_text_block[r]._string.size(), 0.0f); const TextRow &row = _text_block[r]; PN_stdfloat xpos = row._xpos; for (int i = 0; i < c; ++i) { xpos += calc_width(row._string[i]); } return xpos; } } /** * Actually assembles all of the text into a GeomNode, and returns the node * (or possibly a parent of the node, to keep the shadow separate). Once this * has been called, you may query the extents of the text via get_ul(), * get_lr(). */ PT(PandaNode) TextAssembler:: assemble_text() { // Now assemble the text into glyphs. PlacedGlyphs placed_glyphs; assemble_paragraph(placed_glyphs); // Now that we have a bunch of GlyphPlacements, pull out the Geoms and put // them under a common node. PT(PandaNode) parent_node = new PandaNode("common"); PT(PandaNode) shadow_node = new PandaNode("shadow"); PT(GeomNode) shadow_geom_node = new GeomNode("shadow_geom"); shadow_node->add_child(shadow_geom_node); PT(PandaNode) text_node = new PandaNode("text"); PT(GeomNode) text_geom_node = new GeomNode("text_geom"); text_node->add_child(text_geom_node); const TextProperties *properties = NULL; CPT(RenderState) text_state; CPT(RenderState) shadow_state; LVector2 shadow(0); bool any_shadow = false; GeomCollectorMap geom_collector_map; GeomCollectorMap geom_shadow_collector_map; QuadMap quad_map; QuadMap quad_shadow_map; PlacedGlyphs::const_iterator pgi; for (pgi = placed_glyphs.begin(); pgi != placed_glyphs.end(); ++pgi) { const GlyphPlacement &placement = (*pgi); if (placement._properties != properties) { // Get a new set of properties for future glyphs. properties = placement._properties; text_state = properties->get_text_state(); if (properties->has_shadow()) { shadow = properties->get_shadow(); shadow_state = properties->get_shadow_state(); } else { shadow.set(0, 0); shadow_state.clear(); } } if (!placement._glyph.is_null()) { if (properties->has_shadow()) { if (_dynamic_merge) { if (placement._glyph->has_quad()) { placement.assign_quad_to(quad_shadow_map, shadow_state, shadow); } else { placement.assign_append_to(geom_shadow_collector_map, shadow_state, shadow); } } else { placement.assign_to(shadow_geom_node, shadow_state, shadow); } // Don't shadow the graphics. That can result in duplication of button // objects, plus it looks weird. If you want a shadowed graphic, you // can shadow it yourself before you add it. // placement.copy_graphic_to(shadow_node, shadow_state, shadow); any_shadow = true; } if (_dynamic_merge) { if (placement._glyph->has_quad()) { placement.assign_quad_to(quad_map, text_state); } else { placement.assign_append_to(geom_collector_map, text_state); } } else { placement.assign_to(text_geom_node, text_state); } } placement.copy_graphic_to(text_node, text_state); } placed_glyphs.clear(); if (any_shadow) { // The shadow_geom_node must appear first to guarantee the correct // rendering order. parent_node->add_child(shadow_node); } GeomCollectorMap::iterator gc; for (gc = geom_collector_map.begin(); gc != geom_collector_map.end(); ++gc) { (*gc).second.append_geom(text_geom_node, (*gc).first._state); } generate_quads(text_geom_node, quad_map); if (any_shadow) { for (gc = geom_shadow_collector_map.begin(); gc != geom_shadow_collector_map.end(); ++gc) { (*gc).second.append_geom(shadow_geom_node, (*gc).first._state); } generate_quads(shadow_geom_node, quad_shadow_map); } parent_node->add_child(text_node); return parent_node; } /** * Returns the width of a single character, according to its associated font. * This also correctly calculates the width of cheesy ligatures and accented * characters, which may not exist in the font as such. */ PN_stdfloat TextAssembler:: calc_width(wchar_t character, const TextProperties &properties) { if (character == ' ') { // A space is a special case. TextFont *font = properties.get_font(); nassertr(font != (TextFont *)NULL, 0.0f); return font->get_space_advance() * properties.get_glyph_scale() * properties.get_text_scale(); } bool got_glyph; CPT(TextGlyph) first_glyph; CPT(TextGlyph) second_glyph; UnicodeLatinMap::AccentType accent_type; int additional_flags; PN_stdfloat glyph_scale; PN_stdfloat advance_scale; get_character_glyphs(character, &properties, got_glyph, first_glyph, second_glyph, accent_type, additional_flags, glyph_scale, advance_scale); PN_stdfloat advance = 0.0f; if (first_glyph != (TextGlyph *)NULL) { advance = first_glyph->get_advance() * advance_scale; } if (second_glyph != (TextGlyph *)NULL) { advance += second_glyph->get_advance(); } glyph_scale *= properties.get_glyph_scale() * properties.get_text_scale(); return advance * glyph_scale; } /** * Returns the width of a single TextGraphic image. */ PN_stdfloat TextAssembler:: calc_width(const TextGraphic *graphic, const TextProperties &properties) { LVecBase4 frame = graphic->get_frame(); return (frame[1] - frame[0]) * properties.get_glyph_scale() * properties.get_text_scale(); } /** * Returns true if the named character exists in the font exactly as named, * false otherwise. Note that because Panda can assemble glyphs together * automatically using cheesy accent marks, this is not a reliable indicator * of whether a suitable glyph can be rendered for the character. For that, * use has_character() instead. * * This returns true for whitespace and Unicode whitespace characters (if they * exist in the font), but returns false for characters that would render with * the "invalid glyph". It also returns false for characters that would be * synthesized within Panda, but see has_character(). */ bool TextAssembler:: has_exact_character(wchar_t character, const TextProperties &properties) { if (character == ' ' || character == '\n') { // A space is a special case. Every font implicitly has a space. We also // treat newlines specially. return true; } TextFont *font = properties.get_font(); nassertr(font != (TextFont *)NULL, false); CPT(TextGlyph) glyph; return font->get_glyph(character, glyph); } /** * Returns true if the named character exists in the font or can be * synthesized by Panda, false otherwise. (Panda can synthesize some accented * characters by combining similar-looking glyphs from the font.) * * This returns true for whitespace and Unicode whitespace characters (if they * exist in the font), but returns false for characters that would render with * the "invalid glyph". */ bool TextAssembler:: has_character(wchar_t character, const TextProperties &properties) { if (character == ' ' || character == '\n') { // A space is a special case. Every font implicitly has a space. We also // treat newlines specially. return true; } bool got_glyph; CPT(TextGlyph) first_glyph; CPT(TextGlyph) second_glyph; UnicodeLatinMap::AccentType accent_type; int additional_flags; PN_stdfloat glyph_scale; PN_stdfloat advance_scale; get_character_glyphs(character, &properties, got_glyph, first_glyph, second_glyph, accent_type, additional_flags, glyph_scale, advance_scale); return got_glyph; } /** * Returns true if the indicated character represents whitespace in the font, * or false if anything visible will be rendered for it. * * This returns true for whitespace and Unicode whitespace characters (if they * exist in the font), and returns false for any other characters, including * characters that do not exist in the font (these would be rendered with the * "invalid glyph", which is visible). * * Note that this function can be reliably used to identify Unicode whitespace * characters only if the font has all of the whitespace characters defined. * It will return false for any character not in the font, even if it is an * official Unicode whitespace character. */ bool TextAssembler:: is_whitespace(wchar_t character, const TextProperties &properties) { if (character == ' ' || character == '\n') { // A space or a newline is a special case. return true; } TextFont *font = properties.get_font(); nassertr(font != (TextFont *)NULL, false); CPT(TextGlyph) glyph; if (!font->get_glyph(character, glyph)) { return false; } return glyph->is_whitespace(); } /** * Scans through the text string, decoding embedded references to * TextProperties. The decoded string is copied character-by-character into * _text_string. */ void TextAssembler:: scan_wtext(TextAssembler::TextString &output_string, wstring::const_iterator &si, const wstring::const_iterator &send, TextAssembler::ComputedProperties *current_cprops) { while (si != send) { if ((*si) == text_push_properties_key) { // This indicates a nested properties structure. Pull off the name of // the TextProperties structure, which is everything until the next // text_push_properties_key. wstring wname; ++si; while (si != send && (*si) != text_push_properties_key) { wname += (*si); ++si; } if (si == send) { // We didn't close the text_push_properties_key. That's an error. text_cat.warning() << "Unclosed push_properties in text.\n"; return; } ++si; // Define the new properties by extending the current properties. PT(ComputedProperties) new_cprops = new ComputedProperties(current_cprops, wname, _encoder); // And recursively scan with the nested properties. scan_wtext(output_string, si, send, new_cprops); if (text_cat.is_debug()) { if (si == send) { // The push was not closed by a pop. That's not an error, since we // allow people to be sloppy about that; but we'll print a debug // message at least. text_cat.debug() << "push_properties not matched by pop_properties.\n"; } } } else if ((*si) == text_pop_properties_key) { // This indicates the undoing of a previous push_properties_key. We // simply return to the previous level. ++si; return; } else if ((*si) == text_embed_graphic_key) { // This indicates an embedded graphic. Pull off the name of the // TextGraphic structure, which is everything until the next // text_embed_graphic_key. wstring graphic_wname; ++si; while (si != send && (*si) != text_embed_graphic_key) { graphic_wname += (*si); ++si; } if (si == send) { // We didn't close the text_embed_graphic_key. That's an error. text_cat.warning() << "Unclosed embed_graphic in text.\n"; return; } ++si; // Now we have to encode the wstring into a string, for lookup in the // TextPropertiesManager. string graphic_name = _encoder->encode_wtext(graphic_wname); TextPropertiesManager *manager = TextPropertiesManager::get_global_ptr(); // Get the graphic image. const TextGraphic *named_graphic = manager->get_graphic_ptr(graphic_name); if (named_graphic != (TextGraphic *)NULL) { output_string.push_back(TextCharacter(named_graphic, graphic_wname, current_cprops)); } else { text_cat.warning() << "Unknown TextGraphic: " << graphic_name << "\n"; } } else { // A normal character. Apply it. output_string.push_back(TextCharacter(*si, current_cprops)); ++si; } } } /** * Inserts newlines into the _text_string at the appropriate places in order * to make each line be the longest possible line that is not longer than * wordwrap_width (and does not break any words, if possible). Stores the * result in _text_block. * * If _max_rows is greater than zero, no more than _max_rows will be accepted. * Text beyond that will be truncated. * * The return value is true if all the text is accepted, or false if some was * truncated. */ bool TextAssembler:: wordwrap_text() { _text_block.clear(); if (_text_string.empty()) { // A special case: empty text means no rows. return true; } size_t p = 0; _text_block.push_back(TextRow(p)); // Preserve any initial whitespace and newlines. PN_stdfloat initial_width = 0.0f; while (p < _text_string.size() && isspacew(_text_string[p]._character)) { if (_text_string[p]._character == '\n') { initial_width = 0.0f; if (_max_rows > 0 && (int)_text_block.size() >= _max_rows) { // Truncate. return false; } _text_block.back()._eol_cprops = _text_string[p]._cprops; _text_block.push_back(TextRow(p + 1)); } else { initial_width += calc_width(_text_string[p]); _text_block.back()._string.push_back(_text_string[p]); } p++; } bool needs_newline = false; while (p < _text_string.size()) { nassertr(!isspacew(_text_string[p]._character), false); // Scan the next n characters, until the end of the string or an embedded // newline character, or we exceed wordwrap_width. size_t q = p; bool any_spaces = false; size_t last_space = 0; PN_stdfloat last_space_width = 0.0f; bool any_hyphens = false; size_t last_hyphen = 0; bool output_hyphen = false; bool overflow = false; PN_stdfloat wordwrap_width = -1.0f; bool last_was_space = false; PN_stdfloat width = initial_width; while (q < _text_string.size() && _text_string[q]._character != '\n') { if (_text_string[q]._cprops->_properties.has_wordwrap()) { wordwrap_width = _text_string[q]._cprops->_properties.get_wordwrap(); } else { wordwrap_width = -1.0f; } if (isspacew(_text_string[q]._character) || _text_string[q]._character == text_soft_break_key) { if (!last_was_space) { any_spaces = true; // We only care about logging whether there is a soft-hyphen // character to the right of the rightmost space. Each time we // encounter a space, we reset this counter. any_hyphens = false; last_space = q; last_space_width = width; last_was_space = true; } } else { last_was_space = false; } // A soft hyphen character is not printed, but marks a point at which we // might hyphenate a word if we need to. if (_text_string[q]._character == text_soft_hyphen_key) { if (wordwrap_width > 0.0f) { // We only consider this as a possible hyphenation point if (a) it // is not the very first character, and (b) there is enough room for // a hyphen character to be printed following it. if (q != p && width + calc_hyphen_width(_text_string[q]) <= wordwrap_width) { any_hyphens = true; last_hyphen = q; } } } else { // Some normal, printable character. width += calc_width(_text_string[q]); } q++; if (wordwrap_width > 0.0f && width > wordwrap_width) { // Oops, too many. q--; overflow = true; break; } } if (overflow) { // If we stopped because we exceeded the wordwrap width, then try to // find an appropriate place to wrap the line or to hyphenate, if // necessary. nassertr(wordwrap_width > 0.0f, false); if (any_spaces && last_space_width / wordwrap_width >= text_hyphen_ratio) { // If we have a space that ended up within our safety margin, don't // use any soft-hyphen characters. any_hyphens = false; } if (any_hyphens) { // If we still have a soft-hyphen character, use it. q = last_hyphen; output_hyphen = true; } else if (any_spaces) { // Otherwise, break at a space if we can. q = last_space; } else { // Otherwise, this is a forced break. Accept the longest line we can // that does not leave the next line beginning with one of our // forbidden characters. size_t i = 0; while ((int)i < text_max_never_break && q - i > p && get_text_never_break_before().find(_text_string[q - i]._character) != wstring::npos) { i++; } if ((int)i < text_max_never_break) { q -= i; } } } // Skip additional whitespace between the lines. size_t next_start = q; while (next_start < _text_string.size() && isbreakpoint(_text_string[next_start]._character)) { next_start++; } // Trim off any more blanks on the end. while (q > p && isspacew(_text_string[q - 1]._character)) { q--; } if (next_start == p) { // No characters got in at all. This could only happen if the wordwrap // width is narrower than a single character, or if we have a // substantial number of leading spaces in a line. if (initial_width == 0.0f) { // There was no leading whitespace on the line, so the character // itself didn't fit within the margins. Let it in anyway; what else // can we do? q++; next_start++; while (next_start < _text_string.size() && isbreakpoint(_text_string[next_start]._character)) { next_start++; } } } if (needs_newline) { if (_max_rows > 0 && (int)_text_block.size() >= _max_rows) { // Truncate. return false; } _text_block.push_back(TextRow(p)); } if (get_multiline_mode()){ needs_newline = true; } if (_text_string[next_start - 1]._cprops->_properties.get_preserve_trailing_whitespace()) { q = next_start; } for (size_t pi = p; pi < q; pi++) { if (_text_string[pi]._character != text_soft_hyphen_key && _text_string[pi]._character != text_soft_break_key) { _text_block.back()._string.push_back(_text_string[pi]); } else { _text_block.back()._got_soft_hyphens = true; } } if (output_hyphen) { wstring text_soft_hyphen_output = get_text_soft_hyphen_output(); wstring::const_iterator wi; for (wi = text_soft_hyphen_output.begin(); wi != text_soft_hyphen_output.end(); ++wi) { _text_block.back()._string.push_back(TextCharacter(*wi, _text_string[last_hyphen]._cprops)); } } // Now prepare to wrap the next line. if (next_start < _text_string.size() && _text_string[next_start]._character == '\n') { // Preserve a single embedded newline. if (_max_rows > 0 && (int)_text_block.size() >= _max_rows) { // Truncate. return false; } _text_block.back()._eol_cprops = _text_string[next_start]._cprops; next_start++; _text_block.push_back(TextRow(next_start)); needs_newline = false; } p = next_start; // Preserve any initial whitespace and newlines. initial_width = 0.0f; while (p < _text_string.size() && isspacew(_text_string[p]._character)) { if (_text_string[p]._character == '\n') { initial_width = 0.0f; if (_max_rows > 0 && (int)_text_block.size() >= _max_rows) { // Truncate. return false; } _text_block.back()._eol_cprops = _text_string[p]._cprops; _text_block.push_back(TextRow(p + 1)); } else { initial_width += calc_width(_text_string[p]); _text_block.back()._string.push_back(_text_string[p]); } p++; } } return true; } /** * Returns the width of the soft-hyphen replacement string, according to the * indicated character's associated font. */ PN_stdfloat TextAssembler:: calc_hyphen_width(const TextCharacter &tch) { TextFont *font = tch._cprops->_properties.get_font(); nassertr(font != (TextFont *)NULL, 0.0f); PN_stdfloat hyphen_width = 0.0f; wstring text_soft_hyphen_output = get_text_soft_hyphen_output(); wstring::const_iterator wi; for (wi = text_soft_hyphen_output.begin(); wi != text_soft_hyphen_output.end(); ++wi) { hyphen_width += calc_width(*wi, tch._cprops->_properties); } return hyphen_width; } /** * Generates Geoms for the given quads and adds them to the GeomNode. */ void TextAssembler:: generate_quads(GeomNode *geom_node, const QuadMap &quad_map) { QuadMap::const_iterator qmi; for (qmi = quad_map.begin(); qmi != quad_map.end(); ++qmi) { const QuadDefs &quads = qmi->second; GeomTextGlyph::Glyphs glyphs; glyphs.reserve(quads.size()); static CPT(GeomVertexFormat) format; if (format.is_null()) { // The optimized code below assumes 32-bit floats, so let's make sure we // got the right format by creating it ourselves. format = GeomVertexFormat::register_format(new GeomVertexArrayFormat( InternalName::get_vertex(), 3, GeomEnums::NT_float32, GeomEnums::C_point, InternalName::get_texcoord(), 2, GeomEnums::NT_float32, GeomEnums::C_texcoord)); } PT(GeomVertexData) vdata = new GeomVertexData("text", format, Geom::UH_static); PT(GeomTriangles) tris = new GeomTriangles(Geom::UH_static); if (quads.size() > 10922) { tris->set_index_type(GeomEnums::NT_uint32); } else { tris->set_index_type(GeomEnums::NT_uint16); } PT(GeomVertexArrayData) indices = tris->modify_vertices(); int i = 0; // This is quite a critical loop and GeomVertexWriter quickly becomes the // bottleneck. So, I've written this out the hard way instead. Two // versions of the loop: one for 32-bit indices, one for 16-bit. { PT(GeomVertexArrayDataHandle) vtx_handle = vdata->modify_array(0)->modify_handle(); vtx_handle->unclean_set_num_rows(quads.size() * 4); unsigned char *write_ptr = vtx_handle->get_write_pointer(); size_t stride = format->get_array(0)->get_stride() / sizeof(PN_float32); PN_float32 *vtx_ptr = (PN_float32 *) (write_ptr + format->get_column(InternalName::get_vertex())->get_start()); PN_float32 *tex_ptr = (PN_float32 *) (write_ptr + format->get_column(InternalName::get_texcoord())->get_start()); if (tris->get_index_type() == GeomEnums::NT_uint32) { // 32-bit index case. PT(GeomVertexArrayDataHandle) idx_handle = indices->modify_handle(); idx_handle->unclean_set_num_rows(quads.size() * 6); uint32_t *idx_ptr = (uint32_t *)idx_handle->get_write_pointer(); QuadDefs::const_iterator qi; for (qi = quads.begin(); qi != quads.end(); ++qi) { const QuadDef &quad = (*qi); vtx_ptr[0] = quad._dimensions[0] + quad._slanth; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[3]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[0]; tex_ptr[1] = quad._uvs[3]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[0] + quad._slantl; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[1]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[0]; tex_ptr[1] = quad._uvs[1]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[2] + quad._slanth; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[3]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[2]; tex_ptr[1] = quad._uvs[3]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[2] + quad._slantl; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[1]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[2]; tex_ptr[1] = quad._uvs[1]; tex_ptr += stride; *(idx_ptr++) = i + 0; *(idx_ptr++) = i + 1; *(idx_ptr++) = i + 2; *(idx_ptr++) = i + 2; *(idx_ptr++) = i + 1; *(idx_ptr++) = i + 3; i += 4; glyphs.push_back(MOVE(quad._glyph)); } } else { // 16-bit index case. PT(GeomVertexArrayDataHandle) idx_handle = indices->modify_handle(); idx_handle->unclean_set_num_rows(quads.size() * 6); uint16_t *idx_ptr = (uint16_t *)idx_handle->get_write_pointer(); QuadDefs::const_iterator qi; for (qi = quads.begin(); qi != quads.end(); ++qi) { const QuadDef &quad = (*qi); vtx_ptr[0] = quad._dimensions[0] + quad._slanth; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[3]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[0]; tex_ptr[1] = quad._uvs[3]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[0] + quad._slantl; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[1]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[0]; tex_ptr[1] = quad._uvs[1]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[2] + quad._slanth; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[3]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[2]; tex_ptr[1] = quad._uvs[3]; tex_ptr += stride; vtx_ptr[0] = quad._dimensions[2] + quad._slantl; vtx_ptr[1] = 0; vtx_ptr[2] = quad._dimensions[1]; vtx_ptr += stride; tex_ptr[0] = quad._uvs[2]; tex_ptr[1] = quad._uvs[1]; tex_ptr += stride; *(idx_ptr++) = i + 0; *(idx_ptr++) = i + 1; *(idx_ptr++) = i + 2; *(idx_ptr++) = i + 2; *(idx_ptr++) = i + 1; *(idx_ptr++) = i + 3; i += 4; glyphs.push_back(MOVE(quad._glyph)); } } } // We can compute this value much faster than GeomPrimitive can. tris->set_minmax(0, i - 1, NULL, NULL); PT(GeomTextGlyph) geom = new GeomTextGlyph(vdata); geom->_glyphs.swap(glyphs); geom->add_primitive(tris); geom_node->add_geom(geom, qmi->first); } } /** * Fills up placed_glyphs, _ul, _lr with the contents of _text_block. Also * updates _xpos and _ypos within the _text_block structure. */ void TextAssembler:: assemble_paragraph(TextAssembler::PlacedGlyphs &placed_glyphs) { _ul.set(0.0f, 0.0f); _lr.set(0.0f, 0.0f); int num_rows = 0; PN_stdfloat ypos = 0.0f; _next_row_ypos = 0.0f; TextBlock::iterator bi; for (bi = _text_block.begin(); bi != _text_block.end(); ++bi) { TextRow &row = (*bi); // Store the index of the first glyph we're going to place. size_t first_glyph = placed_glyphs.size(); // First, assemble all the glyphs of this row. PN_stdfloat row_width, line_height, wordwrap; TextProperties::Alignment align; assemble_row(row, placed_glyphs, row_width, line_height, align, wordwrap); // Now move the row to its appropriate position. This might involve a // horizontal as well as a vertical translation. if (num_rows == 0) { // If this is the first row, account for its space. _ul[1] = 0.8f * line_height; } else { // If it is not the first row, shift the text downward by line_height // from the previous row. ypos -= line_height; } _lr[1] = ypos - 0.2 * line_height; // Apply the requested horizontal alignment to the row. [fabius] added a // different concept of text alignment based upon a boxed region where his // width is defined by the wordwrap size with the upper left corner // starting from 0,0,0 if the wordwrap size is unspecified the alignment // could eventually result wrong. PN_stdfloat xpos; switch (align) { case TextProperties::A_left: xpos = 0.0f; _lr[0] = max(_lr[0], row_width); break; case TextProperties::A_right: xpos = -row_width; _ul[0] = min(_ul[0], xpos); break; case TextProperties::A_center: xpos = -0.5f * row_width; _ul[0] = min(_ul[0], xpos); _lr[0] = max(_lr[0], -xpos); break; case TextProperties::A_boxed_left: xpos = 0.0f; _lr[0] = max(_lr[0], max(row_width, wordwrap)); break; case TextProperties::A_boxed_right: xpos = wordwrap - row_width; _ul[0] = min(_ul[0], xpos); break; case TextProperties::A_boxed_center: xpos = -0.5f * row_width; if (wordwrap > row_width) xpos += (wordwrap * 0.5f); _ul[0] = min(_ul[0], max(xpos,(wordwrap * 0.5f))); _lr[0] = max(_lr[0], min(-xpos,-(wordwrap * 0.5f))); break; } row._xpos = xpos; row._ypos = ypos; // Now adjust the geoms we've assembled. for (size_t i = first_glyph; i < placed_glyphs.size(); ++i) { placed_glyphs[i]._xpos += xpos; placed_glyphs[i]._ypos += ypos; } // Advance to the next line. num_rows++; _next_row_ypos = ypos - line_height; } // num_rows may be smaller than _text_block.size(), if there are trailing // newlines on the string. } /** * Assembles the letters in the source string, up until the first newline or * the end of the string into a single row (which is parented to _geom_node), * and computes the length of the row and the maximum line_height of all the * fonts used in the row. The source pointer is moved to the terminating * character. */ void TextAssembler:: assemble_row(TextAssembler::TextRow &row, TextAssembler::PlacedGlyphs &placed_glyphs, PN_stdfloat &row_width, PN_stdfloat &line_height, TextProperties::Alignment &align, PN_stdfloat &wordwrap) { Thread *current_thread = Thread::get_current_thread(); line_height = 0.0f; PN_stdfloat xpos = 0.0f; align = TextProperties::A_left; bool underscore = false; PN_stdfloat underscore_start = 0.0f; const TextProperties *underscore_properties = NULL; TextString::const_iterator si; for (si = row._string.begin(); si != row._string.end(); ++si) { const TextCharacter &tch = (*si); wchar_t character = tch._character; const TextGraphic *graphic = tch._graphic; const TextProperties *properties = &(tch._cprops->_properties); if (properties->get_underscore() != underscore || (underscore && (properties->get_text_color() != underscore_properties->get_text_color() || properties->get_underscore_height() != underscore_properties->get_underscore_height()))) { // Change the underscore status. if (underscore && underscore_start != xpos) { draw_underscore(placed_glyphs, underscore_start, xpos, underscore_properties); } underscore = properties->get_underscore(); underscore_start = xpos; underscore_properties = properties; } TextFont *font = properties->get_font(); nassertv(font != (TextFont *)NULL); // We get the row's alignment property from the first character of the row if ((align == TextProperties::A_left) && (properties->get_align() != TextProperties::A_left)) { align = properties->get_align(); } // [fabius] a good place to take wordwrap size if (properties->get_wordwrap() > 0.0f) { wordwrap = properties->get_wordwrap(); } // And the height of the row is the maximum of all the fonts used within // the row. if (graphic != (TextGraphic *)NULL) { LVecBase4 frame = graphic->get_frame(); line_height = max(line_height, frame[3] - frame[2]); } else { // [fabius] this is not the right place to calc line height (see below) // line_height = max(line_height, font->get_line_height()); } if (character == ' ') { // A space is a special case. xpos += properties->get_glyph_scale() * properties->get_text_scale() * font->get_space_advance(); } else if (character == '\t') { // So is a tab character. PN_stdfloat tab_width = properties->get_tab_width(); xpos = (floor(xpos / tab_width) + 1.0f) * tab_width; } else if (character == text_soft_hyphen_key) { // And so is the 'soft-hyphen' key character. } else if (graphic != (TextGraphic *)NULL) { // A special embedded graphic. GlyphPlacement placement; PT(PandaNode) model = graphic->get_model().node(); if (graphic->get_instance_flag()) { // Instance the model in. Create a ModelNode so it doesn't get // flattened. PT(ModelNode) model_node = new ModelNode(""); model_node->set_preserve_transform(ModelNode::PT_no_touch); model_node->add_child(model); placement._graphic_model = model_node.p(); } else { // Copy the model in. This the preferred way; it's a little cheaper // to render than instancing (because flattening is more effective). placement._graphic_model = model->copy_subgraph(); } LVecBase4 frame = graphic->get_frame(); PN_stdfloat glyph_scale = properties->get_glyph_scale() * properties->get_text_scale(); PN_stdfloat advance = (frame[1] - frame[0]); // Now compute the matrix that will transform the glyph (or glyphs) into // position. placement._scale = properties->get_glyph_scale(); placement._xpos = (xpos - frame[0]); placement._ypos = (properties->get_glyph_shift() - frame[2]); placement._properties = properties; placed_glyphs.push_back(placement); xpos += advance * glyph_scale; } else { // A printable character. bool got_glyph; CPT(TextGlyph) first_glyph; CPT(TextGlyph) second_glyph; UnicodeLatinMap::AccentType accent_type; int additional_flags; PN_stdfloat glyph_scale; PN_stdfloat advance_scale; get_character_glyphs(character, properties, got_glyph, first_glyph, second_glyph, accent_type, additional_flags, glyph_scale, advance_scale); if (!got_glyph) { char buffer[512]; sprintf(buffer, "U+%04x", character); text_cat.warning() << "No definition in " << font->get_name() << " for character " << buffer; if (character < 128 && isprint((unsigned int)character)) { text_cat.warning(false) << " ('" << (char)character << "')"; } text_cat.warning(false) << "\n"; } // Build up a GlyphPlacement, indicating all of the Geoms that go into // this character. Normally, there is only one Geom per character, but // it may involve multiple Geoms if we need to add cheesy accents or // ligatures. GlyphPlacement placement; glyph_scale *= properties->get_glyph_scale() * properties->get_text_scale(); placement._glyph = NULL; placement._scale = glyph_scale; placement._xpos = xpos; placement._ypos = properties->get_glyph_shift(); placement._slant = properties->get_slant(); placement._properties = properties; PN_stdfloat advance = 0.0f; if (accent_type != UnicodeLatinMap::AT_none || additional_flags != 0) { // If we have some special handling to perform, do so now. This will // probably require the bounding volume of the glyph, so go get that. LPoint3 min_vert, max_vert; bool found_any = false; if (first_glyph != NULL) { first_glyph->calc_tight_bounds(min_vert, max_vert, found_any, current_thread); } if (second_glyph != NULL) { second_glyph->calc_tight_bounds(min_vert, max_vert, found_any, current_thread); } if (found_any) { LPoint3 centroid = (min_vert + max_vert) / 2.0f; if ((additional_flags & UnicodeLatinMap::AF_turned) != 0) { // Invert the character. Should we also invert the accent mark, // so that an accent that would have been above the glyph will now // be below it? That's what we do here, which is probably the // right thing to do for n-tilde, but not for most of the rest of // the accent marks. For now we'll assume there are no characters // with accent marks that also have the turned flag. // We rotate the character around its centroid, which may not // always be the right point, but it's the best we've got and it's // probably pretty close. placement._scale *= -1; placement._xpos += centroid[0] * 2; placement._ypos += centroid[2] * 2; } if (accent_type != UnicodeLatinMap::AT_none) { GlyphPlacement accent_placement(placement); tack_on_accent(accent_type, min_vert, max_vert, centroid, properties, accent_placement); placed_glyphs.push_back(accent_placement); } } } if (first_glyph != (TextGlyph *)NULL) { assert(!first_glyph->is_whitespace()); advance = first_glyph->get_advance() * advance_scale; swap(placement._glyph, first_glyph); placed_glyphs.push_back(placement); } // Check if there is a second glyph to create a hacky ligature or some // such nonsense. if (second_glyph != (TextGlyph *)NULL) { placement._xpos += advance * glyph_scale; advance += second_glyph->get_advance(); swap(placement._glyph, second_glyph); placed_glyphs.push_back(placement); } xpos += advance * glyph_scale; line_height = max(line_height, font->get_line_height() * glyph_scale); } } if (underscore && underscore_start != xpos) { draw_underscore(placed_glyphs, underscore_start, xpos, underscore_properties); } row_width = xpos; if (row._eol_cprops != (ComputedProperties *)NULL) { // If there's an _eol_cprops, it represents the cprops of the newline // character that ended the line, which should also contribute towards the // line_height. const TextProperties *properties = &(row._eol_cprops->_properties); TextFont *font = properties->get_font(); nassertv(font != (TextFont *)NULL); if (line_height == 0.0f) { PN_stdfloat glyph_scale = properties->get_glyph_scale() * properties->get_text_scale(); line_height = max(line_height, font->get_line_height() * glyph_scale); } } } /** * Creates the geometry to render the underscore line for the indicated range * of glyphs in this row. */ void TextAssembler:: draw_underscore(TextAssembler::PlacedGlyphs &placed_glyphs, PN_stdfloat underscore_start, PN_stdfloat underscore_end, const TextProperties *underscore_properties) { CPT(GeomVertexFormat) format = GeomVertexFormat::get_v3cp(); PT(GeomVertexData) vdata = new GeomVertexData("underscore", format, Geom::UH_static); vdata->unclean_set_num_rows(2); GeomVertexWriter vertex(vdata, InternalName::get_vertex()); GeomVertexWriter color(vdata, InternalName::get_color()); PN_stdfloat y = underscore_properties->get_underscore_height(); vertex.set_data3(underscore_start, 0.0f, y); color.set_data4(underscore_properties->get_text_color()); vertex.set_data3(underscore_end, 0.0f, y); color.set_data4(underscore_properties->get_text_color()); PT(GeomLines) lines = new GeomLines(Geom::UH_static); lines->add_next_vertices(2); lines->close_primitive(); PT(Geom) geom = new Geom(vdata); geom->add_primitive(lines); PT(TextGlyph) glyph = new TextGlyph(0, geom, RenderState::make_empty(), 0); // Eventually we should probably replace this with the set_quad approach, or // better, for improved performance. // glyph->set_quad(LVecBase4(underscore_start, y, underscore_end, y+0.1), // LVecBase4(0), RenderState::make_empty()); GlyphPlacement placement; placement._glyph = MOVE(glyph); placement._xpos = 0; placement._ypos = 0; placement._scale = 1; placement._slant = 0; placement._properties = underscore_properties; placed_glyphs.push_back(placement); } /** * Looks up the glyph(s) from the font for the appropriate character. If the * desired glyph isn't available (especially in the case of an accented * letter), tries to find a suitable replacement. Normally, only one glyph is * returned per character, but in the case in which we have to simulate a * missing ligature in the font, two glyphs might be returned. * * All parameters except the first two are output parameters. got_glyph is * set true if the glyph (or an acceptable substitute) is successfully found, * false otherwise; but even if it is false, glyph might still be non-NULL, * indicating a stand-in glyph for a missing character. */ void TextAssembler:: get_character_glyphs(int character, const TextProperties *properties, bool &got_glyph, CPT(TextGlyph) &glyph, CPT(TextGlyph) &second_glyph, UnicodeLatinMap::AccentType &accent_type, int &additional_flags, PN_stdfloat &glyph_scale, PN_stdfloat &advance_scale) { TextFont *font = properties->get_font(); nassertv_always(font != (TextFont *)NULL); got_glyph = false; glyph = NULL; second_glyph = NULL; accent_type = UnicodeLatinMap::AT_none; additional_flags = 0; glyph_scale = 1.0f; advance_scale = 1.0f; // Maybe we should remap the character to something else--e.g. a small // capital. const UnicodeLatinMap::Entry *map_entry = UnicodeLatinMap::look_up(character); if (map_entry != NULL) { if (properties->get_small_caps() && map_entry->_toupper_character != character) { character = map_entry->_toupper_character; map_entry = UnicodeLatinMap::look_up(character); glyph_scale = properties->get_small_caps_scale(); } } got_glyph = font->get_glyph(character, glyph); if (!got_glyph && map_entry != NULL && map_entry->_ascii_equiv != 0) { // If we couldn't find the Unicode glyph, try the ASCII equivalent // (without the accent marks). if (map_entry->_ascii_equiv == 'i') { // Special case for the i: we want to try the dotless variant first. got_glyph = font->get_glyph(0x0131, glyph) || font->get_glyph('i', glyph); } else if (map_entry->_ascii_equiv == 'j') { // And the dotless j as well. got_glyph = font->get_glyph(0x0237, glyph) || font->get_glyph('j', glyph); } else { got_glyph = font->get_glyph(map_entry->_ascii_equiv, glyph); } if (!got_glyph && map_entry->_toupper_character != character) { // If we still couldn't find it, try the uppercase equivalent. character = map_entry->_toupper_character; map_entry = UnicodeLatinMap::look_up(character); if (map_entry != NULL) { got_glyph = font->get_glyph(map_entry->_ascii_equiv, glyph); } } if (got_glyph) { accent_type = map_entry->_accent_type; additional_flags = map_entry->_additional_flags; bool got_second_glyph = false; if (map_entry->_ascii_additional != 0) { // There's another character, too--probably a ligature. got_second_glyph = font->get_glyph(map_entry->_ascii_additional, second_glyph); } if ((additional_flags & UnicodeLatinMap::AF_ligature) != 0 && got_second_glyph) { // If we have two letters that are supposed to be in a ligature, just // jam them together. additional_flags &= ~UnicodeLatinMap::AF_ligature; advance_scale = ligature_advance_scale; } if ((additional_flags & UnicodeLatinMap::AF_smallcap) != 0) { additional_flags &= ~UnicodeLatinMap::AF_smallcap; glyph_scale = properties->get_small_caps_scale(); } } } } /** * This is a cheesy attempt to tack on an accent to an ASCII letter for which * we don't have the appropriate already-accented glyph in the font. */ void TextAssembler:: tack_on_accent(UnicodeLatinMap::AccentType accent_type, const LPoint3 &min_vert, const LPoint3 &max_vert, const LPoint3 ¢roid, const TextProperties *properties, TextAssembler::GlyphPlacement &placement) const { // Look for a combining accent mark character. wchar_t combine_char = UnicodeLatinMap::get_combining_accent(accent_type); if (combine_char != 0 && tack_on_accent(combine_char, CP_above, CT_none, min_vert, max_vert, centroid, properties, placement)) { return; } switch (accent_type) { case UnicodeLatinMap::AT_grave: // We use the slash as the grave and acute accents. ASCII does have a // grave accent character, but a lot of fonts put the reverse apostrophe // there instead. And some fonts (particularly fonts from mf) don't even // do backslash. tack_on_accent('/', CP_above, CT_small_squash_mirror_y, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_acute: tack_on_accent('/', CP_above, CT_small_squash, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_breve: tack_on_accent(')', CP_above, CT_tiny_rotate_270, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_inverted_breve: tack_on_accent('(', CP_above, CT_tiny_rotate_270, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_circumflex: tack_on_accent('^', CP_above, CT_none, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('v', CP_above, CT_squash_mirror_y, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_circumflex_below: tack_on_accent('^', CP_below, CT_none, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('v', CP_below, CT_squash_mirror_y, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_caron: tack_on_accent('^', CP_above, CT_mirror_y, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('v', CP_above, CT_squash, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_tilde: tack_on_accent('~', CP_above, CT_none, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('s', CP_above, CT_squash_mirror_diag, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_tilde_below: tack_on_accent('~', CP_below, CT_none, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('s', CP_below, CT_squash_mirror_diag, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_diaeresis: tack_on_accent(':', CP_above, CT_small_rotate_270, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_diaeresis_below: tack_on_accent(':', CP_below, CT_small_rotate_270, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_dot_above: tack_on_accent('.', CP_above, CT_none, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_dot_below: tack_on_accent('.', CP_below, CT_none, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_macron: tack_on_accent('-', CP_above, CT_none, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_line_below: tack_on_accent('-', CP_below, CT_none, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_ring_above: tack_on_accent('o', CP_top, CT_tiny, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_ring_below: tack_on_accent('o', CP_bottom, CT_tiny, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_cedilla: tack_on_accent(0xb8, CP_below, CT_none, min_vert, max_vert, centroid, properties, placement) || tack_on_accent('c', CP_bottom, CT_tiny_mirror_x, min_vert, max_vert, centroid, properties, placement); // tack_on_accent(',', CP_bottom, CT_none, min_vert, max_vert, centroid, // properties, placement); break; case UnicodeLatinMap::AT_comma_below: tack_on_accent(',', CP_below, CT_none, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_ogonek: tack_on_accent(',', CP_bottom, CT_mirror_x, min_vert, max_vert, centroid, properties, placement); break; case UnicodeLatinMap::AT_stroke: tack_on_accent('/', CP_within, CT_none, min_vert, max_vert, centroid, properties, placement); break; default: // There are lots of other crazy kinds of accents. Forget 'em. break; } } /** * Generates a cheesy accent mark above (or below, etc.) the character. * Returns true if successful, or false if the named accent character doesn't * exist in the font. */ bool TextAssembler:: tack_on_accent(wchar_t accent_mark, TextAssembler::CheesyPosition position, TextAssembler::CheesyTransform transform, const LPoint3 &min_vert, const LPoint3 &max_vert, const LPoint3 ¢roid, const TextProperties *properties, TextAssembler::GlyphPlacement &placement) const { TextFont *font = properties->get_font(); nassertr(font != (TextFont *)NULL, false); Thread *current_thread = Thread::get_current_thread(); CPT(TextGlyph) accent_glyph; if (font->get_glyph(accent_mark, accent_glyph) || font->get_glyph(toupper(accent_mark), accent_glyph)) { if (!accent_glyph->is_whitespace()) { LPoint3 min_accent, max_accent; bool found_any = false; accent_glyph->calc_tight_bounds(min_accent, max_accent, found_any, current_thread); if (found_any) { PN_stdfloat t, u; LMatrix4 accent_mat; bool has_mat = true; switch (transform) { case CT_none: has_mat = false; break; case CT_mirror_x: accent_mat = LMatrix4::scale_mat(-1.0f, -1.0f, 1.0f); t = min_accent[0]; min_accent[0] = -max_accent[0]; max_accent[0] = -t; break; case CT_mirror_y: accent_mat = LMatrix4::scale_mat(1.0f, -1.0f, -1.0f); t = min_accent[2]; min_accent[2] = -max_accent[2]; max_accent[2] = -t; break; case CT_rotate_90: accent_mat.set_rotate_mat_normaxis(90.0f, LVecBase3(0.0f, -1.0f, 0.0f)); // rotate min, max t = min_accent[0]; u = max_accent[0]; max_accent[0] = -min_accent[2]; min_accent[0] = -max_accent[2]; max_accent[2] = u; min_accent[2] = t; break; case CT_rotate_180: has_mat = false; placement._scale *= -1; t = min_accent[0]; min_accent[0] = -max_accent[0]; max_accent[0] = -t; t = min_accent[2]; min_accent[2] = -max_accent[2]; max_accent[2] = -t; break; case CT_rotate_270: accent_mat.set_rotate_mat_normaxis(270.0f, LVecBase3(0.0f, -1.0f, 0.0f)); // rotate min, max t = min_accent[0]; u = max_accent[0]; min_accent[0] = min_accent[2]; max_accent[0] = max_accent[2]; min_accent[2] = -u; max_accent[2] = -t; break; case CT_squash: accent_mat = LMatrix4::scale_mat(squash_accent_scale_x, 1.0f, squash_accent_scale_y); min_accent[0] *= squash_accent_scale_x; max_accent[0] *= squash_accent_scale_x; min_accent[2] *= squash_accent_scale_y; max_accent[2] *= squash_accent_scale_y; break; case CT_squash_mirror_y: accent_mat = LMatrix4::scale_mat(squash_accent_scale_x, -1.0f, -squash_accent_scale_y); min_accent[0] *= squash_accent_scale_x; max_accent[0] *= squash_accent_scale_x; t = min_accent[2]; min_accent[2] = -max_accent[2] * squash_accent_scale_y; max_accent[2] = -t * squash_accent_scale_y; break; case CT_squash_mirror_diag: accent_mat = LMatrix4::rotate_mat_normaxis(270.0f, LVecBase3(0.0f, -1.0f, 0.0f)) * LMatrix4::scale_mat(-squash_accent_scale_x, -1.0f, squash_accent_scale_y); // rotate min, max t = min_accent[0]; u = max_accent[0]; min_accent[0] = min_accent[2] * -squash_accent_scale_x; max_accent[0] = max_accent[2] * -squash_accent_scale_x; min_accent[2] = -u * squash_accent_scale_y; max_accent[2] = -t * squash_accent_scale_y; break; case CT_small_squash: accent_mat = LMatrix4::scale_mat(small_squash_accent_scale_x, 1.0f, small_squash_accent_scale_y); min_accent[0] *= small_squash_accent_scale_x; max_accent[0] *= small_squash_accent_scale_x; min_accent[2] *= small_squash_accent_scale_y; max_accent[2] *= small_squash_accent_scale_y; break; case CT_small_squash_mirror_y: accent_mat = LMatrix4::scale_mat(small_squash_accent_scale_x, -1.0f, -small_squash_accent_scale_y); min_accent[0] *= small_squash_accent_scale_x; max_accent[0] *= small_squash_accent_scale_x; t = min_accent[2]; min_accent[2] = -max_accent[2] * small_squash_accent_scale_y; max_accent[2] = -t * small_squash_accent_scale_y; break; case CT_small_squash_mirror_diag: accent_mat = LMatrix4::rotate_mat_normaxis(270.0f, LVecBase3(0.0f, -1.0f, 0.0f)) * LMatrix4::scale_mat(-small_squash_accent_scale_x, -1.0f, small_squash_accent_scale_y); // rotate min, max t = min_accent[0]; u = max_accent[0]; min_accent[0] = min_accent[2] * -small_squash_accent_scale_x; max_accent[0] = max_accent[2] * -small_squash_accent_scale_x; min_accent[2] = -u * small_squash_accent_scale_y; max_accent[2] = -t * small_squash_accent_scale_y; break; case CT_small: has_mat = false; placement._scale *= small_accent_scale; min_accent *= small_accent_scale; max_accent *= small_accent_scale; break; case CT_small_rotate_270: accent_mat = LMatrix4::rotate_mat_normaxis(270.0f, LVecBase3(0.0f, -1.0f, 0.0f)) * LMatrix4::scale_mat(small_accent_scale); // rotate min, max t = min_accent[0]; u = max_accent[0]; min_accent[0] = min_accent[2] * small_accent_scale; max_accent[0] = max_accent[2] * small_accent_scale; min_accent[2] = -u * small_accent_scale; max_accent[2] = -t * small_accent_scale; break; case CT_tiny: has_mat = false; placement._scale *= tiny_accent_scale; min_accent *= tiny_accent_scale; max_accent *= tiny_accent_scale; break; case CT_tiny_mirror_x: accent_mat = LMatrix4::scale_mat(-tiny_accent_scale, -1.0f, tiny_accent_scale); t = min_accent[0]; min_accent[0] = -max_accent[0] * tiny_accent_scale; max_accent[0] = -t * tiny_accent_scale; min_accent[2] *= tiny_accent_scale; max_accent[2] *= tiny_accent_scale; break; case CT_tiny_rotate_270: accent_mat = LMatrix4::rotate_mat_normaxis(270.0f, LVecBase3(0.0f, -1.0f, 0.0f)) * LMatrix4::scale_mat(tiny_accent_scale); // rotate min, max t = min_accent[0]; u = max_accent[0]; min_accent[0] = min_accent[2] * tiny_accent_scale; max_accent[0] = max_accent[2] * tiny_accent_scale; min_accent[2] = -u * tiny_accent_scale; max_accent[2] = -t * tiny_accent_scale; break; default: has_mat = false; } PN_stdfloat total_margin = font->get_total_poly_margin(); LPoint3 accent_centroid = (min_accent + max_accent) / 2.0f; PN_stdfloat accent_height = max_accent[2] - min_accent[2] - total_margin * 2; PN_stdfloat accent_x = centroid[0] - accent_centroid[0]; PN_stdfloat accent_y = 0; PN_stdfloat min_y = min_vert[2] + total_margin; PN_stdfloat max_y = max_vert[2] - total_margin; switch (position) { case CP_above: // A little above the character. accent_y = max_y - accent_centroid[2] + accent_height * 0.75f; break; case CP_below: // A little below the character. accent_y = min_y - accent_centroid[2] - accent_height * 0.75f; break; case CP_top: // Touching the top of the character. accent_y = max_y - accent_centroid[2]; break; case CP_bottom: // Touching the bottom of the character. accent_y = min_y - accent_centroid[2]; break; case CP_within: // Centered within the character. accent_y = centroid[2] - accent_centroid[2]; break; } placement._xpos += placement._scale * (accent_x + placement._slant * accent_y); placement._ypos += placement._scale * accent_y; if (has_mat) { // Some non-trivial transformation. Apply it to the Geom. PT(Geom) accent_geom = accent_glyph->get_geom(_usage_hint); accent_geom->transform_vertices(accent_mat); placement._glyph = new TextGlyph(0, accent_geom, accent_glyph->get_state(), 0); } else { // A trivial transformation. placement._glyph = accent_glyph; } return true; } } } return false; } /** * Appends to wtext the control sequences necessary to change from this * ComputedProperties to the indicated ComputedProperties. */ void TextAssembler::ComputedProperties:: append_delta(wstring &wtext, TextAssembler::ComputedProperties *other) { if (this != other) { if (_depth > other->_depth) { // Back up a level from this properties. nassertv(_based_on != NULL); wtext.push_back(text_pop_properties_key); _based_on->append_delta(wtext, other); } else if (other->_depth > _depth) { // Back up a level from the other properties. nassertv(other->_based_on != NULL); append_delta(wtext, other->_based_on); wtext.push_back(text_push_properties_key); wtext += other->_wname; wtext.push_back(text_push_properties_key); } else if (_depth != 0) { // Back up a level from both properties. nassertv(_based_on != NULL && other->_based_on != NULL); wtext.push_back(text_pop_properties_key); _based_on->append_delta(wtext, other->_based_on); wtext.push_back(text_push_properties_key); wtext += other->_wname; wtext.push_back(text_push_properties_key); } } } /** * Puts the pieces of the GlyphPlacement in the indicated GeomNode. The * vertices of the Geoms are modified by this operation. */ void TextAssembler::GlyphPlacement:: assign_to(GeomNode *geom_node, const RenderState *state, const LVector2 &offset) const { LMatrix4 xform(_scale, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, _slant * _scale, 0.0f, _scale, 0.0f, _xpos + offset[0], 0.0f, _ypos - offset[1], 1.0f); PT(Geom) geom = _glyph->get_geom(GeomEnums::UH_static); geom->transform_vertices(xform); geom_node->add_geom(geom, state->compose(_glyph->get_state())); } /** * Puts the pieces of the GlyphPlacement in the indicated GeomNode. This * flavor will append the Geoms with the additional transform applied to the * vertices. */ void TextAssembler::GlyphPlacement:: assign_append_to(GeomCollectorMap &geom_collector_map, const RenderState *state, const LVector2 &offset) const { LMatrix4 xform(_scale, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, _slant * _scale, 0.0f, _scale, 0.0f, _xpos + offset[0], 0.0f, _ypos - offset[1], 1.0f); PT(Geom) geom = _glyph->get_geom(GeomEnums::UH_static); int p, sp, s, e, i; const GeomVertexData *vdata = geom->get_vertex_data(); CPT(RenderState) rs = _glyph->get_state()->compose(state); GeomCollectorKey key(rs, vdata->get_format()); GeomCollectorMap::iterator mi = geom_collector_map.find(key); if (mi == geom_collector_map.end()) { mi = geom_collector_map.insert(GeomCollectorMap::value_type(key, GeomCollector(vdata->get_format()))).first; } GeomCollector &geom_collector = (*mi).second; geom_collector.count_geom(geom); // We use this map to keep track of vertex indices we have already added, so // that we don't needlessly duplicate vertices into our output vertex data. VertexIndexMap vimap; for (p = 0; p < geom->get_num_primitives(); p++) { CPT(GeomPrimitive) primitive = geom->get_primitive(p)->decompose(); // Get a new GeomPrimitive of the corresponding type. GeomPrimitive *new_prim = geom_collector.get_primitive(primitive->get_type()); // Walk through all of the components (e.g. triangles) of the primitive. for (sp = 0; sp < primitive->get_num_primitives(); sp++) { s = primitive->get_primitive_start(sp); e = primitive->get_primitive_end(sp); // Walk through all of the vertices in the component. for (i = s; i < e; i++) { int vi = primitive->get_vertex(i); // Attempt to insert number "vi" into the map. pair added = vimap.insert(VertexIndexMap::value_type(vi, 0)); int new_vertex; if (added.second) { // The insert succeeded. That means this is the first time we have // encountered this vertex. new_vertex = geom_collector.append_vertex(vdata, vi, xform); // Update the map with the newly-created target vertex index. (*(added.first)).second = new_vertex; } else { // The insert failed. This means we have previously encountered // this vertex, and we have already entered its target vertex index // into the vimap. Extract that vertex index, so we can reuse it. new_vertex = (*(added.first)).second; } new_prim->add_vertex(new_vertex); } new_prim->close_primitive(); } } } /** * If this glyph is representable as a single quad, assigns it to the * appropriate position in the map. */ void TextAssembler::GlyphPlacement:: assign_quad_to(QuadMap &quad_map, const RenderState *state, const LVector2 &offset) const { QuadDef quad; if (_glyph->get_quad(quad._dimensions, quad._uvs)) { quad._dimensions *= _scale; quad._slantl = quad._dimensions[1] * _slant; quad._slanth = quad._dimensions[3] * _slant; quad._dimensions += LVecBase4(_xpos, _ypos, _xpos, _ypos); quad._dimensions += LVecBase4(offset[0], -offset[1], offset[0], -offset[1]); quad._glyph = _glyph; quad_map[state->compose(_glyph->get_state())].push_back(MOVE(quad)); } } /** * If the GlyphPlacement includes a special graphic, copies it to the * indicated node. */ void TextAssembler::GlyphPlacement:: copy_graphic_to(PandaNode *node, const RenderState *state) const { if (_graphic_model != (PandaNode *)NULL) { // We need an intermediate node to hold the transform and state. PT(PandaNode) intermediate_node = new PandaNode(""); node->add_child(intermediate_node); intermediate_node->set_transform( TransformState::make_pos_hpr_scale_shear( LVecBase3(_xpos, 0, _ypos), LVecBase3::zero(), LVecBase3(_scale, 1, _scale), LVecBase3(0, _slant, 0) ) ); intermediate_node->set_state(state); intermediate_node->add_child(_graphic_model); } } /** * constructs the GeomCollector class (Geom, GeomTriangles, vertexWriter, * texcoordWriter..) */ TextAssembler::GeomCollector:: GeomCollector(const GeomVertexFormat *format) : _vdata(new GeomVertexData("merged_geom", format, Geom::UH_static)), _geom(new GeomTextGlyph(_vdata)) { } /** * */ TextAssembler::GeomCollector:: GeomCollector(const TextAssembler::GeomCollector ©) : _vdata(copy._vdata), _geom(copy._geom) { } /** * Returns a GeomPrimitive of the appropriate type. If one has not yet been * created, returns a newly-created one; if one has previously been created of * this type, returns the previously-created one. */ GeomPrimitive *TextAssembler::GeomCollector:: get_primitive(TypeHandle prim_type) { if (prim_type == GeomTriangles::get_class_type()) { if (_triangles == (GeomPrimitive *)NULL) { _triangles = new GeomTriangles(Geom::UH_static); _geom->add_primitive(_triangles); } return _triangles; } else if (prim_type == GeomLines::get_class_type()) { if (_lines == (GeomPrimitive *)NULL) { _lines = new GeomLines(Geom::UH_static); _geom->add_primitive(_lines); } return _lines; } else if (prim_type == GeomPoints::get_class_type()) { if (_points == (GeomPrimitive *)NULL) { _points = new GeomPoints(Geom::UH_static); _geom->add_primitive(_points); } return _points; } nassertr(false, NULL); return NULL; } /** * Adds one vertex to the GeomVertexData. Returns the row number of the added * vertex. */ int TextAssembler::GeomCollector:: append_vertex(const GeomVertexData *orig_vdata, int orig_row, const LMatrix4 &xform) { int new_row = _vdata->get_num_rows(); _vdata->copy_row_from(new_row, orig_vdata, orig_row, Thread::get_current_thread()); GeomVertexRewriter vertex_rewriter(_vdata, InternalName::get_vertex()); vertex_rewriter.set_row_unsafe(new_row); LPoint3 point = vertex_rewriter.get_data3(); vertex_rewriter.set_data3(point * xform); return new_row; } /** * closes the geomTriangles and appends the geom to the given GeomNode */ void TextAssembler::GeomCollector:: append_geom(GeomNode *geom_node, const RenderState *state) { if (_geom->get_num_primitives() > 0) { geom_node->add_geom(_geom, state); } }