three-base 2.0.175 → 2.0.180
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/MktMesh/boxLine2.d.ts +42 -0
- package/MktMesh/boxLine2.js +185 -0
- package/MktMesh/boxLine2.ts +222 -0
- package/MktMesh/imagePlane.d.ts +60 -0
- package/MktMesh/imagePlane.js +158 -0
- package/MktMesh/imagePlane.ts +203 -0
- package/MktMesh/imageTileLoader.d.ts +19 -0
- package/MktMesh/imageTileLoader.js +55 -0
- package/MktMesh/imageTileLoader.ts +64 -0
- package/MktMesh/modelSlot.d.ts +123 -0
- package/MktMesh/modelSlot.js +339 -0
- package/MktMesh/modelSlot.ts +383 -0
- package/MktMesh/panoSlotRoster.js +1 -1
- package/MktMesh/panoSlotRoster.ts +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 图片平面元素 —— 皇上旨 2026-10-20「先添加一个平面, 再给这个平面上传图片,
|
|
3
|
+
* 上传的这个图切成 4*4=16 片, 加载的时候也是一帧载入一片」→ 2026-10-21 改口「切成 2*2=4 份,
|
|
4
|
+
* 不用切成 16 份了」。片数只在库里那一个 `IMAGE_TILE_GRID` 常数上,本文件⛔ 不写数字。
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 独立 Mesh(非 InstancedChild):每张图片各有自己的 4 张瓦片纹理 ⇒ 材质实例不可共用,不合批。
|
|
8
|
+
* - 材质 MeshBasicMaterial + FrontSide(皇上旨 2026-10-22「添加的平面图片元素只要单面可见就行了,双面不需要」):
|
|
9
|
+
* 平面本身不承受光照(贴图内容自带明暗)。单面 ⇒ 背面既不画也打不中(three 的射线拾取按材质 side 剔背面,
|
|
10
|
+
* `node_modules/three/src/objects/Mesh.js:343`),与渲染共用同一条式子 ⇒ 站在这一面的反面既看不见也选不中。
|
|
11
|
+
* - 几何 PlaneGeometry(width, height),原点在平面中心;position.y 由调用者设定(通常 height/2 让底部落地)。
|
|
12
|
+
* - ⭐ 2026-10-22 晚旨:纹理**不走拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,片元按 UV 象限选片 +
|
|
13
|
+
* 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。材质用 onBeforeCompile 打补丁,
|
|
14
|
+
* 注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰、逐张就绪逐张显影。
|
|
15
|
+
* - UV 标准 [0,1]²(PlaneGeometry 自带),4 张瓦片按 2×2 象限铺满 ⇒ 不需要投影 shader,也不需要离屏 RT。
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
Color,
|
|
19
|
+
DataTexture,
|
|
20
|
+
FrontSide,
|
|
21
|
+
Mesh,
|
|
22
|
+
MeshBasicMaterial,
|
|
23
|
+
PlaneGeometry,
|
|
24
|
+
RGBAFormat,
|
|
25
|
+
Texture,
|
|
26
|
+
Vector4,
|
|
27
|
+
} from "three";
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
* ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
31
|
+
* 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
32
|
+
* 占位 1×1 灰纹理:sampler 必须绑定有效纹理(未就绪片绑定它、但片元 `uValid` 为 0 时不采样它)。
|
|
33
|
+
*/
|
|
34
|
+
const PLACEHOLDER_TEX: Texture = (() => {
|
|
35
|
+
const t = new DataTexture(new Uint8Array([204, 204, 204, 255]), 1, 1, RGBAFormat);
|
|
36
|
+
t.needsUpdate = true;
|
|
37
|
+
return t;
|
|
38
|
+
})();
|
|
39
|
+
const ZERO_INSET = new Vector4(0, 0, 1, 1);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 皇上旨 2026-10-23「添加的平面 有点隐隐的 边框, 你在代码里面写一下, 把边框的那一点点 discrad 掉」。
|
|
43
|
+
* 整张图**外圈**这一条 UV 余量在片元里一刀 `discard` 掉 —— 边框那一圈来自裁片留下的描边/柔边像素
|
|
44
|
+
* (`alphaTest` 砍不到它:那圈像素的 alpha 高于阈值 —— 0.2 / 0.9 / 0.3 这几档都留得住它,只是颜色比内容浅,于是画成实心的一圈浅边)。
|
|
45
|
+
* 只切外圈:内部那 2×2 拼缝归每片自己的 inset 采样窗口管,两把刀互不重叠。
|
|
46
|
+
* ⚠️ 这一档是**比例**不是毫米:默认平面 2×1.5 m ⇒ 横着每边切 8 mm、竖着每边切 6 mm。尺只许写在这一处。
|
|
47
|
+
*/
|
|
48
|
+
export const IMAGE_PLANE_EDGE_CLIP_UV = 0.004;
|
|
49
|
+
const EDGE_MAX_UV = 1 - IMAGE_PLANE_EDGE_CLIP_UV;
|
|
50
|
+
|
|
51
|
+
/** 片元打补丁:4 张瓦片按 UV 象限选片,缺图那片显示灰(与旧缺失灰同一观感)。索引用字面量 0..3 ⇒ GLSL ES 1.00 合法。 */
|
|
52
|
+
const IMG_PLANE_MAP_FRAG = `
|
|
53
|
+
if ( vUv.x < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.x > ${EDGE_MAX_UV} || vUv.y < ${IMAGE_PLANE_EDGE_CLIP_UV} || vUv.y > ${EDGE_MAX_UV} ) discard;
|
|
54
|
+
vec4 imgTexel = vec4(0.8, 0.8, 0.8, 1.0); // 缺图灰
|
|
55
|
+
vec2 cell = floor(vUv * 2.0);
|
|
56
|
+
vec2 local = fract(vUv * 2.0);
|
|
57
|
+
if (cell.y >= 0.5 && cell.x < 0.5) {
|
|
58
|
+
if (uValid[0] > 0.5) { vec4 ins = uInset[0]; imgTexel = texture2D(uTiles[0], mix(ins.xy, ins.zw, local)); }
|
|
59
|
+
} else if (cell.y >= 0.5 && cell.x >= 0.5) {
|
|
60
|
+
if (uValid[1] > 0.5) { vec4 ins = uInset[1]; imgTexel = texture2D(uTiles[1], mix(ins.xy, ins.zw, local)); }
|
|
61
|
+
} else if (cell.y < 0.5 && cell.x < 0.5) {
|
|
62
|
+
if (uValid[2] > 0.5) { vec4 ins = uInset[2]; imgTexel = texture2D(uTiles[2], mix(ins.xy, ins.zw, local)); }
|
|
63
|
+
} else {
|
|
64
|
+
if (uValid[3] > 0.5) { vec4 ins = uInset[3]; imgTexel = texture2D(uTiles[3], mix(ins.xy, ins.zw, local)); }
|
|
65
|
+
}
|
|
66
|
+
diffuseColor *= imgTexel;
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
/** 图片平面的持久化数据(进 exportData / importData) */
|
|
70
|
+
export interface ImagePlaneData {
|
|
71
|
+
name?: string;
|
|
72
|
+
position: { x: number; y: number; z: number };
|
|
73
|
+
rotation: { x: number; y: number; z: number };
|
|
74
|
+
width: number;
|
|
75
|
+
height: number;
|
|
76
|
+
/** 原图地址(上传后返回的 origin URL,缩略图展示用) */
|
|
77
|
+
url: string;
|
|
78
|
+
/** 瓦片地址(行优先、图像左上起;张数 = 库里 `IMAGE_TILE_GRID` 的 cols×rows,10-21 旨 = 2×2=4) */
|
|
79
|
+
tiles: string[];
|
|
80
|
+
/** 原图自然像素宽(瓦片纹理实际像素,算 inset 采样窗口需要) */
|
|
81
|
+
naturalW?: number;
|
|
82
|
+
/** 原图自然像素高 */
|
|
83
|
+
naturalH?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const DEFAULT_WIDTH = 2;
|
|
87
|
+
const DEFAULT_HEIGHT = 1.5;
|
|
88
|
+
const HIGHLIGHT_COLOR = "#ffd24a";
|
|
89
|
+
|
|
90
|
+
export class ImagePlane extends Mesh<PlaneGeometry, MeshBasicMaterial> {
|
|
91
|
+
private _originalColor: string = "#ffffff";
|
|
92
|
+
private _uTiles!: { value: Texture[] };
|
|
93
|
+
private _uInset!: { value: Vector4[] };
|
|
94
|
+
private _uValid!: { value: number[] };
|
|
95
|
+
private _data: Omit<ImagePlaneData, "position" | "rotation" | "width" | "height"> = {
|
|
96
|
+
url: "",
|
|
97
|
+
tiles: [],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
constructor(name?: string) {
|
|
101
|
+
const geo = new PlaneGeometry(DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
|
102
|
+
const mat = new MeshBasicMaterial({
|
|
103
|
+
color: new Color("#ffffff"),
|
|
104
|
+
side: FrontSide,
|
|
105
|
+
// 皇上旨 2026-10-21「平面的材质需要设置 alphaTest = 0.2」⇒ 同日 10-23 两次改口(先 0.9、后 **0.3**):
|
|
106
|
+
// alpha 不足阈值的像素丢弃,其余实心。0.9 那一档会把图上半透的内容也切掉,现停在 0.3。
|
|
107
|
+
// ⛔ 这个构造里不许再写「走半透明通道」与「不透明度数值」那两行:皇上 10-22 点名「没有说过要设置透明度」,
|
|
108
|
+
// 那是臣 10-21 建这个类时擅自加的(没问过),当日整条删除。平面恒走不透明通道,
|
|
109
|
+
// 要「透」只能靠 alphaTest 这一刀切掉。
|
|
110
|
+
alphaTest: 0.3,
|
|
111
|
+
});
|
|
112
|
+
// ⭐ 2026-10-22 晚旨:图片平面**不用拼合器 RT**,直接把上传切好的 4 张瓦片纹理绑到材质,
|
|
113
|
+
// 片元按 UV 象限选片 + 用每片 `rtStitchTileInset` 窗口采样(与上传切片共用同一份几何 ⇒ 接缝不回潮)。
|
|
114
|
+
// 材质打补丁(onBeforeCompile)注入 4 个 sampler + 每片 inset + 就绪标志;缺失那片显示灰。
|
|
115
|
+
mat.defines = { USE_UV: "" };
|
|
116
|
+
const uTiles = { value: [PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX, PLACEHOLDER_TEX] as Texture[] };
|
|
117
|
+
const uInset = { value: [ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone(), ZERO_INSET.clone()] };
|
|
118
|
+
const uValid = { value: [0, 0, 0, 0] };
|
|
119
|
+
mat.onBeforeCompile = (shader) => {
|
|
120
|
+
shader.uniforms.uTiles = uTiles;
|
|
121
|
+
shader.uniforms.uInset = uInset;
|
|
122
|
+
shader.uniforms.uValid = uValid;
|
|
123
|
+
shader.fragmentShader = shader.fragmentShader
|
|
124
|
+
.replace(
|
|
125
|
+
"#include <common>",
|
|
126
|
+
`#include <common>
|
|
127
|
+
uniform sampler2D uTiles[4];
|
|
128
|
+
uniform vec4 uInset[4];
|
|
129
|
+
uniform float uValid[4];`
|
|
130
|
+
)
|
|
131
|
+
.replace("#include <map_fragment>", IMG_PLANE_MAP_FRAG);
|
|
132
|
+
};
|
|
133
|
+
super(geo, mat);
|
|
134
|
+
this._uTiles = uTiles;
|
|
135
|
+
this._uInset = uInset;
|
|
136
|
+
this._uValid = uValid;
|
|
137
|
+
this.name = name || "图片平面";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get imageWidth(): number { return (this.geometry as PlaneGeometry).parameters.width; }
|
|
141
|
+
get imageHeight(): number { return (this.geometry as PlaneGeometry).parameters.height; }
|
|
142
|
+
|
|
143
|
+
setPlaneSize(w: number, h: number): void {
|
|
144
|
+
this.geometry.dispose();
|
|
145
|
+
this.geometry = new PlaneGeometry(w, h);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setPlaneData(d: Pick<ImagePlaneData, "url" | "tiles" | "naturalW" | "naturalH">): void {
|
|
149
|
+
this._data = { url: d.url, tiles: d.tiles, naturalW: d.naturalW, naturalH: d.naturalH };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get tiles(): string[] { return this._data.tiles; }
|
|
153
|
+
get url(): string { return this._data.url; }
|
|
154
|
+
get naturalW(): number | undefined { return this._data.naturalW; }
|
|
155
|
+
get naturalH(): number | undefined { return this._data.naturalH; }
|
|
156
|
+
|
|
157
|
+
/** 把上传切好的 4 张瓦片纹理 + 每片 inset 绑到材质(缺图那片传 null ⇒ 显示灰)。uTiles 用持久对象引用 ⇒ 改 value 实时生效 */
|
|
158
|
+
setTileTextures(texs: (Texture | null)[], insets: Vector4[]): void {
|
|
159
|
+
for (let i = 0; i < 4; i++) {
|
|
160
|
+
const t = texs[i] || null;
|
|
161
|
+
this._uTiles.value[i] = t || PLACEHOLDER_TEX;
|
|
162
|
+
if (insets[i]) this._uInset.value[i].copy(insets[i]);
|
|
163
|
+
this._uValid.value[i] = t ? 1 : 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
highlight(): void {
|
|
168
|
+
this.material.color.set(HIGHLIGHT_COLOR);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
restoreColor(): void {
|
|
172
|
+
this.material.color.set(this._originalColor);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
toData(): ImagePlaneData {
|
|
176
|
+
return {
|
|
177
|
+
name: this.name || undefined,
|
|
178
|
+
position: { x: this.position.x, y: this.position.y, z: this.position.z },
|
|
179
|
+
rotation: { x: this.rotation.x, y: this.rotation.y, z: this.rotation.z },
|
|
180
|
+
width: this.imageWidth,
|
|
181
|
+
height: this.imageHeight,
|
|
182
|
+
url: this._data.url,
|
|
183
|
+
tiles: this._data.tiles,
|
|
184
|
+
naturalW: this._data.naturalW,
|
|
185
|
+
naturalH: this._data.naturalH,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
applyData(d: ImagePlaneData): void {
|
|
190
|
+
if (d.name) this.name = d.name;
|
|
191
|
+
this.position.set(d.position.x, d.position.y, d.position.z);
|
|
192
|
+
this.rotation.set(d.rotation.x, d.rotation.y, d.rotation.z);
|
|
193
|
+
this.setPlaneSize(d.width, d.height);
|
|
194
|
+
this._data = { url: d.url, tiles: d.tiles || [], naturalW: d.naturalW, naturalH: d.naturalH };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dispose(): void {
|
|
198
|
+
this.geometry.dispose();
|
|
199
|
+
this.material.dispose();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export const IMAGE_PLANE_DEFAULTS = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Texture, Vector4 } from "three";
|
|
2
|
+
interface TileEntry {
|
|
3
|
+
tex: Texture | null;
|
|
4
|
+
w: number;
|
|
5
|
+
h: number;
|
|
6
|
+
}
|
|
7
|
+
export declare class ImagePlaneTileLoader {
|
|
8
|
+
private cache;
|
|
9
|
+
private inflight;
|
|
10
|
+
/** 触发某张瓦片下载 + 解码(已缓存 / 在途则跳过);完成后填入 cache,下一帧 feed 即取到 */
|
|
11
|
+
preload(url: string): void;
|
|
12
|
+
/** 取已就绪的瓦片(未就绪返回 null) */
|
|
13
|
+
get(url: string): TileEntry | null;
|
|
14
|
+
/** 第 i 片(行优先、图像左上起)的采样窗口;瓦片未就绪时返回整张 [0,0,1,1] */
|
|
15
|
+
inset(entry: TileEntry, i: number): Vector4;
|
|
16
|
+
/** 释放本 loader 持有的全部瓦片纹理(项目重置 / 卸载场景时调用;⛔ 不关共享占位纹理) */
|
|
17
|
+
clear(): void;
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 图片平面(「加平面」)的瓦片纹理加载器 —— ⭐ 2026-10-22 晚旨配套:图片平面**不用拼合器 RT**,
|
|
3
|
+
* 直接把上传切好的 4 张瓦片纹理绑到材质。本类负责「按 url 下载 + 异步解码 + 缓存在本 renderer 上下文」,
|
|
4
|
+
* 并把第 i 片的采样窗口(inset)算好交出去。
|
|
5
|
+
*
|
|
6
|
+
* 设计要点:
|
|
7
|
+
* - 纹理是 **WebGL 上下文私有**资源:编辑页 / 预览页各持一个实例(各自 GL context),⛔ 不跨 renderer 共享同一 Texture。
|
|
8
|
+
* - 复用库里 `loadPanoTexture`(fetch + createImageBitmap 工作线程解码、`imageOrientation:"flipY"` 预翻 + flipY=false,
|
|
9
|
+
* 与全景同一套上下翻约定);本类只补 `colorSpace = SRGBColorSpace`(硬件采样即解 sRGB→linear,与旧 RT 方案逐字节同口径)。
|
|
10
|
+
* - inset 用库的 `rtStitchTileInset(i, tw, th, IMAGE_TILE_GRID)`:上传多切 8px、采样只取中间干净块 ⇒ 接缝不回潮,
|
|
11
|
+
* 与上传切片 `rtStitchTileRects` 共用同一份几何(⛔ 不许在这里另算一份)。
|
|
12
|
+
* - 逐张就绪、逐张显影:未就绪那张 `get()` 返回 null ⇒ 调方把那片传 null(材质显示灰),下一帧就绪后即亮。
|
|
13
|
+
*/
|
|
14
|
+
import { SRGBColorSpace, Vector4 } from "three";
|
|
15
|
+
import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
|
|
16
|
+
import { IMAGE_TILE_GRID, rtStitchTileInset } from "./mktShader";
|
|
17
|
+
export class ImagePlaneTileLoader {
|
|
18
|
+
cache = new Map();
|
|
19
|
+
inflight = new Map();
|
|
20
|
+
/** 触发某张瓦片下载 + 解码(已缓存 / 在途则跳过);完成后填入 cache,下一帧 feed 即取到 */
|
|
21
|
+
preload(url) {
|
|
22
|
+
if (!url || this.cache.has(url) || this.inflight.has(url))
|
|
23
|
+
return;
|
|
24
|
+
const p = loadPanoTexture(url)
|
|
25
|
+
.then((tex) => {
|
|
26
|
+
tex.colorSpace = SRGBColorSpace; // 硬件采样解 sRGB→linear(与旧 RT 方案逐字节同口径)
|
|
27
|
+
const img = tex.image;
|
|
28
|
+
this.cache.set(url, { tex, w: img?.width ?? 0, h: img?.height ?? 0 });
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
// 下载 / 解码失败:留空,下一帧 preload 重新触发(不把半截纹理绑上去)
|
|
32
|
+
})
|
|
33
|
+
.finally(() => {
|
|
34
|
+
this.inflight.delete(url);
|
|
35
|
+
});
|
|
36
|
+
this.inflight.set(url, p);
|
|
37
|
+
}
|
|
38
|
+
/** 取已就绪的瓦片(未就绪返回 null) */
|
|
39
|
+
get(url) {
|
|
40
|
+
return this.cache.get(url) || null;
|
|
41
|
+
}
|
|
42
|
+
/** 第 i 片(行优先、图像左上起)的采样窗口;瓦片未就绪时返回整张 [0,0,1,1] */
|
|
43
|
+
inset(entry, i) {
|
|
44
|
+
if (!entry.w || !entry.h)
|
|
45
|
+
return new Vector4(0, 0, 1, 1);
|
|
46
|
+
const [u0, v0, u1, v1] = rtStitchTileInset(i, entry.w, entry.h, IMAGE_TILE_GRID);
|
|
47
|
+
return new Vector4(u0, v0, u1, v1);
|
|
48
|
+
}
|
|
49
|
+
/** 释放本 loader 持有的全部瓦片纹理(项目重置 / 卸载场景时调用;⛔ 不关共享占位纹理) */
|
|
50
|
+
clear() {
|
|
51
|
+
this.cache.forEach((e) => e.tex && releasePanoTexture(e.tex));
|
|
52
|
+
this.cache.clear();
|
|
53
|
+
this.inflight.clear();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Description: 图片平面(「加平面」)的瓦片纹理加载器 —— ⭐ 2026-10-22 晚旨配套:图片平面**不用拼合器 RT**,
|
|
3
|
+
* 直接把上传切好的 4 张瓦片纹理绑到材质。本类负责「按 url 下载 + 异步解码 + 缓存在本 renderer 上下文」,
|
|
4
|
+
* 并把第 i 片的采样窗口(inset)算好交出去。
|
|
5
|
+
*
|
|
6
|
+
* 设计要点:
|
|
7
|
+
* - 纹理是 **WebGL 上下文私有**资源:编辑页 / 预览页各持一个实例(各自 GL context),⛔ 不跨 renderer 共享同一 Texture。
|
|
8
|
+
* - 复用库里 `loadPanoTexture`(fetch + createImageBitmap 工作线程解码、`imageOrientation:"flipY"` 预翻 + flipY=false,
|
|
9
|
+
* 与全景同一套上下翻约定);本类只补 `colorSpace = SRGBColorSpace`(硬件采样即解 sRGB→linear,与旧 RT 方案逐字节同口径)。
|
|
10
|
+
* - inset 用库的 `rtStitchTileInset(i, tw, th, IMAGE_TILE_GRID)`:上传多切 8px、采样只取中间干净块 ⇒ 接缝不回潮,
|
|
11
|
+
* 与上传切片 `rtStitchTileRects` 共用同一份几何(⛔ 不许在这里另算一份)。
|
|
12
|
+
* - 逐张就绪、逐张显影:未就绪那张 `get()` 返回 null ⇒ 调方把那片传 null(材质显示灰),下一帧就绪后即亮。
|
|
13
|
+
*/
|
|
14
|
+
import { SRGBColorSpace, Texture, Vector4 } from "three";
|
|
15
|
+
import { loadPanoTexture, releasePanoTexture } from "./panoBitmapTexture";
|
|
16
|
+
import { IMAGE_TILE_GRID, rtStitchTileInset } from "./mktShader";
|
|
17
|
+
|
|
18
|
+
interface TileEntry {
|
|
19
|
+
tex: Texture | null;
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ImagePlaneTileLoader {
|
|
25
|
+
private cache = new Map<string, TileEntry>();
|
|
26
|
+
private inflight = new Map<string, Promise<void>>();
|
|
27
|
+
|
|
28
|
+
/** 触发某张瓦片下载 + 解码(已缓存 / 在途则跳过);完成后填入 cache,下一帧 feed 即取到 */
|
|
29
|
+
preload(url: string): void {
|
|
30
|
+
if (!url || this.cache.has(url) || this.inflight.has(url)) return;
|
|
31
|
+
const p = loadPanoTexture(url)
|
|
32
|
+
.then((tex) => {
|
|
33
|
+
tex.colorSpace = SRGBColorSpace; // 硬件采样解 sRGB→linear(与旧 RT 方案逐字节同口径)
|
|
34
|
+
const img = tex.image as { width?: number; height?: number } | undefined;
|
|
35
|
+
this.cache.set(url, { tex, w: img?.width ?? 0, h: img?.height ?? 0 });
|
|
36
|
+
})
|
|
37
|
+
.catch(() => {
|
|
38
|
+
// 下载 / 解码失败:留空,下一帧 preload 重新触发(不把半截纹理绑上去)
|
|
39
|
+
})
|
|
40
|
+
.finally(() => {
|
|
41
|
+
this.inflight.delete(url);
|
|
42
|
+
});
|
|
43
|
+
this.inflight.set(url, p);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 取已就绪的瓦片(未就绪返回 null) */
|
|
47
|
+
get(url: string): TileEntry | null {
|
|
48
|
+
return this.cache.get(url) || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 第 i 片(行优先、图像左上起)的采样窗口;瓦片未就绪时返回整张 [0,0,1,1] */
|
|
52
|
+
inset(entry: TileEntry, i: number): Vector4 {
|
|
53
|
+
if (!entry.w || !entry.h) return new Vector4(0, 0, 1, 1);
|
|
54
|
+
const [u0, v0, u1, v1] = rtStitchTileInset(i, entry.w, entry.h, IMAGE_TILE_GRID);
|
|
55
|
+
return new Vector4(u0, v0, u1, v1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 释放本 loader 持有的全部瓦片纹理(项目重置 / 卸载场景时调用;⛔ 不关共享占位纹理) */
|
|
59
|
+
clear(): void {
|
|
60
|
+
this.cache.forEach((e) => e.tex && releasePanoTexture(e.tex));
|
|
61
|
+
this.cache.clear();
|
|
62
|
+
this.inflight.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Group } from "three";
|
|
2
|
+
/** 模型槽的持久化数据(进 exportData / importData / 预览 loadData) */
|
|
3
|
+
export interface ModelSlotData {
|
|
4
|
+
name?: string;
|
|
5
|
+
position: {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
z: number;
|
|
9
|
+
};
|
|
10
|
+
rotation: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
z: number;
|
|
14
|
+
};
|
|
15
|
+
/** 归一化后模型最长边的实际米数(GLB 按包围盒等比缩到这里;上传时那一档自动填成模型原最长边 ⇒ 一比一) */
|
|
16
|
+
size: number;
|
|
17
|
+
/** glb 地址;空串 = 还没上传 ⇒ 画面显示占位小立方体 */
|
|
18
|
+
url: string;
|
|
19
|
+
/** 落在地面还是墙面(决定占位方块的落位与落点口径;10-22 旨之后**不再**挪动用户上传模型的位置) */
|
|
20
|
+
surface: "floor" | "wall";
|
|
21
|
+
/** 模型原始包围盒尺寸(米,在 GLB 根节点**原姿势**下量,归一之前)—— 面板显示「原模型多大」用 */
|
|
22
|
+
naturalSize?: {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
z: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** 占位小立方体的棱长(米)—— 皇上要的是「小立方体」,比「加立方体」那颗 1m 的单位方块小一档 */
|
|
29
|
+
export declare const MODEL_SLOT_PLACEHOLDER = 0.3;
|
|
30
|
+
/** 默认尺寸档(米,最长边):还没上传模型时占位方块用不上它;上传成功后 `useNativeSize()` 会把它填成模型原最长边 ⇒ 一比一 */
|
|
31
|
+
export declare const MODEL_SLOT_DEFAULT_SIZE = 1;
|
|
32
|
+
/** 墙面落点沿法线外推的距离(米):模型中心压在墙面上会让一半陷进墙皮,且贴墙的平面片会 z-fighting */
|
|
33
|
+
export declare const MODEL_SLOT_WALL_OFFSET = 0.02;
|
|
34
|
+
export declare class ModelSlot extends Group {
|
|
35
|
+
/** 拾取路由用标记(与 three 的 `isInstancedMesh` 同款判据):射线命中任意子对象后向上找带这个标记的祖先 */
|
|
36
|
+
readonly isModelSlot = true;
|
|
37
|
+
/** 占位小立方体(未上传模型时可见) */
|
|
38
|
+
private readonly _placeholder;
|
|
39
|
+
/** GLB 挂载点:内容在这里被等比归一(10-22 旨之后位置保持 GLB 原生,⛔ 不再挪) */
|
|
40
|
+
private readonly _contentRoot;
|
|
41
|
+
/**
|
|
42
|
+
* 选中高亮框(贴合当前内容的包围盒;10-22 旨整搬 archives2026 的 `BoxLine2`)。
|
|
43
|
+
* ⚠️ 懒建 —— null = 这个槽从没被选中过(预览那一路永远是 null);⛔ 不参与射线拾取(见 `pickIgnored`)。
|
|
44
|
+
*/
|
|
45
|
+
private _box;
|
|
46
|
+
/** 高亮框该套在哪、套多大(`_layout()` 每次算好存这里;框还没建起来时先记着,`highlight()` 建好再贴) */
|
|
47
|
+
private _boxFit;
|
|
48
|
+
/** 当前 GLB 子树;null = 没有模型(显示占位方块) */
|
|
49
|
+
private _model;
|
|
50
|
+
/**
|
|
51
|
+
* GLB **载入时的原姿势**(根节点自带的那份 scale/position,导出器常在根上带 cm→m 单位换算缩放)。
|
|
52
|
+
* ⚠️ 每次排布都先从这两个值复位再量再归一 ⇒ 「原尺寸」有唯一基准,且第二次 `_layout()` 不会量到
|
|
53
|
+
* 上一次 `scaleTo` 之后的盒子(那是重复缩放的源头)。`_baseFor` 记录这份基准属于哪一棵子树(换模型要重取)。
|
|
54
|
+
*/
|
|
55
|
+
private _baseFor;
|
|
56
|
+
private _baseScale;
|
|
57
|
+
private _basePos;
|
|
58
|
+
private _url;
|
|
59
|
+
private _size;
|
|
60
|
+
private _surface;
|
|
61
|
+
private _naturalSize?;
|
|
62
|
+
/** 载入令牌:换地址/删除后迟到的回调靠它作废(否则旧模型会挂进新槽) */
|
|
63
|
+
private _loadToken;
|
|
64
|
+
private _disposed;
|
|
65
|
+
constructor(name?: string);
|
|
66
|
+
get size(): number;
|
|
67
|
+
get url(): string;
|
|
68
|
+
get surface(): "floor" | "wall";
|
|
69
|
+
get naturalSize(): {
|
|
70
|
+
x: number;
|
|
71
|
+
y: number;
|
|
72
|
+
z: number;
|
|
73
|
+
} | undefined;
|
|
74
|
+
/** 是否已经挂上真模型(false = 仍是占位小立方体) */
|
|
75
|
+
get hasModel(): boolean;
|
|
76
|
+
/** 改尺寸档(米,最长边):立刻重缩内容 */
|
|
77
|
+
setSize(size: number): void;
|
|
78
|
+
/**
|
|
79
|
+
* 把尺寸档填成**模型自己的原最长边** ⇒ 之后那步归一算出的倍率正好是 1 ⇒ 画面上一比一,⛔ 不改模型大小。
|
|
80
|
+
* ⭐ 皇上旨 2026-10-21「上传模型的时候,不要去修改模型的大小」:只给**上传**那一路调(渲染器 `setModelGlb`);
|
|
81
|
+
* 存档导入 / 预览重建那一路**不能**调,否则他在面板里存的尺寸每次重开项目都被抹回原大小。
|
|
82
|
+
* @returns 是否真的改动了(还没挂上模型 ⇒ false,尺寸档保持原样)
|
|
83
|
+
*/
|
|
84
|
+
useNativeSize(): boolean;
|
|
85
|
+
/** 改落点面(地面/墙面):重摆占位方块的落位 + 重算高亮框(用户上传的模型位置不动,见 10-22 旨) */
|
|
86
|
+
setSurface(surface: "floor" | "wall"): void;
|
|
87
|
+
/**
|
|
88
|
+
* 载入并挂上 GLB(`url` 必须是可直接 fetch 的绝对地址;空串 = 退回占位小立方体)。
|
|
89
|
+
* @returns 是否真的挂上了(地址无效/载入失败/这次结果已作废 ⇒ false)
|
|
90
|
+
*/
|
|
91
|
+
setModelUrl(url: string): Promise<boolean>;
|
|
92
|
+
/**
|
|
93
|
+
* 把存档里的数据写进来(位置 / 旋转 / 尺寸 / 落点面 / 地址)。
|
|
94
|
+
* ⚠️ 刻意**不**在这里发起 GLB 载入:载入是异步的,落地那一刻必须由调用方(编辑器导入 / 预览)
|
|
95
|
+
* 显式标脏才能画出来 —— 换成 `modelRoot` 的子对象不改任何材质版本号。
|
|
96
|
+
* 故本方法只把 `url` 记好,取模型由调用方 `setModelUrl(d.url).then(...)` 驱动(⛔ 别在这儿偷偷 void)。
|
|
97
|
+
*/
|
|
98
|
+
applyData(d: ModelSlotData): void;
|
|
99
|
+
toData(): ModelSlotData;
|
|
100
|
+
highlight(): void;
|
|
101
|
+
restoreColor(): void;
|
|
102
|
+
dispose(): void;
|
|
103
|
+
/**
|
|
104
|
+
* 取出(并第一次需要时建起)高亮框:挂进槽、贴上最近一次算好的包围盒。
|
|
105
|
+
* ⚠️ 建好即 `visible = false` —— 调用方拿到对象后自己决定亮不亮(`highlight()` 里点亮)。
|
|
106
|
+
*/
|
|
107
|
+
private _ensureBox;
|
|
108
|
+
/** 把 `_boxFit` 写进框(尺寸 = 包围盒外扩 `BOX_FIT_SCALE`,中心 = 本槽局部系下的盒心) */
|
|
109
|
+
private _applyBoxFit;
|
|
110
|
+
/** 摘掉 GLB(几何/材质/贴图一起释放,否则换一只模型就漏一份显存) */
|
|
111
|
+
private _detachModel;
|
|
112
|
+
/**
|
|
113
|
+
* 内容排布的唯一出口:GLB 等比归一 + 占位方块显隐 + 高亮框贴合(⛔ 不再挪模型的位置,见 `_layoutNeutral`)。
|
|
114
|
+
* ⚠️ 每次改 size / surface / 模型都要走这里(⛔ 别在别处直接写 `_contentRoot.position`)。
|
|
115
|
+
* ⚠️ 皇上旨 2026-10-21「模型的位置和对应的边框不在一起」:这段曾把 `Box3.setFromObject` 量到的
|
|
116
|
+
* **世界**盒子直接写进子对象的**局部** `position`(内容补偿与框中心都是),于是槽离原点越远偏得越远
|
|
117
|
+
* —— 框比模型多偏一整份槽的位置。归一与套框两件事都只该**相对本槽**算,故量之前先把槽自身的位移与
|
|
118
|
+
* 旋转摘掉,算完原样装回(10-22 旨删掉内容补偿后,这把「摘了再装回」的尺仍要为高亮框留着)。
|
|
119
|
+
*/
|
|
120
|
+
private _layout;
|
|
121
|
+
/** 只在「本槽自身位移/旋转已摘除」的世界帧里跑 ⇒ 下面量的每个盒子都等于相对本槽的盒子 */
|
|
122
|
+
private _layoutNeutral;
|
|
123
|
+
}
|