@bitbybit-dev/base
Version:
Bit By Bit Developers Base CAD Library to Program Geometry
585 lines (584 loc) • 18.8 kB
JavaScript
import * as Inputs from "../inputs";
import { defaultsVectorParams } from "../models/simplex";
/**
* Contains various text methods.
*/
export class TextBitByBit {
constructor(point) {
this.point = point;
}
/**
* Creates and returns a text string (pass-through for text input).
* Example: text='Hello World' → 'Hello World'
* @param inputs a text
* @returns text
* @group create
* @shortname text
* @drawable false
*/
create(inputs) {
return inputs.text;
}
/**
* Splits text into multiple pieces using a separator string.
* Example: text='apple,banana,cherry', separator=',' → ['apple', 'banana', 'cherry']
* @param inputs a text
* @returns text
* @group transform
* @shortname split
* @drawable false
*/
split(inputs) {
return inputs.text.split(inputs.separator);
}
/**
* Replaces all occurrences of a search string with a replacement string.
* Example: text='hello hello', search='hello', replaceWith='hi' → 'hi hi'
* @param inputs a text
* @returns text
* @group transform
* @shortname replaceAll
* @drawable false
*/
replaceAll(inputs) {
return inputs.text.split(inputs.search).join(inputs.replaceWith);
}
/**
* Joins multiple items into a single text string using a separator.
* Example: list=['apple', 'banana', 'cherry'], separator=', ' → 'apple, banana, cherry'
* @param inputs a list of items
* @returns text
* @group transform
* @shortname join
* @drawable false
*/
join(inputs) {
return inputs.list.join(inputs.separator);
}
/**
* Transform any item to text
* @param inputs any item
* @returns text
* @group transform
* @shortname to string
* @drawable false
*/
toString(inputs) {
return inputs.item.toString();
}
/**
* Transform each item in list to text
* @param inputs list of items
* @returns texts
* @group transform
* @shortname to strings
* @drawable false
*/
toStringEach(inputs) {
return inputs.list.map(i => i.toString());
}
/**
* Formats text with placeholder values using {0}, {1}, etc. syntax.
* Example: text='Point: ({0}, {1})', values=[10, 5] → 'Point: (10, 5)'
* @param inputs a text and values
* @returns formatted text
* @group transform
* @shortname format
* @drawable false
*/
format(inputs) {
return inputs.text.replace(/{(\d+)}/g, (match, number) => {
return typeof inputs.values[number] !== "undefined" ? inputs.values[number] : match;
});
}
/**
* Checks if text contains a search string.
* Example: text='hello world', search='world' → true
* @param inputs a text and search string
* @returns boolean
* @group query
* @shortname includes
* @drawable false
*/
includes(inputs) {
return inputs.text.includes(inputs.search);
}
/**
* Checks if text starts with a search string.
* Example: text='hello world', search='hello' → true
* @param inputs a text and search string
* @returns boolean
* @group query
* @shortname starts with
* @drawable false
*/
startsWith(inputs) {
return inputs.text.startsWith(inputs.search);
}
/**
* Checks if text ends with a search string.
* Example: text='hello world', search='world' → true
* @param inputs a text and search string
* @returns boolean
* @group query
* @shortname ends with
* @drawable false
*/
endsWith(inputs) {
return inputs.text.endsWith(inputs.search);
}
/**
* Returns the index of the first occurrence of a search string.
* Example: text='hello world', search='world' → 6
* @param inputs a text and search string
* @returns index or -1 if not found
* @group query
* @shortname index of
* @drawable false
*/
indexOf(inputs) {
return inputs.text.indexOf(inputs.search);
}
/**
* Returns the index of the last occurrence of a search string.
* Example: text='hello world hello', search='hello' → 12
* @param inputs a text and search string
* @returns index or -1 if not found
* @group query
* @shortname last index of
* @drawable false
*/
lastIndexOf(inputs) {
return inputs.text.lastIndexOf(inputs.search);
}
/**
* Extracts a section of text between two indices.
* Example: text='hello world', start=0, end=5 → 'hello'
* @param inputs a text, start and end indices
* @returns extracted text
* @group transform
* @shortname substring
* @drawable false
*/
substring(inputs) {
return inputs.text.substring(inputs.start, inputs.end);
}
/**
* Extracts a section of text and returns a new string.
* Example: text='hello world', start=0, end=5 → 'hello'
* @param inputs a text, start and end indices
* @returns extracted text
* @group transform
* @shortname slice
* @drawable false
*/
slice(inputs) {
return inputs.text.slice(inputs.start, inputs.end);
}
/**
* Returns the character at the specified index.
* Example: text='hello', index=1 → 'e'
* @param inputs a text and index
* @returns character
* @group query
* @shortname char at
* @drawable false
*/
charAt(inputs) {
return inputs.text.charAt(inputs.index);
}
/**
* Removes whitespace from both ends of text.
* Example: text=' hello ' → 'hello'
* @param inputs a text
* @returns trimmed text
* @group transform
* @shortname trim
* @drawable false
*/
trim(inputs) {
return inputs.text.trim();
}
/**
* Removes whitespace from the start of text.
* Example: text=' hello ' → 'hello '
* @param inputs a text
* @returns trimmed text
* @group transform
* @shortname trim start
* @drawable false
*/
trimStart(inputs) {
return inputs.text.trimStart();
}
/**
* Removes whitespace from the end of text.
* Example: text=' hello ' → ' hello'
* @param inputs a text
* @returns trimmed text
* @group transform
* @shortname trim end
* @drawable false
*/
trimEnd(inputs) {
return inputs.text.trimEnd();
}
/**
* Pads text from the start to reach target length.
* Example: text='x', length=3, padString='a' → 'aax'
* @param inputs a text, target length and pad string
* @returns padded text
* @group transform
* @shortname pad start
* @drawable false
*/
padStart(inputs) {
return inputs.text.padStart(inputs.length, inputs.padString);
}
/**
* Pads text from the end to reach target length.
* Example: text='x', length=3, padString='a' → 'xaa'
* @param inputs a text, target length and pad string
* @returns padded text
* @group transform
* @shortname pad end
* @drawable false
*/
padEnd(inputs) {
return inputs.text.padEnd(inputs.length, inputs.padString);
}
/**
* Converts text to uppercase.
* Example: text='hello' → 'HELLO'
* @param inputs a text
* @returns uppercase text
* @group transform
* @shortname to upper case
* @drawable false
*/
toUpperCase(inputs) {
return inputs.text.toUpperCase();
}
/**
* Converts text to lowercase.
* Example: text='HELLO' → 'hello'
* @param inputs a text
* @returns lowercase text
* @group transform
* @shortname to lower case
* @drawable false
*/
toLowerCase(inputs) {
return inputs.text.toLowerCase();
}
/**
* Capitalizes the first character of text.
* Example: text='hello world' → 'Hello world'
* @param inputs a text
* @returns text with first character uppercase
* @group transform
* @shortname capitalize first
* @drawable false
*/
toUpperCaseFirst(inputs) {
if (!inputs.text)
return inputs.text;
return inputs.text.charAt(0).toUpperCase() + inputs.text.slice(1);
}
/**
* Lowercases the first character of text.
* Example: text='Hello World' → 'hello World'
* @param inputs a text
* @returns text with first character lowercase
* @group transform
* @shortname uncapitalize first
* @drawable false
*/
toLowerCaseFirst(inputs) {
if (!inputs.text)
return inputs.text;
return inputs.text.charAt(0).toLowerCase() + inputs.text.slice(1);
}
/**
* Repeats text a specified number of times.
* Example: text='ha', count=3 → 'hahaha'
* @param inputs a text and count
* @returns repeated text
* @group transform
* @shortname repeat
* @drawable false
*/
repeat(inputs) {
return inputs.text.repeat(inputs.count);
}
/**
* Reverses the characters in text.
* Example: text='hello' → 'olleh'
* @param inputs a text
* @returns reversed text
* @group transform
* @shortname reverse
* @drawable false
*/
reverse(inputs) {
return inputs.text.split("").reverse().join("");
}
/**
* Returns the length of text.
* Example: text='hello' → 5
* @param inputs a text
* @returns length
* @group query
* @shortname length
* @drawable false
*/
length(inputs) {
return inputs.text.length;
}
/**
* Checks if text is empty or only whitespace.
* Example: text=' ' → true
* @param inputs a text
* @returns boolean
* @group query
* @shortname is empty
* @drawable false
*/
isEmpty(inputs) {
return !inputs.text || inputs.text.trim().length === 0;
}
/**
* Concatenates multiple text strings.
* Example: texts=['hello', ' ', 'world'] → 'hello world'
* @param inputs array of texts
* @returns concatenated text
* @group transform
* @shortname concat
* @drawable false
*/
concat(inputs) {
return inputs.texts.join("");
}
/**
* Tests if text matches a regular expression pattern.
* Example: text='hello123', pattern='[0-9]+' → true
* @param inputs a text and regex pattern
* @returns boolean
* @group regex
* @shortname test regex
* @drawable false
*/
regexTest(inputs) {
const regex = new RegExp(inputs.pattern, inputs.flags);
return regex.test(inputs.text);
}
/**
* Matches text against a regular expression and returns matches.
* Example: text='hello123world456', pattern='[0-9]+', flags='g' → ['123', '456']
* @param inputs a text and regex pattern
* @returns array of matches or null
* @group regex
* @shortname regex match
* @drawable false
*/
regexMatch(inputs) {
const regex = new RegExp(inputs.pattern, inputs.flags);
const result = inputs.text.match(regex);
return result ? Array.from(result) : null;
}
/**
* Replaces text matching a regular expression pattern.
* Example: text='hello123world456', pattern='[0-9]+', flags='g', replaceWith='X' → 'helloXworldX'
* @param inputs a text, regex pattern, and replacement
* @returns text with replacements
* @group regex
* @shortname regex replace
* @drawable false
*/
regexReplace(inputs) {
const regex = new RegExp(inputs.pattern, inputs.flags);
return inputs.text.replace(regex, inputs.replaceWith);
}
/**
* Searches text for a regular expression pattern and returns the index.
* Example: text='hello123', pattern='[0-9]+' → 5
* @param inputs a text and regex pattern
* @returns index or -1 if not found
* @group regex
* @shortname regex search
* @drawable false
*/
regexSearch(inputs) {
const regex = new RegExp(inputs.pattern, inputs.flags);
return inputs.text.search(regex);
}
/**
* Splits text using a regular expression pattern.
* Example: text='a1b2c3', pattern='[0-9]+' → ['a', 'b', 'c']
* @param inputs a text and regex pattern
* @returns array of split strings
* @group regex
* @shortname regex split
* @drawable false
*/
regexSplit(inputs) {
const regex = new RegExp(inputs.pattern, inputs.flags);
return inputs.text.split(regex);
}
/**
* Converts a character to vector paths (polylines) with width and height data for rendering.
* Uses simplex stroke font to generate 2D line segments representing the character shape.
* Example: char='A', height=10 → {width:8, height:10, paths:[[points forming A shape]]}
* @param inputs a text
* @returns width, height and segments as json
* @group vector
* @shortname vector char
* @drawable false
*/
vectorChar(inputs) {
const { xOffset, yOffset, font, input, height, extrudeOffset } = this.vectorParamsChar(inputs);
let code = input.charCodeAt(0);
if (!code || !font[code]) {
code = 63;
}
const glyph = [].concat(font[code]);
const ratio = (height - extrudeOffset) / font.height;
const extrudeYOffset = (extrudeOffset / 2);
const width = glyph.shift() * ratio;
const paths = [];
let polyline = [];
for (let i = 0, il = glyph.length; i < il; i += 2) {
const gx = ratio * glyph[i] + xOffset;
const gy = ratio * glyph[i + 1] + yOffset + extrudeYOffset;
if (glyph[i] !== undefined) {
polyline.push([gx, 0, gy]);
continue;
}
paths.push(polyline);
polyline = [];
i--;
}
if (polyline.length) {
paths.push(polyline);
}
return { width, height, paths };
}
/**
* Converts multi-line text to vector paths (polylines) with alignment and spacing controls.
* Supports line breaks, letter spacing, line spacing, horizontal alignment, and origin centering.
* Example: text='Hello\nWorld', height=10, align=center → [{line1 chars}, {line2 chars}]
* @param inputs a text as string
* @returns segments
* @group vector
* @shortname vector text
* @drawable false
*/
vectorText(inputs) {
const { xOffset, yOffset, height, align, extrudeOffset, lineSpacing, letterSpacing } = Object.assign({}, defaultsVectorParams, inputs);
const text = inputs.text;
if (typeof text !== "string")
throw new Error("text must be a string");
// NOTE: Just like CSS letter-spacing, the spacing could be positive or negative
const extraLetterSpacing = (height * letterSpacing);
// manage the list of lines
let maxWidth = 0; // keep track of max width for final alignment
let line = { width: 0, height: 0, chars: [] };
let lines = [];
const pushLine = () => {
maxWidth = Math.max(maxWidth, line.width);
if (line.chars.length)
lines.push(line);
line = { width: 0, height: 0, chars: [] };
};
// convert the text into a list of vector lines
let x = xOffset;
let y = yOffset;
let vchar;
const il = text.length;
for (let i = 0; i < il; i++) {
const character = text[i];
if (character === "\n") {
pushLine();
// reset x and y for a new line
x = xOffset;
y -= height * lineSpacing;
continue;
}
// convert the character
vchar = this.vectorChar({ xOffset: x, yOffset: y, height, extrudeOffset, char: character });
const width = vchar.width + extraLetterSpacing;
x += width;
// update current line
line.width += width;
line.height = Math.max(line.height, vchar.height);
if (character !== " ") {
line.chars = line.chars.concat(vchar);
}
}
if (line.chars.length)
pushLine();
// align all lines as requested
lines = lines.map((line) => {
const diff = maxWidth - line.width;
if (align === Inputs.Base.horizontalAlignEnum.right) {
return this.translateLine({ x: diff }, line);
}
else if (align === Inputs.Base.horizontalAlignEnum.center) {
return this.translateLine({ x: diff / 2 }, line);
}
else {
return line;
}
});
if (inputs.centerOnOrigin) {
const pointsFlat = [];
// flatten the lines into a single array of points
lines.forEach((line) => {
line.chars.forEach((vchar) => {
vchar.paths.forEach((path) => {
pointsFlat.push(...path);
});
});
});
const bbox = this.point.boundingBoxOfPoints({
points: pointsFlat,
});
lines.forEach((line) => {
line.chars.forEach((vchar) => {
vchar.paths = vchar.paths.map((path) => {
const pts = this.point.translatePoints({
points: path,
translation: [
-bbox.center[0],
-bbox.center[1],
-bbox.center[2],
],
});
return pts;
});
return vchar;
});
});
}
return lines;
}
vectorParamsChar(inputs) {
const params = Object.assign({}, defaultsVectorParams, inputs);
params.input = inputs.char || params.char;
return params;
}
translateLine(options, line) {
const { x, y } = Object.assign({ x: 0, y: 0 }, options);
line.chars = line.chars.map((vchar) => {
vchar.paths = vchar.paths.map((path) => {
const pts = this.point.translatePoints({
points: path,
translation: [x, 0, y],
});
return pts;
});
return vchar;
});
return line;
}
}