Skip to content

Commit 3cdb10e

Browse files
committed
feat: roundedBox and text
1 parent 9b4cb21 commit 3cdb10e

2 files changed

Lines changed: 127 additions & 1 deletion

File tree

projects/examples/A.solids.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ export const hexagonalPrism = (): Solid => {
6767
return Solid.prism(6, 6, 12, { color: 'cyan' }); // 6-sided, radius 6, height 12
6868
};
6969

70+
/**
71+
* Rounded Box - box with filleted/rounded edges
72+
* Parameters: width (X), height (Y), depth (Z), options object with color, radius, segments
73+
* Note: Radius defaults to 10% of smallest dimension. Segments control rounding quality.
74+
*/
75+
export const roundedBox = (): Solid => {
76+
return Solid.roundedBox(10, 10, 10, { color: 'teal', radius: 2 }); // 10×10×10 with 2-unit radius
77+
};
78+
79+
/**
80+
* Text - 3D extruded text
81+
* Parameters: text string, options object with color, size, height (extrusion depth)
82+
* Note: Uses Helvetiker Regular font. Text is centered on XZ plane and aligned to Y=0
83+
*/
84+
export const textShape = (): Solid => {
85+
return Solid.text('CSG', { color: 'blue', size: 8, height: 3 }); // "CSG" text
86+
};
87+
88+
/**
89+
* Text as Cutter - demonstrates using text in CSG subtraction (embossing/engraving)
90+
* Creates a plate with text cut into it
91+
*/
92+
export const textCutter = (): Solid => {
93+
const plate = Solid.cube(30, 20, 5, { color: 'gray' }).center().align('bottom');
94+
const text = Solid.text('HELLO', { size: 4, height: 10, color: 'gray' }).move({ y: 10 });
95+
return Solid.SUBTRACT(plate, text);
96+
};
97+
7098
/**
7199
* Component registration map
72100
* Keys: Component names (appear in UI dropdown)
@@ -80,5 +108,8 @@ export const components: ComponentsMap = {
80108
'A3. Solids: Sphere': sphere,
81109
'A4. Solids: Cone': cone,
82110
'A5. Solids: Triangle Prism': trianglePrism,
83-
'A6. Solids: Hexagonal Prism': hexagonalPrism
111+
'A6. Solids: Hexagonal Prism': hexagonalPrism,
112+
'A7. Solids: Rounded Box': roundedBox,
113+
'A8. Solids: Text': textShape,
114+
'A9. Solids: Text Cutter': textCutter
84115
};

src/lib/3d/Solid.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
SphereGeometry,
1313
Vector3
1414
} from 'three';
15+
import helvetikerFont from 'three/examples/fonts/helvetiker_regular.typeface.json';
16+
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
17+
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
18+
import type { Font } from 'three/examples/jsm/loaders/FontLoader.js';
19+
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
1520
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
1621
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
1722
import { ADDITION, Brush, Evaluator, INTERSECTION, SUBTRACTION } from 'three-bvh-csg';
@@ -56,6 +61,16 @@ export class Solid {
5661
// ============================================================================
5762

5863
private static evaluator: Evaluator = new Evaluator();
64+
private static defaultFont: Font | undefined = undefined;
65+
66+
// Helper to load and cache the default font for text geometry
67+
private static getDefaultFont(): Font {
68+
if (!this.defaultFont) {
69+
const loader = new FontLoader();
70+
this.defaultFont = loader.parse(helvetikerFont);
71+
}
72+
return this.defaultFont;
73+
}
5974

6075
// Helper to convert degrees to radians
6176
private static degreesToRadians = (degrees: number): number => degrees * (Math.PI / 180);
@@ -133,11 +148,13 @@ export class Solid {
133148
private static geometryToBrush(
134149
geometry:
135150
| BoxGeometry
151+
| RoundedBoxGeometry
136152
| CylinderGeometry
137153
| SphereGeometry
138154
| ConeGeometry
139155
| ExtrudeGeometry
140156
| LatheGeometry
157+
| TextGeometry
141158
| BufferGeometry
142159
): Brush {
143160
const result = new Brush(geometry.translate(0, 0, 0));
@@ -172,6 +189,44 @@ export class Solid {
172189
).normalize();
173190
};
174191

192+
static roundedBox = (
193+
width: number,
194+
height: number,
195+
depth: number,
196+
options?: {
197+
color?: string;
198+
radius?: number;
199+
segments?: number;
200+
}
201+
): Solid => {
202+
// Validate dimensions (check finite first, then positive)
203+
if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(depth))
204+
throw new Error(
205+
`RoundedBox dimensions must be finite (got width: ${width}, height: ${height}, depth: ${depth})`
206+
);
207+
if (width <= 0 || height <= 0 || depth <= 0)
208+
throw new Error(
209+
`RoundedBox dimensions must be positive (got width: ${width}, height: ${height}, depth: ${depth})`
210+
);
211+
212+
const color = options?.color ?? 'gray';
213+
const radius = options?.radius ?? Math.min(width, height, depth) * 0.1;
214+
const segments = options?.segments ?? 2;
215+
216+
// Validate radius is within bounds
217+
const maxRadius = Math.min(width, height, depth) / 2;
218+
if (radius > maxRadius)
219+
throw new Error(
220+
`RoundedBox radius (${radius}) cannot exceed half of smallest dimension (${maxRadius})`
221+
);
222+
if (radius < 0) throw new Error(`RoundedBox radius must be non-negative (got ${radius})`);
223+
224+
return new Solid(
225+
this.geometryToBrush(new RoundedBoxGeometry(width, height, depth, segments, radius)),
226+
color
227+
).normalize();
228+
};
229+
175230
static cylinder = (
176231
radius: number,
177232
height: number,
@@ -388,6 +443,46 @@ export class Solid {
388443
}
389444
): Solid => this.prism(3, radius, height, options);
390445

446+
static text = (
447+
text: string,
448+
options?: {
449+
color?: string;
450+
size?: number;
451+
height?: number;
452+
curveSegments?: number;
453+
bevelEnabled?: boolean;
454+
}
455+
): Solid => {
456+
// Validate text parameter
457+
if (!text || text.length === 0) throw new Error('Text cannot be empty');
458+
459+
const color = options?.color ?? 'gray';
460+
const size = options?.size ?? 10;
461+
const height = options?.height ?? 2;
462+
const curveSegments = options?.curveSegments ?? 12;
463+
const bevelEnabled = options?.bevelEnabled ?? false;
464+
465+
// Get the default font
466+
const font = this.getDefaultFont();
467+
468+
// Create text geometry
469+
// Note: TextGeometry's 'depth' parameter is what we call 'height' (extrusion depth)
470+
const geometry = new TextGeometry(text, {
471+
font,
472+
size,
473+
depth: height,
474+
curveSegments,
475+
bevelEnabled
476+
});
477+
478+
// Create solid and normalize
479+
const solid = new Solid(this.geometryToBrush(geometry), color).normalize();
480+
481+
// Center text on XZ plane and align to bottom (Y=0)
482+
// This makes text easier to position and orient consistently
483+
return solid.center({ x: true, z: true }).align('bottom');
484+
};
485+
391486
// ============================================================================
392487
// SECTION 4.5: Import Methods (Static)
393488
// ============================================================================

0 commit comments

Comments
 (0)