From 5982fdde8c95cfb4034857ba0a10269363f218af Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Sun, 23 Mar 2025 17:18:42 -0500 Subject: [PATCH] feat: dragger --- .../components/DissectorSceneComponent.tsx | 122 +++++++---- .../TankSandbox/components/Control.tsx | 206 +++++++++--------- .../components/SceneProps/index.tsx | 136 ++++++++---- packages/website/src/core/blitzkit/dissect.ts | 5 +- 4 files changed, 283 insertions(+), 186 deletions(-) diff --git a/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx b/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx index 07b56f250..74ed31b25 100644 --- a/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx +++ b/packages/website/src/components/Armor/components/DissectorSceneComponent.tsx @@ -1,4 +1,16 @@ -import { asset } from '@blitzkit/core'; +import { asset, J_HAT } from '@blitzkit/core'; +import { + amberDark, + blueDark, + bronzeDark, + indigoDark, + mauveDark, + orangeDark, + plumDark, + redDark, + skyDark, + slateDark, +} from '@radix-ui/colors'; import { Base, Geometry, @@ -15,6 +27,7 @@ import { LineBasicMaterial, Material, MeshStandardMaterial, + Plane, Vector3, } from 'three'; import { @@ -33,28 +46,28 @@ interface DissectorSceneComponentProps { const NEGATIVE_SIZE = 20; -const armorColor = new Color().multiplyScalar(2 ** -8); +const armorColor = new Color().multiplyScalar(2 ** -4); const armorOutlineColor = armorColor.clone().multiplyScalar(2 ** 4.5); const moduleColor: Record = { Armor: armorColor, ArmorScreen: armorColor, - Commander: new Color('white'), - Driver: new Color('white'), - Gunner1: new Color('white'), - Loader1: new Color('white'), + Commander: new Color(mauveDark.mauve11), + Driver: new Color(mauveDark.mauve11), + Gunner1: new Color(mauveDark.mauve11), + Loader1: new Color(mauveDark.mauve11), - SurveyingDevice: new Color('purple'), - Radio: new Color('indigo'), - Gun: new Color('pink'), - FuelTank: new Color('yellow'), - Engine: new Color('orange'), - AmmoBay: new Color('gold'), - LeftTrack: new Color('green'), - RightTrack: new Color('green'), - TurretRotator: new Color('blue'), - Transmission: new Color('cyan'), + SurveyingDevice: new Color(plumDark.plum8), + Radio: new Color(slateDark.slate8), + Gun: new Color(blueDark.blue8), + FuelTank: new Color(orangeDark.orange8), + Engine: new Color(redDark.red8), + AmmoBay: new Color(amberDark.amber8), + LeftTrack: new Color(bronzeDark.bronze8), + RightTrack: new Color(bronzeDark.bronze8), + TurretRotator: new Color(indigoDark.indigo8), + Transmission: new Color(skyDark.sky8), }; const moduleOutlineColor: Record = { Armor: armorOutlineColor, @@ -87,27 +100,56 @@ export function DissectorSceneComponent({ const armorName = mesh.material.name; const armorPlate = groupArmor.armors[armorName]; - - if (armorPlate.type === 'Armor' || armorPlate.type === 'ArmorScreen') { - return null; - } + const isModule = + armorPlate.type !== 'Armor' && armorPlate.type !== 'ArmorScreen'; + const isTrack = + armorPlate.type === 'LeftTrack' || armorPlate.type === 'RightTrack'; if (!(armorPlate.type in moduleColor)) { throw new TypeError(`Unhandled armor type: ${armorPlate.type}`); } - const material = new MeshStandardMaterial({ - side: DoubleSide, - color: moduleColor[armorPlate.type], - }); - const outlineMaterial = new LineBasicMaterial({ - side: DoubleSide, - color: moduleOutlineColor[armorPlate.type], - }); + const clippingPlane = useRef(new Plane()); + const clippingPlaneInverse = useRef(new Plane()); + const material = useRef( + new MeshStandardMaterial({ + transparent: isTrack, + opacity: 2 ** -1, + side: DoubleSide, + color: moduleColor[armorPlate.type], + clippingPlanes: isModule ? undefined : [clippingPlane.current], + }), + ); + const outlineMaterial = useRef( + new LineBasicMaterial({ + transparent: !isModule, + opacity: 2 ** -3, + side: DoubleSide, + color: moduleOutlineColor[armorPlate.type], + clippingPlanes: isModule ? undefined : [clippingPlane.current], + }), + ); + const outlineMaterialInverse = useRef( + new LineBasicMaterial({ + transparent: !isModule, + opacity: 2 ** -6, + side: DoubleSide, + color: moduleOutlineColor[armorPlate.type], + clippingPlanes: isModule ? undefined : [clippingPlaneInverse.current], + }), + ); const worldPosition = new Vector3(); useEffect(() => { + const offset = new Vector3(); + function handleDissectEvent(event: DissectEventData) { + clippingPlane.current.normal + .set(0, 0, 1) + .applyAxisAngle(J_HAT, event.rotation - Math.PI / 2); + clippingPlane.current.constant = event.offset; + clippingPlaneInverse.current.copy(clippingPlane.current).negate(); + if ( !subtractionWrapper.current || !csg.current || @@ -116,9 +158,10 @@ export function DissectorSceneComponent({ return; baseWrapper.current.getWorldPosition(worldPosition); - + offset.set(-event.offset, 0, 0).applyAxisAngle(J_HAT, event.rotation); subtractionWrapper.current.position .copy(worldPosition) + .add(offset) .multiplyScalar(-1); subtractionWrapper.current.rotation.y = event.rotation; csg.current.update(); @@ -132,13 +175,11 @@ export function DissectorSceneComponent({ }; }, []); - return ( - <> - {/* */} - - + if (isModule) { + return ( + - + @@ -149,10 +190,17 @@ export function DissectorSceneComponent({ + ); + } - {/* + return ( + <> + - */} + + + + ); }, diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx index 6d5004c3d..0c2844abe 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/Control.tsx @@ -85,9 +85,9 @@ export function Controls({ naked }: ControlsProps) { // protagonistGunOrigin.y; const gunHeight = 2; const inspectModeInitialPosition = [ - -8, + 13, gunHeight + (naked ? 10 : 4), - -13, + -8, ] as const; // const protagonistGunOriginOnlyY = new Vector3( // 0, @@ -99,116 +99,120 @@ export function Controls({ naked }: ControlsProps) { // .add(protagonistTurretOrigin) // .add(protagonistGunOriginOnlyY); - // useEffect(() => { - // const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe( - // (state) => state.controlsEnabled, - // (enabled) => { - // if (orbitControls.current) orbitControls.current.enabled = enabled; - // }, - // ); + useEffect(() => { + const unsubscribeTankopediaEphemeral = tankopediaEphemeralStore.subscribe( + (state) => state.controlsEnabled, + (enabled) => { + if (orbitControls.current) orbitControls.current.enabled = enabled; + }, + ); - // async function handlePoseEvent(event: Pose) { - // const duel = duelStore.getState(); - // const hasImprovedVerticalStabilizer = await hasEquipment( - // 122, - // duel.protagonist.tank.equipment_preset, - // duel.protagonist.equipmentMatrix, - // ); - // const hasDownImprovedVerticalStabilizer = await hasEquipment( - // 124, - // duel.protagonist.tank.equipment_preset, - // duel.protagonist.equipmentMatrix, - // ); + // async function handlePoseEvent(event: Pose) { + // const duel = duelStore.getState(); + // const hasImprovedVerticalStabilizer = await hasEquipment( + // 122, + // duel.protagonist.tank.equipment_preset, + // duel.protagonist.equipmentMatrix, + // ); + // const hasDownImprovedVerticalStabilizer = await hasEquipment( + // 124, + // duel.protagonist.tank.equipment_preset, + // duel.protagonist.equipmentMatrix, + // ); - // switch (event) { - // case Pose.HullDown: { - // const [pitch] = applyPitchYawLimits( - // -Infinity, - // 0, - // protagonistGunModelDefinition.pitch, - // protagonistTurretModelDefinition.yaw, - // hasImprovedVerticalStabilizer, - // hasDownImprovedVerticalStabilizer, - // ); + // switch (event) { + // case Pose.HullDown: { + // const [pitch] = applyPitchYawLimits( + // -Infinity, + // 0, + // protagonistGunModelDefinition.pitch, + // protagonistTurretModelDefinition.yaw, + // hasImprovedVerticalStabilizer, + // hasDownImprovedVerticalStabilizer, + // ); - // camera.position - // .set(0, 0, 0) - // .add(protagonistHullOrigin) - // .add(protagonistTurretOrigin) - // .add(protagonistGunOrigin) - // .add( - // new Vector3( - // 0, - // poseDistances[event] * Math.sin(pitch), - // poseDistances[event] * -Math.cos(pitch), - // ), - // ); - // camera.lookAt( - // protagonistHullOrigin - // .clone() - // .add(protagonistTurretOrigin) - // .add(protagonistGunOrigin), - // ); - // orbitControls.current?.target.set(0, antagonistGunHeight, 0); + // camera.position + // .set(0, 0, 0) + // .add(protagonistHullOrigin) + // .add(protagonistTurretOrigin) + // .add(protagonistGunOrigin) + // .add( + // new Vector3( + // 0, + // poseDistances[event] * Math.sin(pitch), + // poseDistances[event] * -Math.cos(pitch), + // ), + // ); + // camera.lookAt( + // protagonistHullOrigin + // .clone() + // .add(protagonistTurretOrigin) + // .add(protagonistGunOrigin), + // ); + // orbitControls.current?.target.set(0, antagonistGunHeight, 0); - // break; - // } + // break; + // } - // case Pose.FaceHug: { - // const [pitch] = applyPitchYawLimits( - // 0, - // 0, - // protagonistGunModelDefinition.pitch, - // protagonistTurretModelDefinition.yaw, - // hasImprovedVerticalStabilizer, - // hasDownImprovedVerticalStabilizer, - // ); + // case Pose.FaceHug: { + // const [pitch] = applyPitchYawLimits( + // 0, + // 0, + // protagonistGunModelDefinition.pitch, + // protagonistTurretModelDefinition.yaw, + // hasImprovedVerticalStabilizer, + // hasDownImprovedVerticalStabilizer, + // ); - // camera.position - // .set(0, 0, 0) - // .add(protagonistHullOrigin) - // .add(protagonistTurretOrigin) - // .add(protagonistGunOrigin) - // .add( - // new Vector3( - // 0, - // poseDistances[event] * Math.sin(pitch), - // poseDistances[event] * -Math.cos(pitch), - // ), - // ); - // orbitControls.current?.target - // .set(0, 0, 0) - // .add(protagonistHullOrigin) - // .add(protagonistTurretOrigin) - // .add(protagonistGunOrigin) - // .add( - // new Vector3( - // 0, - // 0.5 * poseDistances[event] * Math.sin(pitch), - // 0.5 * poseDistances[event] * -Math.cos(pitch), - // ), - // ); + // camera.position + // .set(0, 0, 0) + // .add(protagonistHullOrigin) + // .add(protagonistTurretOrigin) + // .add(protagonistGunOrigin) + // .add( + // new Vector3( + // 0, + // poseDistances[event] * Math.sin(pitch), + // poseDistances[event] * -Math.cos(pitch), + // ), + // ); + // orbitControls.current?.target + // .set(0, 0, 0) + // .add(protagonistHullOrigin) + // .add(protagonistTurretOrigin) + // .add(protagonistGunOrigin) + // .add( + // new Vector3( + // 0, + // 0.5 * poseDistances[event] * Math.sin(pitch), + // 0.5 * poseDistances[event] * -Math.cos(pitch), + // ), + // ); - // break; - // } + // break; + // } - // case Pose.Default: { - // camera.position.set(...inspectModeInitialPosition); - // orbitControls.current?.target.set(0, 1.25, 0); - // break; - // } - // } + // case Pose.Default: { + // camera.position.set(...inspectModeInitialPosition); + // orbitControls.current?.target.set(0, 1.25, 0); + // break; + // } + // } - // invalidate(); - // } + // invalidate(); + // } - // poseEvent.on(handlePoseEvent); + // poseEvent.on(handlePoseEvent); - // return () => { - // unsubscribeTankopediaEphemeral(); - // poseEvent.off(handlePoseEvent); - // }; - // }, [camera, protagonistTank.id, antagonistTank.id]); + return () => { + unsubscribeTankopediaEphemeral(); + // poseEvent.off(handlePoseEvent); + }; + }, [ + camera, + // protagonistTank.id, + // antagonistTank.id + ]); useEffect(() => { let isInitialIteration = true; diff --git a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx index 048def742..f2e4349ea 100644 --- a/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx +++ b/packages/website/src/components/Tankopedia/HeroSection/components/TankSandbox/components/SceneProps/index.tsx @@ -1,33 +1,32 @@ -import { imgur } from '@blitzkit/core'; -import { useFrame, useLoader } from '@react-three/fiber'; -import { clamp } from 'lodash-es'; -import { useCallback, useRef } from 'react'; -import { MeshStandardMaterial, TextureLoader, Vector2 } from 'three'; -import { awaitableEquipmentDefinitions } from '../../../../../../../core/awaitables/equipmentDefinitions'; -import { awaitableModelDefinitions } from '../../../../../../../core/awaitables/modelDefinitions'; -import { awaitableProvisionDefinitions } from '../../../../../../../core/awaitables/provisionDefinitions'; +import { imgur, J_HAT } from '@blitzkit/core'; +import { useLoader } from '@react-three/fiber'; +import { useCallback, useEffect, useRef } from 'react'; +import { Group, Plane, TextureLoader, Vector2 } from 'three'; +import { clamp } from 'three/src/math/MathUtils.js'; import { dissectEvent, lastDissection, + type DissectEventData, } from '../../../../../../../core/blitzkit/dissect'; import { TankopediaEphemeral } from '../../../../../../../stores/tankopediaEphemeral'; import { TankopediaDisplay } from '../../../../../../../stores/tankopediaPersistent/constants'; -const [modelDefinitions, equipmentDefinitions, provisionDefinitions] = - await Promise.all([ - awaitableModelDefinitions, - awaitableEquipmentDefinitions, - awaitableProvisionDefinitions, - ]); +// const [modelDefinitions, equipmentDefinitions, provisionDefinitions] = +// await Promise.all([ +// awaitableModelDefinitions, +// awaitableEquipmentDefinitions, +// awaitableProvisionDefinitions, +// ]); -const emptyVector = new Vector2(); +// const emptyVector = new Vector2(); + +const GRID_SIZE = 10; export function SceneProps() { const mutateTankopediaEphemeral = TankopediaEphemeral.useMutation(); // const targetCircleWrapper = useRef(null); // const clientTargetCircle = useRef(null); // const serverTargetCircle = useRef(null); - const material = useRef(null); // const playground = useRef(null); // let lastPitch = Duel.use((state) => state.protagonist.pitch); @@ -90,19 +89,12 @@ export function SceneProps() { // let { dispersion } = characteristics; // const gunRotationSpeed = degToRad(characteristics.gunTraverseSpeed); // const mockTank = useLoader(GLTFLoader, asset(`3d/tanks/models/6929.glb`)); - const texture = useLoader( - TextureLoader, - imgur(display === TankopediaDisplay.Dissector ? 'GgYPSVA' : 'C28Z8nU'), - ); - texture.anisotropy = 2; + const gridTexture = useLoader(TextureLoader, imgur('iq0iOCJ')); + const dragTexture = useLoader(TextureLoader, imgur('Vi3xXsb')); + gridTexture.anisotropy = 2; // const path = new Vector3(); // const direction = new Vector3(); - useFrame(({ camera }) => { - if (!material.current) return; - material.current.opacity = clamp(0.5 * camera.position.y, 0, 1); - }); - // let lastTime = 0; // let DEBUG_disable = false; @@ -225,15 +217,23 @@ export function SceneProps() { // serverTargetCircle.current.style.height = targetCircleDotsSize; // }); - const position = new Vector2(); - const delta = new Vector2(); + const dissector = useRef(null); + const position = useRef(new Vector2()); + const delta = useRef(new Vector2()); + const clippingPlane = useRef(new Plane()); const handlePointerMove = useCallback((event: PointerEvent) => { - delta.set(event.clientX, event.clientY).sub(position); - position.set(event.clientX, event.clientY); + delta.current.set(event.clientX, event.clientY).sub(position.current); + position.current.set(event.clientX, event.clientY); dissectEvent.emit({ - rotation: lastDissection.rotation + delta.x / window.innerWidth, - offset: lastDissection.offset, + rotation: + lastDissection.rotation + (4 * delta.current.x) / window.innerWidth, + // offset: 0, + offset: clamp( + lastDissection.offset + (4 * delta.current.y) / window.innerHeight, + -5, + 5, + ), }); }, []); const handlePointerUp = useCallback(() => { @@ -245,26 +245,68 @@ export function SceneProps() { window.removeEventListener('pointerup', handlePointerUp); }, []); + useEffect(() => { + function handleDissectEvent(event: DissectEventData) { + clippingPlane.current.normal + .set(0, 0, 1) + .applyAxisAngle(J_HAT, event.rotation - Math.PI / 2); + clippingPlane.current.constant = event.offset; + + if (!dissector.current) return; + + dissector.current.rotation.set(0, event.rotation, 0); + dissector.current.position + .set(event.offset, 0, 0) + .applyAxisAngle(J_HAT, event.rotation); + } + + dissectEvent.on(handleDissectEvent); + handleDissectEvent(lastDissection); + + return () => { + dissectEvent.off(handleDissectEvent); + }; + }, []); + return ( <> {display !== TankopediaDisplay.ShootingRange && ( - { - event.stopPropagation(); + <> + + + + - position.set(event.nativeEvent.clientX, event.nativeEvent.clientY); - mutateTankopediaEphemeral((draft) => { - draft.controlsEnabled = false; - }); + + { + if (display !== TankopediaDisplay.Dissector) return; - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - }} - > - - - + event.stopPropagation(); + + position.current.set( + event.nativeEvent.clientX, + event.nativeEvent.clientY, + ); + mutateTankopediaEphemeral((draft) => { + draft.controlsEnabled = false; + }); + + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + }} + > + + + + + )} {/* {display === TankopediaDisplay.ShootingRange && ( diff --git a/packages/website/src/core/blitzkit/dissect.ts b/packages/website/src/core/blitzkit/dissect.ts index 9c5ad713d..da2702014 100644 --- a/packages/website/src/core/blitzkit/dissect.ts +++ b/packages/website/src/core/blitzkit/dissect.ts @@ -7,6 +7,9 @@ export interface DissectEventData { export const dissectEvent = new EventManager(); -export const lastDissection: DissectEventData = { rotation: 0, offset: 0 }; +export const lastDissection: DissectEventData = { + rotation: Math.PI / 2, + offset: 0, +}; dissectEvent.on((data) => Object.assign(lastDissection, data));