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_POSITION = new THREE.Vector3(-0.113, -0.149, 10.911);
|
||||||
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.35, 0);
|
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.35, 0);
|
||||||
const MODEL_FRONT_ROTATION_Y = -Math.PI / 2;
|
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[] = [
|
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: "thumb",
|
||||||
{ id: "middle", rows: 12, cols: 7, x: 0.03, y: 4.18, width: 0.6, height: 1.14, rotation: 0 },
|
rows: 12,
|
||||||
{ id: "ring", rows: 12, cols: 7, x: 0.8, y: 4, width: 0.54, height: 1, rotation: THREE.MathUtils.degToRad(-5) },
|
cols: 7,
|
||||||
{ id: "pinky", rows: 12, cols: 7, x: 1.58, y: 3.36, width: 0.48, height: 0.92, rotation: THREE.MathUtils.degToRad(-8) },
|
x: -1.38,
|
||||||
{ id: "palm-horizontal", rows: 5, cols: 14, x: 0.18, y: 1.34, width: 1.84, height: 0.54, rotation: 0 },
|
y: 1.98,
|
||||||
{ id: "palm-vertical", rows: 11, cols: 4, x: 1.02, y: 0.2, width: 0.44, height: 1.46, rotation: 0 }
|
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";
|
$: canvasLabel = locale === "zh-CN" ? "3D 模型" : "3D Model";
|
||||||
@@ -89,14 +153,18 @@
|
|||||||
color: 0x0c1824,
|
color: 0x0c1824,
|
||||||
emissive: 0x07131f,
|
emissive: 0x07131f,
|
||||||
metalness: 0.62,
|
metalness: 0.62,
|
||||||
roughness: 0.34
|
roughness: 0.34,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
platform.position.y = 0.18;
|
platform.position.y = 0.18;
|
||||||
group.add(platform);
|
group.add(platform);
|
||||||
|
|
||||||
const ringGeometry = new THREE.TorusGeometry(4.35, 0.035, 10, 128);
|
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) {
|
for (let index = 0; index < 3; index += 1) {
|
||||||
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
||||||
ring.position.y = 0.52 + index * 0.52;
|
ring.position.y = 0.52 + index * 0.52;
|
||||||
@@ -110,17 +178,31 @@
|
|||||||
metalness: 0.48,
|
metalness: 0.48,
|
||||||
roughness: 0.42,
|
roughness: 0.42,
|
||||||
transparent: true,
|
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.position.y = 2.4;
|
||||||
core.rotation.y = -0.36;
|
core.rotation.y = -0.36;
|
||||||
group.add(core);
|
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) {
|
for (let index = 0; index < 7; index += 1) {
|
||||||
const bead = new THREE.Mesh(new THREE.SphereGeometry(0.13, 18, 18), sensorMaterial);
|
const bead = new THREE.Mesh(
|
||||||
bead.position.set(-0.72 + index * 0.24, 3.18 + Math.sin(index * 0.72) * 0.18, 0.6);
|
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);
|
group.add(bead);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +213,11 @@
|
|||||||
return Math.min(max, Math.max(min, value));
|
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));
|
return low.clone().lerp(high, clamp(amount, 0, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +245,14 @@
|
|||||||
sensorPointCount = positions.length / 3;
|
sensorPointCount = positions.length / 3;
|
||||||
sensorColors = new Float32Array(sensorPointCount * 3);
|
sensorColors = new Float32Array(sensorPointCount * 3);
|
||||||
sensorGeometry = new THREE.BufferGeometry();
|
sensorGeometry = new THREE.BufferGeometry();
|
||||||
sensorGeometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
|
sensorGeometry.setAttribute(
|
||||||
sensorGeometry.setAttribute("color", new THREE.BufferAttribute(sensorColors, 3));
|
"position",
|
||||||
|
new THREE.Float32BufferAttribute(positions, 3),
|
||||||
|
);
|
||||||
|
sensorGeometry.setAttribute(
|
||||||
|
"color",
|
||||||
|
new THREE.BufferAttribute(sensorColors, 3),
|
||||||
|
);
|
||||||
|
|
||||||
const points = new THREE.Points(
|
const points = new THREE.Points(
|
||||||
sensorGeometry,
|
sensorGeometry,
|
||||||
@@ -171,8 +263,8 @@
|
|||||||
opacity: 0.92,
|
opacity: 0.92,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
sizeAttenuation: true
|
sizeAttenuation: true,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
points.renderOrder = 20;
|
points.renderOrder = 20;
|
||||||
points.name = "hand-sensor-dot-overlay";
|
points.name = "hand-sensor-dot-overlay";
|
||||||
@@ -185,7 +277,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const palette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
const palette =
|
||||||
|
pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
|
||||||
const base = new THREE.Color(palette.labelZero);
|
const base = new THREE.Color(palette.labelZero);
|
||||||
const mid = new THREE.Color(palette.surfaceHigh);
|
const mid = new THREE.Color(palette.surfaceHigh);
|
||||||
const hot = new THREE.Color(palette.surfaceHot);
|
const hot = new THREE.Color(palette.surfaceHot);
|
||||||
@@ -194,7 +287,9 @@
|
|||||||
|
|
||||||
for (let index = 0; index < sensorPointCount; index += 1) {
|
for (let index = 0; index < sensorPointCount; index += 1) {
|
||||||
const value = pressureMatrix?.[index];
|
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 =
|
const color =
|
||||||
normalized <= 0.5
|
normalized <= 0.5
|
||||||
? mixColors(base, mid, normalized * 2)
|
? mixColors(base, mid, normalized * 2)
|
||||||
@@ -212,7 +307,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function materialToUnlit(material: THREE.Material): THREE.Material {
|
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({
|
const unlit = new THREE.MeshBasicMaterial({
|
||||||
color: source.color?.clone() ?? new THREE.Color(0xffffff),
|
color: source.color?.clone() ?? new THREE.Color(0xffffff),
|
||||||
map: source.map ?? null,
|
map: source.map ?? null,
|
||||||
@@ -223,7 +319,7 @@
|
|||||||
depthWrite: source.depthWrite,
|
depthWrite: source.depthWrite,
|
||||||
depthTest: source.depthTest,
|
depthTest: source.depthTest,
|
||||||
vertexColors: source.vertexColors,
|
vertexColors: source.vertexColors,
|
||||||
toneMapped: false
|
toneMapped: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return unlit;
|
return unlit;
|
||||||
@@ -250,7 +346,11 @@
|
|||||||
let bounds = new THREE.Box3().setFromObject(object);
|
let bounds = new THREE.Box3().setFromObject(object);
|
||||||
const size = bounds.getSize(new THREE.Vector3());
|
const size = bounds.getSize(new THREE.Vector3());
|
||||||
const currentHeight = Math.max(size.y, 0.001);
|
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.scale.multiplyScalar(scale);
|
||||||
object.updateMatrixWorld(true);
|
object.updateMatrixWorld(true);
|
||||||
@@ -265,7 +365,11 @@
|
|||||||
return new THREE.Box3().setFromObject(object);
|
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);
|
normalizeObjectToStage(object);
|
||||||
camera.position.copy(FIXED_CAMERA_POSITION);
|
camera.position.copy(FIXED_CAMERA_POSITION);
|
||||||
camera.near = 0.05;
|
camera.near = 0.05;
|
||||||
@@ -286,7 +390,7 @@
|
|||||||
canvas: canvasEl,
|
canvas: canvasEl,
|
||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: true,
|
alpha: true,
|
||||||
powerPreference: "high-performance"
|
powerPreference: "high-performance",
|
||||||
});
|
});
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
renderer.setClearColor(0x03070d, 1);
|
renderer.setClearColor(0x03070d, 1);
|
||||||
@@ -347,8 +451,8 @@
|
|||||||
metalness: 0.28,
|
metalness: 0.28,
|
||||||
roughness: 0.64,
|
roughness: 0.64,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: 0.72
|
opacity: 0.72,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
floor.rotation.x = -Math.PI / 2;
|
floor.rotation.x = -Math.PI / 2;
|
||||||
floor.position.y = FLOOR_Y - 0.018;
|
floor.position.y = FLOOR_Y - 0.018;
|
||||||
@@ -386,7 +490,7 @@
|
|||||||
loadError = message || "Unknown model loader error";
|
loadError = message || "Unknown model loader error";
|
||||||
loadState = message.toLowerCase().includes("404") ? "missing" : "error";
|
loadState = message.toLowerCase().includes("404") ? "missing" : "error";
|
||||||
loadProgress = 0;
|
loadProgress = 0;
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const resize = () => {
|
const resize = () => {
|
||||||
@@ -432,7 +536,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="model-stage" bind:this={rootEl}>
|
<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-vignette" aria-hidden="true"></div>
|
||||||
<div class="model-scanlines" aria-hidden="true"></div>
|
<div class="model-scanlines" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -442,9 +547,16 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background: radial-gradient(
|
||||||
radial-gradient(circle at 52% 62%, rgb(94 231 255 / 0.12), transparent 26%),
|
circle at 52% 62%,
|
||||||
radial-gradient(circle at 24% 18%, rgb(166 255 122 / 0.07), transparent 24%),
|
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%);
|
linear-gradient(180deg, #03070d 0%, #07111b 48%, #02050a 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,16 +579,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.model-vignette {
|
.model-vignette {
|
||||||
background:
|
background: linear-gradient(
|
||||||
linear-gradient(90deg, rgb(0 0 0 / 0.36), transparent 22%, transparent 78%, rgb(0 0 0 / 0.34)),
|
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%);
|
radial-gradient(circle at center, transparent 48%, rgb(0 0 0 / 0.58) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-scanlines {
|
.model-scanlines {
|
||||||
opacity: 0.32;
|
opacity: 0.32;
|
||||||
background:
|
background: repeating-linear-gradient(
|
||||||
repeating-linear-gradient(180deg, rgb(94 231 255 / 0.045) 0, rgb(94 231 255 / 0.045) 1px, transparent 1px, transparent 4px);
|
180deg,
|
||||||
|
rgb(94 231 255 / 0.045) 0,
|
||||||
|
rgb(94 231 255 / 0.045) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 4px
|
||||||
|
);
|
||||||
mix-blend-mode: screen;
|
mix-blend-mode: screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user