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