feat: advanced jsxTree

This commit is contained in:
tresabhi committed 2025-04-15 00:11:49 -05:00
1 parent 2bef96475c
commit 0e6aa734cb
10 files changed
+209 -130

No files matched your search

@@ -211,16 +211,25 @@ export function PrimaryArmorSceneComponent({
return ( return (
<> <>
{jsxTree( {jsxTree(node, {
node, mesh(_, props, key) {
{ mesh: { renderOrder: 0, material: excludeMaterial } }, return (
`${node.uuid}-exclude`, <mesh
)} {...props}
{jsxTree( key={key}
node, renderOrder={0}
{ mesh: { renderOrder: 1, material } }, material={excludeMaterial}
`${node.uuid}-include`, />
)} );
},
})}
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh {...props} key={key} renderOrder={1} material={material} />
);
},
})}
</> </>
); );
} }
@@ -114,33 +114,40 @@ export function SpacedArmorSubExternal({
return ( return (
<> <>
{jsxTree( {jsxTree(node, {
node, mesh(_, props, key) {
{ return (
mesh: { <mesh
renderOrder: 3, {...props}
material: new MeshBasicMaterial({ key={key}
renderOrder={3}
material={
new MeshBasicMaterial({
colorWrite: false, colorWrite: false,
depthTest: true, depthTest: true,
depthWrite: true, depthWrite: true,
...(clip ? { clippingPlanes: [clip] } : {}), ...(clip ? { clippingPlanes: [clip] } : {}),
}), })
onClick() {}, }
userData: { userData={
{
type: ArmorType.External, type: ArmorType.External,
thickness, thickness,
variant, variant,
} satisfies ArmorUserData, } satisfies ArmorUserData
}
/>
);
}, },
}, })}
`${node.uuid}-exclude`,
)}
{jsxTree( {jsxTree(node, {
node, mesh(_, props, key) {
{ mesh: { renderOrder: 4, material } }, return (
`${node.uuid}-include`, <mesh {...props} key={key} renderOrder={4} material={material} />
)} );
},
})}
</> </>
); );
} }
@@ -124,32 +124,38 @@ export function SpacedArmorSubSpaced({
return ( return (
<> <>
{jsxTree( {jsxTree(node, {
node, mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={2}
material={material}
onClick={() => {}}
userData={
{ {
mesh: {
renderOrder: 2,
material,
onClick() {},
userData: {
type: ArmorType.Spaced, type: ArmorType.Spaced,
thickness, thickness,
} satisfies ArmorUserData, } satisfies ArmorUserData
}
/>
);
}, },
}, })}
`${node.uuid}-exclude`,
)}
{jsxTree( {jsxTree(node, {
node, mesh(_, props, key) {
{ return (
mesh: { <mesh
renderOrder: 5, {...props}
material: depthWriteMaterial, key={key}
renderOrder={5}
material={depthWriteMaterial}
/>
);
}, },
}, })}
`${node.uuid}-include`,
)}
</> </>
); );
} }
@@ -372,17 +372,21 @@ export function SpacedArmorSceneComponent({
return ( return (
<> <>
{props.type === ArmorType.Primary && {props.type === ArmorType.Primary &&
jsxTree( jsxTree(node, {
node, mesh(mesh, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={0}
material={omitMaterial}
userData={
{ {
mesh: {
renderOrder: 0,
material: omitMaterial,
userData: {
type: ArmorType.Primary, type: ArmorType.Primary,
thickness, thickness,
} satisfies ArmorUserData, } satisfies ArmorUserData
async onClick(event) { }
onClick={async (event) => {
event.stopPropagation(); event.stopPropagation();
const shot = (await shoot( const shot = (await shoot(
event.point, event.point,
@@ -390,11 +394,11 @@ export function SpacedArmorSceneComponent({
true, true,
))!; ))!;
tankopediaEphemeralStore.setState({ shot }); tankopediaEphemeralStore.setState({ shot });
}}
/>
);
}, },
}, })}
},
node.uuid,
)}
{props.type === ArmorType.Spaced && ( {props.type === ArmorType.Spaced && (
<SpacedArmorSubSpaced node={node} thickness={thickness} /> <SpacedArmorSubSpaced node={node} thickness={thickness} />
@@ -215,16 +215,21 @@ export function StaticArmorSceneComponent({
return ( return (
<> <>
{jsxTree(node, { {jsxTree(node, {
mesh: { mesh(_, meshProps, key) {
material: surfaceMaterial, return (
onPointerDown, <mesh
userData: { {...meshProps}
key={key}
material={surfaceMaterial}
userData={
{
type: props.type, type: props.type,
variant: props.type === ArmorType.External ? props.variant : 'gun', variant:
props.type === ArmorType.External ? props.variant : 'gun',
thickness, thickness,
} satisfies ArmorUserData, } satisfies ArmorUserData
}
onClick(event) { onClick={(event) => {
if (discardClippingPlane(event.object, event.point)) return; if (discardClippingPlane(event.object, event.point)) return;
event.stopPropagation(); event.stopPropagation();
@@ -237,14 +242,18 @@ export function StaticArmorSceneComponent({
.add(bounds.max) .add(bounds.max)
.divideScalar(2) .divideScalar(2)
.setY(bounds.max.y); .setY(bounds.max.y);
const cameraNormal = camera.position.clone().sub(point).normalize(); const cameraNormal = camera.position
.clone()
.sub(point)
.normalize();
const surfaceNormal = event const surfaceNormal = event
.normal!.clone() .normal!.clone()
.applyQuaternion( .applyQuaternion(
event.object.getWorldQuaternion(new Quaternion()), event.object.getWorldQuaternion(new Quaternion()),
); );
const angle = surfaceNormal.angleTo(cameraNormal); const angle = surfaceNormal.angleTo(cameraNormal);
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle); const thicknessAngled =
thickness / Math.sin(Math.PI / 2 - angle);
mutateTankopediaEphemeralStore((draft) => { mutateTankopediaEphemeralStore((draft) => {
draft.highlightArmor = { draft.highlightArmor = {
@@ -258,7 +267,9 @@ export function StaticArmorSceneComponent({
editingPlate: editStatic, editingPlate: editStatic,
}; };
}); });
}, }}
/>
);
}, },
})} })}
@@ -1,5 +1,6 @@
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons'; import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';
import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes'; import { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember'; import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from '../../hooks/useLocale';
@@ -15,7 +16,10 @@ interface ControlsProps {
onAddTankDialogOpenChange: (open: boolean) => void; onAddTankDialogOpenChange: (open: boolean) => void;
} }
const provisionDefinitions = await awaitableProvisionDefinitions; const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function Controls({ export function Controls({
addTankDialogOpen, addTankDialogOpen,
@@ -56,9 +60,11 @@ export function Controls({
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members.push( draft.members.push(
tankToCompareMember(tank, provisionDefinitions), tankToCompareMember(tank, model, provisionDefinitions),
); );
draft.sorting = undefined; draft.sorting = undefined;
}); });
@@ -67,9 +73,15 @@ export function Controls({
onSelectAll={(tanks) => { onSelectAll={(tanks) => {
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members.push( draft.members.push(
...tanks.map((tank) => ...tanks.map((tank) => {
tankToCompareMember(tank, provisionDefinitions), const model = modelDefinitions.models[tank.id];
),
return tankToCompareMember(
tank,
model,
provisionDefinitions,
);
}),
); );
draft.sorting = undefined; draft.sorting = undefined;
}); });
@@ -1,6 +1,7 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons'; import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes'; import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { useState } from 'react'; import { useState } from 'react';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions'; import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember'; import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale'; import { useLocale } from '../../hooks/useLocale';
@@ -13,7 +14,10 @@ interface TankControlProps {
id: number; id: number;
} }
const provisionDefinitions = await awaitableProvisionDefinitions; const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function TankControl({ index, id }: TankControlProps) { export function TankControl({ index, id }: TankControlProps) {
const { locale } = useLocale(); const { locale } = useLocale();
@@ -57,9 +61,12 @@ export function TankControl({ index, id }: TankControlProps) {
<TankSearch <TankSearch
compact compact
onSelect={(tank) => { onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => { mutateCompareEphemeral((draft) => {
draft.members[index] = tankToCompareMember( draft.members[index] = tankToCompareMember(
tank, tank,
model,
provisionDefinitions, provisionDefinitions,
); );
draft.sorting = undefined; draft.sorting = undefined;
@@ -114,10 +114,18 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
} }
return jsxTree( return jsxTree(node, {
node, mesh(_, props, key) {
{ return (
mesh: { onPointerDown, castShadow: true }, <mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
// group(group) { // group(group) {
// return { // return {
// visible: // visible:
@@ -134,9 +142,7 @@ export function TankModel() {
// (!isDynamicArmorActive && group.name.endsWith('shields'))), // (!isDynamicArmorActive && group.name.endsWith('shields'))),
// }; // };
// }, // },
}, });
node.uuid,
);
})} })}
<group ref={turretContainer}> <group ref={turretContainer}>
@@ -217,10 +223,18 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
} }
return jsxTree( return jsxTree(node, {
node, mesh(_, props, key) {
{ return (
mesh: { onPointerDown, castShadow: true }, <mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
// group(group) { // group(group) {
// return { // return {
// visible: // visible:
@@ -238,9 +252,7 @@ export function TankModel() {
// group.name.endsWith('shields'))), // group.name.endsWith('shields'))),
// }; // };
// }, // },
}, });
node.uuid,
);
})} })}
<group ref={gunContainer}> <group ref={gunContainer}>
@@ -324,10 +336,18 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
} }
return jsxTree( return jsxTree(node, {
node, mesh(_, props, key) {
{ return (
mesh: { onPointerDown, castShadow: true }, <mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
// group(group) { // group(group) {
// return { // return {
// visible: // visible:
@@ -345,9 +365,7 @@ export function TankModel() {
// group.name.endsWith('shields'))), // group.name.endsWith('shields'))),
// }; // };
// }, // },
}, });
node.uuid,
);
})} })}
</group> </group>
</group> </group>
+32 -31
View File
@@ -1,43 +1,44 @@
import type { GroupProps, MeshProps } from '@react-three/fiber'; import type { MeshProps, Object3DProps } from '@react-three/fiber';
import type { ReactNode } from 'react';
import { Mesh, Object3D } from 'three'; import { Mesh, Object3D } from 'three';
export function jsxTree( export function jsxTree(
node: Object3D, node: Object3D,
mergers?: { mergers?: {
mesh?: MeshProps | ((mesh: Mesh) => MeshProps); mesh?: (mesh: Mesh, props: MeshProps, key: string) => ReactNode;
group?: GroupProps | ((object3d: Object3D) => GroupProps); group?: (
object3d: Object3D,
props: Object3DProps,
key: string,
) => ReactNode;
}, },
key?: string, ): ReactNode {
) {
if (node instanceof Mesh) { if (node instanceof Mesh) {
return ( const props = {
<mesh geometry: node.geometry,
key={key} material: node.material,
geometry={node.geometry} position: node.position,
material={node.material} rotation: node.rotation,
position={node.position} scale: node.scale,
rotation={node.rotation} };
scale={node.scale}
{...(mergers?.mesh && return mergers?.mesh ? (
(typeof mergers.mesh === 'function' mergers.mesh(node, props, node.uuid)
? mergers.mesh(node) ) : (
: mergers.mesh))} <mesh key={node.uuid} {...props} />
/>
); );
} else if (node instanceof Object3D) { } else if (node instanceof Object3D) {
return ( const props = {
<group position: node.position,
key={key} rotation: node.rotation,
position={node.position} scale: node.scale,
rotation={node.rotation} children: node.children.map((child) => jsxTree(child, mergers)),
scale={node.scale} };
{...(mergers?.group &&
(typeof mergers.group === 'function' return mergers?.group ? (
? mergers.group(node) mergers.group(node, props, node.uuid)
: mergers.group))} ) : (
> <group key={node.uuid} {...props} />
{node.children.map((child) => jsxTree(child, mergers, child.uuid))}
</group>
); );
} }
@@ -113,7 +113,11 @@ function Content() {
if (tank === undefined) return; if (tank === undefined) return;
draft.members.push(tankToCompareMember(tank, provisionDefinitions)); const model = modelDefinitions.models[tank.id];
draft.members.push(
tankToCompareMember(tank, model, provisionDefinitions),
);
}); });
}); });
}, []); }, []);