@xiaozhu2007/koishi-plugin-litematic-renderer 0.4.30
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/LICENSE +21 -0
- package/README.md +15 -0
- package/assets/java-bridge/litematic-render-bridge-0.1.2.jar +0 -0
- package/assets/standalone-renderer/litematic-standalone-renderer-0.2.6.jar +0 -0
- package/assets/threeviews/index.html +1270 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/api.js./344/270/213/350/275/275 +917 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/assets.js./344/270/213/350/275/275 +1 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/atlas.png +0 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/deepslate-helpers.js./344/270/213/350/275/275 +118 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/deepslate.js./344/270/213/350/275/275 +2 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/gl-matrix-min.js./344/270/213/350/275/275 +28 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/litematic-utils.js./344/270/213/350/275/275 +174 -0
- package/assets/threeviews//346/210/221/347/232/204/344/270/226/347/225/214-Litematic/346/212/225/345/275/261/344/270/211/350/247/206/345/233/276/351/242/204/350/247/210_files/opaque.js./344/270/213/350/275/275 +413 -0
- package/assets/vanilla-resources/Minecraft-26.2-Vanilla-Resources.zip +0 -0
- package/lib/index.d.ts +114 -0
- package/lib/index.js +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
// Litematic Viewer API Service
|
|
2
|
+
const { vec3, mat4 } = glMatrix;
|
|
3
|
+
|
|
4
|
+
class LitematicViewerAPI {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.structureLitematic = null;
|
|
7
|
+
this.viewer = null;
|
|
8
|
+
this.canvas = null;
|
|
9
|
+
this.webglContext = null;
|
|
10
|
+
this.cameraPos = vec3.create();
|
|
11
|
+
this.cameraPitch = 0.8;
|
|
12
|
+
this.cameraYaw = 0.5;
|
|
13
|
+
this.isManualPosition = false; // 添加标记,用于判断是否手动设置过相机位置
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从文件加载 Litematic 数据
|
|
18
|
+
* @param {File} file - Litematic 文件
|
|
19
|
+
* @returns {Promise<Object>} - 加载的 Litematic 数据
|
|
20
|
+
*/
|
|
21
|
+
async loadFromFile(file) {
|
|
22
|
+
if (!deepslateResources) {
|
|
23
|
+
throw new Error('Deepslate resources not initialized');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const reader = new FileReader();
|
|
28
|
+
reader.onload = (event) => {
|
|
29
|
+
try {
|
|
30
|
+
const nbtData = deepslate.readNbt(new Uint8Array(event.target.result));
|
|
31
|
+
this.structureLitematic = readLitematicFromNBTData(nbtData);
|
|
32
|
+
resolve(this.structureLitematic);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
reject(error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
reader.onerror = (error) => reject(error);
|
|
38
|
+
reader.readAsArrayBuffer(file);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 从 URL 加载 Litematic 数据
|
|
44
|
+
* @param {string} url - Litematic 文件的 URL
|
|
45
|
+
* @returns {Promise<Object>} - 加载的 Litematic 数据
|
|
46
|
+
*/
|
|
47
|
+
async loadFromURL(url) {
|
|
48
|
+
if (!deepslateResources) {
|
|
49
|
+
throw new Error('Deepslate resources not initialized');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const proxyUrl = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url);
|
|
53
|
+
const response = await fetch(proxyUrl);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error('Failed to fetch file from URL');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const blob = await response.blob();
|
|
59
|
+
return this.loadFromFile(blob);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 获取方块统计信息
|
|
64
|
+
* @returns {Object} - 方块统计信息
|
|
65
|
+
*/
|
|
66
|
+
getMaterialList() {
|
|
67
|
+
if (!this.structureLitematic) {
|
|
68
|
+
throw new Error('No litematic data loaded');
|
|
69
|
+
}
|
|
70
|
+
return getMaterialList(this.structureLitematic);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 设置查看器视图范围
|
|
75
|
+
* @param {number} yMin - Y轴最小值
|
|
76
|
+
* @param {number} yMax - Y轴最大值
|
|
77
|
+
*/
|
|
78
|
+
setViewRange(yMin, yMax) {
|
|
79
|
+
if (!this.structureLitematic) {
|
|
80
|
+
throw new Error('No litematic data loaded');
|
|
81
|
+
}
|
|
82
|
+
setStructure(structureFromLitematic(this.structureLitematic, yMin, yMax));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 初始化查看器
|
|
87
|
+
* @param {HTMLElement} container - 查看器容器元素
|
|
88
|
+
*/
|
|
89
|
+
initViewer(container) {
|
|
90
|
+
if (!deepslateResources) {
|
|
91
|
+
throw new Error('Deepslate resources not initialized');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 创建查看器容器
|
|
95
|
+
const viewer = document.createElement('div');
|
|
96
|
+
viewer.id = 'viewer';
|
|
97
|
+
viewer.style.width = '100%';
|
|
98
|
+
viewer.style.height = '100%';
|
|
99
|
+
container.appendChild(viewer);
|
|
100
|
+
|
|
101
|
+
// 创建渲染画布
|
|
102
|
+
createRenderCanvas();
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 导出材料列表为 CSV
|
|
109
|
+
* @returns {string} - CSV 格式的材料列表
|
|
110
|
+
*/
|
|
111
|
+
exportMaterialListCSV() {
|
|
112
|
+
const blockCounts = this.getMaterialList();
|
|
113
|
+
return Object.entries(blockCounts)
|
|
114
|
+
.sort(([,a], [,b]) => b - a)
|
|
115
|
+
.map(([key, val]) => `${key},${val}`)
|
|
116
|
+
.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 设置相机位置和角度
|
|
121
|
+
* @param {Object} options - 相机选项
|
|
122
|
+
* @param {Array<number>} options.position - 相机位置 [x, y, z]
|
|
123
|
+
* @param {number} options.pitch - 俯仰角(弧度)
|
|
124
|
+
* @param {number} options.yaw - 偏航角(弧度)
|
|
125
|
+
*/
|
|
126
|
+
setCameraPosition(options = {}) {
|
|
127
|
+
const { position, pitch, yaw } = options;
|
|
128
|
+
|
|
129
|
+
// 更新实例状态
|
|
130
|
+
if (position && position.length === 3) {
|
|
131
|
+
vec3.set(this.cameraPos, position[0], position[1], position[2]);
|
|
132
|
+
// 同步更新全局状态
|
|
133
|
+
if (window.cameraPos) {
|
|
134
|
+
vec3.set(window.cameraPos, position[0], position[1], position[2]);
|
|
135
|
+
}
|
|
136
|
+
this.isManualPosition = true; // 标记为手动设置位置
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof pitch === 'number') {
|
|
140
|
+
this.cameraPitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
|
|
141
|
+
// 同步更新全局状态
|
|
142
|
+
if (window.cameraPitch !== undefined) {
|
|
143
|
+
window.cameraPitch = this.cameraPitch;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (typeof yaw === 'number') {
|
|
148
|
+
this.cameraYaw = yaw % (Math.PI * 2);
|
|
149
|
+
// 同步更新全局状态
|
|
150
|
+
if (window.cameraYaw !== undefined) {
|
|
151
|
+
window.cameraYaw = this.cameraYaw;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 触发重新渲染
|
|
156
|
+
if (window.render && typeof window.render === 'function') {
|
|
157
|
+
requestAnimationFrame(window.render);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 获取当前相机位置和角度
|
|
163
|
+
* @returns {Object} 相机位置和角度信息
|
|
164
|
+
*/
|
|
165
|
+
getCameraPosition() {
|
|
166
|
+
return {
|
|
167
|
+
position: [this.cameraPos[0], this.cameraPos[1], this.cameraPos[2]],
|
|
168
|
+
pitch: this.cameraPitch,
|
|
169
|
+
yaw: this.cameraYaw
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 渲染结构并返回图片
|
|
175
|
+
* @param {Object} options - 渲染选项
|
|
176
|
+
* @param {number} [options.width=800] - 图片宽度
|
|
177
|
+
* @param {number} [options.height=600] - 图片高度
|
|
178
|
+
* @param {number} [options.cameraPitch=0.8] - 相机俯仰角
|
|
179
|
+
* @param {number} [options.cameraYaw=0.5] - 相机偏航角
|
|
180
|
+
* @param {boolean} [options.autoPosition=true] - 是否自动定位
|
|
181
|
+
* @param {boolean} [options.antialias=true] - 是否启用多重采样抗锯齿
|
|
182
|
+
* @param {string} [options.quality='high'] - 渲染质量选项
|
|
183
|
+
* @returns {Promise<string>} - 返回 base64 格式的图片数据
|
|
184
|
+
*/
|
|
185
|
+
async renderToImage(options = {}) {
|
|
186
|
+
if (!this.structureLitematic) {
|
|
187
|
+
throw new Error('No litematic data loaded');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const {
|
|
191
|
+
width = 800,
|
|
192
|
+
height = 600,
|
|
193
|
+
cameraPitch = this.cameraPitch,
|
|
194
|
+
cameraYaw = this.cameraYaw,
|
|
195
|
+
autoPosition = true,
|
|
196
|
+
antialias = true,
|
|
197
|
+
quality = 'high', // 新增:渲染质量选项
|
|
198
|
+
preserveRenderResolution = false,
|
|
199
|
+
transparentBackground = false,
|
|
200
|
+
orthographic = false,
|
|
201
|
+
modelScale = 1,
|
|
202
|
+
drawGrid = true
|
|
203
|
+
} = options;
|
|
204
|
+
|
|
205
|
+
console.log('开始渲染,参数:', { width, height, cameraPitch, cameraYaw, autoPosition, antialias, quality });
|
|
206
|
+
|
|
207
|
+
// 清理旧的画布
|
|
208
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
209
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 创建新的画布
|
|
213
|
+
this.canvas = document.createElement('canvas');
|
|
214
|
+
document.body.appendChild(this.canvas);
|
|
215
|
+
|
|
216
|
+
// 根据质量设置调整实际渲染分辨率
|
|
217
|
+
let renderWidth = width;
|
|
218
|
+
let renderHeight = height;
|
|
219
|
+
if (quality === 'high') {
|
|
220
|
+
renderWidth = width * 2;
|
|
221
|
+
renderHeight = height * 2;
|
|
222
|
+
} else if (quality === 'ultra') {
|
|
223
|
+
renderWidth = width * 4;
|
|
224
|
+
renderHeight = height * 4;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.canvas.width = renderWidth;
|
|
228
|
+
this.canvas.height = renderHeight;
|
|
229
|
+
|
|
230
|
+
// 创建 WebGL 上下文,增加更多质量相关选项
|
|
231
|
+
this.webglContext = this.canvas.getContext('webgl', {
|
|
232
|
+
antialias: antialias,
|
|
233
|
+
depth: true,
|
|
234
|
+
preserveDrawingBuffer: true,
|
|
235
|
+
alpha: true,
|
|
236
|
+
stencil: true,
|
|
237
|
+
premultipliedAlpha: true,
|
|
238
|
+
failIfMajorPerformanceCaveat: false,
|
|
239
|
+
powerPreference: 'high-performance', // 优先使用高性能模式
|
|
240
|
+
desynchronized: false, // 禁用异步渲染以确保最高质量
|
|
241
|
+
xrCompatible: false, // 禁用XR兼容性以提高性能
|
|
242
|
+
preserveDrawingBuffer: true, // 保留绘图缓冲区
|
|
243
|
+
alpha: true, // 启用alpha通道
|
|
244
|
+
depth: true, // 启用深度测试
|
|
245
|
+
stencil: true, // 启用模板测试
|
|
246
|
+
antialias: true, // 启用抗锯齿
|
|
247
|
+
premultipliedAlpha: true, // 启用预乘alpha
|
|
248
|
+
failIfMajorPerformanceCaveat: false // 即使性能较低也继续
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!this.webglContext) {
|
|
252
|
+
throw new Error('无法创建 WebGL 上下文');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 设置 WebGL 状态
|
|
256
|
+
this.webglContext.viewport(0, 0, renderWidth, renderHeight);
|
|
257
|
+
if (transparentBackground) {
|
|
258
|
+
this.webglContext.clearColor(0, 0, 0, 0);
|
|
259
|
+
} else {
|
|
260
|
+
this.webglContext.clearColor(35/255, 35/255, 35/255, 1.0);
|
|
261
|
+
}
|
|
262
|
+
this.webglContext.clear(this.webglContext.COLOR_BUFFER_BIT | this.webglContext.DEPTH_BUFFER_BIT);
|
|
263
|
+
|
|
264
|
+
// 添加背景纹理
|
|
265
|
+
if (!transparentBackground) {
|
|
266
|
+
this._renderBackgroundTexture(renderWidth, renderHeight);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.webglContext.enable(this.webglContext.DEPTH_TEST);
|
|
270
|
+
this.webglContext.enable(this.webglContext.CULL_FACE);
|
|
271
|
+
|
|
272
|
+
// 启用多重采样抗锯齿
|
|
273
|
+
if (antialias) {
|
|
274
|
+
this.webglContext.enable(this.webglContext.SAMPLE_COVERAGE);
|
|
275
|
+
this.webglContext.sampleCoverage(1.0, false);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 设置纹理过滤质量
|
|
279
|
+
this.webglContext.texParameteri(this.webglContext.TEXTURE_2D, this.webglContext.TEXTURE_MIN_FILTER, this.webglContext.LINEAR_MIPMAP_LINEAR);
|
|
280
|
+
this.webglContext.texParameteri(this.webglContext.TEXTURE_2D, this.webglContext.TEXTURE_MAG_FILTER, this.webglContext.LINEAR);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// 创建结构
|
|
284
|
+
const structure = structureFromLitematic(this.structureLitematic);
|
|
285
|
+
const size = structure.getSize();
|
|
286
|
+
console.log('结构大小:', size);
|
|
287
|
+
console.log('结构坐标范围:');
|
|
288
|
+
console.log('X: 0 ->', size[0]);
|
|
289
|
+
console.log('Y: 0 ->', size[1]);
|
|
290
|
+
console.log('Z: 0 ->', size[2]);
|
|
291
|
+
|
|
292
|
+
// 创建渲染器,增加更多渲染选项
|
|
293
|
+
const renderer = new deepslate.StructureRenderer(
|
|
294
|
+
this.webglContext,
|
|
295
|
+
structure,
|
|
296
|
+
deepslateResources,
|
|
297
|
+
{
|
|
298
|
+
// 基础渲染设置
|
|
299
|
+
chunkSize: 8, // 区块大小,影响渲染性能
|
|
300
|
+
|
|
301
|
+
// 光照和阴影
|
|
302
|
+
enableShadows: true, // 启用阴影渲染
|
|
303
|
+
enableAmbientOcclusion: true, // 启用环境光遮蔽
|
|
304
|
+
|
|
305
|
+
// 纹理和质量设置
|
|
306
|
+
textureQuality: quality, // 纹理质量设置
|
|
307
|
+
|
|
308
|
+
// 大气效果
|
|
309
|
+
enableFog: true, // 启用雾效果
|
|
310
|
+
fogDensity: 0.01, // 雾的密度
|
|
311
|
+
|
|
312
|
+
// 后期处理效果
|
|
313
|
+
enableBloom: true, // 启用泛光效果
|
|
314
|
+
bloomIntensity: 0.5, // 泛光强度
|
|
315
|
+
enableSSAO: true, // 启用屏幕空间环境光遮蔽
|
|
316
|
+
ssaoRadius: 0.5, // SSAO采样半径
|
|
317
|
+
enableFXAA: true, // 启用快速近似抗锯齿
|
|
318
|
+
enableTAA: true, // 启用时间性抗锯齿
|
|
319
|
+
enableMotionBlur: false, // 禁用动态模糊
|
|
320
|
+
enableDepthOfField: false, // 禁用景深效果
|
|
321
|
+
|
|
322
|
+
// 透明材质优化
|
|
323
|
+
transparentSorting: true, // 启用透明材质排序
|
|
324
|
+
transparentDepthWrite: false, // 禁用透明材质的深度写入
|
|
325
|
+
transparentBlending: 'normal', // 使用标准混合模式
|
|
326
|
+
transparentAlphaTest: 0.1, // 设置透明测试阈值
|
|
327
|
+
transparentRefraction: true, // 启用折射效果
|
|
328
|
+
transparentRefractionIndex: 1.5, // 设置折射率(玻璃的折射率)
|
|
329
|
+
transparentReflection: true, // 启用反射效果
|
|
330
|
+
transparentReflectionStrength: 0.8, // 设置反射强度
|
|
331
|
+
transparentFresnel: true, // 启用菲涅尔效应
|
|
332
|
+
transparentFresnelBias: 0.1, // 设置菲涅尔偏差
|
|
333
|
+
transparentFresnelScale: 1.0, // 设置菲涅尔缩放
|
|
334
|
+
transparentFresnelPower: 2.0 // 设置菲涅尔幂次
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (orthographic) {
|
|
339
|
+
const aspectRatio = renderWidth / renderHeight;
|
|
340
|
+
renderer.projMatrix = mat4.create();
|
|
341
|
+
mat4.ortho(renderer.projMatrix, -aspectRatio, aspectRatio, -1, 1, -1000, 3000);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 设置WebGL混合模式
|
|
345
|
+
this.webglContext.enable(this.webglContext.BLEND);
|
|
346
|
+
this.webglContext.blendFuncSeparate(
|
|
347
|
+
this.webglContext.SRC_ALPHA,
|
|
348
|
+
this.webglContext.ONE_MINUS_SRC_ALPHA,
|
|
349
|
+
this.webglContext.ONE,
|
|
350
|
+
this.webglContext.ONE_MINUS_SRC_ALPHA
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// 更新全局相机状态
|
|
354
|
+
if (window.cameraPitch !== undefined) {
|
|
355
|
+
window.cameraPitch = cameraPitch;
|
|
356
|
+
}
|
|
357
|
+
if (window.cameraYaw !== undefined) {
|
|
358
|
+
window.cameraYaw = cameraYaw;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 如果是自动定位且未手动设置位置,计算最佳相机位置
|
|
362
|
+
if (autoPosition && !this.isManualPosition) {
|
|
363
|
+
// 计算相机位置
|
|
364
|
+
// 使用参考结构 (9x9x16) 的比例来计算
|
|
365
|
+
const refX = 9, refY = 9, refZ = 16;
|
|
366
|
+
const refCamY = -15, refCamZ = -20;
|
|
367
|
+
|
|
368
|
+
// 根据结构大小计算缩放比例
|
|
369
|
+
const scaleY = size[1] / refY;
|
|
370
|
+
const scaleZ = size[2] / refZ;
|
|
371
|
+
|
|
372
|
+
// 计算相机位置
|
|
373
|
+
const cameraX = 0; // 保持在中心线上
|
|
374
|
+
const cameraY = refCamY * scaleY; // 根据高度缩放Y轴位置
|
|
375
|
+
const cameraZ = refCamZ * scaleZ; // 根据深度缩放Z轴位置
|
|
376
|
+
|
|
377
|
+
if (window.cameraPos) {
|
|
378
|
+
vec3.set(window.cameraPos, cameraX, cameraY, cameraZ);
|
|
379
|
+
console.log('自动计算的相机位置:', [cameraX, cameraY, cameraZ]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 同时更新实例的相机位置
|
|
383
|
+
vec3.set(this.cameraPos, cameraX, cameraY, cameraZ);
|
|
384
|
+
} else {
|
|
385
|
+
console.log('使用当前相机位置:', [this.cameraPos[0], this.cameraPos[1], this.cameraPos[2]]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 创建视图矩阵
|
|
389
|
+
const view = mat4.create();
|
|
390
|
+
|
|
391
|
+
// 应用相机变换
|
|
392
|
+
mat4.scale(view, view, [modelScale, modelScale, modelScale]);
|
|
393
|
+
mat4.rotateX(view, view, cameraPitch);
|
|
394
|
+
mat4.rotateY(view, view, cameraYaw);
|
|
395
|
+
mat4.translate(view, view, this.cameraPos);
|
|
396
|
+
|
|
397
|
+
// 渲染结构和网格
|
|
398
|
+
renderer.drawStructure(view);
|
|
399
|
+
if (drawGrid) renderer.drawGrid(view);
|
|
400
|
+
|
|
401
|
+
// 渲染完成后,如果使用了高质量渲染,需要将图像缩放到目标尺寸
|
|
402
|
+
let finalImageData;
|
|
403
|
+
if (quality !== 'normal' && !preserveRenderResolution) {
|
|
404
|
+
// 创建临时画布用于缩放
|
|
405
|
+
const tempCanvas = document.createElement('canvas');
|
|
406
|
+
tempCanvas.width = width;
|
|
407
|
+
tempCanvas.height = height;
|
|
408
|
+
const tempCtx = tempCanvas.getContext('2d');
|
|
409
|
+
|
|
410
|
+
// 使用高质量缩放
|
|
411
|
+
tempCtx.imageSmoothingEnabled = true;
|
|
412
|
+
tempCtx.imageSmoothingQuality = 'high';
|
|
413
|
+
tempCtx.drawImage(this.canvas, 0, 0, width, height);
|
|
414
|
+
|
|
415
|
+
finalImageData = tempCanvas.toDataURL('image/png');
|
|
416
|
+
} else {
|
|
417
|
+
finalImageData = this.canvas.toDataURL('image/png');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 清理资源
|
|
421
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
422
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
423
|
+
}
|
|
424
|
+
this.canvas = null;
|
|
425
|
+
this.webglContext = null;
|
|
426
|
+
|
|
427
|
+
return finalImageData;
|
|
428
|
+
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error('渲染错误:', error);
|
|
431
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
432
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
433
|
+
}
|
|
434
|
+
this.canvas = null;
|
|
435
|
+
this.webglContext = null;
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 渲染背景纹理
|
|
442
|
+
* @private
|
|
443
|
+
* @param {number} width - 画布宽度
|
|
444
|
+
* @param {number} height - 画布高度
|
|
445
|
+
*/
|
|
446
|
+
_renderBackgroundTexture(width, height) {
|
|
447
|
+
const gl = this.webglContext;
|
|
448
|
+
|
|
449
|
+
// 创建着色器程序
|
|
450
|
+
const vsSource = `
|
|
451
|
+
attribute vec4 aVertexPosition;
|
|
452
|
+
attribute vec2 aTextureCoord;
|
|
453
|
+
varying vec2 vTextureCoord;
|
|
454
|
+
void main() {
|
|
455
|
+
gl_Position = aVertexPosition;
|
|
456
|
+
vTextureCoord = aTextureCoord;
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
const fsSource = `
|
|
461
|
+
precision mediump float;
|
|
462
|
+
varying vec2 vTextureCoord;
|
|
463
|
+
uniform vec2 uResolution;
|
|
464
|
+
|
|
465
|
+
// 网格纹理生成函数
|
|
466
|
+
float grid(vec2 st, float gridSize, float lineWidth) {
|
|
467
|
+
vec2 grid = fract(st * gridSize);
|
|
468
|
+
vec2 smoothGrid = smoothstep(0.0, lineWidth, grid) *
|
|
469
|
+
(1.0 - smoothstep(1.0 - lineWidth, 1.0, grid));
|
|
470
|
+
return max(smoothGrid.x, smoothGrid.y);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
void main() {
|
|
474
|
+
// 使用uv坐标生成网格
|
|
475
|
+
vec2 st = vTextureCoord * uResolution / min(uResolution.x, uResolution.y);
|
|
476
|
+
|
|
477
|
+
// 背景色 (35, 35, 35)
|
|
478
|
+
vec3 baseColor = vec3(35.0/255.0, 35.0/255.0, 35.0/255.0);
|
|
479
|
+
|
|
480
|
+
// 生成两种不同大小的网格
|
|
481
|
+
float gridSmall = grid(st, 50.0, 0.02); // 小网格
|
|
482
|
+
float gridLarge = grid(st, 10.0, 0.02); // 大网格
|
|
483
|
+
|
|
484
|
+
// 网格线颜色 (稍亮一点的灰色)
|
|
485
|
+
vec3 gridColor = vec3(55.0/255.0, 55.0/255.0, 55.0/255.0);
|
|
486
|
+
|
|
487
|
+
// 混合两种网格和背景色
|
|
488
|
+
vec3 finalColor = mix(baseColor, gridColor, gridSmall * 0.08 + gridLarge * 0.15);
|
|
489
|
+
|
|
490
|
+
gl_FragColor = vec4(finalColor, 1.0);
|
|
491
|
+
}
|
|
492
|
+
`;
|
|
493
|
+
|
|
494
|
+
// 创建着色器
|
|
495
|
+
function createShader(gl, type, source) {
|
|
496
|
+
const shader = gl.createShader(type);
|
|
497
|
+
gl.shaderSource(shader, source);
|
|
498
|
+
gl.compileShader(shader);
|
|
499
|
+
|
|
500
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
501
|
+
console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
|
|
502
|
+
gl.deleteShader(shader);
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return shader;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 创建并链接着色器程序
|
|
509
|
+
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
510
|
+
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
511
|
+
|
|
512
|
+
if (!vertexShader || !fragmentShader) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const shaderProgram = gl.createProgram();
|
|
517
|
+
gl.attachShader(shaderProgram, vertexShader);
|
|
518
|
+
gl.attachShader(shaderProgram, fragmentShader);
|
|
519
|
+
gl.linkProgram(shaderProgram);
|
|
520
|
+
|
|
521
|
+
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
|
|
522
|
+
console.error('无法初始化着色器程序:', gl.getProgramInfoLog(shaderProgram));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 创建全屏四边形
|
|
527
|
+
const positionBuffer = gl.createBuffer();
|
|
528
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
529
|
+
const positions = [
|
|
530
|
+
-1.0, 1.0,
|
|
531
|
+
1.0, 1.0,
|
|
532
|
+
1.0, -1.0,
|
|
533
|
+
-1.0, -1.0,
|
|
534
|
+
];
|
|
535
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
|
536
|
+
|
|
537
|
+
// 创建纹理坐标
|
|
538
|
+
const textureCoordBuffer = gl.createBuffer();
|
|
539
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
|
|
540
|
+
const textureCoordinates = [
|
|
541
|
+
0.0, 0.0,
|
|
542
|
+
1.0, 0.0,
|
|
543
|
+
1.0, 1.0,
|
|
544
|
+
0.0, 1.0,
|
|
545
|
+
];
|
|
546
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordinates), gl.STATIC_DRAW);
|
|
547
|
+
|
|
548
|
+
// 创建索引缓冲
|
|
549
|
+
const indexBuffer = gl.createBuffer();
|
|
550
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
551
|
+
const indices = [
|
|
552
|
+
0, 1, 2,
|
|
553
|
+
0, 2, 3,
|
|
554
|
+
];
|
|
555
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
|
556
|
+
|
|
557
|
+
// 绘制背景
|
|
558
|
+
gl.useProgram(shaderProgram);
|
|
559
|
+
|
|
560
|
+
// 设置顶点位置属性
|
|
561
|
+
const vertexPosition = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
|
|
562
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
563
|
+
gl.vertexAttribPointer(vertexPosition, 2, gl.FLOAT, false, 0, 0);
|
|
564
|
+
gl.enableVertexAttribArray(vertexPosition);
|
|
565
|
+
|
|
566
|
+
// 设置纹理坐标属性
|
|
567
|
+
const textureCoord = gl.getAttribLocation(shaderProgram, 'aTextureCoord');
|
|
568
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, textureCoordBuffer);
|
|
569
|
+
gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
|
|
570
|
+
gl.enableVertexAttribArray(textureCoord);
|
|
571
|
+
|
|
572
|
+
// 设置分辨率
|
|
573
|
+
const resolutionLocation = gl.getUniformLocation(shaderProgram, 'uResolution');
|
|
574
|
+
gl.uniform2f(resolutionLocation, width, height);
|
|
575
|
+
|
|
576
|
+
// 绘制
|
|
577
|
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
578
|
+
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
|
|
579
|
+
|
|
580
|
+
// 清除深度缓冲,但保留颜色缓冲
|
|
581
|
+
gl.clear(gl.DEPTH_BUFFER_BIT);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 渲染三视图(正视图、侧视图、俯视图)
|
|
586
|
+
* @param {Object} options - 渲染选项
|
|
587
|
+
* @param {number} [options.width=800] - 单个视图的宽度
|
|
588
|
+
* @param {number} [options.height=600] - 单个视图的高度
|
|
589
|
+
* @returns {Promise<Object>} - 返回包含三个视图的 base64 图片数据
|
|
590
|
+
*/
|
|
591
|
+
async renderThreeViews(options = {}) {
|
|
592
|
+
if (!this.structureLitematic) {
|
|
593
|
+
throw new Error('没有加载 litematic 数据');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const {
|
|
597
|
+
width = 800,
|
|
598
|
+
height = 600
|
|
599
|
+
} = options;
|
|
600
|
+
|
|
601
|
+
// 保存当前相机状态
|
|
602
|
+
const originalPos = vec3.clone(this.cameraPos);
|
|
603
|
+
const originalPitch = this.cameraPitch;
|
|
604
|
+
const originalYaw = this.cameraYaw;
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
// 创建结构
|
|
608
|
+
const structure = structureFromLitematic(this.structureLitematic);
|
|
609
|
+
const size = structure.getSize();
|
|
610
|
+
console.log('结构大小:', size);
|
|
611
|
+
|
|
612
|
+
// 计算结构中心点
|
|
613
|
+
const centerX = size[0] / 2;
|
|
614
|
+
const centerY = size[1] / 2;
|
|
615
|
+
const centerZ = size[2] / 2;
|
|
616
|
+
|
|
617
|
+
// 计算合适的观察距离
|
|
618
|
+
const maxDimension = Math.max(size[0], size[1], size[2]);
|
|
619
|
+
const distance = maxDimension * 2; // 基础观察距离
|
|
620
|
+
|
|
621
|
+
// 渲染正视图 (正面)
|
|
622
|
+
this.cameraPitch = 0;
|
|
623
|
+
this.cameraYaw = 0;
|
|
624
|
+
vec3.set(this.cameraPos,
|
|
625
|
+
-centerX, // X轴对齐中心
|
|
626
|
+
-centerY, // Y轴对齐中心
|
|
627
|
+
-distance/1.8 - centerZ // Z轴在结构前方,距离减半使视角更近
|
|
628
|
+
);
|
|
629
|
+
const frontView = await this.renderToImage({
|
|
630
|
+
width,
|
|
631
|
+
height,
|
|
632
|
+
cameraPitch: this.cameraPitch,
|
|
633
|
+
cameraYaw: this.cameraYaw,
|
|
634
|
+
autoPosition: false
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// 渲染侧视图 (右侧)
|
|
638
|
+
this.cameraPitch = 0;
|
|
639
|
+
this.cameraYaw = -Math.PI / 2;
|
|
640
|
+
vec3.set(this.cameraPos,
|
|
641
|
+
-distance/1.8 - centerX, // X轴在结构右侧,距离减半使视角更近
|
|
642
|
+
-centerY, // Y轴对齐中心
|
|
643
|
+
-centerZ, // Z轴对齐中心
|
|
644
|
+
);
|
|
645
|
+
const sideView = await this.renderToImage({
|
|
646
|
+
width,
|
|
647
|
+
height,
|
|
648
|
+
cameraPitch: this.cameraPitch,
|
|
649
|
+
cameraYaw: this.cameraYaw,
|
|
650
|
+
autoPosition: false
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// 渲染俯视图 (顶部)
|
|
654
|
+
this.cameraPitch = Math.PI / 2; // 相机向下看
|
|
655
|
+
this.cameraYaw = 0;
|
|
656
|
+
vec3.set(this.cameraPos,
|
|
657
|
+
-centerX, // X轴对齐中心
|
|
658
|
+
-distance/1.8 - centerY, // Y轴在结构上方,距离减半使视角更近
|
|
659
|
+
-centerZ // Z轴对齐中心
|
|
660
|
+
);
|
|
661
|
+
const topView = await this.renderToImage({
|
|
662
|
+
width,
|
|
663
|
+
height,
|
|
664
|
+
cameraPitch: this.cameraPitch,
|
|
665
|
+
cameraYaw: this.cameraYaw,
|
|
666
|
+
autoPosition: false
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// 恢复原始相机状态
|
|
670
|
+
vec3.copy(this.cameraPos, originalPos);
|
|
671
|
+
this.cameraPitch = originalPitch;
|
|
672
|
+
this.cameraYaw = originalYaw;
|
|
673
|
+
|
|
674
|
+
return {
|
|
675
|
+
frontView, // 正视图
|
|
676
|
+
sideView, // 侧视图
|
|
677
|
+
topView // 俯视图
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
} catch (error) {
|
|
681
|
+
// 发生错误时也要恢复相机状态
|
|
682
|
+
vec3.copy(this.cameraPos, originalPos);
|
|
683
|
+
this.cameraPitch = originalPitch;
|
|
684
|
+
this.cameraYaw = originalYaw;
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* 单独渲染轴测图
|
|
691
|
+
* @param {Object} options - 渲染选项
|
|
692
|
+
* @param {number} [options.width=800] - 图片宽度
|
|
693
|
+
* @param {number} [options.height=600] - 图片高度
|
|
694
|
+
* @returns {Promise<string>} - 返回轴测图的 base64 图片数据
|
|
695
|
+
*/
|
|
696
|
+
async renderIsometricView(options = {}) {
|
|
697
|
+
if (!this.structureLitematic) {
|
|
698
|
+
throw new Error('没有加载 litematic 数据');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const { width = 800, height = 600 } = options;
|
|
702
|
+
const square = options.isometricSquare !== false;
|
|
703
|
+
const outputWidth = square ? Math.max(width, height) : width;
|
|
704
|
+
const outputHeight = square ? outputWidth : height;
|
|
705
|
+
|
|
706
|
+
// 保存当前相机状态
|
|
707
|
+
const originalPos = vec3.clone(this.cameraPos);
|
|
708
|
+
const originalPitch = this.cameraPitch;
|
|
709
|
+
const originalYaw = this.cameraYaw;
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
// 创建结构
|
|
713
|
+
const structure = structureFromLitematic(this.structureLitematic);
|
|
714
|
+
const size = structure.getSize();
|
|
715
|
+
|
|
716
|
+
// 计算结构中心点
|
|
717
|
+
const centerX = size[0] / 2;
|
|
718
|
+
const centerY = size[1] / 2;
|
|
719
|
+
const centerZ = size[2] / 2;
|
|
720
|
+
|
|
721
|
+
// Isometric Renders preset: orthographic projection, 135° rotation and 36° slant.
|
|
722
|
+
const rotation = Number.isFinite(Number(options.isometricRotation)) ? Number(options.isometricRotation) : 135;
|
|
723
|
+
const slant = Number.isFinite(Number(options.isometricSlant)) ? Number(options.isometricSlant) : 36;
|
|
724
|
+
this.cameraPitch = slant * Math.PI / 180;
|
|
725
|
+
this.cameraYaw = rotation * Math.PI / 180;
|
|
726
|
+
vec3.set(this.cameraPos, -centerX, -centerY, -centerZ);
|
|
727
|
+
const modelScale = this._calculateOrthographicScale(
|
|
728
|
+
size,
|
|
729
|
+
this.cameraPitch,
|
|
730
|
+
this.cameraYaw,
|
|
731
|
+
outputWidth / outputHeight
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const isometricView = await this.renderToImage({
|
|
735
|
+
width: outputWidth,
|
|
736
|
+
height: outputHeight,
|
|
737
|
+
cameraPitch: this.cameraPitch,
|
|
738
|
+
cameraYaw: this.cameraYaw,
|
|
739
|
+
autoPosition: false,
|
|
740
|
+
quality: options.quality || 'high', // 使用高质量渲染
|
|
741
|
+
// 保留高质量模式的超采样结果,裁切后再一次性缩小,避免纹理被重复缩放。
|
|
742
|
+
preserveRenderResolution: true,
|
|
743
|
+
// Alpha 通道能准确区分背景和深色方块,最终再合成到纯黑画布。
|
|
744
|
+
transparentBackground: true,
|
|
745
|
+
orthographic: true,
|
|
746
|
+
modelScale,
|
|
747
|
+
drawGrid: false
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const centeredView = await this._centerIsometricImage(
|
|
751
|
+
isometricView,
|
|
752
|
+
outputWidth,
|
|
753
|
+
outputHeight,
|
|
754
|
+
options.isometricFill
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
// 恢复原始相机状态
|
|
758
|
+
vec3.copy(this.cameraPos, originalPos);
|
|
759
|
+
this.cameraPitch = originalPitch;
|
|
760
|
+
this.cameraYaw = originalYaw;
|
|
761
|
+
|
|
762
|
+
return centeredView;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
// 恢复相机状态并传播错误
|
|
765
|
+
vec3.copy(this.cameraPos, originalPos);
|
|
766
|
+
this.cameraPitch = originalPitch;
|
|
767
|
+
this.cameraYaw = originalYaw;
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Fit the rotated structure into Isometric Renders' -1..1 orthographic viewport.
|
|
774
|
+
* @private
|
|
775
|
+
*/
|
|
776
|
+
_calculateOrthographicScale(size, pitch, yaw, aspectRatio) {
|
|
777
|
+
const rotation = mat4.create();
|
|
778
|
+
mat4.rotateX(rotation, rotation, pitch);
|
|
779
|
+
mat4.rotateY(rotation, rotation, yaw);
|
|
780
|
+
let maxX = 0;
|
|
781
|
+
let maxY = 0;
|
|
782
|
+
for (const x of [-size[0] / 2, size[0] / 2]) {
|
|
783
|
+
for (const y of [-size[1] / 2, size[1] / 2]) {
|
|
784
|
+
for (const z of [-size[2] / 2, size[2] / 2]) {
|
|
785
|
+
const point = vec3.fromValues(x, y, z);
|
|
786
|
+
vec3.transformMat4(point, point, rotation);
|
|
787
|
+
maxX = Math.max(maxX, Math.abs(point[0]));
|
|
788
|
+
maxY = Math.max(maxY, Math.abs(point[1]));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return Math.min(
|
|
793
|
+
aspectRatio * 0.9 / Math.max(0.001, maxX),
|
|
794
|
+
0.9 / Math.max(0.001, maxY)
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* 按实际渲染像素裁切并居中正二轴测图。
|
|
800
|
+
* @private
|
|
801
|
+
*/
|
|
802
|
+
async _centerIsometricImage(dataUrl, width, height, requestedFill) {
|
|
803
|
+
const image = await new Promise((resolve, reject) => {
|
|
804
|
+
const element = new Image();
|
|
805
|
+
element.onload = () => resolve(element);
|
|
806
|
+
element.onerror = () => reject(new Error('无法读取正二轴测渲染结果'));
|
|
807
|
+
element.src = dataUrl;
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const scanCanvas = document.createElement('canvas');
|
|
811
|
+
scanCanvas.width = image.naturalWidth;
|
|
812
|
+
scanCanvas.height = image.naturalHeight;
|
|
813
|
+
const scanContext = scanCanvas.getContext('2d', { willReadFrequently: true });
|
|
814
|
+
scanContext.drawImage(image, 0, 0);
|
|
815
|
+
|
|
816
|
+
const sourceWidth = scanCanvas.width;
|
|
817
|
+
const sourceHeight = scanCanvas.height;
|
|
818
|
+
const scanImage = scanContext.getImageData(0, 0, sourceWidth, sourceHeight);
|
|
819
|
+
const pixels = scanImage.data;
|
|
820
|
+
const corners = [
|
|
821
|
+
0,
|
|
822
|
+
(sourceWidth - 1) * 4,
|
|
823
|
+
(sourceHeight - 1) * sourceWidth * 4,
|
|
824
|
+
(sourceHeight * sourceWidth - 1) * 4
|
|
825
|
+
];
|
|
826
|
+
const background = [0, 1, 2].map(channel =>
|
|
827
|
+
corners.reduce((sum, offset) => sum + pixels[offset + channel], 0) / corners.length
|
|
828
|
+
);
|
|
829
|
+
const hasTransparentBackground = corners.every(offset => pixels[offset + 3] === 0);
|
|
830
|
+
|
|
831
|
+
let minX = sourceWidth;
|
|
832
|
+
let minY = sourceHeight;
|
|
833
|
+
let maxX = -1;
|
|
834
|
+
let maxY = -1;
|
|
835
|
+
const threshold = 12;
|
|
836
|
+
for (let y = 0; y < sourceHeight; y++) {
|
|
837
|
+
for (let x = 0; x < sourceWidth; x++) {
|
|
838
|
+
const offset = (y * sourceWidth + x) * 4;
|
|
839
|
+
const distance = Math.max(
|
|
840
|
+
Math.abs(pixels[offset] - background[0]),
|
|
841
|
+
Math.abs(pixels[offset + 1] - background[1]),
|
|
842
|
+
Math.abs(pixels[offset + 2] - background[2])
|
|
843
|
+
);
|
|
844
|
+
const isForeground = hasTransparentBackground
|
|
845
|
+
? pixels[offset + 3] > 8
|
|
846
|
+
: pixels[offset + 3] > 0 && distance > threshold;
|
|
847
|
+
if (!isForeground) {
|
|
848
|
+
pixels[offset] = 0;
|
|
849
|
+
pixels[offset + 1] = 0;
|
|
850
|
+
pixels[offset + 2] = 0;
|
|
851
|
+
pixels[offset + 3] = 255;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
minX = Math.min(minX, x);
|
|
855
|
+
minY = Math.min(minY, y);
|
|
856
|
+
maxX = Math.max(maxX, x);
|
|
857
|
+
maxY = Math.max(maxY, y);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
scanContext.putImageData(scanImage, 0, 0);
|
|
861
|
+
|
|
862
|
+
const outputCanvas = document.createElement('canvas');
|
|
863
|
+
outputCanvas.width = width;
|
|
864
|
+
outputCanvas.height = height;
|
|
865
|
+
const outputContext = outputCanvas.getContext('2d');
|
|
866
|
+
outputContext.fillStyle = '#000000';
|
|
867
|
+
outputContext.fillRect(0, 0, width, height);
|
|
868
|
+
|
|
869
|
+
if (maxX >= minX && maxY >= minY) {
|
|
870
|
+
const margin = 2;
|
|
871
|
+
minX = Math.max(0, minX - margin);
|
|
872
|
+
minY = Math.max(0, minY - margin);
|
|
873
|
+
maxX = Math.min(sourceWidth - 1, maxX + margin);
|
|
874
|
+
maxY = Math.min(sourceHeight - 1, maxY + margin);
|
|
875
|
+
|
|
876
|
+
const cropWidth = maxX - minX + 1;
|
|
877
|
+
const cropHeight = maxY - minY + 1;
|
|
878
|
+
const numericFill = Number(requestedFill);
|
|
879
|
+
const fill = Number.isFinite(numericFill) ? Math.min(0.98, Math.max(0.1, numericFill)) : 0.78;
|
|
880
|
+
const scale = Math.min(width * fill / cropWidth, height * fill / cropHeight);
|
|
881
|
+
const drawWidth = Math.max(1, Math.round(cropWidth * scale));
|
|
882
|
+
const drawHeight = Math.max(1, Math.round(cropHeight * scale));
|
|
883
|
+
const drawX = Math.round((width - drawWidth) / 2);
|
|
884
|
+
const drawY = Math.round((height - drawHeight) / 2);
|
|
885
|
+
|
|
886
|
+
outputContext.imageSmoothingEnabled = true;
|
|
887
|
+
outputContext.imageSmoothingQuality = 'high';
|
|
888
|
+
outputContext.drawImage(
|
|
889
|
+
scanCanvas,
|
|
890
|
+
minX, minY, cropWidth, cropHeight,
|
|
891
|
+
drawX, drawY, drawWidth, drawHeight
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return outputCanvas.toDataURL('image/png');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/** Render the Isometric Renders preset and the exact 180° reverse angle. */
|
|
899
|
+
async renderIsometricViews(options = {}) {
|
|
900
|
+
const rotation = Number.isFinite(Number(options.isometricRotation)) ? Number(options.isometricRotation) : 135;
|
|
901
|
+
const isometricView = await this.renderIsometricView({ ...options, isometricRotation: rotation });
|
|
902
|
+
const reverseIsometricView = await this.renderIsometricView({
|
|
903
|
+
...options,
|
|
904
|
+
isometricRotation: (rotation + 180) % 360
|
|
905
|
+
});
|
|
906
|
+
return { isometricView, reverseIsometricView };
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Compatibility entry point for callers of the previous API.
|
|
910
|
+
async renderAllViews(options = {}) {
|
|
911
|
+
return this.renderIsometricViews(options);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// 创建并导出 API 实例
|
|
916
|
+
const litematicViewerAPI = new LitematicViewerAPI();
|
|
917
|
+
window.litematicViewerAPI = litematicViewerAPI; // 确保在全局作用域中可用
|