@nice2dev/ui-3d 1.3.15 → 1.3.17
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/CHANGELOG.md +13 -0
- package/dist/cjs/collaborative/collaborativeScene.js +160 -206
- package/dist/cjs/core/editorAsk.js +51 -41
- package/dist/cjs/core/i18n.js +13 -14
- package/dist/cjs/core/threeCompat.js +29 -42
- package/dist/cjs/dance/DanceBridge.js +148 -177
- package/dist/cjs/dance/DanceScoreEngine.js +209 -221
- package/dist/cjs/dance/PoseDetector.js +302 -275
- package/dist/cjs/index.js +110 -113
- package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
- package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
- package/dist/cjs/material/materialEditorTypes.js +35 -49
- package/dist/cjs/material/materialEditorUtils.js +684 -747
- package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
- package/dist/cjs/model/ModelEditor.js +90 -40
- package/dist/cjs/model/ModelEditor.module.css.js +257 -5
- package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
- package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
- package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
- package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
- package/dist/cjs/model/ModelEditorTimeline.js +232 -34
- package/dist/cjs/model/ModelEditorToolbar.js +170 -14
- package/dist/cjs/model/ModelEditorViewport.js +231 -39
- package/dist/cjs/model/ModelViewer.js +388 -63
- package/dist/cjs/model/ModelViewer.module.css.js +77 -5
- package/dist/cjs/model/NiceArmatureEditor.js +473 -274
- package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
- package/dist/cjs/model/NiceOctree.js +372 -320
- package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
- package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
- package/dist/cjs/model/NiceTerrainEditor.js +595 -385
- package/dist/cjs/model/NiceWeightPainter.js +384 -261
- package/dist/cjs/model/NiceXRPreview.js +385 -308
- package/dist/cjs/model/cadModeUtils.js +103 -106
- package/dist/cjs/model/editorShortcuts.js +171 -194
- package/dist/cjs/model/modelEditorTypes.js +100 -118
- package/dist/cjs/model/modelEditorUtils.js +1061 -1146
- package/dist/cjs/model/simsModeUtils.js +1023 -1053
- package/dist/cjs/model/useModelEditor.js +1977 -1933
- package/dist/cjs/model/useModelViewer.js +633 -665
- package/dist/cjs/nice2dev-ui-3d.css +1 -1
- package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
- package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
- package/dist/cjs/particle/particleEditorTypes.js +62 -70
- package/dist/cjs/particle/particleEditorUtils.js +998 -1013
- package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
- package/dist/cjs/rendering/NiceRenderExport.js +424 -337
- package/dist/cjs/rendering/NiceSSAO.js +454 -337
- package/dist/cjs/rendering/NiceSSR.js +387 -261
- package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
- package/dist/cjs/uv/NiceUVEditor.js +787 -550
- package/dist/cjs/uv/UVEditor.module.css.js +53 -5
- package/dist/cjs/uv/uvEditorTypes.js +59 -76
- package/dist/cjs/uv/uvEditorUtils.js +501 -575
- package/dist/esm/collaborative/collaborativeScene.js +163 -205
- package/dist/esm/core/editorAsk.js +53 -40
- package/dist/esm/core/i18n.js +15 -13
- package/dist/esm/core/threeCompat.js +18 -26
- package/dist/esm/dance/DanceBridge.js +151 -176
- package/dist/esm/dance/DanceScoreEngine.js +211 -220
- package/dist/esm/dance/PoseDetector.js +304 -274
- package/dist/esm/index.js +258 -36
- package/dist/esm/material/MaterialEditor.module.css.js +242 -3
- package/dist/esm/material/NiceMaterialEditor.js +1215 -756
- package/dist/esm/material/materialEditorTypes.js +45 -48
- package/dist/esm/material/materialEditorUtils.js +713 -746
- package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
- package/dist/esm/model/ModelEditor.js +91 -39
- package/dist/esm/model/ModelEditor.module.css.js +257 -3
- package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
- package/dist/esm/model/ModelEditorMenuBar.js +359 -30
- package/dist/esm/model/ModelEditorRightPanel.js +634 -114
- package/dist/esm/model/ModelEditorSubComponents.js +265 -94
- package/dist/esm/model/ModelEditorTimeline.js +232 -32
- package/dist/esm/model/ModelEditorToolbar.js +170 -12
- package/dist/esm/model/ModelEditorViewport.js +231 -37
- package/dist/esm/model/ModelViewer.js +389 -62
- package/dist/esm/model/ModelViewer.module.css.js +77 -3
- package/dist/esm/model/NiceArmatureEditor.js +462 -258
- package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
- package/dist/esm/model/NiceOctree.js +361 -304
- package/dist/esm/model/NicePhysicsSimulation.js +517 -282
- package/dist/esm/model/NiceProceduralGeometry.js +667 -284
- package/dist/esm/model/NiceTerrainEditor.js +584 -369
- package/dist/esm/model/NiceWeightPainter.js +373 -245
- package/dist/esm/model/NiceXRPreview.js +374 -292
- package/dist/esm/model/cadModeUtils.js +95 -88
- package/dist/esm/model/editorShortcuts.js +172 -193
- package/dist/esm/model/modelEditorTypes.js +90 -100
- package/dist/esm/model/modelEditorUtils.js +1056 -1128
- package/dist/esm/model/simsModeUtils.js +1021 -1035
- package/dist/esm/model/useModelEditor.js +1946 -1920
- package/dist/esm/model/useModelViewer.js +619 -647
- package/dist/esm/nice2dev-ui-3d.css +1 -1
- package/dist/esm/particle/NiceParticleEditor.js +1188 -570
- package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
- package/dist/esm/particle/particleEditorTypes.js +69 -69
- package/dist/esm/particle/particleEditorUtils.js +1027 -1012
- package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
- package/dist/esm/rendering/NiceRenderExport.js +413 -321
- package/dist/esm/rendering/NiceSSAO.js +450 -328
- package/dist/esm/rendering/NiceSSR.js +383 -252
- package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
- package/dist/esm/uv/NiceUVEditor.js +788 -549
- package/dist/esm/uv/UVEditor.module.css.js +53 -3
- package/dist/esm/uv/uvEditorTypes.js +68 -75
- package/dist/esm/uv/uvEditorUtils.js +515 -557
- package/package.json +7 -12
|
@@ -1,638 +1,596 @@
|
|
|
1
|
-
import * as THREE from
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* UV Editor Utilities
|
|
5
|
-
*
|
|
6
|
-
* Core algorithms and utilities for UV mapping operations.
|
|
7
|
-
* @module @nice2dev/ui-3d
|
|
8
|
-
*/
|
|
9
|
-
/* ═══════════════════════════════════════════
|
|
10
|
-
ID Generation
|
|
11
|
-
═══════════════════════════════════════════ */
|
|
1
|
+
import * as THREE from "three";
|
|
12
2
|
let idCounter = 0;
|
|
13
3
|
const generateId = (prefix) => `${prefix}_${Date.now()}_${++idCounter}`;
|
|
14
|
-
/* ═══════════════════════════════════════════
|
|
15
|
-
UV Math Utilities
|
|
16
|
-
═══════════════════════════════════════════ */
|
|
17
4
|
const uvDistance = (a, b) => Math.sqrt((a.u - b.u) ** 2 + (a.v - b.v) ** 2);
|
|
18
5
|
const uvLerp = (a, b, t) => ({
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
u: a.u + (b.u - a.u) * t,
|
|
7
|
+
v: a.v + (b.v - a.v) * t
|
|
21
8
|
});
|
|
22
9
|
const uvAdd = (a, b) => ({
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
u: a.u + b.u,
|
|
11
|
+
v: a.v + b.v
|
|
25
12
|
});
|
|
26
13
|
const uvSub = (a, b) => ({
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
u: a.u - b.u,
|
|
15
|
+
v: a.v - b.v
|
|
29
16
|
});
|
|
30
17
|
const uvScale = (p, s) => ({
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
u: p.u * s,
|
|
19
|
+
v: p.v * s
|
|
33
20
|
});
|
|
34
21
|
const uvRotate = (p, angle, center = { u: 0.5, v: 0.5 }) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
const cos = Math.cos(angle);
|
|
23
|
+
const sin = Math.sin(angle);
|
|
24
|
+
const du = p.u - center.u;
|
|
25
|
+
const dv = p.v - center.v;
|
|
26
|
+
return {
|
|
27
|
+
u: center.u + du * cos - dv * sin,
|
|
28
|
+
v: center.v + du * sin + dv * cos
|
|
29
|
+
};
|
|
43
30
|
};
|
|
44
31
|
const uvCross = (a, b) => a.u * b.v - a.v * b.u;
|
|
45
|
-
/* ═══════════════════════════════════════════
|
|
46
|
-
Bounds Utilities
|
|
47
|
-
═══════════════════════════════════════════ */
|
|
48
32
|
const createEmptyBounds = () => ({
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
minU: Infinity,
|
|
34
|
+
maxU: -Infinity,
|
|
35
|
+
minV: Infinity,
|
|
36
|
+
maxV: -Infinity
|
|
53
37
|
});
|
|
54
38
|
const expandBounds = (bounds, point) => ({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
minU: Math.min(bounds.minU, point.u),
|
|
40
|
+
maxU: Math.max(bounds.maxU, point.u),
|
|
41
|
+
minV: Math.min(bounds.minV, point.v),
|
|
42
|
+
maxV: Math.max(bounds.maxV, point.v)
|
|
59
43
|
});
|
|
60
44
|
const mergeBounds = (a, b) => ({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
45
|
+
minU: Math.min(a.minU, b.minU),
|
|
46
|
+
maxU: Math.max(a.maxU, b.maxU),
|
|
47
|
+
minV: Math.min(a.minV, b.minV),
|
|
48
|
+
maxV: Math.max(a.maxV, b.maxV)
|
|
65
49
|
});
|
|
66
50
|
const getBoundsCenter = (bounds) => ({
|
|
67
|
-
|
|
68
|
-
|
|
51
|
+
u: (bounds.minU + bounds.maxU) / 2,
|
|
52
|
+
v: (bounds.minV + bounds.maxV) / 2
|
|
69
53
|
});
|
|
70
54
|
const getBoundsSize = (bounds) => ({
|
|
71
|
-
|
|
72
|
-
|
|
55
|
+
u: bounds.maxU - bounds.minU,
|
|
56
|
+
v: bounds.maxV - bounds.minV
|
|
73
57
|
});
|
|
74
58
|
const getBoundsArea = (bounds) => (bounds.maxU - bounds.minU) * (bounds.maxV - bounds.minV);
|
|
75
|
-
const boundsContains = (bounds, point) => point.u >= bounds.minU &&
|
|
76
|
-
point.u <= bounds.maxU &&
|
|
77
|
-
point.v >= bounds.minV &&
|
|
78
|
-
point.v <= bounds.maxV;
|
|
59
|
+
const boundsContains = (bounds, point) => point.u >= bounds.minU && point.u <= bounds.maxU && point.v >= bounds.minV && point.v <= bounds.maxV;
|
|
79
60
|
const boundsOverlap = (a, b) => a.minU <= b.maxU && a.maxU >= b.minU && a.minV <= b.maxV && a.maxV >= b.minV;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
61
|
+
function extractUVMeshData(geometry, channel = "uv") {
|
|
62
|
+
const uvAttr = geometry.getAttribute(channel);
|
|
63
|
+
const posAttr = geometry.getAttribute("position");
|
|
64
|
+
const indexAttr = geometry.getIndex();
|
|
65
|
+
if (!uvAttr || !posAttr) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const vertices = /* @__PURE__ */ new Map();
|
|
69
|
+
const edges = /* @__PURE__ */ new Map();
|
|
70
|
+
const faces = /* @__PURE__ */ new Map();
|
|
71
|
+
for (let i = 0; i < uvAttr.count; i++) {
|
|
72
|
+
const id = generateId("v");
|
|
73
|
+
vertices.set(id, {
|
|
74
|
+
id,
|
|
75
|
+
position: { u: uvAttr.getX(i), v: uvAttr.getY(i) },
|
|
76
|
+
index: i,
|
|
77
|
+
pinned: false,
|
|
78
|
+
selected: false,
|
|
79
|
+
vertexIndex: i
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const vertexIds = Array.from(vertices.keys());
|
|
83
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
84
|
+
const faceCount = indexAttr ? indexAttr.count / 3 : posAttr.count / 3;
|
|
85
|
+
for (let i = 0; i < faceCount; i++) {
|
|
86
|
+
const faceVertexIds = [];
|
|
87
|
+
const indices = [];
|
|
88
|
+
for (let j = 0; j < 3; j++) {
|
|
89
|
+
const idx = indexAttr ? indexAttr.getX(i * 3 + j) : i * 3 + j;
|
|
90
|
+
indices.push(idx);
|
|
91
|
+
faceVertexIds.push(vertexIds[idx]);
|
|
104
92
|
}
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
93
|
+
const v0 = vertices.get(faceVertexIds[0]).position;
|
|
94
|
+
const v1 = vertices.get(faceVertexIds[1]).position;
|
|
95
|
+
const v2 = vertices.get(faceVertexIds[2]).position;
|
|
96
|
+
const uvArea = Math.abs(uvCross(uvSub(v1, v0), uvSub(v2, v0))) / 2;
|
|
97
|
+
const p0 = new THREE.Vector3().fromBufferAttribute(posAttr, indices[0]);
|
|
98
|
+
const p1 = new THREE.Vector3().fromBufferAttribute(posAttr, indices[1]);
|
|
99
|
+
const p2 = new THREE.Vector3().fromBufferAttribute(posAttr, indices[2]);
|
|
100
|
+
const e1 = p1.clone().sub(p0);
|
|
101
|
+
const e2 = p2.clone().sub(p0);
|
|
102
|
+
const area3D = e1.cross(e2).length() / 2;
|
|
103
|
+
const faceId = generateId("f");
|
|
104
|
+
faces.set(faceId, {
|
|
105
|
+
id: faceId,
|
|
106
|
+
vertices: faceVertexIds,
|
|
107
|
+
area: uvArea,
|
|
108
|
+
area3D,
|
|
109
|
+
distortion: area3D > 0 ? uvArea / area3D : 1,
|
|
110
|
+
selected: false,
|
|
111
|
+
faceIndex: i
|
|
112
|
+
});
|
|
113
|
+
for (let j = 0; j < 3; j++) {
|
|
114
|
+
const v1Id = faceVertexIds[j];
|
|
115
|
+
const v2Id = faceVertexIds[(j + 1) % 3];
|
|
116
|
+
const edgeKey = [v1Id, v2Id].sort().join("_");
|
|
117
|
+
if (!edgeMap.has(edgeKey)) {
|
|
118
|
+
const edgeId = generateId("e");
|
|
119
|
+
const v1Pos = vertices.get(v1Id).position;
|
|
120
|
+
const v2Pos = vertices.get(v2Id).position;
|
|
121
|
+
edges.set(edgeId, {
|
|
122
|
+
id: edgeId,
|
|
123
|
+
startVertex: v1Id,
|
|
124
|
+
endVertex: v2Id,
|
|
125
|
+
seam: false,
|
|
126
|
+
selected: false,
|
|
127
|
+
length: uvDistance(v1Pos, v2Pos)
|
|
138
128
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const v1Id = faceVertexIds[j];
|
|
142
|
-
const v2Id = faceVertexIds[(j + 1) % 3];
|
|
143
|
-
const edgeKey = [v1Id, v2Id].sort().join('_');
|
|
144
|
-
if (!edgeMap.has(edgeKey)) {
|
|
145
|
-
const edgeId = generateId('e');
|
|
146
|
-
const v1Pos = vertices.get(v1Id).position;
|
|
147
|
-
const v2Pos = vertices.get(v2Id).position;
|
|
148
|
-
edges.set(edgeId, {
|
|
149
|
-
id: edgeId,
|
|
150
|
-
startVertex: v1Id,
|
|
151
|
-
endVertex: v2Id,
|
|
152
|
-
seam: false,
|
|
153
|
-
selected: false,
|
|
154
|
-
length: uvDistance(v1Pos, v2Pos),
|
|
155
|
-
});
|
|
156
|
-
edgeMap.set(edgeKey, edgeId);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
129
|
+
edgeMap.set(edgeKey, edgeId);
|
|
130
|
+
}
|
|
159
131
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
132
|
+
}
|
|
133
|
+
const islands = computeIslands(vertices, edges, faces);
|
|
134
|
+
return {
|
|
135
|
+
vertices,
|
|
136
|
+
edges,
|
|
137
|
+
faces,
|
|
138
|
+
islands,
|
|
139
|
+
geometryId: geometry.uuid,
|
|
140
|
+
dirty: false
|
|
141
|
+
};
|
|
170
142
|
}
|
|
171
|
-
/* ═══════════════════════════════════════════
|
|
172
|
-
Island Detection
|
|
173
|
-
═══════════════════════════════════════════ */
|
|
174
143
|
function computeIslands(vertices, edges, faces) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
144
|
+
const islands = /* @__PURE__ */ new Map();
|
|
145
|
+
const visited = /* @__PURE__ */ new Set();
|
|
146
|
+
const faceToIsland = /* @__PURE__ */ new Map();
|
|
147
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const face of faces.values()) {
|
|
149
|
+
adjacency.set(face.id, /* @__PURE__ */ new Set());
|
|
150
|
+
}
|
|
151
|
+
const vertexToFaces = /* @__PURE__ */ new Map();
|
|
152
|
+
for (const face of faces.values()) {
|
|
153
|
+
for (const vId of face.vertices) {
|
|
154
|
+
if (!vertexToFaces.has(vId)) {
|
|
155
|
+
vertexToFaces.set(vId, /* @__PURE__ */ new Set());
|
|
156
|
+
}
|
|
157
|
+
vertexToFaces.get(vId).add(face.id);
|
|
182
158
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
for (const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
vertexToFaces.get(vId).add(face.id);
|
|
159
|
+
}
|
|
160
|
+
for (const face of faces.values()) {
|
|
161
|
+
for (const vId of face.vertices) {
|
|
162
|
+
const connectedFaces = vertexToFaces.get(vId);
|
|
163
|
+
for (const otherFaceId of connectedFaces) {
|
|
164
|
+
if (otherFaceId !== face.id) {
|
|
165
|
+
adjacency.get(face.id).add(otherFaceId);
|
|
191
166
|
}
|
|
167
|
+
}
|
|
192
168
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
169
|
+
}
|
|
170
|
+
const floodFill = (startFaceId) => {
|
|
171
|
+
const stack = [startFaceId];
|
|
172
|
+
const islandFaces = [];
|
|
173
|
+
while (stack.length > 0) {
|
|
174
|
+
const faceId = stack.pop();
|
|
175
|
+
if (visited.has(faceId)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
visited.add(faceId);
|
|
179
|
+
islandFaces.push(faceId);
|
|
180
|
+
for (const neighborId of adjacency.get(faceId) ?? []) {
|
|
181
|
+
if (!visited.has(neighborId)) {
|
|
182
|
+
stack.push(neighborId);
|
|
201
183
|
}
|
|
184
|
+
}
|
|
202
185
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
while (stack.length > 0) {
|
|
209
|
-
const faceId = stack.pop();
|
|
210
|
-
if (visited.has(faceId)) {
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
visited.add(faceId);
|
|
214
|
-
islandFaces.push(faceId);
|
|
215
|
-
for (const neighborId of (_a = adjacency.get(faceId)) !== null && _a !== void 0 ? _a : []) {
|
|
216
|
-
if (!visited.has(neighborId)) {
|
|
217
|
-
stack.push(neighborId);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return islandFaces;
|
|
222
|
-
};
|
|
223
|
-
for (const face of faces.values()) {
|
|
224
|
-
if (visited.has(face.id)) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const islandFaces = floodFill(face.id);
|
|
228
|
-
const islandId = generateId('i');
|
|
229
|
-
// Gather island vertices and edges
|
|
230
|
-
const islandVertices = new Set();
|
|
231
|
-
const islandEdges = new Set();
|
|
232
|
-
for (const faceId of islandFaces) {
|
|
233
|
-
faceToIsland.set(faceId, islandId);
|
|
234
|
-
const f = faces.get(faceId);
|
|
235
|
-
for (const vId of f.vertices) {
|
|
236
|
-
islandVertices.add(vId);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
for (const edge of edges.values()) {
|
|
240
|
-
if (islandVertices.has(edge.startVertex) && islandVertices.has(edge.endVertex)) {
|
|
241
|
-
islandEdges.add(edge.id);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
// Calculate bounds
|
|
245
|
-
let bounds = createEmptyBounds();
|
|
246
|
-
let totalArea = 0;
|
|
247
|
-
let centerU = 0, centerV = 0;
|
|
248
|
-
for (const vId of islandVertices) {
|
|
249
|
-
const v = vertices.get(vId);
|
|
250
|
-
bounds = expandBounds(bounds, v.position);
|
|
251
|
-
centerU += v.position.u;
|
|
252
|
-
centerV += v.position.v;
|
|
253
|
-
}
|
|
254
|
-
for (const faceId of islandFaces) {
|
|
255
|
-
totalArea += faces.get(faceId).area;
|
|
256
|
-
}
|
|
257
|
-
const vertexCount = islandVertices.size;
|
|
258
|
-
islands.set(islandId, {
|
|
259
|
-
id: islandId,
|
|
260
|
-
faces: islandFaces,
|
|
261
|
-
vertices: Array.from(islandVertices),
|
|
262
|
-
edges: Array.from(islandEdges),
|
|
263
|
-
bounds,
|
|
264
|
-
area: totalArea,
|
|
265
|
-
selected: false,
|
|
266
|
-
center: { u: centerU / vertexCount, v: centerV / vertexCount },
|
|
267
|
-
rotation: 0,
|
|
268
|
-
scale: { u: 1, v: 1 },
|
|
269
|
-
});
|
|
186
|
+
return islandFaces;
|
|
187
|
+
};
|
|
188
|
+
for (const face of faces.values()) {
|
|
189
|
+
if (visited.has(face.id)) {
|
|
190
|
+
continue;
|
|
270
191
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
/*
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
geometry.setAttribute('uv', uvAttr);
|
|
192
|
+
const islandFaces = floodFill(face.id);
|
|
193
|
+
const islandId = generateId("i");
|
|
194
|
+
const islandVertices = /* @__PURE__ */ new Set();
|
|
195
|
+
const islandEdges = /* @__PURE__ */ new Set();
|
|
196
|
+
for (const faceId of islandFaces) {
|
|
197
|
+
faceToIsland.set(faceId, islandId);
|
|
198
|
+
const f = faces.get(faceId);
|
|
199
|
+
for (const vId of f.vertices) {
|
|
200
|
+
islandVertices.add(vId);
|
|
201
|
+
}
|
|
282
202
|
}
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
break;
|
|
288
|
-
case 'cylindrical':
|
|
289
|
-
applyCylindricalProjection(geometry, settings, indices);
|
|
290
|
-
break;
|
|
291
|
-
case 'spherical':
|
|
292
|
-
applySphericalProjection(geometry, settings, indices);
|
|
293
|
-
break;
|
|
294
|
-
case 'box':
|
|
295
|
-
applyBoxProjection(geometry, settings, indices);
|
|
296
|
-
break;
|
|
297
|
-
default:
|
|
298
|
-
applyPlanarProjection(geometry, { ...settings, axis: 'z'}, indices);
|
|
203
|
+
for (const edge of edges.values()) {
|
|
204
|
+
if (islandVertices.has(edge.startVertex) && islandVertices.has(edge.endVertex)) {
|
|
205
|
+
islandEdges.add(edge.id);
|
|
206
|
+
}
|
|
299
207
|
}
|
|
300
|
-
|
|
208
|
+
let bounds = createEmptyBounds();
|
|
209
|
+
let totalArea = 0;
|
|
210
|
+
let centerU = 0, centerV = 0;
|
|
211
|
+
for (const vId of islandVertices) {
|
|
212
|
+
const v = vertices.get(vId);
|
|
213
|
+
bounds = expandBounds(bounds, v.position);
|
|
214
|
+
centerU += v.position.u;
|
|
215
|
+
centerV += v.position.v;
|
|
216
|
+
}
|
|
217
|
+
for (const faceId of islandFaces) {
|
|
218
|
+
totalArea += faces.get(faceId).area;
|
|
219
|
+
}
|
|
220
|
+
const vertexCount = islandVertices.size;
|
|
221
|
+
islands.set(islandId, {
|
|
222
|
+
id: islandId,
|
|
223
|
+
faces: islandFaces,
|
|
224
|
+
vertices: Array.from(islandVertices),
|
|
225
|
+
edges: Array.from(islandEdges),
|
|
226
|
+
bounds,
|
|
227
|
+
area: totalArea,
|
|
228
|
+
selected: false,
|
|
229
|
+
center: { u: centerU / vertexCount, v: centerV / vertexCount },
|
|
230
|
+
rotation: 0,
|
|
231
|
+
scale: { u: 1, v: 1 }
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return islands;
|
|
235
|
+
}
|
|
236
|
+
function applyProjection(geometry, settings, selectedFaces) {
|
|
237
|
+
const posAttr = geometry.getAttribute("position");
|
|
238
|
+
let uvAttr = geometry.getAttribute("uv");
|
|
239
|
+
if (!uvAttr) {
|
|
240
|
+
uvAttr = new THREE.BufferAttribute(new Float32Array(posAttr.count * 2), 2);
|
|
241
|
+
geometry.setAttribute("uv", uvAttr);
|
|
242
|
+
}
|
|
243
|
+
const indices = selectedFaces ?? Array.from({ length: posAttr.count }, (_, i) => i);
|
|
244
|
+
switch (settings.type) {
|
|
245
|
+
case "planar":
|
|
246
|
+
applyPlanarProjection(geometry, settings, indices);
|
|
247
|
+
break;
|
|
248
|
+
case "cylindrical":
|
|
249
|
+
applyCylindricalProjection(geometry, settings, indices);
|
|
250
|
+
break;
|
|
251
|
+
case "spherical":
|
|
252
|
+
applySphericalProjection(geometry, settings, indices);
|
|
253
|
+
break;
|
|
254
|
+
case "box":
|
|
255
|
+
applyBoxProjection(geometry, settings, indices);
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
applyPlanarProjection(
|
|
259
|
+
geometry,
|
|
260
|
+
{ ...settings, axis: "z" },
|
|
261
|
+
indices
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
uvAttr.needsUpdate = true;
|
|
301
265
|
}
|
|
302
266
|
function applyPlanarProjection(geometry, settings, indices) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
uvAttr.setXY(i, u, v);
|
|
267
|
+
const posAttr = geometry.getAttribute("position");
|
|
268
|
+
const uvAttr = geometry.getAttribute("uv");
|
|
269
|
+
const { scale, offset, axis } = settings;
|
|
270
|
+
for (const i of indices) {
|
|
271
|
+
const x = posAttr.getX(i);
|
|
272
|
+
const y = posAttr.getY(i);
|
|
273
|
+
const z = posAttr.getZ(i);
|
|
274
|
+
let u, v;
|
|
275
|
+
switch (axis) {
|
|
276
|
+
case "x":
|
|
277
|
+
u = (z - offset.z) * scale.z;
|
|
278
|
+
v = (y - offset.y) * scale.y;
|
|
279
|
+
break;
|
|
280
|
+
case "y":
|
|
281
|
+
u = (x - offset.x) * scale.x;
|
|
282
|
+
v = (z - offset.z) * scale.z;
|
|
283
|
+
break;
|
|
284
|
+
case "z":
|
|
285
|
+
default:
|
|
286
|
+
u = (x - offset.x) * scale.x;
|
|
287
|
+
v = (y - offset.y) * scale.y;
|
|
288
|
+
break;
|
|
327
289
|
}
|
|
290
|
+
uvAttr.setXY(i, u, v);
|
|
291
|
+
}
|
|
328
292
|
}
|
|
329
293
|
function applyCylindricalProjection(geometry, settings, indices) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
uvAttr.setXY(i, u, v);
|
|
294
|
+
const posAttr = geometry.getAttribute("position");
|
|
295
|
+
const uvAttr = geometry.getAttribute("uv");
|
|
296
|
+
const { scale, offset, axis } = settings;
|
|
297
|
+
for (const i of indices) {
|
|
298
|
+
const x = posAttr.getX(i) - offset.x;
|
|
299
|
+
const y = posAttr.getY(i) - offset.y;
|
|
300
|
+
const z = posAttr.getZ(i) - offset.z;
|
|
301
|
+
let u, v;
|
|
302
|
+
switch (axis) {
|
|
303
|
+
case "x":
|
|
304
|
+
u = (Math.atan2(z, y) / (2 * Math.PI) + 0.5) * scale.x;
|
|
305
|
+
v = x * scale.y;
|
|
306
|
+
break;
|
|
307
|
+
case "y":
|
|
308
|
+
u = (Math.atan2(x, z) / (2 * Math.PI) + 0.5) * scale.x;
|
|
309
|
+
v = y * scale.y;
|
|
310
|
+
break;
|
|
311
|
+
case "z":
|
|
312
|
+
default:
|
|
313
|
+
u = (Math.atan2(y, x) / (2 * Math.PI) + 0.5) * scale.x;
|
|
314
|
+
v = z * scale.y;
|
|
315
|
+
break;
|
|
354
316
|
}
|
|
317
|
+
uvAttr.setXY(i, u, v);
|
|
318
|
+
}
|
|
355
319
|
}
|
|
356
320
|
function applySphericalProjection(geometry, settings, indices) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
321
|
+
const posAttr = geometry.getAttribute("position");
|
|
322
|
+
const uvAttr = geometry.getAttribute("uv");
|
|
323
|
+
const { scale, offset } = settings;
|
|
324
|
+
for (const i of indices) {
|
|
325
|
+
const x = posAttr.getX(i) - offset.x;
|
|
326
|
+
const y = posAttr.getY(i) - offset.y;
|
|
327
|
+
const z = posAttr.getZ(i) - offset.z;
|
|
328
|
+
const r = Math.sqrt(x * x + y * y + z * z);
|
|
329
|
+
const theta = Math.acos(y / r);
|
|
330
|
+
const phi = Math.atan2(z, x);
|
|
331
|
+
const u = (phi / (2 * Math.PI) + 0.5) * scale.x;
|
|
332
|
+
const v = theta / Math.PI * scale.y;
|
|
333
|
+
uvAttr.setXY(i, u, v);
|
|
334
|
+
}
|
|
371
335
|
}
|
|
372
336
|
function applyBoxProjection(geometry, settings, indices) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
v = z * scale.z;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
// Project on XY plane
|
|
401
|
-
u = x * scale.x;
|
|
402
|
-
v = y * scale.y;
|
|
403
|
-
}
|
|
404
|
-
uvAttr.setXY(i, u, v);
|
|
337
|
+
const posAttr = geometry.getAttribute("position");
|
|
338
|
+
const normalAttr = geometry.getAttribute("normal");
|
|
339
|
+
const uvAttr = geometry.getAttribute("uv");
|
|
340
|
+
if (!normalAttr) {
|
|
341
|
+
geometry.computeVertexNormals();
|
|
342
|
+
}
|
|
343
|
+
const { scale, offset } = settings;
|
|
344
|
+
for (const i of indices) {
|
|
345
|
+
const x = posAttr.getX(i) - offset.x;
|
|
346
|
+
const y = posAttr.getY(i) - offset.y;
|
|
347
|
+
const z = posAttr.getZ(i) - offset.z;
|
|
348
|
+
const nx = Math.abs(normalAttr?.getX(i) ?? 0);
|
|
349
|
+
const ny = Math.abs(normalAttr?.getY(i) ?? 1);
|
|
350
|
+
const nz = Math.abs(normalAttr?.getZ(i) ?? 0);
|
|
351
|
+
let u, v;
|
|
352
|
+
if (nx >= ny && nx >= nz) {
|
|
353
|
+
u = z * scale.z;
|
|
354
|
+
v = y * scale.y;
|
|
355
|
+
} else if (ny >= nx && ny >= nz) {
|
|
356
|
+
u = x * scale.x;
|
|
357
|
+
v = z * scale.z;
|
|
358
|
+
} else {
|
|
359
|
+
u = x * scale.x;
|
|
360
|
+
v = y * scale.y;
|
|
405
361
|
}
|
|
362
|
+
uvAttr.setXY(i, u, v);
|
|
363
|
+
}
|
|
406
364
|
}
|
|
407
|
-
/* ═══════════════════════════════════════════
|
|
408
|
-
UV Packing
|
|
409
|
-
═══════════════════════════════════════════ */
|
|
410
365
|
function packIslands(meshData, options) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// Move to next row
|
|
435
|
-
currentX = margin;
|
|
436
|
-
currentY += rowHeight + padding;
|
|
437
|
-
rowHeight = 0;
|
|
438
|
-
}
|
|
439
|
-
// Calculate translation to move island to current position
|
|
440
|
-
const translation = {
|
|
441
|
-
u: currentX + padding - island.bounds.minU,
|
|
442
|
-
v: currentY + padding - island.bounds.minV,
|
|
443
|
-
};
|
|
444
|
-
transforms.set(island.id, {
|
|
445
|
-
translation,
|
|
446
|
-
rotation: 0,
|
|
447
|
-
scale: 1,
|
|
448
|
-
});
|
|
449
|
-
currentX += width;
|
|
450
|
-
rowHeight = Math.max(rowHeight, height);
|
|
451
|
-
maxX = Math.max(maxX, currentX);
|
|
452
|
-
maxY = Math.max(maxY, currentY + height);
|
|
366
|
+
const transforms = /* @__PURE__ */ new Map();
|
|
367
|
+
const islands = Array.from(meshData.islands.values());
|
|
368
|
+
if (islands.length === 0) {
|
|
369
|
+
return { transforms, utilization: 0, success: true, iterations: 0 };
|
|
370
|
+
}
|
|
371
|
+
if (options.areaPriority) {
|
|
372
|
+
islands.sort((a, b) => b.area - a.area);
|
|
373
|
+
}
|
|
374
|
+
const padding = options.padding / options.resolution;
|
|
375
|
+
const margin = options.margin;
|
|
376
|
+
let currentX = margin;
|
|
377
|
+
let currentY = margin;
|
|
378
|
+
let rowHeight = 0;
|
|
379
|
+
let maxX = 0;
|
|
380
|
+
let maxY = 0;
|
|
381
|
+
for (const island of islands) {
|
|
382
|
+
const size = getBoundsSize(island.bounds);
|
|
383
|
+
const width = size.u + padding * 2;
|
|
384
|
+
const height = size.v + padding * 2;
|
|
385
|
+
if (currentX + width > 1 - margin) {
|
|
386
|
+
currentX = margin;
|
|
387
|
+
currentY += rowHeight + padding;
|
|
388
|
+
rowHeight = 0;
|
|
453
389
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const utilization = usedArea > 0 ? totalIslandArea / usedArea : 0;
|
|
458
|
-
return {
|
|
459
|
-
transforms,
|
|
460
|
-
utilization,
|
|
461
|
-
success: maxX <= 1 && maxY <= 1,
|
|
462
|
-
iterations: 1,
|
|
390
|
+
const translation = {
|
|
391
|
+
u: currentX + padding - island.bounds.minU,
|
|
392
|
+
v: currentY + padding - island.bounds.minV
|
|
463
393
|
};
|
|
394
|
+
transforms.set(island.id, {
|
|
395
|
+
translation,
|
|
396
|
+
rotation: 0,
|
|
397
|
+
scale: 1
|
|
398
|
+
});
|
|
399
|
+
currentX += width;
|
|
400
|
+
rowHeight = Math.max(rowHeight, height);
|
|
401
|
+
maxX = Math.max(maxX, currentX);
|
|
402
|
+
maxY = Math.max(maxY, currentY + height);
|
|
403
|
+
}
|
|
404
|
+
const totalIslandArea = islands.reduce((sum, i) => sum + i.area, 0);
|
|
405
|
+
const usedArea = maxX * maxY;
|
|
406
|
+
const utilization = usedArea > 0 ? totalIslandArea / usedArea : 0;
|
|
407
|
+
return {
|
|
408
|
+
transforms,
|
|
409
|
+
utilization,
|
|
410
|
+
success: maxX <= 1 && maxY <= 1,
|
|
411
|
+
iterations: 1
|
|
412
|
+
};
|
|
464
413
|
}
|
|
465
414
|
function applyIslandTransforms(meshData, transforms) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
const center = island.center;
|
|
472
|
-
for (const vertexId of island.vertices) {
|
|
473
|
-
const vertex = meshData.vertices.get(vertexId);
|
|
474
|
-
if (!vertex) {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
let pos = vertex.position;
|
|
478
|
-
// Apply rotation around center
|
|
479
|
-
if (transform.rotation !== 0) {
|
|
480
|
-
pos = uvRotate(pos, transform.rotation, center);
|
|
481
|
-
}
|
|
482
|
-
// Apply scale from center
|
|
483
|
-
if (transform.scale !== 1) {
|
|
484
|
-
pos = {
|
|
485
|
-
u: center.u + (pos.u - center.u) * transform.scale,
|
|
486
|
-
v: center.v + (pos.v - center.v) * transform.scale,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
// Apply translation
|
|
490
|
-
pos = uvAdd(pos, transform.translation);
|
|
491
|
-
vertex.position = pos;
|
|
492
|
-
}
|
|
493
|
-
// Update island bounds
|
|
494
|
-
let bounds = createEmptyBounds();
|
|
495
|
-
for (const vertexId of island.vertices) {
|
|
496
|
-
bounds = expandBounds(bounds, meshData.vertices.get(vertexId).position);
|
|
497
|
-
}
|
|
498
|
-
island.bounds = bounds;
|
|
499
|
-
island.center = getBoundsCenter(bounds);
|
|
415
|
+
for (const [islandId, transform] of transforms) {
|
|
416
|
+
const island = meshData.islands.get(islandId);
|
|
417
|
+
if (!island) {
|
|
418
|
+
continue;
|
|
500
419
|
}
|
|
501
|
-
|
|
420
|
+
const center = island.center;
|
|
421
|
+
for (const vertexId of island.vertices) {
|
|
422
|
+
const vertex = meshData.vertices.get(vertexId);
|
|
423
|
+
if (!vertex) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
let pos = vertex.position;
|
|
427
|
+
if (transform.rotation !== 0) {
|
|
428
|
+
pos = uvRotate(pos, transform.rotation, center);
|
|
429
|
+
}
|
|
430
|
+
if (transform.scale !== 1) {
|
|
431
|
+
pos = {
|
|
432
|
+
u: center.u + (pos.u - center.u) * transform.scale,
|
|
433
|
+
v: center.v + (pos.v - center.v) * transform.scale
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
pos = uvAdd(pos, transform.translation);
|
|
437
|
+
vertex.position = pos;
|
|
438
|
+
}
|
|
439
|
+
let bounds = createEmptyBounds();
|
|
440
|
+
for (const vertexId of island.vertices) {
|
|
441
|
+
bounds = expandBounds(bounds, meshData.vertices.get(vertexId).position);
|
|
442
|
+
}
|
|
443
|
+
island.bounds = bounds;
|
|
444
|
+
island.center = getBoundsCenter(bounds);
|
|
445
|
+
}
|
|
446
|
+
meshData.dirty = true;
|
|
502
447
|
}
|
|
503
|
-
/* ═══════════════════════════════════════════
|
|
504
|
-
UV Relaxation
|
|
505
|
-
═══════════════════════════════════════════ */
|
|
506
448
|
function relaxUVs(meshData, options, selectedVertices) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
else if (edge.endVertex === vertexId) {
|
|
522
|
-
neighbors.push(meshData.vertices.get(edge.startVertex).position);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (neighbors.length === 0) {
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
// Move towards average of neighbors (Laplacian smoothing)
|
|
529
|
-
const avg = {
|
|
530
|
-
u: neighbors.reduce((s, n) => s + n.u, 0) / neighbors.length,
|
|
531
|
-
v: neighbors.reduce((s, n) => s + n.v, 0) / neighbors.length,
|
|
532
|
-
};
|
|
533
|
-
const factor = 0.5; // Relaxation factor
|
|
534
|
-
vertex.position = uvLerp(vertex.position, avg, factor);
|
|
535
|
-
if (!affectedVertices.includes(vertexId)) {
|
|
536
|
-
affectedVertices.push(vertexId);
|
|
537
|
-
}
|
|
449
|
+
const verticesToRelax = selectedVertices ?? new Set(meshData.vertices.keys());
|
|
450
|
+
const affectedVertices = [];
|
|
451
|
+
for (let iter = 0; iter < options.iterations; iter++) {
|
|
452
|
+
for (const vertexId of verticesToRelax) {
|
|
453
|
+
const vertex = meshData.vertices.get(vertexId);
|
|
454
|
+
if (!vertex || options.preservePinned && vertex.pinned) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const neighbors = [];
|
|
458
|
+
for (const edge of meshData.edges.values()) {
|
|
459
|
+
if (edge.startVertex === vertexId) {
|
|
460
|
+
neighbors.push(meshData.vertices.get(edge.endVertex).position);
|
|
461
|
+
} else if (edge.endVertex === vertexId) {
|
|
462
|
+
neighbors.push(meshData.vertices.get(edge.startVertex).position);
|
|
538
463
|
}
|
|
464
|
+
}
|
|
465
|
+
if (neighbors.length === 0) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const avg = {
|
|
469
|
+
u: neighbors.reduce((s, n) => s + n.u, 0) / neighbors.length,
|
|
470
|
+
v: neighbors.reduce((s, n) => s + n.v, 0) / neighbors.length
|
|
471
|
+
};
|
|
472
|
+
const factor = 0.5;
|
|
473
|
+
vertex.position = uvLerp(vertex.position, avg, factor);
|
|
474
|
+
if (!affectedVertices.includes(vertexId)) {
|
|
475
|
+
affectedVertices.push(vertexId);
|
|
476
|
+
}
|
|
539
477
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
478
|
+
}
|
|
479
|
+
meshData.dirty = true;
|
|
480
|
+
return {
|
|
481
|
+
success: true,
|
|
482
|
+
affectedVertices
|
|
483
|
+
};
|
|
545
484
|
}
|
|
546
|
-
/* ═══════════════════════════════════════════
|
|
547
|
-
Selection Utilities
|
|
548
|
-
═══════════════════════════════════════════ */
|
|
549
485
|
function selectVerticesInRect(meshData, rect, additive = false) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
vertex.selected = false;
|
|
558
|
-
}
|
|
486
|
+
const selected = [];
|
|
487
|
+
for (const [id, vertex] of meshData.vertices) {
|
|
488
|
+
if (boundsContains(rect, vertex.position)) {
|
|
489
|
+
vertex.selected = true;
|
|
490
|
+
selected.push(id);
|
|
491
|
+
} else if (!additive) {
|
|
492
|
+
vertex.selected = false;
|
|
559
493
|
}
|
|
560
|
-
|
|
494
|
+
}
|
|
495
|
+
return selected;
|
|
561
496
|
}
|
|
562
497
|
function selectIsland(meshData, islandId, additive = false) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
for (const e of meshData.edges.values()) {
|
|
568
|
-
e.selected = false;
|
|
569
|
-
}
|
|
570
|
-
for (const f of meshData.faces.values()) {
|
|
571
|
-
f.selected = false;
|
|
572
|
-
}
|
|
573
|
-
for (const i of meshData.islands.values()) {
|
|
574
|
-
i.selected = false;
|
|
575
|
-
}
|
|
498
|
+
if (!additive) {
|
|
499
|
+
for (const v of meshData.vertices.values()) {
|
|
500
|
+
v.selected = false;
|
|
576
501
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
return;
|
|
502
|
+
for (const e of meshData.edges.values()) {
|
|
503
|
+
e.selected = false;
|
|
580
504
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const v = meshData.vertices.get(vId);
|
|
584
|
-
if (v) {
|
|
585
|
-
v.selected = true;
|
|
586
|
-
}
|
|
505
|
+
for (const f of meshData.faces.values()) {
|
|
506
|
+
f.selected = false;
|
|
587
507
|
}
|
|
588
|
-
for (const
|
|
589
|
-
|
|
590
|
-
if (e) {
|
|
591
|
-
e.selected = true;
|
|
592
|
-
}
|
|
508
|
+
for (const i of meshData.islands.values()) {
|
|
509
|
+
i.selected = false;
|
|
593
510
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
511
|
+
}
|
|
512
|
+
const island = meshData.islands.get(islandId);
|
|
513
|
+
if (!island) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
island.selected = true;
|
|
517
|
+
for (const vId of island.vertices) {
|
|
518
|
+
const v = meshData.vertices.get(vId);
|
|
519
|
+
if (v) {
|
|
520
|
+
v.selected = true;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const eId of island.edges) {
|
|
524
|
+
const e = meshData.edges.get(eId);
|
|
525
|
+
if (e) {
|
|
526
|
+
e.selected = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const fId of island.faces) {
|
|
530
|
+
const f = meshData.faces.get(fId);
|
|
531
|
+
if (f) {
|
|
532
|
+
f.selected = true;
|
|
599
533
|
}
|
|
534
|
+
}
|
|
600
535
|
}
|
|
601
536
|
function getSelectedVertices(meshData) {
|
|
602
|
-
|
|
537
|
+
return Array.from(meshData.vertices.values()).filter((v) => v.selected);
|
|
603
538
|
}
|
|
604
539
|
function getSelectedIslands(meshData) {
|
|
605
|
-
|
|
540
|
+
return Array.from(meshData.islands.values()).filter((i) => i.selected);
|
|
606
541
|
}
|
|
607
|
-
/* ═══════════════════════════════════════════
|
|
608
|
-
Seam Operations
|
|
609
|
-
═══════════════════════════════════════════ */
|
|
610
542
|
function markSeam(meshData, edgeId, isSeam) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
543
|
+
const edge = meshData.edges.get(edgeId);
|
|
544
|
+
if (edge) {
|
|
545
|
+
edge.seam = isSeam;
|
|
546
|
+
meshData.dirty = true;
|
|
547
|
+
}
|
|
616
548
|
}
|
|
617
549
|
function clearAllSeams(meshData) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
550
|
+
for (const edge of meshData.edges.values()) {
|
|
551
|
+
edge.seam = false;
|
|
552
|
+
}
|
|
553
|
+
meshData.dirty = true;
|
|
622
554
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
uvAttr.needsUpdate = true;
|
|
635
|
-
meshData.dirty = false;
|
|
555
|
+
function syncToGeometry(meshData, geometry, channel = "uv") {
|
|
556
|
+
const uvAttr = geometry.getAttribute(channel);
|
|
557
|
+
if (!uvAttr) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
for (const vertex of meshData.vertices.values()) {
|
|
561
|
+
uvAttr.setXY(vertex.index, vertex.position.u, vertex.position.v);
|
|
562
|
+
}
|
|
563
|
+
uvAttr.needsUpdate = true;
|
|
564
|
+
meshData.dirty = false;
|
|
636
565
|
}
|
|
637
|
-
|
|
638
|
-
|
|
566
|
+
export {
|
|
567
|
+
applyIslandTransforms,
|
|
568
|
+
applyProjection,
|
|
569
|
+
boundsContains,
|
|
570
|
+
boundsOverlap,
|
|
571
|
+
clearAllSeams,
|
|
572
|
+
computeIslands,
|
|
573
|
+
createEmptyBounds,
|
|
574
|
+
expandBounds,
|
|
575
|
+
extractUVMeshData,
|
|
576
|
+
generateId,
|
|
577
|
+
getBoundsArea,
|
|
578
|
+
getBoundsCenter,
|
|
579
|
+
getBoundsSize,
|
|
580
|
+
getSelectedIslands,
|
|
581
|
+
getSelectedVertices,
|
|
582
|
+
markSeam,
|
|
583
|
+
mergeBounds,
|
|
584
|
+
packIslands,
|
|
585
|
+
relaxUVs,
|
|
586
|
+
selectIsland,
|
|
587
|
+
selectVerticesInRect,
|
|
588
|
+
syncToGeometry,
|
|
589
|
+
uvAdd,
|
|
590
|
+
uvCross,
|
|
591
|
+
uvDistance,
|
|
592
|
+
uvLerp,
|
|
593
|
+
uvRotate,
|
|
594
|
+
uvScale,
|
|
595
|
+
uvSub
|
|
596
|
+
};
|