@visant/extrude3d 0.1.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 ADDED
@@ -0,0 +1,126 @@
1
+ # @visant/extrude3d
2
+
3
+ **SVG / text → extruded 3D geometry**, in pure [three.js](https://threejs.org). Parse an SVG (or opentype.js glyphs) into shapes, extrude them with bevel / curve / smoothness controls, smooth crease normals, project triplanar UVs, center + scale — then either hand the `BufferGeometry` to your renderer or serialize it to a binary glTF (`.glb`).
4
+
5
+ No React, no [@react-three](https://github.com/pmndrs/react-three-fiber). Just functions over three.js objects, so the same code runs in the browser and in plain Node.
6
+
7
+ It powers the Visant Labs Studio3D engine (the `useExtrudedGeometry` hook and `<ExtrudedSVG>` component are thin wrappers around this) and the server-side GLB export.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @visant/extrude3d three
13
+ # optional — only if you use textToSvg():
14
+ npm i opentype.js
15
+ ```
16
+
17
+ `three` is a peer dependency. `opentype.js` is an **optional** peer — this package never imports it; `textToSvg` takes any font object that structurally matches `OpenTypeFontLike`.
18
+
19
+ > The browser geometry path uses `three/examples/jsm` (`SVGLoader`, `BufferGeometryUtils`). The `./glb` path is DOM-free except that `SVGLoader` needs a `DOMParser` — in Node, install one (e.g. via `jsdom`) on `globalThis` before calling.
20
+
21
+ ## Quick start
22
+
23
+ ### Browser: SVG → BufferGeometry
24
+
25
+ ```ts
26
+ import { buildExtrudedGeometry } from '@visant/extrude3d';
27
+ import * as THREE from 'three';
28
+
29
+ const result = buildExtrudedGeometry(svgString, {
30
+ depth: 2, // extrusion depth knob
31
+ smoothness: 0.5, // 0–1 → bevel segments + curve subdivisions
32
+ bevelEnabled: true,
33
+ });
34
+
35
+ if (result) {
36
+ const mesh = new THREE.Mesh(
37
+ result.geometry,
38
+ new THREE.MeshStandardMaterial({ color: '#c0c0c0' })
39
+ );
40
+ // center + fit to a 4-unit box, matching the Studio3D engine:
41
+ mesh.position.set(-result.center.x, -result.center.y, -result.center.z);
42
+ mesh.scale.setScalar(result.baseScale);
43
+
44
+ // you own the geometry — dispose when done:
45
+ // result.geometry.dispose();
46
+ }
47
+ ```
48
+
49
+ ### Node: SVG → GLB buffer
50
+
51
+ ```ts
52
+ import { svgToGlb } from '@visant/extrude3d/glb';
53
+ import { JSDOM } from 'jsdom';
54
+ import { writeFile } from 'node:fs/promises';
55
+
56
+ // SVGLoader needs a DOMParser in Node:
57
+ globalThis.DOMParser = new JSDOM('').window.DOMParser;
58
+
59
+ const { glb } = svgToGlb(svgString, {
60
+ depth: 0.9,
61
+ smoothness: 0.5,
62
+ color: '#c0c0c0',
63
+ metalness: 0.6,
64
+ roughness: 0.3,
65
+ });
66
+
67
+ await writeFile('logo.glb', glb); // glb is a Uint8Array (glTF magic header)
68
+ ```
69
+
70
+ ### Text → SVG → geometry
71
+
72
+ ```ts
73
+ import * as opentype from 'opentype.js';
74
+ import { textToSvg, buildExtrudedGeometry } from '@visant/extrude3d';
75
+
76
+ const font = opentype.parse(fontArrayBuffer);
77
+ const svg = textToSvg('Visant', font); // centered 200×200 SVG of glyph paths
78
+ const result = buildExtrudedGeometry(svg, { depth: 2, smoothness: 0.6 });
79
+ ```
80
+
81
+ ### Material presets
82
+
83
+ ```ts
84
+ import { getSimpleMaterialProps, materialPresets } from '@visant/extrude3d/materials';
85
+
86
+ // flat prop bag for a <meshPhysicalMaterial>:
87
+ const props = getSimpleMaterialProps('chrome', '#ff0066');
88
+ // → { color, metalness, roughness, clearcoat, transmission, ior, ... }
89
+ ```
90
+
91
+ ## Exports
92
+
93
+ | Entry | Purpose |
94
+ | --- | --- |
95
+ | `.` | everything below, flat |
96
+ | `./materials` | PBR preset library (`materialPresets`, `MATERIAL_LIB`, `getSimpleMaterialProps`, `resolveMaterial`) |
97
+ | `./fonts` | `textToSvg(text, font)` — opentype glyphs → centered SVG |
98
+ | `./glb` | `svgToGlb(svg, opts)` — dependency-free binary glTF serializer |
99
+
100
+ ### Geometry API
101
+
102
+ | Export | Signature |
103
+ | --- | --- |
104
+ | `buildExtrudedGeometry` | `(svg \| Shape[], opts) => { geometry, center, baseScale, shapeCount } \| null` |
105
+ | `parseShapesFromSVG` | `(svg) => THREE.Shape[]` — SVGLoader fills + tessellated strokes, drops the viewBox rect |
106
+ | `buildExtrudeSettings` | `(maxFlatDim, shapeCount, opts) => ExtrudeSettings` — the engine's depth/bevel/segment math |
107
+ | `measureFlatMaxDim` | `(shapes) => number` — larger of flat width/height (≥ 1) |
108
+ | `smoothCreaseNormals` | `(geometry, creaseAngleRad) => BufferGeometry` — averages normals below the crease angle |
109
+ | `recomputeTriplanarUVs` | `(geometry, box3) => void` — box-projected UVs, in place |
110
+
111
+ ## Options
112
+
113
+ `buildExtrudedGeometry(svg, opts)`:
114
+
115
+ | Option | Default | Meaning |
116
+ | --- | --- | --- |
117
+ | `depth` | — | extrusion depth knob (scaled by flat bounds) |
118
+ | `smoothness` | — | 0–1 → bevel segments (`4 + s·8`) + curve subdivisions (`32 + s·64`) |
119
+ | `bevelEnabled` | `true` | |
120
+ | `bevelThickness` / `bevelSize` | `0.5` | scaled by `min(maxFlatDim·0.05, 1)`, clamped to half the depth |
121
+ | `vertexBudget` | `600000` | total vertex budget; segment counts shrink to fit |
122
+ | `creaseAngle` | `Math.PI/6` | crease-smoothing threshold; `null` → plain `computeVertexNormals` |
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,14 @@
1
+ import type { OpenTypeFontLike } from './types.js';
2
+ /**
3
+ * Render `text` with an opentype.js font into a centered, auto-fitted SVG
4
+ * string whose `<path>` glyphs are ready for {@link buildExtrudedGeometry}.
5
+ *
6
+ * The font is fitted into a 200×200 viewBox (10px padding), shrinking the font
7
+ * size in 4px steps until it fits, then centered. Per-glyph kerning is applied.
8
+ * Returns `''` for empty/whitespace-only text or on any failure.
9
+ *
10
+ * Pure: the caller supplies an already-loaded font object (this package never
11
+ * imports or depends on `opentype.js`).
12
+ */
13
+ export declare function textToSvg(text: string, font: OpenTypeFontLike): string;
14
+ //# sourceMappingURL=fonts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../src/fonts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAOtE"}
package/dist/fonts.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Render `text` with an opentype.js font into a centered, auto-fitted SVG
3
+ * string whose `<path>` glyphs are ready for {@link buildExtrudedGeometry}.
4
+ *
5
+ * The font is fitted into a 200×200 viewBox (10px padding), shrinking the font
6
+ * size in 4px steps until it fits, then centered. Per-glyph kerning is applied.
7
+ * Returns `''` for empty/whitespace-only text or on any failure.
8
+ *
9
+ * Pure: the caller supplies an already-loaded font object (this package never
10
+ * imports or depends on `opentype.js`).
11
+ */
12
+ export function textToSvg(text, font) {
13
+ try {
14
+ return _textToSvg(text, font);
15
+ }
16
+ catch (err) {
17
+ console.warn('textToSvg failed:', err);
18
+ return '';
19
+ }
20
+ }
21
+ function _textToSvg(text, font) {
22
+ const size = 200;
23
+ const available = size - 20;
24
+ let fontSize = 180;
25
+ let fullPath = font.getPath(text, 0, 0, fontSize);
26
+ let bb = fullPath.getBoundingBox();
27
+ let w = bb.x2 - bb.x1;
28
+ let h = bb.y2 - bb.y1;
29
+ while ((w > available || h > available) && fontSize > 8) {
30
+ fontSize -= 4;
31
+ fullPath = font.getPath(text, 0, 0, fontSize);
32
+ bb = fullPath.getBoundingBox();
33
+ w = bb.x2 - bb.x1;
34
+ h = bb.y2 - bb.y1;
35
+ }
36
+ const offsetX = (size - w) / 2 - bb.x1;
37
+ const offsetY = (size - h) / 2 - bb.y1;
38
+ const glyphs = font.stringToGlyphs(text);
39
+ let x = offsetX;
40
+ const paths = [];
41
+ const unitsPerEm = font.unitsPerEm || 1000;
42
+ for (let i = 0; i < glyphs.length; i++) {
43
+ const glyph = glyphs[i];
44
+ const glyphPath = glyph.getPath(x, offsetY, fontSize);
45
+ const d = glyphPath.toPathData(2);
46
+ if (d) {
47
+ paths.push(`<path d="${d}" fill="black" fill-rule="evenodd"/>`);
48
+ }
49
+ const advance = (glyph.advanceWidth || 0) * (fontSize / unitsPerEm);
50
+ if (i < glyphs.length - 1) {
51
+ const kerning = font.getKerningValue(glyphs[i], glyphs[i + 1]);
52
+ x += advance + kerning * (fontSize / unitsPerEm);
53
+ }
54
+ else {
55
+ x += advance;
56
+ }
57
+ }
58
+ if (paths.length === 0)
59
+ return '';
60
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">${paths.join('')}</svg>`;
61
+ }
62
+ //# sourceMappingURL=fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fonts.js","sourceRoot":"","sources":["../src/fonts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ import * as THREE from 'three';
2
+ import type { BuildExtrudedGeometryOptions, ExtrudedGeometryResult } from './types.js';
3
+ /**
4
+ * Average face normals within a vertex's coincident group when the angle
5
+ * between them is below `creaseAngleRad` (sharp edges above it stay crisp).
6
+ * Falls back to `computeVertexNormals` past {@link SMOOTH_VERTEX_LIMIT} verts.
7
+ * Returns a new, non-indexed geometry (the input is left untouched).
8
+ */
9
+ export declare function smoothCreaseNormals(geometry: THREE.BufferGeometry, creaseAngleRad: number): THREE.BufferGeometry;
10
+ /**
11
+ * Recompute box-projected (triplanar) UVs in place, projecting each vertex onto
12
+ * the axis plane that best matches its normal and normalizing by the bounding
13
+ * box's largest dimension.
14
+ */
15
+ export declare function recomputeTriplanarUVs(geo: THREE.BufferGeometry, bb: THREE.Box3): void;
16
+ /**
17
+ * True when `shape` is (approximately) the SVG's full viewBox rectangle — used
18
+ * to drop background rects that SVGLoader emits as a fill shape.
19
+ */
20
+ export declare function isViewBoxRect(shape: THREE.Shape, vbW: number, vbH: number): boolean;
21
+ /**
22
+ * Parse an SVG string into three.js `Shape`s ready for extrusion. Fills become
23
+ * shapes directly (dropping the viewBox-sized background rect); strokes are
24
+ * tessellated into a ribbon polygon of `strokeWidth`; paths with no explicit
25
+ * fill/stroke fall back to fill shapes.
26
+ *
27
+ * Accepts both whitespace- and comma-separated viewBox values.
28
+ */
29
+ export declare function parseShapesFromSVG(svgString: string): THREE.Shape[];
30
+ /** Three.js ExtrudeGeometry settings, derived from the engine's knobs. */
31
+ export interface ExtrudeSettings {
32
+ depth: number;
33
+ bevelEnabled: boolean;
34
+ bevelThickness: number;
35
+ bevelSize: number;
36
+ bevelSegments: number;
37
+ curveSegments: number;
38
+ }
39
+ /**
40
+ * Translate the engine's high-level knobs (`depth`, `smoothness`, bevel
41
+ * thickness/size) + the SVG's flat bounds + shape complexity into concrete
42
+ * three.js `ExtrudeGeometry` settings, including the vertex-budget reduction.
43
+ * This is the exact math used by both the client geometry pipeline and the
44
+ * server GLB export, so they never drift.
45
+ */
46
+ export declare function buildExtrudeSettings(maxFlatDim: number, shapeCount: number, opts: BuildExtrudedGeometryOptions): ExtrudeSettings;
47
+ /**
48
+ * Measure the flat (un-extruded) bounding box of a set of shapes and return the
49
+ * larger of width/height (clamped to ≥ 1) — the `maxFlatDim` that drives depth
50
+ * and bevel scaling. Disposes the temporary geometry it builds.
51
+ */
52
+ export declare function measureFlatMaxDim(shapes: THREE.Shape[]): number;
53
+ /**
54
+ * SVG (or pre-parsed shapes) → a single extruded, merged, normal-smoothed,
55
+ * triplanar-UV'd `BufferGeometry`, plus its center and a fit-to-4-units scale.
56
+ *
57
+ * This is the pure heart of the Studio3D engine's `useExtrudedGeometry` hook
58
+ * (which keeps React state, progress, cancellation and disposal). The geometry
59
+ * math here is identical to the legacy hook — extracting it changes nothing.
60
+ *
61
+ * The caller owns the returned `geometry` and must `dispose()` it.
62
+ */
63
+ export declare function buildExtrudedGeometry(svg: string | THREE.Shape[], opts: BuildExtrudedGeometryOptions): ExtrudedGeometryResult | null;
64
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EACV,4BAA4B,EAC5B,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAIpB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,KAAK,CAAC,cAAc,EAC9B,cAAc,EAAE,MAAM,GACrB,KAAK,CAAC,cAAc,CA+EtB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,KAAK,CAAC,cAAc,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,GAAG,IAAI,CA8BrF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CASnF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,CA8DnE;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,4BAA4B,GACjC,eAAe,CAoCjB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,MAAM,CAQ/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,KAAK,EAAE,EAC3B,IAAI,EAAE,4BAA4B,GACjC,sBAAsB,GAAG,IAAI,CAsC/B"}
@@ -0,0 +1,283 @@
1
+ import * as THREE from 'three';
2
+ import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
3
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
4
+ const SMOOTH_VERTEX_LIMIT = 300_000;
5
+ /**
6
+ * Average face normals within a vertex's coincident group when the angle
7
+ * between them is below `creaseAngleRad` (sharp edges above it stay crisp).
8
+ * Falls back to `computeVertexNormals` past {@link SMOOTH_VERTEX_LIMIT} verts.
9
+ * Returns a new, non-indexed geometry (the input is left untouched).
10
+ */
11
+ export function smoothCreaseNormals(geometry, creaseAngleRad) {
12
+ const tempGeo = geometry.index ? geometry.toNonIndexed() : geometry.clone();
13
+ const posAttr = tempGeo.attributes.position;
14
+ if (!posAttr)
15
+ return geometry;
16
+ const count = posAttr.count;
17
+ if (count > SMOOTH_VERTEX_LIMIT) {
18
+ tempGeo.computeVertexNormals();
19
+ return tempGeo;
20
+ }
21
+ const flatNormals = [];
22
+ for (let i = 0; i < count; i += 3) {
23
+ const vA = new THREE.Vector3(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i));
24
+ const vB = new THREE.Vector3(posAttr.getX(i + 1), posAttr.getY(i + 1), posAttr.getZ(i + 1));
25
+ const vC = new THREE.Vector3(posAttr.getX(i + 2), posAttr.getY(i + 2), posAttr.getZ(i + 2));
26
+ const cb = new THREE.Vector3().subVectors(vC, vB);
27
+ const ab = new THREE.Vector3().subVectors(vA, vB);
28
+ cb.cross(ab).normalize();
29
+ flatNormals.push(cb.clone(), cb.clone(), cb.clone());
30
+ }
31
+ const posToIndices = new Map();
32
+ const precision = 100000;
33
+ for (let i = 0; i < count; i++) {
34
+ const px = Math.round(posAttr.getX(i) * precision);
35
+ const py = Math.round(posAttr.getY(i) * precision);
36
+ const pz = Math.round(posAttr.getZ(i) * precision);
37
+ const hash = `${px},${py},${pz}`;
38
+ if (!posToIndices.has(hash)) {
39
+ posToIndices.set(hash, []);
40
+ }
41
+ posToIndices.get(hash).push(i);
42
+ }
43
+ const cosThreshold = Math.cos(creaseAngleRad);
44
+ const newNormals = new Float32Array(count * 3);
45
+ for (const indices of posToIndices.values()) {
46
+ const visited = new Set();
47
+ for (const idx of indices) {
48
+ if (visited.has(idx))
49
+ continue;
50
+ const n1 = flatNormals[idx];
51
+ const smoothGroup = [idx];
52
+ visited.add(idx);
53
+ for (const otherIdx of indices) {
54
+ if (visited.has(otherIdx))
55
+ continue;
56
+ const n2 = flatNormals[otherIdx];
57
+ if (n1.dot(n2) >= cosThreshold) {
58
+ smoothGroup.push(otherIdx);
59
+ visited.add(otherIdx);
60
+ }
61
+ }
62
+ const avgNormal = new THREE.Vector3();
63
+ for (const i of smoothGroup) {
64
+ avgNormal.add(flatNormals[i]);
65
+ }
66
+ avgNormal.normalize();
67
+ for (const i of smoothGroup) {
68
+ newNormals[i * 3] = avgNormal.x;
69
+ newNormals[i * 3 + 1] = avgNormal.y;
70
+ newNormals[i * 3 + 2] = avgNormal.z;
71
+ }
72
+ }
73
+ }
74
+ tempGeo.setAttribute('normal', new THREE.BufferAttribute(newNormals, 3));
75
+ return tempGeo;
76
+ }
77
+ /**
78
+ * Recompute box-projected (triplanar) UVs in place, projecting each vertex onto
79
+ * the axis plane that best matches its normal and normalizing by the bounding
80
+ * box's largest dimension.
81
+ */
82
+ export function recomputeTriplanarUVs(geo, bb) {
83
+ const bbSize = new THREE.Vector3();
84
+ bb.getSize(bbSize);
85
+ const uvAttr = geo.attributes.uv;
86
+ const posAttr = geo.attributes.position;
87
+ const normalAttr = geo.attributes.normal;
88
+ const maxDimUv = Math.max(bbSize.x, bbSize.y, bbSize.z) || 1;
89
+ for (let j = 0; j < uvAttr.count; j++) {
90
+ const px = posAttr.getX(j);
91
+ const py = posAttr.getY(j);
92
+ const pz = posAttr.getZ(j);
93
+ const nx = Math.abs(normalAttr.getX(j));
94
+ const ny = Math.abs(normalAttr.getY(j));
95
+ const nz = Math.abs(normalAttr.getZ(j));
96
+ let u, v;
97
+ if (nz >= nx && nz >= ny) {
98
+ u = (px - bb.min.x) / maxDimUv;
99
+ v = 1 - (py - bb.min.y) / maxDimUv;
100
+ }
101
+ else if (nx >= ny) {
102
+ u = (pz - bb.min.z) / maxDimUv;
103
+ v = 1 - (py - bb.min.y) / maxDimUv;
104
+ }
105
+ else {
106
+ u = (px - bb.min.x) / maxDimUv;
107
+ v = (pz - bb.min.z) / maxDimUv;
108
+ }
109
+ uvAttr.setXY(j, u, v);
110
+ }
111
+ uvAttr.needsUpdate = true;
112
+ }
113
+ /**
114
+ * True when `shape` is (approximately) the SVG's full viewBox rectangle — used
115
+ * to drop background rects that SVGLoader emits as a fill shape.
116
+ */
117
+ export function isViewBoxRect(shape, vbW, vbH) {
118
+ const pts = shape.getPoints(4);
119
+ if (pts.length !== 4 && pts.length !== 5)
120
+ return false;
121
+ const bb = new THREE.Box2();
122
+ for (const p of pts)
123
+ bb.expandByPoint(p);
124
+ const size = new THREE.Vector2();
125
+ bb.getSize(size);
126
+ const tolerance = 0.01;
127
+ return Math.abs(size.x - vbW) / vbW < tolerance && Math.abs(size.y - vbH) / vbH < tolerance;
128
+ }
129
+ /**
130
+ * Parse an SVG string into three.js `Shape`s ready for extrusion. Fills become
131
+ * shapes directly (dropping the viewBox-sized background rect); strokes are
132
+ * tessellated into a ribbon polygon of `strokeWidth`; paths with no explicit
133
+ * fill/stroke fall back to fill shapes.
134
+ *
135
+ * Accepts both whitespace- and comma-separated viewBox values.
136
+ */
137
+ export function parseShapesFromSVG(svgString) {
138
+ const loader = new SVGLoader();
139
+ const svgData = loader.parse(svgString);
140
+ const allShapes = [];
141
+ const vbMatch = svgString.match(/viewBox\s*=\s*["']\s*([\d.\-]+)[\s,]+([\d.\-]+)[\s,]+([\d.\-]+)[\s,]+([\d.\-]+)/);
142
+ const vbW = vbMatch ? parseFloat(vbMatch[3]) : null;
143
+ const vbH = vbMatch ? parseFloat(vbMatch[4]) : null;
144
+ svgData.paths.forEach((path) => {
145
+ const style = path.userData?.style;
146
+ const hasFill = style?.fill && style.fill !== 'none' && style.fill !== 'transparent';
147
+ const hasStroke = style?.stroke && style.stroke !== 'none' && style.stroke !== 'transparent';
148
+ if (hasFill) {
149
+ const shapes = SVGLoader.createShapes(path);
150
+ for (const shape of shapes) {
151
+ if (vbW && vbH && isViewBoxRect(shape, vbW, vbH))
152
+ continue;
153
+ allShapes.push(shape);
154
+ }
155
+ }
156
+ if (hasStroke) {
157
+ const strokeWidth = parseFloat(style?.strokeWidth ?? '2');
158
+ const divisions = 12;
159
+ path.subPaths.forEach((subPath) => {
160
+ const points = subPath.getPoints(divisions);
161
+ if (points.length < 2)
162
+ return;
163
+ const shape = new THREE.Shape();
164
+ const halfWidth = strokeWidth / 2;
165
+ const leftSide = [];
166
+ const rightSide = [];
167
+ for (let i = 0; i < points.length; i++) {
168
+ const curr = points[i];
169
+ const prev = points[Math.max(0, i - 1)];
170
+ const next = points[Math.min(points.length - 1, i + 1)];
171
+ const dx = next.x - prev.x;
172
+ const dy = next.y - prev.y;
173
+ const len = Math.sqrt(dx * dx + dy * dy) || 1;
174
+ const nx = -dy / len;
175
+ const ny = dx / len;
176
+ leftSide.push(new THREE.Vector2(curr.x + nx * halfWidth, curr.y + ny * halfWidth));
177
+ rightSide.push(new THREE.Vector2(curr.x - nx * halfWidth, curr.y - ny * halfWidth));
178
+ }
179
+ shape.moveTo(leftSide[0].x, leftSide[0].y);
180
+ for (let i = 1; i < leftSide.length; i++)
181
+ shape.lineTo(leftSide[i].x, leftSide[i].y);
182
+ for (let i = rightSide.length - 1; i >= 0; i--)
183
+ shape.lineTo(rightSide[i].x, rightSide[i].y);
184
+ shape.closePath();
185
+ allShapes.push(shape);
186
+ });
187
+ }
188
+ if (!hasFill && !hasStroke) {
189
+ allShapes.push(...SVGLoader.createShapes(path));
190
+ }
191
+ });
192
+ return allShapes;
193
+ }
194
+ /**
195
+ * Translate the engine's high-level knobs (`depth`, `smoothness`, bevel
196
+ * thickness/size) + the SVG's flat bounds + shape complexity into concrete
197
+ * three.js `ExtrudeGeometry` settings, including the vertex-budget reduction.
198
+ * This is the exact math used by both the client geometry pipeline and the
199
+ * server GLB export, so they never drift.
200
+ */
201
+ export function buildExtrudeSettings(maxFlatDim, shapeCount, opts) {
202
+ const { depth, smoothness, bevelEnabled = true, bevelThickness: userThickness = 0.5, bevelSize: userSize = 0.5, vertexBudget = 600_000, } = opts;
203
+ const complexity = shapeCount;
204
+ const vertsBudgetPerShape = Math.max(Math.floor(vertexBudget / Math.max(complexity, 1)), 500);
205
+ const scaledDepth = (depth / 10) * maxFlatDim;
206
+ const bevelScale = Math.min(maxFlatDim * 0.05, 1);
207
+ const idealBevel = Math.round(4 + smoothness * 8);
208
+ const idealCurve = Math.round(32 + smoothness * 64);
209
+ const estimatedVerts = idealBevel * idealCurve * 6;
210
+ const reductionFactor = estimatedVerts > vertsBudgetPerShape ? Math.sqrt(vertsBudgetPerShape / estimatedVerts) : 1;
211
+ const bevelSegments = Math.max(2, Math.min(Math.round(idealBevel * reductionFactor), 64));
212
+ const curveSegments = Math.max(8, Math.min(Math.round(idealCurve * reductionFactor), 128));
213
+ const maxBevel = Math.max(0.01, scaledDepth * 0.5);
214
+ const bevelThickness = Math.min(bevelScale * userThickness, maxBevel);
215
+ const bevelSize = Math.min(bevelScale * userSize, maxBevel);
216
+ return {
217
+ depth: scaledDepth,
218
+ bevelEnabled,
219
+ bevelThickness,
220
+ bevelSize,
221
+ bevelSegments,
222
+ curveSegments,
223
+ };
224
+ }
225
+ /**
226
+ * Measure the flat (un-extruded) bounding box of a set of shapes and return the
227
+ * larger of width/height (clamped to ≥ 1) — the `maxFlatDim` that drives depth
228
+ * and bevel scaling. Disposes the temporary geometry it builds.
229
+ */
230
+ export function measureFlatMaxDim(shapes) {
231
+ const tempGeo = new THREE.ShapeGeometry(shapes);
232
+ tempGeo.computeBoundingBox();
233
+ const flatSize = new THREE.Vector3();
234
+ tempGeo.boundingBox.getSize(flatSize);
235
+ const maxFlatDim = Math.max(flatSize.x, flatSize.y, 1);
236
+ tempGeo.dispose();
237
+ return maxFlatDim;
238
+ }
239
+ /**
240
+ * SVG (or pre-parsed shapes) → a single extruded, merged, normal-smoothed,
241
+ * triplanar-UV'd `BufferGeometry`, plus its center and a fit-to-4-units scale.
242
+ *
243
+ * This is the pure heart of the Studio3D engine's `useExtrudedGeometry` hook
244
+ * (which keeps React state, progress, cancellation and disposal). The geometry
245
+ * math here is identical to the legacy hook — extracting it changes nothing.
246
+ *
247
+ * The caller owns the returned `geometry` and must `dispose()` it.
248
+ */
249
+ export function buildExtrudedGeometry(svg, opts) {
250
+ const allShapes = typeof svg === 'string' ? parseShapesFromSVG(svg) : svg;
251
+ if (allShapes.length === 0)
252
+ return null;
253
+ const maxFlatDim = measureFlatMaxDim(allShapes);
254
+ const extrudeSettings = buildExtrudeSettings(maxFlatDim, allShapes.length, opts);
255
+ const individualGeos = [];
256
+ for (let i = 0; i < allShapes.length; i++) {
257
+ individualGeos.push(new THREE.ExtrudeGeometry(allShapes[i], extrudeSettings));
258
+ }
259
+ let merged = BufferGeometryUtils.mergeGeometries(individualGeos, false);
260
+ individualGeos.forEach((g) => g.dispose());
261
+ if (!merged)
262
+ return null;
263
+ const creaseAngle = opts.creaseAngle === undefined ? Math.PI / 6 : opts.creaseAngle;
264
+ if (creaseAngle != null) {
265
+ const smoothed = smoothCreaseNormals(merged, creaseAngle);
266
+ merged.dispose();
267
+ merged = smoothed;
268
+ }
269
+ else {
270
+ merged.computeVertexNormals();
271
+ }
272
+ merged.computeBoundingBox();
273
+ recomputeTriplanarUVs(merged, merged.boundingBox);
274
+ const bb = merged.boundingBox;
275
+ const ctr = new THREE.Vector3();
276
+ bb.getCenter(ctr);
277
+ const size = new THREE.Vector3();
278
+ bb.getSize(size);
279
+ const maxDim = Math.max(size.x, size.y, size.z);
280
+ const s = maxDim > 0 ? 4 / maxDim : 1;
281
+ return { geometry: merged, center: ctr, baseScale: s, shapeCount: allShapes.length };
282
+ }
283
+ //# sourceMappingURL=geometry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.js","sourceRoot":"","sources":["../src/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,KAAK,mBAAmB,MAAM,iDAAiD,CAAC;AAMvF,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAA8B,EAC9B,cAAsB;IAEtB,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;IAC5C,IAAI,CAAC,OAAO;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAE5B,IAAI,KAAK,GAAG,mBAAmB,EAAE,CAAC;QAChC,OAAO,CAAC,oBAAoB,EAAE,CAAC;QAC/B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,WAAW,GAAoB,EAAE,CAAC;IAExC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5F,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAE5F,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClD,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClD,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC;QAEzB,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,GAAG,EAAoB,CAAC;IACjD,MAAM,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;QACnD,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;QACnD,MAAM,IAAI,GAAG,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QAEjC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC7B,CAAC;QACD,YAAY,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC9C,MAAM,UAAU,GAAG,IAAI,YAAY,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAE/C,KAAK,MAAM,OAAO,IAAI,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;QAElC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAE/B,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAEjB,KAAK,MAAM,QAAQ,IAAI,OAAO,EAAE,CAAC;gBAC/B,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAAE,SAAS;gBACpC,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;gBAEjC,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,YAAY,EAAE,CAAC;oBAC/B,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAC3B,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACxB,CAAC;YACH,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACtC,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;gBAC5B,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;YAChC,CAAC;YACD,SAAS,CAAC,SAAS,EAAE,CAAC;YAEtB,KAAK,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC;gBAC5B,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;gBAChC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;gBACpC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,KAAK,CAAC,eAAe,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IACzE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAyB,EAAE,EAAc;IAC7E,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACnC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,EAA2B,CAAC;IAC1D,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,QAAiC,CAAC;IACjE,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,MAA+B,CAAC;IAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAE7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,CAAS,EAAE,CAAS,CAAC;QAEzB,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;YACzB,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;QACrC,CAAC;aAAM,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;YACpB,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;QACrC,CAAC;aAAM,CAAC;YACN,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;YAC/B,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC;QACjC,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,GAAW,EAAE,GAAW;IACxE,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACvD,MAAM,EAAE,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,GAAG;QAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACjC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,SAAS,GAAG,IAAI,CAAC;IACvB,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,SAAS,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,SAAS,CAAC;AAC9F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAiB;IAClD,MAAM,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;IAC/B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,SAAS,GAAkB,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAC7B,iFAAiF,CAClF,CAAC;IACF,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEpD,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAI,IAA0D,CAAC,QAAQ,EAAE,KAAK,CAAC;QAC1F,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,CAAC;QACrF,MAAM,SAAS,GAAG,KAAK,EAAE,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,aAAa,CAAC;QAE7F,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YAC5C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,IAAI,GAAG,IAAI,GAAG,IAAI,aAAa,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC;oBAAE,SAAS;gBAC3D,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,WAAW,GAAG,UAAU,CAAC,KAAK,EAAE,WAAW,IAAI,GAAG,CAAC,CAAC;YAC1D,MAAM,SAAS,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAChC,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBAC5C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;oBAAE,OAAO;gBAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBAChC,MAAM,SAAS,GAAG,WAAW,GAAG,CAAC,CAAC;gBAClC,MAAM,QAAQ,GAAoB,EAAE,CAAC;gBACrC,MAAM,SAAS,GAAoB,EAAE,CAAC;gBAEtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACvC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;oBACvB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;oBACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;oBACxD,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;oBAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;oBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;oBAC9C,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC;oBACrB,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC;oBACpB,QAAQ,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;oBACnF,SAAS,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,SAAS,EAAE,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC;gBACtF,CAAC;gBAED,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE;oBAAE,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrF,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;oBAC5C,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/C,KAAK,CAAC,SAAS,EAAE,CAAC;gBAClB,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,SAAS,CAAC;AACnB,CAAC;AAYD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAAkB,EAClB,UAAkB,EAClB,IAAkC;IAElC,MAAM,EACJ,KAAK,EACL,UAAU,EACV,YAAY,GAAG,IAAI,EACnB,cAAc,EAAE,aAAa,GAAG,GAAG,EACnC,SAAS,EAAE,QAAQ,GAAG,GAAG,EACzB,YAAY,GAAG,OAAO,GACvB,GAAG,IAAI,CAAC;IAET,MAAM,UAAU,GAAG,UAAU,CAAC;IAC9B,MAAM,mBAAmB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9F,MAAM,WAAW,GAAG,CAAC,KAAK,GAAG,EAAE,CAAC,GAAG,UAAU,CAAC;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,UAAU,GAAG,EAAE,CAAC,CAAC;IACpD,MAAM,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,CAAC,CAAC;IACnD,MAAM,eAAe,GACnB,cAAc,GAAG,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7F,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,eAAe,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1F,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,eAAe,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAE3F,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,GAAG,GAAG,CAAC,CAAC;IACnD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,aAAa,EAAE,QAAQ,CAAC,CAAC;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAE5D,OAAO;QACL,KAAK,EAAE,WAAW;QAClB,YAAY;QACZ,cAAc;QACd,SAAS;QACT,aAAa;QACb,aAAa;KACd,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAqB;IACrD,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;IAChD,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACrC,OAAO,CAAC,WAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,OAAO,CAAC,OAAO,EAAE,CAAC;IAClB,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,GAA2B,EAC3B,IAAkC;IAElC,MAAM,SAAS,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1E,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,UAAU,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,eAAe,GAAG,oBAAoB,CAAC,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAEjF,MAAM,cAAc,GAA4B,EAAE,CAAC;IACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,cAAc,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC;IAChF,CAAC;IAED,IAAI,MAAM,GAAG,mBAAmB,CAAC,eAAe,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;IACxE,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAE3C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;IACpF,IAAI,WAAW,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC1D,MAAM,CAAC,OAAO,EAAE,CAAC;QACjB,MAAM,GAAG,QAAQ,CAAC;IACpB,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,oBAAoB,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,CAAC,kBAAkB,EAAE,CAAC;IAC5B,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,WAAY,CAAC,CAAC;IAEnD,MAAM,EAAE,GAAG,MAAM,CAAC,WAAY,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IAChC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAClB,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;IACjC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAEtC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;AACvF,CAAC"}
package/dist/glb.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Options for {@link svgToGlb}. All optional; defaults match the Studio3D
3
+ * server export. `depth`/`smoothness`/bevel drive the same extrude math the
4
+ * client uses; `color`/`metalness`/`roughness` become the GLB PBR material.
5
+ */
6
+ export interface SvgToGlbOptions {
7
+ depth?: number;
8
+ smoothness?: number;
9
+ bevelEnabled?: boolean;
10
+ bevelThickness?: number;
11
+ bevelSize?: number;
12
+ color?: string;
13
+ metalness?: number;
14
+ roughness?: number;
15
+ }
16
+ /**
17
+ * Output of {@link svgToGlb}: the raw GLB bytes plus the interleaved geometry
18
+ * arrays they were built from (handy for tests / further processing).
19
+ */
20
+ export interface GlbResult {
21
+ /** Raw `.glb` bytes (`glTF` magic header). */
22
+ glb: Uint8Array;
23
+ positions: Float32Array;
24
+ normals: Float32Array;
25
+ indices: Uint32Array;
26
+ }
27
+ /**
28
+ * SVG string → extruded, merged, centered geometry serialized as a binary glTF
29
+ * (`.glb`). This is the pure core of the Studio3D server export
30
+ * (`studio3dExportService.svgToGlb`); the service keeps only the DOMParser/JSDOM
31
+ * environment shim and storage. Geometry math (extrude settings, vertex budget)
32
+ * matches the legacy server path exactly — plain `computeVertexNormals`, no
33
+ * crease smoothing, no bevel clamp.
34
+ *
35
+ * Returns the GLB bytes plus the geometry arrays. Throws if the SVG yields no
36
+ * shapes or the merge fails. Requires a DOM `DOMParser` to be available (the
37
+ * server installs one via JSDOM before calling).
38
+ */
39
+ export declare function svgToGlb(svgString: string, opts?: SvgToGlbOptions): GlbResult;
40
+ //# sourceMappingURL=glb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glb.d.ts","sourceRoot":"","sources":["../src/glb.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,GAAG,EAAE,UAAU,CAAC;IAChB,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;CACtB;AAyGD;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,SAAS,CA4DjF"}