trid3d 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,19 +4,20 @@
4
4
 
5
5
  <h1 align="center">Trid</h1>
6
6
 
7
- <p align="center">A small 3D library for the HTML canvas. No dependencies, no build step.</p>
7
+ <p align="center">Model, light and draw 3D on the HTML canvas. No dependencies, no build step.</p>
8
8
 
9
9
  ---
10
10
 
11
- Trid gives you a scene, a camera, shapes, model loading and mouse/keyboard tools, all in plain ES modules you can drop next to your page.
11
+ Trid gives you a scene, shapes you can model in code, lights, effects, model loading and mouse/keyboard tools, all in plain ES modules you can drop next to your page. See the [docs](https://zachr622.github.io/Trid/) for live demos.
12
12
 
13
13
  > **Status:** early (0.x): the API can still change between minor versions. See [Upgrading](#upgrading).
14
14
 
15
15
  ## Features
16
16
 
17
- - **Scene** — holds a canvas, a camera and your objects, and runs the frame loop for you.
17
+ - **Scene** — holds a canvas, a camera, lights and your objects, and runs the frame loop for you. The background can be a color or see-through (`"transparent"`), so a model can float over your page.
18
18
  - **Groups** — give any object `children` and they move, turn and scale with it (models made of parts, a moon orbiting a planet).
19
- - **Shapes** — cubes, boxes, spheres, cylinders, cones, tori and planes built in, `lathe` for round shapes and `extrude` for flat ones, or any mesh you define as vertices and edges.
19
+ - **Shapes** — cubes, boxes (sharp or rounded), spheres, cylinders, cones, tori and planes built in, `lathe` for round shapes (a point on the axis closes it to a tip) and `extrude` for flat ones, or any mesh you define as vertices, faces and edges.
20
+ - **Modeling** — smooth curves through a few points (`spline`), shapes swept along a 3D path (`sweep`: tails, horns, pipes, rails), surfaces through cross-sections (`loft`: bodies, bottles, hulls, heads), subdivision surfaces with creases (`subdivideMesh`: box modeling), shapes that melt together (`blobMesh`: creatures, clay; or any distance field with `sdfMesh`), rounded or cut edges on any mesh (`bevelMesh`), taper, twist, bend and bulge deformers, mesh cleanup and checks (weld, clean, validate, fix winding), and rays, inside tests and distances against meshes and scenes (`meshQuery`, `raycastScene`).
20
21
  - **Mesh helpers** — clone, transform, displace, merge and measure meshes; seeded random numbers and smooth noise (`fbm2`, `fbm3`, tileable) for rocks, terrain and procedural textures.
21
22
  - **Filled faces** — give an object a `material` to fill and shade its faces, flat or smooth.
22
23
  - **Lights** — ambient, directional and point lights, with colors and intensities, as many as you need (up to 8 at once besides ambient).
@@ -26,7 +27,7 @@ Trid gives you a scene, a camera, shapes, model loading and mouse/keyboard tools
26
27
  - **Two renderers** — the canvas 2D context by default, or `WebGLRenderer` for exact depth and big models, with the same scene code.
27
28
  - **OBJ files** — load models from a URL (with their `.mtl` colors and textures), or let users drop their own `.obj` files on the canvas.
28
29
  - **Export** — `scene.screenshot()` and `scene.record()` save images and videos of the canvas; `exportOBJ` / `exportModel` write meshes, objects or whole scenes as OBJ files with their MTL materials and textures.
29
- - **Camera** — position, yaw, pitch and field of view, with near-plane clipping.
30
+ - **Camera** — position, yaw, pitch and field of view (50° by default), with near-plane clipping.
30
31
  - **Tools** — orbit and fly cameras, object dragging, hover highlight, a move / rotate / scale gizmo (with a pivot of your choice), a view axes gizmo, a floor grid, an FPS meter and an OBJ uploader, one line each. Exact picking by triangles, with the hit point and texture coordinates under the mouse. Writing your own is a plain object with a few methods.
31
32
  - **Pure math** — every transform and projection step is exported, plus 4×4 matrices and rotations, so you can use the pipeline piece by piece.
32
33
 
@@ -40,7 +41,7 @@ No bundler? Use the CDN with an import map, or copy the `trid/` folder next to y
40
41
 
41
42
  ```html
42
43
  <script type="importmap">
43
- { "imports": { "trid3d": "https://cdn.jsdelivr.net/npm/trid3d@0.3/trid/index.js" } }
44
+ { "imports": { "trid3d": "https://cdn.jsdelivr.net/npm/trid3d@0.6/trid/index.js" } }
44
45
  </script>
45
46
  ```
46
47
 
@@ -55,13 +56,22 @@ No bundler? Use the CDN with an import map, or copy the `trid/` folder next to y
55
56
  // main.js
56
57
  import { Scene, makeShape, OrbitCamera } from "trid3d";
57
58
 
58
- const scene = new Scene(document.getElementById("canvas"), { width: 800, height: 400 });
59
- const cube = scene.add({ mesh: makeShape("cube"), color: "#E9806E" }); // wireframe
60
- const ball = scene.add({ mesh: makeShape("sphere"), material: { fill: "#8ab4f8" } }); // filled and shaded
61
- ball.pos.x = 0.8;
59
+ const scene = new Scene(document.getElementById("canvas"), {
60
+ width: 800, height: 400,
61
+ camera: { z: -1.5 }, // a step back
62
+ });
63
+
64
+ // A cube with rounded edges, filled and smoothly shaded
65
+ const cube = scene.add({
66
+ mesh: makeShape("cube", { radius: 0.08 }),
67
+ material: { fill: "#E9806E", smooth: true },
68
+ });
62
69
 
63
70
  scene.use(new OrbitCamera()); // drag to orbit, scroll to zoom
64
- scene.start((dt) => (cube.rot.y += dt));
71
+ scene.start((dt) => {
72
+ cube.rot.x += dt * 0.5;
73
+ cube.rot.y += dt * 0.35;
74
+ });
65
75
  ```
66
76
 
67
77
  Without a bundler, serve the folder (`python3 -m http.server`) instead of opening the file directly: browsers block module imports from `file://` pages.
@@ -86,6 +96,7 @@ Or let users load their own: `scene.use(new ObjUploader())` accepts dropped `.ob
86
96
  | `trid/sceneGraph.js` | Groups: `children`, world matrices |
87
97
  | `trid/math/` | Transforms, camera directions, projection, matrices, rotations, random numbers and noise |
88
98
  | `trid/render/` | Renderers (Canvas 2D, WebGL), lights, effects, post effects, shapes, grids, mesh helpers, procedural textures, drawing, frame loop |
99
+ | `trid/modeling/` | Modeling tools: curves, sweep, loft, subdivision, blobs, bevel, deformers, cleanup, queries (rays, inside, distance) |
89
100
  | `trid/loaders/` | OBJ, MTL and texture loading |
90
101
  | `trid/export/` | Screenshots, video recording, OBJ and MTL export |
91
102
  | `trid/tools/` | Camera, object, gizmo and overlay tools |
@@ -111,10 +122,16 @@ See [`docs/README.md`](https://github.com/ZachR622/Trid/blob/master/docs/README.
111
122
  npm test
112
123
  ```
113
124
 
114
- Uses Node's built-in test runner (Node 21+), so there's nothing to install. The tests cover shapes, mesh helpers, noise, textures, OBJ / MTL, picking, tools, groups and matrices, lights, post effects, grids, and the WebGL renderer's GPU memory on a fake WebGL context.
125
+ Uses Node's built-in test runner (Node 21+), so there's nothing to install. The tests cover shapes (lathe poles included), mesh helpers, curves, sweeps, lofts, subdivision, blobs, bevels, deformers, mesh cleanup, mesh and scene queries, noise, textures, OBJ / MTL, picking, tools, groups and matrices, lights, post effects, grids, the field of view, see-through backgrounds, and the WebGL renderer's GPU memory on a fake WebGL context.
115
126
 
116
127
  ## Upgrading
117
128
 
129
+ ### From 0.5 to 0.6
130
+
131
+ - **The cone's tip is a single point** (a pole), so the cone is closed: `isClosed` and `validateMesh` no longer report a hole there. It has fewer vertices than before (its tip ring of 32 is now 1), and its side faces are triangles. Any `lathe` profile point with `r: 0` works the same way.
132
+ - **The default field of view is 50°** (`DEFAULT_FOV`), instead of 90°. Scenes whose camera has no `fov` look about twice as close, like in most 3D tools. To keep the old view, give the camera `fov: Math.PI / 2`.
133
+ - **`background: null` is see-through**, like `"transparent"` (it used to come out black). `clear()` now empties the canvas before painting the background, so see-through and half see-through backgrounds work; opaque ones look the same.
134
+
118
135
  ### From 0.3 to 0.4
119
136
 
120
137
  - **Lights are a list**: `scene.lights`. A scene still starts with one directional light and an ambient level, and looks the same. `scene.light` and `scene.ambient` are now shortcuts for the first directional light's direction and the first ambient light's intensity.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trid3d",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "A small 3D library for the HTML canvas: wireframes, filled and smooth faces, textures and shader effects, drawn with Canvas 2D or WebGL. No dependencies.",
5
5
  "keywords": [
6
6
  "3d",
package/trid/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export interface Camera {
43
43
  yaw?: number;
44
44
  /** Tilt around X in radians, > 0 looks up (default 0) */
45
45
  pitch?: number;
46
- /** Vertical field of view in radians (default Math.PI / 2) */
46
+ /** Vertical field of view in radians (default DEFAULT_FOV: 50°) */
47
47
  fov?: number;
48
48
  /** Near plane distance; closer geometry is clipped (default 0.01) */
49
49
  near?: number;
@@ -192,8 +192,8 @@ export interface SceneOptions {
192
192
  width?: number;
193
193
  /** Sets canvas.height (keeps the current one if omitted) */
194
194
  height?: number;
195
- /** Clear color (default "#141414") */
196
- background?: string;
195
+ /** Clear color (default "#141414"); "transparent", a see-through rgba() or null: the page shows behind */
196
+ background?: string | null;
197
197
  /** Default wireframe color (default "#ffffff") */
198
198
  color?: string;
199
199
  /** Default wireframe width (default 3) */
@@ -221,7 +221,7 @@ export class Scene {
221
221
 
222
222
  canvas: HTMLCanvasElement;
223
223
  ctx: CanvasRenderingContext2D;
224
- background: string;
224
+ background: string | null;
225
225
  color: string;
226
226
  lineWidth: number;
227
227
  camera: Camera;
@@ -284,8 +284,10 @@ export interface ShapeOptions {
284
284
  r?: number;
285
285
  /** Sphere latitude bands (default 12) */
286
286
  rings?: number;
287
- /** Points around: sphere (24), cylinder / cone (32), torus (48) */
287
+ /** Points around: sphere (24), cylinder / cone (32), torus (48); rows per rounded edge of a cube / box with `radius` (4) */
288
288
  segments?: number;
289
+ /** Cube / box: round the edges and corners by this much (default 0: sharp) */
290
+ radius?: number;
289
291
  /** Cone top radius: 0 = pointed, > 0 = tapered cylinder (default 0) */
290
292
  rTop?: number;
291
293
  /** Torus: distance from the center to the middle of the tube (default 0.35) */
@@ -1424,8 +1426,8 @@ export function worldPerPixel(scene: Scene, depth: number): number;
1424
1426
  // Drawing and the frame loop
1425
1427
  // ---------------------------------------------------------------------------
1426
1428
 
1427
- /** Fills the whole canvas with a color. */
1428
- export function clear(ctx: CanvasRenderingContext2D, w: number, h: number, color: string): void;
1429
+ /** Clears the whole canvas, then fills it with a color ("transparent" or null: left see-through). */
1430
+ export function clear(ctx: CanvasRenderingContext2D, w: number, h: number, color?: string | null): void;
1429
1431
  /** Draws a square point centered at a screen position. */
1430
1432
  export function point(ctx: CanvasRenderingContext2D, p: Vec2, color: string, size?: number): void;
1431
1433
  /** Draws a line between two screen points. */
@@ -1465,7 +1467,7 @@ export function parseColor(ctx: CanvasRenderingContext2D, color: string): RGBA;
1465
1467
  // Math
1466
1468
  // ---------------------------------------------------------------------------
1467
1469
 
1468
- /** Default vertical field of view (90°). */
1470
+ /** Default vertical field of view: 50° (in radians). */
1469
1471
  export const DEFAULT_FOV: number;
1470
1472
  /** Default near plane distance. */
1471
1473
  export const DEFAULT_NEAR: number;
@@ -1632,3 +1634,416 @@ export function noise3(x: number, y: number, z: number, opts?: NoiseOptions): nu
1632
1634
  export function fbm2(x: number, y: number, opts?: FbmOptions): number;
1633
1635
  /** Fractal 3D noise: octaves of noise3, each twice as fine. About [0, 1), averaging 0.5. */
1634
1636
  export function fbm3(x: number, y: number, z: number, opts?: FbmOptions): number;
1637
+
1638
+ // Modeling: curves
1639
+
1640
+ export interface SplineOptions {
1641
+ /** Points made between two control points (default 8) */
1642
+ segments?: number;
1643
+ /** The last point joins the first (a loop); the result doesn't repeat the first point (default false) */
1644
+ closed?: boolean;
1645
+ /** 0 = uniform, 0.5 = centripetal: no loops or overshoot (default), 1 = chordal */
1646
+ alpha?: number;
1647
+ /** 0 = smooth (default), 1 = straight lines between the points */
1648
+ tension?: number;
1649
+ /** Instead of `segments`: this many points in all, evenly spaced by length */
1650
+ count?: number;
1651
+ /** Instead of `segments`: points about this far apart, evenly spaced */
1652
+ spacing?: number;
1653
+ }
1654
+
1655
+ /**
1656
+ * A smooth curve through `points` (Catmull-Rom: it passes through every
1657
+ * point). Works with 2D, 3D and lathe `{ r, y }` points: every number field
1658
+ * of the first point is a coordinate, and the result has the same fields.
1659
+ */
1660
+ export function spline<P extends Point2 | Vec3 | ProfilePoint>(points: P[], opts?: SplineOptions): P[];
1661
+
1662
+ export interface ResampleOptions {
1663
+ /** Number of points in all */
1664
+ count?: number;
1665
+ /** Distance between points (used when `count` isn't given) */
1666
+ spacing?: number;
1667
+ /** The points form a loop (default false) */
1668
+ closed?: boolean;
1669
+ }
1670
+
1671
+ /** Points along a polyline, evenly spaced by length. The ends stay where they are. */
1672
+ export function resamplePath<P extends Point2 | Vec3 | ProfilePoint>(points: P[], opts?: ResampleOptions): P[];
1673
+
1674
+ // Modeling: sweep
1675
+
1676
+ /** A size along a path: a number, numbers spread evenly from start to end, or a function of t (0 at the start, 1 at the end). */
1677
+ export type SizeAlong = number | number[] | ((t: number) => number);
1678
+
1679
+ export interface SweepOptions {
1680
+ /** The cross-section: x to the path's side, y to its "up" (default: a circle of `radius`) */
1681
+ profile?: Point2[];
1682
+ /** The profile is a closed outline (default true); false = an open line, for ribbons, leaves and roads */
1683
+ profileClosed?: boolean;
1684
+ /** Radius of the default circle (default 0.1). Ignored with a `profile` */
1685
+ radius?: SizeAlong;
1686
+ /** Points around the default circle (default 16) */
1687
+ segments?: number;
1688
+ /** Size of the section along the path (default 1). 0 at an end closes it into a point */
1689
+ scale?: SizeAlong;
1690
+ /** Turn of the section from start to end, in radians (default 0). Whole turns on a closed path */
1691
+ twist?: number;
1692
+ /** The path is a loop: its end joins its start (default false) */
1693
+ closed?: boolean;
1694
+ /** Close the ends with flat caps (default true; open paths with closed profiles) */
1695
+ caps?: boolean;
1696
+ /** Keep the section's y pointed this way (roads, rails, frames). Default: carried along the path without spinning */
1697
+ up?: Vec3;
1698
+ /** Stretch the section at sharp corners so the tube keeps its thickness (default true) */
1699
+ miter?: boolean;
1700
+ }
1701
+
1702
+ /**
1703
+ * Moves a cross-section along a 3D path: tails, horns, tentacles, handles,
1704
+ * pipes, cables, rails. Without `profile`, a round tube of `radius`.
1705
+ * Clockwise faces; uvs: u around the section, v along the path.
1706
+ */
1707
+ export function sweep(path: Vec3[], opts?: SweepOptions): Mesh;
1708
+
1709
+ // Modeling: loft
1710
+
1711
+ /** A 2D section, placed along the loft's axis or path. */
1712
+ export interface LoftOutline {
1713
+ /** Closed outline: x to the side, y up (either direction; one point = a tip) */
1714
+ outline: Point2[];
1715
+ /** Where it is: the position on the axis, or 0 (start) to 1 (end) along a path (default: evenly spread) */
1716
+ at?: number;
1717
+ /** Size, the same both ways or per direction (default 1); 0 = a tip */
1718
+ scale?: number | { x?: number; y?: number };
1719
+ /** Turn in its own plane, in radians (default 0) */
1720
+ rotate?: number;
1721
+ /** Move in its own plane (default none) */
1722
+ offset?: Partial<Point2>;
1723
+ }
1724
+
1725
+ /** A section as a ring of 3D points, used where it is. */
1726
+ export interface LoftRing {
1727
+ points: Vec3[];
1728
+ }
1729
+
1730
+ export interface LoftOptions {
1731
+ /** Straight axis the outlines are placed along (default "z"). x/y of an outline: "z" → X/Y, "y" → X/Z, "x" → Z/Y */
1732
+ axis?: "x" | "y" | "z";
1733
+ /** A 3D path to place the outlines along instead of the axis */
1734
+ path?: Vec3[];
1735
+ /** With `path`: keep the outlines' y pointed this way (default: carried along without spinning) */
1736
+ up?: Vec3;
1737
+ /** The surface loops back to its first section (a closed path, or 3D rings in a loop; default false) */
1738
+ closed?: boolean;
1739
+ /** Close the ends with flat caps (default true) */
1740
+ caps?: boolean;
1741
+ /** Rows between two sections, on average (default 6) */
1742
+ segments?: number;
1743
+ /** Points around each section (default: kept when every section has the same count, else the most any has, at least 24) */
1744
+ points?: number;
1745
+ /** Smooth curves between sections, never past them (default true); false = straight */
1746
+ smooth?: boolean;
1747
+ /** Line sections up so the surface doesn't twist (default true) */
1748
+ align?: boolean;
1749
+ }
1750
+
1751
+ /**
1752
+ * A surface through cross-sections: bodies, bottles, hulls, car bodies,
1753
+ * heads. Every section is an `outline` (2D, placed along an axis or a path)
1754
+ * or every section is `points` (a 3D ring). Clockwise faces; uvs: u around,
1755
+ * v along. Throws when the two kinds are mixed.
1756
+ */
1757
+ export function loft(sections: LoftOutline[] | LoftRing[], opts?: LoftOptions): Mesh;
1758
+
1759
+ // Modeling: deformers
1760
+
1761
+ export interface AxisRange {
1762
+ /** Axis the effect goes along (default "y") */
1763
+ axis?: "x" | "y" | "z";
1764
+ /** Start of the part that changes: 0 = the mesh's low end on the axis, 1 = its high end (default 0) */
1765
+ from?: number;
1766
+ /** End of the part that changes, same measure (default 1). Past it, the full effect carries on */
1767
+ to?: number;
1768
+ /** A point the axis line goes through (default: the middle of the mesh's box) */
1769
+ center?: Partial<Vec3>;
1770
+ }
1771
+
1772
+ export interface TaperOptions extends AxisRange {
1773
+ /** Size at the end of the range (default 0.5), or a function of how far into the range (0 to 1) giving the size */
1774
+ amount?: number | ((w: number) => number);
1775
+ }
1776
+
1777
+ export interface TwistOptions extends AxisRange {
1778
+ /** Turn at the end of the range, in radians (default π/2), or a function of how far into the range (0 to 1) */
1779
+ angle?: number | ((w: number) => number);
1780
+ }
1781
+
1782
+ export interface BendOptions extends AxisRange {
1783
+ /** Angle of the arc, in radians (default π/2); negative curves the other way */
1784
+ angle?: number;
1785
+ /** Axis it curves toward (default: "z" for a "y" axis, "z" for "x", "y" for "z") */
1786
+ toward?: "x" | "y" | "z";
1787
+ }
1788
+
1789
+ export interface BulgeOptions {
1790
+ /** Middle of the bulge */
1791
+ center: Vec3;
1792
+ /** Distance where it fades to nothing */
1793
+ radius: number;
1794
+ /** How far the middle moves (default 0.1); negative = a dent */
1795
+ amount?: number;
1796
+ /** "normal": along the surface, works on flat sides (default); "radial": away from `center` (put it inside the part) */
1797
+ mode?: "normal" | "radial";
1798
+ }
1799
+
1800
+ /** Narrows or widens a mesh along an axis, in place. Returns the same mesh. */
1801
+ export function taperMesh<M extends Mesh>(mesh: M, opts?: TaperOptions): M;
1802
+ /** Turns a mesh around an axis, more and more along it, in place (same direction as rotateX / Y / Z). Returns the same mesh. */
1803
+ export function twistMesh<M extends Mesh>(mesh: M, opts?: TwistOptions): M;
1804
+ /** Bends a mesh into an arc along an axis, in place; past the range it goes on straight. Returns the same mesh. */
1805
+ export function bendMesh<M extends Mesh>(mesh: M, opts?: BendOptions): M;
1806
+ /** Pushes the surface out (or in) around a point, fading at `radius`, in place. Returns the same mesh. */
1807
+ export function bulgeMesh<M extends Mesh>(mesh: M, opts: BulgeOptions): M;
1808
+
1809
+ // Modeling: cleanup and checks
1810
+
1811
+ /**
1812
+ * Merges vertices closer than `epsilon` (default: a ten-millionth of the
1813
+ * mesh's size), in place. Texture coordinates and normals (per corner) are
1814
+ * kept; corners that meet are removed. Returns the same mesh.
1815
+ */
1816
+ export function weldVertices<M extends Mesh>(mesh: M, epsilon?: number): M;
1817
+
1818
+ export interface CleanOptions {
1819
+ /** Merge vertices at the same place first: true, or the distance (default false) */
1820
+ weld?: boolean | number;
1821
+ }
1822
+
1823
+ /**
1824
+ * Removes what's broken, in place: faces to missing or bad vertices, repeated
1825
+ * corners, faces with fewer than 3 corners, crossed or of no area, repeated
1826
+ * faces, bad corner lists (set to null), bad edges, and unused vertices,
1827
+ * texture coordinates and normals. Returns the same mesh.
1828
+ */
1829
+ export function cleanMesh<M extends Mesh>(mesh: M, opts?: CleanOptions): M;
1830
+
1831
+ export interface MeshProblem {
1832
+ /** What's wrong, e.g. "bad-index", "no-area", "turned-face", "inside-out", "open-edge" */
1833
+ kind: string;
1834
+ /** "error": breaks drawing or export; "warning": draws wrong; "info": worth knowing */
1835
+ level: "error" | "warning" | "info";
1836
+ /** A short sentence, with the first few faces / vertices / edges concerned */
1837
+ message: string;
1838
+ faces?: number[];
1839
+ vertices?: number[];
1840
+ edges?: number[];
1841
+ }
1842
+
1843
+ /** Lists what's wrong with a mesh, without changing it (one entry per kind of problem; empty when fine). */
1844
+ export function validateMesh(mesh: Mesh): MeshProblem[];
1845
+
1846
+ /**
1847
+ * Turns faces in place so neighbors agree and each piece points outward
1848
+ * (clockwise from outside). Works without welding. Returns the same mesh.
1849
+ */
1850
+ export function fixWinding<M extends Mesh>(mesh: M): M;
1851
+
1852
+ // Modeling: subdivision
1853
+
1854
+ export interface SubdivideOptions {
1855
+ /** Times to subdivide (default 1, at most 6); each level makes about 4 times the faces */
1856
+ levels?: number;
1857
+ /** false: only split the faces, keep the shape (default true) */
1858
+ smooth?: boolean;
1859
+ /** Edges where faces meet sharper than this (degrees) stay hard (default: none) */
1860
+ creaseAngle?: number;
1861
+ /** Edges that stay hard: pairs or polylines of vertex indices, e.g. [[0, 1], [4, 5, 6, 7, 4]] */
1862
+ creases?: number[][];
1863
+ /** "smooth": texture coordinates smoothed with the surface, seams kept (default); "linear": only split */
1864
+ uvs?: "smooth" | "linear";
1865
+ }
1866
+
1867
+ /**
1868
+ * Smooths a mesh by subdividing it (Catmull-Clark): a rough cage becomes a
1869
+ * smooth surface. Returns a new mesh of quads (the original isn't changed),
1870
+ * welded, with texture coordinates and materials; own normals are dropped.
1871
+ */
1872
+ export function subdivideMesh(mesh: Mesh, opts?: SubdivideOptions): Mesh;
1873
+
1874
+ // Modeling: blobs (distance fields)
1875
+
1876
+ /** How a blob shape joins what's before it: melts in (default), carves it, or keeps only the overlap. */
1877
+ export type BlobOp = "add" | "subtract" | "intersect";
1878
+
1879
+ interface BlobBase {
1880
+ /** How it joins what's before it (default "add") */
1881
+ op?: BlobOp;
1882
+ /** Same as op: "subtract" */
1883
+ subtract?: boolean;
1884
+ /** How far it melts into what's before it, as a distance (default: the blobMesh / blobField `blend`) */
1885
+ blend?: number;
1886
+ }
1887
+
1888
+ export type BlobShape = BlobBase & (
1889
+ | { kind: "sphere"; center: Vec3; radius: number }
1890
+ /** Turned by `rot` (radians, X then Y then Z) */
1891
+ | { kind: "ellipsoid"; center: Vec3; radii: Vec3; rot?: Vec3 }
1892
+ /** A capsule between two points; `ra` / `rb` instead of `radius` make a rounded cone */
1893
+ | { kind: "capsule"; a: Vec3; b: Vec3; radius?: number; ra?: number; rb?: number }
1894
+ /** A capped cylinder between two points, edges rounded by `round` */
1895
+ | { kind: "cylinder"; a: Vec3; b: Vec3; radius: number; round?: number }
1896
+ /** A box of `size`, edges rounded by `round`, turned by `rot` */
1897
+ | { kind: "box"; center: Vec3; size: Vec3; round?: number; rot?: Vec3 }
1898
+ /** A ring lying flat in XZ (like makeShape("torus")), turned by `rot` */
1899
+ | { kind: "torus"; center: Vec3; R: number; tube: number; rot?: Vec3 }
1900
+ );
1901
+
1902
+ export interface SdfMeshOptions {
1903
+ /** Grid cells along the longest side (default 64, 4 to 256); more is finer and slower */
1904
+ resolution?: number;
1905
+ /** "nets": surface nets, even quads (default); "cubes": marching cubes, triangles. Both are watertight */
1906
+ method?: "nets" | "cubes";
1907
+ /** Pull vertices exactly onto the surface (default true) */
1908
+ project?: boolean;
1909
+ /** Normals from the field, for smooth shading (default true) */
1910
+ normals?: boolean;
1911
+ /** Texture mapping for the uvs, as for computeUVs (default "box"); false: none */
1912
+ uvs?: MappingType | Mapping | false;
1913
+ /** Skip grid blocks far from the surface, trusting the field to be about the distance (default true) */
1914
+ sparse?: boolean;
1915
+ }
1916
+
1917
+ export interface BlobMeshOptions extends Omit<SdfMeshOptions, "sparse"> {
1918
+ /** How far shapes melt into each other, as a distance (default: a tenth of the model's size). A shape's own `blend` wins */
1919
+ blend?: number;
1920
+ }
1921
+
1922
+ /** One smooth mesh of shapes that melt together (added, subtracted or intersected, in order). */
1923
+ export function blobMesh(shapes: BlobShape[], opts?: BlobMeshOptions): Mesh;
1924
+
1925
+ /** The distance field of blob shapes (negative inside), to mix with your own math and mesh with sdfMesh. */
1926
+ export function blobField(shapes: BlobShape[], opts?: { blend?: number }): (p: Vec3) => number;
1927
+
1928
+ /** The surface where `field(p)` is 0 (negative inside), within `bounds`, as a mesh. */
1929
+ export function sdfMesh(field: (p: Vec3) => number, bounds: { min: Vec3; max: Vec3 }, opts?: SdfMeshOptions): Mesh;
1930
+
1931
+ // Modeling: bevel
1932
+
1933
+ export interface BevelOptions {
1934
+ /** Size of the rounding: the radius of the ball rolled along the edges (default: a twentieth of the mesh's size). Shrinks where bevels would overlap */
1935
+ radius?: number;
1936
+ /** Rows across each rounded edge (default 3); 1 = a flat cut (chamfer) */
1937
+ segments?: number;
1938
+ /** Edges where faces meet sharper than this (degrees) are beveled (default 30) */
1939
+ angle?: number;
1940
+ /** "edges": round the edges, corners become pieces of a sphere (default); "vertices": cut only the corners */
1941
+ mode?: "edges" | "vertices";
1942
+ /** Instead of `angle`: the edges to bevel, as pairs or polylines of vertex indices */
1943
+ edges?: number[][];
1944
+ /** With mode "vertices", instead of `angle`: the corners to cut, as vertex indices */
1945
+ vertices?: number[];
1946
+ }
1947
+
1948
+ /**
1949
+ * Rounds (or cuts flat) the sharp edges of a mesh, like a bevel modifier.
1950
+ * Returns a new mesh, welded, with texture coordinates and materials kept.
1951
+ * Edges along an open border stay sharp.
1952
+ */
1953
+ export function bevelMesh(mesh: Mesh, opts?: BevelOptions): Mesh;
1954
+
1955
+ // Modeling: queries (rays, inside, distance)
1956
+
1957
+ /** Which faces a ray counts: "both" (default), "front" (hit from outside) or "back" (hit from inside). */
1958
+ export type RaySide = "both" | "front" | "back";
1959
+
1960
+ export interface RaycastOptions {
1961
+ /** Ignore hits farther than this (default Infinity) */
1962
+ maxDistance?: number;
1963
+ /** Which faces count (default "both") */
1964
+ side?: RaySide;
1965
+ }
1966
+
1967
+ /** Where a ray met a mesh, in the mesh's own space. */
1968
+ export interface MeshHit {
1969
+ point: Vec3;
1970
+ /** Unit normal of the face, pointing out of the mesh (whichever side the ray came from) */
1971
+ normal: Vec3;
1972
+ /** From the ray's origin */
1973
+ distance: number;
1974
+ /** Index in `mesh.faces` */
1975
+ face: number;
1976
+ /** true when the ray met the face from outside */
1977
+ front: boolean;
1978
+ /** The 3 vertex indices of the triangle hit (faces with more corners are fans from their first corner) */
1979
+ triangle: [number, number, number];
1980
+ /** Weights of those vertices at the point (they add up to 1) */
1981
+ barycentric: [number, number, number];
1982
+ }
1983
+
1984
+ /** The nearest point of a mesh's surface. */
1985
+ export type MeshClosest = Omit<MeshHit, "front">;
1986
+
1987
+ /** A mesh prepared for many questions (a snapshot: editing the mesh later doesn't change it). */
1988
+ export interface MeshQuery {
1989
+ /** Triangles in the query (faces split into fans) */
1990
+ readonly triangleCount: number;
1991
+ /** The mesh's box */
1992
+ readonly bounds: { min: Vec3; max: Vec3 };
1993
+ /** The first place a ray meets the surface, or null. `direction` can be any length but zero */
1994
+ raycast(origin: Vec3, direction: Vec3, opts?: RaycastOptions): MeshHit | null;
1995
+ /** Whether a point is inside (only meaningful for a closed mesh, see `isClosed`) */
1996
+ contains(p: Vec3): boolean;
1997
+ /** The nearest point of the surface, or null when nothing is within `maxDistance` */
1998
+ closest(p: Vec3, opts?: { maxDistance?: number }): MeshClosest | null;
1999
+ /** Distance to the surface (Infinity when nothing is within `maxDistance`) */
2000
+ distance(p: Vec3, maxDistance?: number): number;
2001
+ }
2002
+
2003
+ /** Prepares a mesh for many ray, inside and distance questions (builds a BVH once). */
2004
+ export function meshQuery(mesh: Mesh): MeshQuery;
2005
+
2006
+ /** One-off `meshQuery(mesh).raycast(...)`. For many rays on one mesh, keep a `meshQuery`. */
2007
+ export function raycastMesh(mesh: Mesh, origin: Vec3, direction: Vec3, opts?: RaycastOptions): MeshHit | null;
2008
+
2009
+ /** One-off `meshQuery(mesh).contains(p)`. */
2010
+ export function pointInMesh(mesh: Mesh, p: Vec3): boolean;
2011
+
2012
+ /** One-off `meshQuery(mesh).distance(p, maxDistance)`. */
2013
+ export function distanceToMesh(mesh: Mesh, p: Vec3, maxDistance?: number): number;
2014
+
2015
+ /** One-off `meshQuery(mesh).closest(p, opts)`. */
2016
+ export function closestPointOnMesh(mesh: Mesh, p: Vec3, opts?: { maxDistance?: number }): MeshClosest | null;
2017
+
2018
+ /** Whether a mesh is closed (watertight): every edge, matched by position, shared by exactly two faces. */
2019
+ export function isClosed(mesh: Mesh): boolean;
2020
+
2021
+ /** What `raycastScene` found: a `MeshHit` in world space, plus the object. */
2022
+ export interface SceneHit extends MeshHit {
2023
+ object: SceneObject;
2024
+ /** The object, then its groups up to the top: `[part, group, ..., root]` */
2025
+ path: SceneObject[];
2026
+ /** Where the hit is, in the mesh's own space (`point` is in the world) */
2027
+ local: Vec3;
2028
+ }
2029
+
2030
+ export interface RaycastSceneOptions extends RaycastOptions {
2031
+ /** Return false to skip an object */
2032
+ filter?: (obj: SceneObject) => boolean;
2033
+ /** Only these objects (and their children) */
2034
+ objects?: SceneObjectInput[];
2035
+ }
2036
+
2037
+ /**
2038
+ * The first object a ray meets, in world space (the 3D version of `pickHit`).
2039
+ * Hidden objects, `pickable: false` ones and objects without faces are skipped.
2040
+ */
2041
+ export function raycastScene(
2042
+ scene: Scene | SceneObjectInput[],
2043
+ origin: Vec3,
2044
+ direction: Vec3,
2045
+ opts?: RaycastSceneOptions,
2046
+ ): SceneHit | null;
2047
+
2048
+ /** Forgets the prepared query `raycastScene` keeps for a mesh (after editing it in place once `dirty` was cleared). */
2049
+ export function clearQueryCache(mesh: Mesh): void;
package/trid/index.js CHANGED
@@ -81,3 +81,16 @@ export {
81
81
 
82
82
  // Random numbers and noise
83
83
  export { rng, noise2, noise3, fbm2, fbm3 } from "./math/noise.js";
84
+
85
+ // Modeling
86
+ export { spline, resamplePath } from "./modeling/curves.js";
87
+ export { sweep } from "./modeling/sweep.js";
88
+ export { loft } from "./modeling/loft.js";
89
+ export { taperMesh, twistMesh, bendMesh, bulgeMesh } from "./modeling/deform.js";
90
+ export { weldVertices, cleanMesh, validateMesh, fixWinding } from "./modeling/cleanup.js";
91
+ export { subdivideMesh } from "./modeling/subdivide.js";
92
+ export { blobMesh, blobField, sdfMesh } from "./modeling/blobs.js";
93
+ export { bevelMesh } from "./modeling/bevel.js";
94
+ export {
95
+ meshQuery, raycastMesh, pointInMesh, distanceToMesh, closestPointOnMesh, isClosed, raycastScene, clearQueryCache,
96
+ } from "./modeling/query.js";
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Default vertical field of view: 50°, a normal lens (like most 3D tools).
3
+ * Wider shows more but makes everything smaller; set `camera.fov` to change it.
4
+ */
5
+ export const DEFAULT_FOV = (50 * Math.PI) / 180;
6
+
1
7
  /**
2
8
  * Converts normalized device coordinates (-1 to +1)
3
9
  * into actual screen (pixel) coordinates.
@@ -36,11 +42,11 @@ export function screen(p, w, h) {
36
42
  * - larger z => appears smaller
37
43
  *
38
44
  * @param {{x:number, y:number, z:number}} p - 3D point (camera space)
39
- * @param {number} fov - Vertical field of view in radians (default: 90°)
45
+ * @param {number} fov - Vertical field of view in radians (default: `DEFAULT_FOV`, 50°)
40
46
  * @param {number} aspect - Canvas width / height (default: 1)
41
47
  * @returns {{x:number, y:number}} 2D projected point
42
48
  */
43
- export function project(p, fov = Math.PI / 2, aspect = 1) {
49
+ export function project(p, fov = DEFAULT_FOV, aspect = 1) {
44
50
  const f = 1 / Math.tan(fov / 2);
45
51
  return {
46
52
  x: (p.x * f) / (p.z * aspect),
@@ -1,4 +1,6 @@
1
- import { project, screen } from "./projection.js";
1
+ import { project, screen, DEFAULT_FOV } from "./projection.js";
2
+
3
+ export { DEFAULT_FOV };
2
4
 
3
5
  /**
4
6
  * Camera object. Every field is optional and falls back to its default.
@@ -11,14 +13,11 @@ import { project, screen } from "./projection.js";
11
13
  * @property {number} [z=0] - Position on Z
12
14
  * @property {number} [yaw=0] - Turn around Y in radians (> 0 turns right)
13
15
  * @property {number} [pitch=0] - Tilt around X in radians (> 0 looks up)
14
- * @property {number} [fov=Math.PI/2] - Vertical field of view in radians
16
+ * @property {number} [fov=DEFAULT_FOV] - Vertical field of view in radians (default 50°)
15
17
  * @property {number} [near=0.01] - Near plane distance; closer geometry is clipped
16
18
  * @property {number} [far=1000] - Far distance; farther geometry isn't drawn (WebGL renderer only)
17
19
  */
18
20
 
19
- /** Default vertical field of view (90°). */
20
- export const DEFAULT_FOV = Math.PI / 2;
21
-
22
21
  /** Default near plane distance. */
23
22
  export const DEFAULT_NEAR = 0.01;
24
23