From dd30e0e6af9526e4775d82d5ce37b6312ada2cb6 Mon Sep 17 00:00:00 2001 From: lenn Date: Tue, 23 Jun 2026 10:22:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20ModelStage.svelte=20=E8=BF=9B?= =?UTF-8?q?=E4=B8=80=E6=AD=A5=E5=8A=9F=E8=83=BD=E6=89=A9=E5=B1=95=E4=B8=8E?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/ModelStage.svelte | 201 +++++++++++++++++++++------ 1 file changed, 161 insertions(+), 40 deletions(-) diff --git a/src/lib/components/ModelStage.svelte b/src/lib/components/ModelStage.svelte index df1d027..a790fa9 100644 --- a/src/lib/components/ModelStage.svelte +++ b/src/lib/components/ModelStage.svelte @@ -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 @@
- +
@@ -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; } -