发布:完整项目代码(三视图 + 词条数据 + 加载页等)
This commit is contained in:
+107
-57
@@ -1,7 +1,7 @@
|
||||
import * as THREE from 'three';
|
||||
import { ENTRIES } from '../data/entries.js';
|
||||
import { PAGE_W, PAGE_H, HOLE_U, acquirePageTextures, releasePageTextures, getPlaceholderTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||
import { clamp, easeOutCubic } from '../utils/easing.js';
|
||||
import { PAGE_W, PAGE_H, HOLE_U, acquirePageTextures, releasePageTextures, getPlaceholderTextures, prefetchPageTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||
import { clamp, easeOutCubic, easeInOutCubic } from '../utils/easing.js';
|
||||
|
||||
const RING_R = 0.085;
|
||||
const RING_TUBE = 0.0115;
|
||||
@@ -234,7 +234,7 @@ function makeLeatherNormalMap() {
|
||||
|
||||
// 内侧绒面法线:密集细绒毛(高频噪点)
|
||||
function makeSuedeNormalMap() {
|
||||
const w = 512, h = 640; // 降分辨率省 75% 逐像素计算,视觉几乎无差
|
||||
const w = 512, h = 640;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = w; c.height = h;
|
||||
const ctx = c.getContext('2d', { willReadFrequently: true });
|
||||
@@ -278,6 +278,7 @@ export class Binder {
|
||||
this.pages = [];
|
||||
this.current = 0;
|
||||
this.flip = null; // { index, from, to, t, duration }
|
||||
this.resetTween = null; // 回卷重置动画
|
||||
this.onChange = null;
|
||||
|
||||
this.suedeTex = makeSuedeTexture();
|
||||
@@ -379,23 +380,25 @@ export class Binder {
|
||||
return geo;
|
||||
}
|
||||
|
||||
_ensureHiGeo(p) {
|
||||
if (!p.hiGeo) {
|
||||
p.hiGeo = this._makePageGeometry(96, 8);
|
||||
p.hiBase = Float32Array.from(p.hiGeo.attributes.position.array);
|
||||
}
|
||||
}
|
||||
|
||||
_buildPages() {
|
||||
const segX = 96, segY = 8;
|
||||
// 共享极简几何(所有栈内页相同,只露边缘无需弯曲)
|
||||
this.loGeo = this._makePageGeometry(1, 1);
|
||||
|
||||
ENTRIES.forEach((entry, i) => {
|
||||
// 高分段几何:翻页/顶层页(弯曲需要足够分段)
|
||||
const hiGeo = this._makePageGeometry(segX, segY);
|
||||
const hiBase = Float32Array.from(hiGeo.attributes.position.array);
|
||||
// 极简几何:栈内页(只露边缘,无需弯曲,2 个三角形)
|
||||
const loGeo = this._makePageGeometry(1, 1);
|
||||
|
||||
// 单 mesh + 双面材质:正面登记卡、背面素描(gl_FrontFacing 自动选择)
|
||||
// 活页孔用 alphaTest 直接丢弃透明像素(不启用 transparent,避免透明+双面拆两次 draw call)
|
||||
const mat = makeTwoSidedMaterial(this.placeholder.front, this.placeholder.back, {
|
||||
alphaTest: 0.5,
|
||||
});
|
||||
const mesh = new THREE.Mesh(loGeo, mat); // 初始为栈内页,用极简几何
|
||||
mesh.castShadow = false; // 由 _restack 依据是否为顶层页控制
|
||||
const mesh = new THREE.Mesh(this.loGeo, mat); // 初始为栈内页,用极简几何
|
||||
mesh.castShadow = false; // 由 _restackState 依据是否为顶层页控制
|
||||
|
||||
const g = new THREE.Group();
|
||||
g.add(mesh);
|
||||
@@ -404,9 +407,9 @@ export class Binder {
|
||||
|
||||
this.pages.push({
|
||||
group: g,
|
||||
hiGeo,
|
||||
loGeo,
|
||||
hiBase,
|
||||
hiGeo: null, // 高分段几何延迟创建(成为顶层页时才需要弯曲)
|
||||
hiBase: null,
|
||||
loGeo: this.loGeo,
|
||||
material: mat,
|
||||
mesh,
|
||||
isPlaceholder: true,
|
||||
@@ -428,9 +431,16 @@ export class Binder {
|
||||
const angle = j < this.current ? -Math.PI : 0;
|
||||
p.angle = angle;
|
||||
p.group.rotation.y = angle;
|
||||
const onTop = (j === this.current || j === this.current - 1);
|
||||
const depth = j < this.current ? (this.current - 1 - j) : (j - this.current);
|
||||
// 顶层页抬高,与栈内页拉开深度,避免低精度深度缓冲下的穿模(z-fighting)
|
||||
});
|
||||
this._restackState(this.current, instant);
|
||||
this._updateCurvature();
|
||||
}
|
||||
|
||||
// 堆叠状态(z 目标/亮度/投影/几何 LOD),不碰角度;可传未来 current 实现与翻页并行
|
||||
_restackState(cur, instant = false) {
|
||||
this.pages.forEach((p, j) => {
|
||||
const onTop = (j === cur || j === cur - 1);
|
||||
const depth = j < cur ? (cur - 1 - j) : (j - cur);
|
||||
p.targetZ = onTop ? TOP_LIFT : -depth * STACK_GAP;
|
||||
if (instant) {
|
||||
p.z = p.targetZ;
|
||||
@@ -438,28 +448,34 @@ export class Binder {
|
||||
}
|
||||
p.group.position.y = 0;
|
||||
p.group.position.x = 0;
|
||||
// 下层页略暗
|
||||
const dim = 1 - Math.min(depth * 0.06, 0.22);
|
||||
p.material.color.setScalar(dim);
|
||||
// 只让顶层(当前右页 + 左侧页)投影,栈内被遮挡页不投影,省阴影 pass
|
||||
p.mesh.castShadow = onTop;
|
||||
// 顶层页用高分段几何(翻页弯曲),栈内页用极简几何(省顶点)
|
||||
if ((p.mesh.geometry === p.hiGeo) !== onTop) {
|
||||
p.mesh.geometry = onTop ? p.hiGeo : p.loGeo;
|
||||
if (onTop) {
|
||||
this._ensureHiGeo(p);
|
||||
if (p.mesh.geometry !== p.hiGeo) p.mesh.geometry = p.hiGeo;
|
||||
} else if (p.mesh.geometry !== p.loGeo) {
|
||||
p.mesh.geometry = p.loGeo;
|
||||
}
|
||||
});
|
||||
this._updateCurvature();
|
||||
this._ensureTextures();
|
||||
}
|
||||
|
||||
// 需要真实纹理的窗口:当前页 ±2(左右可见页 + 翻页时会露出的下一层页 + 缓冲)
|
||||
// 需要真实纹理的窗口:左右可见页 + 翻页时会露出的下一层页(按需,首屏最小)
|
||||
_pageWindow() {
|
||||
const len = this.pages.length;
|
||||
const win = new Set();
|
||||
for (let d = -2; d <= 2; d++) {
|
||||
// 左页 + 右页(当前)
|
||||
for (let d = -1; d <= 0; d++) {
|
||||
const i = this.current + d;
|
||||
if (i >= 0 && i < len) win.add(i);
|
||||
}
|
||||
// 向前翻页会露出的下一层右页
|
||||
const fwd = this.current + 1;
|
||||
if (fwd >= 0 && fwd < len) win.add(fwd);
|
||||
// 向后翻页会露出的下一层左页
|
||||
const bwd = this.current - 2;
|
||||
if (bwd >= 0 && bwd < len) win.add(bwd);
|
||||
return win;
|
||||
}
|
||||
|
||||
@@ -485,10 +501,17 @@ export class Binder {
|
||||
}
|
||||
}
|
||||
|
||||
// 预生成即将进入纹理窗口的页:推迟到下一帧,避免占用点击这一帧造成卡顿
|
||||
_prefetchForFlip(index) {
|
||||
if (index < 0 || index >= ENTRIES.length) return;
|
||||
requestAnimationFrame(() => prefetchPageTextures(ENTRIES[index]));
|
||||
}
|
||||
|
||||
_updateCurvature(index = null) {
|
||||
// 翻页时整页柔软弧弯:自由边最高、中段有“腹”、书脊侧贴环;顶边略强于底边;静止时完全平整
|
||||
const pages = index === null ? this.pages : [this.pages[index]];
|
||||
pages.forEach((p) => {
|
||||
if (!p.hiGeo) return; // 栈内页未创建高分段几何,跳过
|
||||
// 弯曲只作用于高分段几何(顶层/翻页页);栈内极简几何不弯曲
|
||||
const pos = p.hiGeo.attributes.position;
|
||||
const arr = pos.array;
|
||||
@@ -514,12 +537,13 @@ export class Binder {
|
||||
syncTo(index) {
|
||||
this.current = clamp(index, 0, this.pages.length - 1);
|
||||
this.flip = null;
|
||||
this.resetTween = null; // 外部跳转时终止回卷/快进动画,避免旧状态残留
|
||||
this._restack(true);
|
||||
if (this.onChange) this.onChange(this.current);
|
||||
}
|
||||
|
||||
canFlipForward() { return !this.flip; }
|
||||
canFlipBackward() { return !this.flip && this.current > 0; }
|
||||
canFlipForward() { return !this.flip && !this.resetTween; }
|
||||
canFlipBackward() { return !this.flip && !this.resetTween; }
|
||||
|
||||
// 页面 z 是否仍在向目标位置过渡(用于按需渲染:过渡期间需要持续重绘)
|
||||
isSettling() {
|
||||
@@ -535,46 +559,74 @@ export class Binder {
|
||||
p.group.rotation.y = f.to;
|
||||
p.z = TOP_LIFT;
|
||||
p.group.position.z = TOP_LIFT;
|
||||
if (f.to === -Math.PI) {
|
||||
this.current = (this.current + 1) % this.pages.length;
|
||||
} else {
|
||||
this.current -= 1;
|
||||
}
|
||||
this.current = f.nextCurrent;
|
||||
this.flip = null;
|
||||
this._restack(false);
|
||||
}
|
||||
|
||||
_startWrap(targetCur) {
|
||||
// 回卷/快进:所有页从各自当前角度平滑转到目标角度(左页→右侧 或 右页→左侧),current 跳到目标页
|
||||
this.current = targetCur;
|
||||
this.flip = null;
|
||||
this.resetTween = { t: 0, duration: 0.5 };
|
||||
this.pages.forEach((p, j) => {
|
||||
p._resetFrom = p.angle;
|
||||
p._resetTo = j < targetCur ? -Math.PI : 0;
|
||||
});
|
||||
this._restackState(targetCur, false);
|
||||
if (this.onChange) this.onChange(targetCur);
|
||||
}
|
||||
|
||||
flipForward() {
|
||||
if (this.flip) this._finalizeFlip(); // 前一页没翻完也能继续翻
|
||||
if (!this.canFlipForward()) return;
|
||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38 };
|
||||
// 提前把即将露出的下一层右页切到顶层状态(高分段几何/亮度/投影),避免翻页完成瞬间跳变
|
||||
const reveal = this.pages[(this.current + 1) % this.pages.length];
|
||||
if (reveal) {
|
||||
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
|
||||
reveal.mesh.castShadow = true;
|
||||
reveal.material.color.setScalar(1.0);
|
||||
// END 页(所有页都已翻到左侧,current = pages.length):向前 → 回卷到第一页
|
||||
if (this.current >= this.pages.length) {
|
||||
this._startWrap(0);
|
||||
return;
|
||||
}
|
||||
// 最后一页翻完即进入 END 页:右侧无纸,左页为最后一页的背面
|
||||
const nextCurrent = this.current + 1;
|
||||
this.flip = { index: this.current, from: 0, to: -Math.PI, t: 0, duration: 0.38, nextCurrent };
|
||||
// 堆叠重排与旋转并行:翻页开始即设好目标位置,z-lerp 随旋转同步过渡
|
||||
this._restackState(nextCurrent, false);
|
||||
this._prefetchForFlip((nextCurrent + 2) % this.pages.length);
|
||||
// 优先更新底部导航与信息卡(翻页开始即切换目标页)
|
||||
if (this.onChange) this.onChange((this.current + 1) % this.pages.length);
|
||||
if (this.onChange) this.onChange(nextCurrent);
|
||||
}
|
||||
|
||||
flipBackward() {
|
||||
if (this.flip) this._finalizeFlip();
|
||||
if (!this.canFlipBackward()) return;
|
||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38 };
|
||||
// 提前把即将露出的下一层左页切到顶层状态,避免翻页完成瞬间跳变
|
||||
const reveal = this.pages[this.current - 2];
|
||||
if (reveal) {
|
||||
if (reveal.mesh.geometry !== reveal.hiGeo) reveal.mesh.geometry = reveal.hiGeo;
|
||||
reveal.mesh.castShadow = true;
|
||||
reveal.material.color.setScalar(1.0);
|
||||
// 第一页向后:快进到 END 页(所有未翻的右页都翻到左侧,展示末页背面)
|
||||
if (this.current === 0) {
|
||||
this._startWrap(this.pages.length);
|
||||
return;
|
||||
}
|
||||
// 优先更新底部导航与信息卡
|
||||
if (this.onChange) this.onChange(this.current - 1);
|
||||
const nextCurrent = this.current - 1;
|
||||
this.flip = { index: this.current - 1, from: -Math.PI, to: 0, t: 0, duration: 0.38, nextCurrent };
|
||||
this._restackState(nextCurrent, false);
|
||||
this._prefetchForFlip((nextCurrent - 2 + this.pages.length) % this.pages.length);
|
||||
if (this.onChange) this.onChange(nextCurrent);
|
||||
}
|
||||
|
||||
update(dt) {
|
||||
// 回卷重置动画:所有页角度从各自起点平滑回到 0(右侧)
|
||||
if (this.resetTween) {
|
||||
const r = this.resetTween;
|
||||
r.t += dt / r.duration;
|
||||
const k = clamp(r.t, 0, 1);
|
||||
const e = easeInOutCubic(k);
|
||||
this.pages.forEach((p) => {
|
||||
p.angle = p._resetFrom + (p._resetTo - p._resetFrom) * e;
|
||||
p.group.rotation.y = p.angle;
|
||||
});
|
||||
if (k >= 1) {
|
||||
this.resetTween = null;
|
||||
this._restack(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.flip) {
|
||||
const f = this.flip;
|
||||
f.t += dt / f.duration;
|
||||
@@ -591,12 +643,10 @@ export class Binder {
|
||||
this._updateCurvature(f.index);
|
||||
|
||||
if (k >= 1) {
|
||||
if (f.to === -Math.PI) {
|
||||
// 最后一页翻完后循环回第一页
|
||||
this.current = (this.current + 1) % this.pages.length;
|
||||
} else this.current -= 1;
|
||||
this.current = f.nextCurrent;
|
||||
this.flip = null;
|
||||
this._restack(false);
|
||||
this._updateCurvature();
|
||||
this._ensureTextures();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,9 +660,9 @@ export class Binder {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.loGeo) this.loGeo.dispose();
|
||||
this.pages.forEach((p) => {
|
||||
p.hiGeo.dispose();
|
||||
p.loGeo.dispose();
|
||||
if (p.hiGeo) p.hiGeo.dispose();
|
||||
p.material.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
+51
-22
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import { ENTRIES } from '../data/entries.js';
|
||||
import { acquirePageTextures, releasePageTextures, getPlaceholderTextures, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||
import { acquirePageTexturesLow, releasePageTexturesLow, getPlaceholderTexturesLow, makeTwoSidedMaterial } from '../textures/pages.js';
|
||||
import { makeTextSprite } from '../utils/labels.js';
|
||||
import { clamp, easeInOutCubic } from '../utils/easing.js';
|
||||
|
||||
@@ -22,12 +22,13 @@ export class Timeline {
|
||||
this.group = new THREE.Group();
|
||||
this.cards = [];
|
||||
this.selected = -1;
|
||||
this.placeholder = getPlaceholderTextures();
|
||||
this.placeholder = getPlaceholderTexturesLow();
|
||||
this._textured = new Set();
|
||||
this._visRange = null;
|
||||
this._ready = false;
|
||||
this._pending = [];
|
||||
this._dirty = false;
|
||||
this._paused = false;
|
||||
|
||||
this._buildLine();
|
||||
this._buildCards();
|
||||
@@ -44,15 +45,16 @@ export class Timeline {
|
||||
]);
|
||||
this.group.add(new THREE.Line(lineGeo, lineMat));
|
||||
|
||||
// 刻度:向上指向卡片、向下指向时间标签
|
||||
// 刻度:合并为单个 LineSegments,一次绘制,避免上百次 draw call
|
||||
const tickMat = new THREE.LineBasicMaterial({ color: 0xc9a227, transparent: true, opacity: 0.5 });
|
||||
const tickPts = [];
|
||||
ENTRIES.forEach((e, i) => {
|
||||
const x = cardX(i, n);
|
||||
const g = new THREE.BufferGeometry().setFromPoints([
|
||||
new THREE.Vector3(x, AXIS_Y - 0.14, 0), new THREE.Vector3(x, AXIS_Y + 0.12, 0),
|
||||
]);
|
||||
this.group.add(new THREE.Line(g, tickMat));
|
||||
tickPts.push(x, AXIS_Y - 0.14, 0, x, AXIS_Y + 0.12, 0);
|
||||
});
|
||||
const tickGeo = new THREE.BufferGeometry();
|
||||
tickGeo.setAttribute('position', new THREE.Float32BufferAttribute(tickPts, 3));
|
||||
this.group.add(new THREE.LineSegments(tickGeo, tickMat));
|
||||
}
|
||||
|
||||
_buildCards() {
|
||||
@@ -62,7 +64,7 @@ export class Timeline {
|
||||
const x = cardX(i, n);
|
||||
|
||||
// 每张卡片独立几何(带分段,便于翻面时弯曲出纸感)
|
||||
const cardGeo = new THREE.PlaneGeometry(0.4, 0.565, 24, 6);
|
||||
const cardGeo = new THREE.PlaneGeometry(0.4, 0.565, 12, 3);
|
||||
const base = Float32Array.from(cardGeo.attributes.position.array);
|
||||
|
||||
// 翻转容器:单 mesh + 双面材质(翻面时 gl_FrontFacing 自动切换正/背)
|
||||
@@ -76,25 +78,45 @@ export class Timeline {
|
||||
|
||||
flipGroup.add(cardMesh);
|
||||
|
||||
// 活页上方:主题(常显,随选中上浮,超宽居中换行)
|
||||
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.12, maxWidth: 0.9 });
|
||||
name.position.set(x, NAME_Y, 0);
|
||||
|
||||
// 轴下方:时间(常显,固定不动)
|
||||
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
|
||||
time.position.set(x, TIME_Y, 0);
|
||||
|
||||
this.group.add(flipGroup, name, time);
|
||||
this.cards.push({ card: flipGroup, mesh: cardMesh, material: mat, geo: cardGeo, base, name, nameBaseY: NAME_Y, index: i, flipped: false, flipAnim: null, isPlaceholder: true });
|
||||
// 文字标签(名称/时间)延迟到首屏后由 initLabels() 分帧生成,避免阻塞首屏
|
||||
this.group.add(flipGroup);
|
||||
this.cards.push({ card: flipGroup, mesh: cardMesh, material: mat, geo: cardGeo, base, name: null, time: null, nameBaseY: NAME_Y, index: i, flipped: false, flipAnim: null, isPlaceholder: true });
|
||||
});
|
||||
}
|
||||
|
||||
// 时间轴文字标签:延迟到首屏后空闲时分帧生成(364 个 canvas 文字精灵较慢,避免阻塞首屏)
|
||||
initLabels() {
|
||||
if (this._labelsDone) return;
|
||||
this._labelsDone = true;
|
||||
const n = ENTRIES.length;
|
||||
let i = 0;
|
||||
const next = () => window.requestAnimationFrame(step);
|
||||
const step = () => {
|
||||
const budget = 4; // 每帧生成 4 个标签,避免卡帧
|
||||
for (let k = 0; k < budget && i < n; k++, i++) {
|
||||
const c = this.cards[i];
|
||||
const entry = ENTRIES[i];
|
||||
const x = cardX(i, n);
|
||||
const name = makeTextSprite(entry.name, { fontSize: 17, color: '#e8d9b4', scale: 0.12, maxWidth: 0.9 });
|
||||
name.position.set(x, NAME_Y, 0);
|
||||
const time = makeTextSprite(entry.time, { fontSize: 13, color: '#c9a227', scale: 0.11 });
|
||||
time.position.set(x, TIME_Y, 0);
|
||||
this.group.add(name, time);
|
||||
c.name = name;
|
||||
c.time = time;
|
||||
if (this.selected === i) name.position.y = c.nameBaseY + CARD_FLOAT;
|
||||
}
|
||||
if (i < n) next();
|
||||
};
|
||||
next();
|
||||
}
|
||||
|
||||
setSelected(index) {
|
||||
this.selected = index;
|
||||
this.cards.forEach((c) => {
|
||||
const on = c.index === index;
|
||||
c.card.position.y = on ? CARD_FLOAT : 0;
|
||||
c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0);
|
||||
if (c.name) c.name.position.y = c.nameBaseY + (on ? CARD_FLOAT : 0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,7 +128,7 @@ export class Timeline {
|
||||
// App 每帧根据可视区域调用:minI/maxI 为可见卡片索引范围(可为小数)
|
||||
setVisibleRange(minI, maxI) {
|
||||
const len = this.cards.length;
|
||||
const PAD = 4; // 缓冲:可视范围外多生成几页,拖拽时提前就绪
|
||||
const PAD = 6; // 缓冲:可视范围外多生成几页,拖拽时提前就绪
|
||||
const lo = Math.max(0, Math.floor(minI) - PAD);
|
||||
const hi = Math.min(len - 1, Math.ceil(maxI) + PAD);
|
||||
if (this._visRange && this._visRange.min === lo && this._visRange.max === hi) return;
|
||||
@@ -138,7 +160,7 @@ export class Timeline {
|
||||
for (const i of [...this._textured]) {
|
||||
if (win.has(i)) continue;
|
||||
const c = this.cards[i];
|
||||
releasePageTextures(ENTRIES[i]);
|
||||
releasePageTexturesLow(ENTRIES[i]);
|
||||
c.material.setPageMaps(this.placeholder.front, this.placeholder.back);
|
||||
c.isPlaceholder = true;
|
||||
this._textured.delete(i);
|
||||
@@ -152,8 +174,15 @@ export class Timeline {
|
||||
this.drainPending();
|
||||
}
|
||||
|
||||
// 拖拽期间暂停纹理生成,避免主线程同步绘制导致掉帧
|
||||
setPausedGeneration(paused) {
|
||||
this._paused = paused;
|
||||
if (!paused) this.drainPending(); // 恢复时立即补生成
|
||||
}
|
||||
|
||||
// 每帧由 App._animate 驱动,逐批生成窗口内纹理(避免阻塞)
|
||||
drainPending() {
|
||||
if (this._paused) return false;
|
||||
if (!this._pending || !this._pending.length) return false;
|
||||
const budget = 2; // 每帧最多生成 2 页
|
||||
let did = false;
|
||||
@@ -161,7 +190,7 @@ export class Timeline {
|
||||
const i = this._pending.shift();
|
||||
const c = this.cards[i];
|
||||
if (!c || this._textured.has(i)) continue;
|
||||
const { front, back } = acquirePageTextures(ENTRIES[i]);
|
||||
const { front, back } = acquirePageTexturesLow(ENTRIES[i]);
|
||||
c.material.setPageMaps(front, back);
|
||||
c.isPlaceholder = false;
|
||||
this._textured.add(i);
|
||||
|
||||
Reference in New Issue
Block a user