feat(ui): ModelStage.svelte 进一步功能扩展与重构
Some checks failed
Playwright Tests / test (push) Has been cancelled
Some checks failed
Playwright Tests / test (push) Has been cancelled
This commit is contained in:
@@ -41,15 +41,79 @@
|
||||
const FIXED_CAMERA_POSITION = new THREE.Vector3(-0.113, -0.149, 10.911);
|
||||
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.35, 0);
|
||||
const MODEL_FRONT_ROTATION_Y = -Math.PI / 2;
|
||||
const MODEL_UPRIGHT_ROTATION_Z = Math.PI * 1.5 - THREE.MathUtils.degToRad(2.2);
|
||||
const MODEL_UPRIGHT_ROTATION_Z =
|
||||
Math.PI * 1.5 - THREE.MathUtils.degToRad(2.2);
|
||||
const sensorRegions: SensorRegion[] = [
|
||||
{ id: "thumb", rows: 12, cols: 7, x: -1.48, y: 1.95, width: 0.54, height: 1.02, rotation: THREE.MathUtils.degToRad(35) },
|
||||
{ id: "index", rows: 12, cols: 7, x: -0.72, y: 3.84, width: 0.54, height: 1.02, rotation: THREE.MathUtils.degToRad(6) },
|
||||
{ id: "middle", rows: 12, cols: 7, x: 0.03, y: 4.18, width: 0.6, height: 1.14, rotation: 0 },
|
||||
{ id: "ring", rows: 12, cols: 7, x: 0.8, y: 4, width: 0.54, height: 1, rotation: THREE.MathUtils.degToRad(-5) },
|
||||
{ id: "pinky", rows: 12, cols: 7, x: 1.58, y: 3.36, width: 0.48, height: 0.92, rotation: THREE.MathUtils.degToRad(-8) },
|
||||
{ id: "palm-horizontal", rows: 5, cols: 14, x: 0.18, y: 1.34, width: 1.84, height: 0.54, rotation: 0 },
|
||||
{ id: "palm-vertical", rows: 11, cols: 4, x: 1.02, y: 0.2, width: 0.44, height: 1.46, rotation: 0 }
|
||||
{
|
||||
id: "thumb",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: -1.38,
|
||||
y: 1.98,
|
||||
width: 0.52,
|
||||
height: 0.98,
|
||||
rotation: THREE.MathUtils.degToRad(35),
|
||||
},
|
||||
{
|
||||
id: "index",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: -0.6,
|
||||
y: 3.75,
|
||||
width: 0.52,
|
||||
height: 0.98,
|
||||
rotation: THREE.MathUtils.degToRad(6),
|
||||
},
|
||||
{
|
||||
id: "middle",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 0.1,
|
||||
y: 4.0,
|
||||
width: 0.58,
|
||||
height: 1.06,
|
||||
rotation: 0,
|
||||
},
|
||||
{
|
||||
id: "ring",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 0.82,
|
||||
y: 3.9,
|
||||
width: 0.52,
|
||||
height: 0.96,
|
||||
rotation: THREE.MathUtils.degToRad(-5),
|
||||
},
|
||||
{
|
||||
id: "pinky",
|
||||
rows: 12,
|
||||
cols: 7,
|
||||
x: 1.42,
|
||||
y: 3.2,
|
||||
width: 0.46,
|
||||
height: 0.88,
|
||||
rotation: THREE.MathUtils.degToRad(-8),
|
||||
},
|
||||
{
|
||||
id: "palm-horizontal",
|
||||
rows: 5,
|
||||
cols: 14,
|
||||
x: 0.26,
|
||||
y: 1.68,
|
||||
width: 1.84,
|
||||
height: 0.5,
|
||||
rotation: THREE.MathUtils.degToRad(-4),
|
||||
},
|
||||
{
|
||||
id: "palm-vertical",
|
||||
rows: 11,
|
||||
cols: 4,
|
||||
x: 0.86,
|
||||
y: 0.5,
|
||||
width: 0.42,
|
||||
height: 1.26,
|
||||
rotation: THREE.MathUtils.degToRad(-4),
|
||||
},
|
||||
];
|
||||
|
||||
$: canvasLabel = locale === "zh-CN" ? "3D 模型" : "3D Model";
|
||||
@@ -89,14 +153,18 @@
|
||||
color: 0x0c1824,
|
||||
emissive: 0x07131f,
|
||||
metalness: 0.62,
|
||||
roughness: 0.34
|
||||
})
|
||||
roughness: 0.34,
|
||||
}),
|
||||
);
|
||||
platform.position.y = 0.18;
|
||||
group.add(platform);
|
||||
|
||||
const ringGeometry = new THREE.TorusGeometry(4.35, 0.035, 10, 128);
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({ color: cyan, transparent: true, opacity: 0.78 });
|
||||
const ringMaterial = new THREE.MeshBasicMaterial({
|
||||
color: cyan,
|
||||
transparent: true,
|
||||
opacity: 0.78,
|
||||
});
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||
ring.position.y = 0.52 + index * 0.52;
|
||||
@@ -110,17 +178,31 @@
|
||||
metalness: 0.48,
|
||||
roughness: 0.42,
|
||||
transparent: true,
|
||||
opacity: 0.72
|
||||
opacity: 0.72,
|
||||
});
|
||||
const core = new THREE.Mesh(new THREE.BoxGeometry(2.2, 3.4, 1.1), coreMaterial);
|
||||
const core = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2.2, 3.4, 1.1),
|
||||
coreMaterial,
|
||||
);
|
||||
core.position.y = 2.4;
|
||||
core.rotation.y = -0.36;
|
||||
group.add(core);
|
||||
|
||||
const sensorMaterial = new THREE.MeshBasicMaterial({ color: lime, transparent: true, opacity: 0.88 });
|
||||
const sensorMaterial = new THREE.MeshBasicMaterial({
|
||||
color: lime,
|
||||
transparent: true,
|
||||
opacity: 0.88,
|
||||
});
|
||||
for (let index = 0; index < 7; index += 1) {
|
||||
const bead = new THREE.Mesh(new THREE.SphereGeometry(0.13, 18, 18), sensorMaterial);
|
||||
bead.position.set(-0.72 + index * 0.24, 3.18 + Math.sin(index * 0.72) * 0.18, 0.6);
|
||||
const bead = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.13, 18, 18),
|
||||
sensorMaterial,
|
||||
);
|
||||
bead.position.set(
|
||||
-0.72 + index * 0.24,
|
||||
3.18 + Math.sin(index * 0.72) * 0.18,
|
||||
0.6,
|
||||
);
|
||||
group.add(bead);
|
||||
}
|
||||
|
||||
@@ -131,7 +213,11 @@
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function mixColors(low: THREE.Color, high: THREE.Color, amount: number): THREE.Color {
|
||||
function mixColors(
|
||||
low: THREE.Color,
|
||||
high: THREE.Color,
|
||||
amount: number,
|
||||
): THREE.Color {
|
||||
return low.clone().lerp(high, clamp(amount, 0, 1));
|
||||
}
|
||||
|
||||
@@ -159,8 +245,14 @@
|
||||
sensorPointCount = positions.length / 3;
|
||||
sensorColors = new Float32Array(sensorPointCount * 3);
|
||||
sensorGeometry = new THREE.BufferGeometry();
|
||||
sensorGeometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
||||
sensorGeometry.setAttribute("color", new THREE.BufferAttribute(sensorColors, 3));
|
||||
sensorGeometry.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(positions, 3),
|
||||
);
|
||||
sensorGeometry.setAttribute(
|
||||
"color",
|
||||
new THREE.BufferAttribute(sensorColors, 3),
|
||||
);
|
||||
|
||||
const points = new THREE.Points(
|
||||
sensorGeometry,
|
||||
@@ -171,8 +263,8 @@
|
||||
opacity: 0.92,
|
||||
depthTest: false,
|
||||
depthWrite: false,
|
||||
sizeAttenuation: true
|
||||
})
|
||||
sizeAttenuation: true,
|
||||
}),
|
||||
);
|
||||
points.renderOrder = 20;
|
||||
points.name = "hand-sensor-dot-overlay";
|
||||
@@ -185,7 +277,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const palette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||
const palette =
|
||||
pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||
const base = new THREE.Color(palette.labelZero);
|
||||
const mid = new THREE.Color(palette.surfaceHigh);
|
||||
const hot = new THREE.Color(palette.surfaceHot);
|
||||
@@ -194,7 +287,9 @@
|
||||
|
||||
for (let index = 0; index < sensorPointCount; index += 1) {
|
||||
const value = pressureMatrix?.[index];
|
||||
const normalized = Number.isFinite(value) ? clamp((Number(value) - rangeMin) / span, 0, 1) : 0;
|
||||
const normalized = Number.isFinite(value)
|
||||
? clamp((Number(value) - rangeMin) / span, 0, 1)
|
||||
: 0;
|
||||
const color =
|
||||
normalized <= 0.5
|
||||
? mixColors(base, mid, normalized * 2)
|
||||
@@ -212,7 +307,8 @@
|
||||
}
|
||||
|
||||
function materialToUnlit(material: THREE.Material): THREE.Material {
|
||||
const source = material as THREE.MeshStandardMaterial & THREE.MeshPhysicalMaterial;
|
||||
const source = material as THREE.MeshStandardMaterial &
|
||||
THREE.MeshPhysicalMaterial;
|
||||
const unlit = new THREE.MeshBasicMaterial({
|
||||
color: source.color?.clone() ?? new THREE.Color(0xffffff),
|
||||
map: source.map ?? null,
|
||||
@@ -223,7 +319,7 @@
|
||||
depthWrite: source.depthWrite,
|
||||
depthTest: source.depthTest,
|
||||
vertexColors: source.vertexColors,
|
||||
toneMapped: false
|
||||
toneMapped: false,
|
||||
});
|
||||
|
||||
return unlit;
|
||||
@@ -250,7 +346,11 @@
|
||||
let bounds = new THREE.Box3().setFromObject(object);
|
||||
const size = bounds.getSize(new THREE.Vector3());
|
||||
const currentHeight = Math.max(size.y, 0.001);
|
||||
const scale = clamp(MODEL_TARGET_HEIGHT / currentHeight, MODEL_MIN_SCALE, MODEL_MAX_SCALE);
|
||||
const scale = clamp(
|
||||
MODEL_TARGET_HEIGHT / currentHeight,
|
||||
MODEL_MIN_SCALE,
|
||||
MODEL_MAX_SCALE,
|
||||
);
|
||||
|
||||
object.scale.multiplyScalar(scale);
|
||||
object.updateMatrixWorld(true);
|
||||
@@ -265,7 +365,11 @@
|
||||
return new THREE.Box3().setFromObject(object);
|
||||
}
|
||||
|
||||
function frameObject(object: THREE.Object3D, camera: THREE.PerspectiveCamera, controls: OrbitControls): void {
|
||||
function frameObject(
|
||||
object: THREE.Object3D,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
controls: OrbitControls,
|
||||
): void {
|
||||
normalizeObjectToStage(object);
|
||||
camera.position.copy(FIXED_CAMERA_POSITION);
|
||||
camera.near = 0.05;
|
||||
@@ -286,7 +390,7 @@
|
||||
canvas: canvasEl,
|
||||
antialias: true,
|
||||
alpha: true,
|
||||
powerPreference: "high-performance"
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||
renderer.setClearColor(0x03070d, 1);
|
||||
@@ -347,8 +451,8 @@
|
||||
metalness: 0.28,
|
||||
roughness: 0.64,
|
||||
transparent: true,
|
||||
opacity: 0.72
|
||||
})
|
||||
opacity: 0.72,
|
||||
}),
|
||||
);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = FLOOR_Y - 0.018;
|
||||
@@ -386,7 +490,7 @@
|
||||
loadError = message || "Unknown model loader error";
|
||||
loadState = message.toLowerCase().includes("404") ? "missing" : "error";
|
||||
loadProgress = 0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const resize = () => {
|
||||
@@ -432,7 +536,8 @@
|
||||
</script>
|
||||
|
||||
<div class="model-stage" bind:this={rootEl}>
|
||||
<canvas class="model-canvas" bind:this={canvasEl} aria-label={canvasLabel}></canvas>
|
||||
<canvas class="model-canvas" bind:this={canvasEl} aria-label={canvasLabel}
|
||||
></canvas>
|
||||
<div class="model-vignette" aria-hidden="true"></div>
|
||||
<div class="model-scanlines" aria-hidden="true"></div>
|
||||
</div>
|
||||
@@ -442,9 +547,16 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 52% 62%, rgb(94 231 255 / 0.12), transparent 26%),
|
||||
radial-gradient(circle at 24% 18%, rgb(166 255 122 / 0.07), transparent 24%),
|
||||
background: radial-gradient(
|
||||
circle at 52% 62%,
|
||||
rgb(94 231 255 / 0.12),
|
||||
transparent 26%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 24% 18%,
|
||||
rgb(166 255 122 / 0.07),
|
||||
transparent 24%
|
||||
),
|
||||
linear-gradient(180deg, #03070d 0%, #07111b 48%, #02050a 100%);
|
||||
}
|
||||
|
||||
@@ -467,16 +579,25 @@
|
||||
}
|
||||
|
||||
.model-vignette {
|
||||
background:
|
||||
linear-gradient(90deg, rgb(0 0 0 / 0.36), transparent 22%, transparent 78%, rgb(0 0 0 / 0.34)),
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(0 0 0 / 0.36),
|
||||
transparent 22%,
|
||||
transparent 78%,
|
||||
rgb(0 0 0 / 0.34)
|
||||
),
|
||||
radial-gradient(circle at center, transparent 48%, rgb(0 0 0 / 0.58) 100%);
|
||||
}
|
||||
|
||||
.model-scanlines {
|
||||
opacity: 0.32;
|
||||
background:
|
||||
repeating-linear-gradient(180deg, rgb(94 231 255 / 0.045) 0, rgb(94 231 255 / 0.045) 1px, transparent 1px, transparent 4px);
|
||||
background: repeating-linear-gradient(
|
||||
180deg,
|
||||
rgb(94 231 255 / 0.045) 0,
|
||||
rgb(94 231 255 / 0.045) 1px,
|
||||
transparent 1px,
|
||||
transparent 4px
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user