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
+305 -226

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}
colorWrite: false, renderOrder={3}
depthTest: true, material={
depthWrite: true, new MeshBasicMaterial({
...(clip ? { clippingPlanes: [clip] } : {}), colorWrite: false,
}), depthTest: true,
onClick() {}, depthWrite: true,
userData: { ...(clip ? { clippingPlanes: [clip] } : {}),
type: ArmorType.External, })
thickness, }
variant, userData={
} satisfies ArmorUserData, {
}, type: ArmorType.External,
thickness,
variant,
} 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: { <mesh
renderOrder: 2, {...props}
material, key={key}
onClick() {}, renderOrder={2}
userData: { material={material}
type: ArmorType.Spaced, onClick={() => {}}
thickness, userData={
} satisfies ArmorUserData, {
}, type: ArmorType.Spaced,
thickness,
} 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,29 +372,33 @@ export function SpacedArmorSceneComponent({
return ( return (
<> <>
{props.type === ArmorType.Primary && {props.type === ArmorType.Primary &&
jsxTree( jsxTree(node, {
node, mesh(mesh, props, key) {
{ return (
mesh: { <mesh
renderOrder: 0, {...props}
material: omitMaterial, key={key}
userData: { renderOrder={0}
type: ArmorType.Primary, material={omitMaterial}
thickness, userData={
} satisfies ArmorUserData, {
async onClick(event) { type: ArmorType.Primary,
event.stopPropagation(); thickness,
const shot = (await shoot( } satisfies ArmorUserData
event.point, }
event.intersections, onClick={async (event) => {
true, event.stopPropagation();
))!; const shot = (await shoot(
tankopediaEphemeralStore.setState({ shot }); event.point,
}, event.intersections,
}, true,
))!;
tankopediaEphemeralStore.setState({ shot });
}}
/>
);
}, },
node.uuid, })}
)}
{props.type === ArmorType.Spaced && ( {props.type === ArmorType.Spaced && (
<SpacedArmorSubSpaced node={node} thickness={thickness} /> <SpacedArmorSubSpaced node={node} thickness={thickness} />
@@ -215,50 +215,61 @@ export function StaticArmorSceneComponent({
return ( return (
<> <>
{jsxTree(node, { {jsxTree(node, {
mesh: { mesh(_, meshProps, key) {
material: surfaceMaterial, return (
onPointerDown, <mesh
userData: { {...meshProps}
type: props.type, key={key}
variant: props.type === ArmorType.External ? props.variant : 'gun', material={surfaceMaterial}
thickness, userData={
} satisfies ArmorUserData, {
type: props.type,
variant:
props.type === ArmorType.External ? props.variant : 'gun',
thickness,
} satisfies ArmorUserData
}
onClick={(event) => {
if (discardClippingPlane(event.object, event.point)) return;
onClick(event) { event.stopPropagation();
if (discardClippingPlane(event.object, event.point)) return;
event.stopPropagation(); const { editStatic } = tankopediaEphemeralStore.getState();
const { editStatic } = tankopediaEphemeralStore.getState(); const bounds = new Box3().setFromObject(event.object);
const point = bounds.min
.clone()
.add(bounds.max)
.divideScalar(2)
.setY(bounds.max.y);
const cameraNormal = camera.position
.clone()
.sub(point)
.normalize();
const surfaceNormal = event
.normal!.clone()
.applyQuaternion(
event.object.getWorldQuaternion(new Quaternion()),
);
const angle = surfaceNormal.angleTo(cameraNormal);
const thicknessAngled =
thickness / Math.sin(Math.PI / 2 - angle);
const bounds = new Box3().setFromObject(event.object); mutateTankopediaEphemeralStore((draft) => {
const point = bounds.min draft.highlightArmor = {
.clone() type: props.type,
.add(bounds.max) name,
.divideScalar(2) point,
.setY(bounds.max.y); thickness,
const cameraNormal = camera.position.clone().sub(point).normalize(); thicknessAngled,
const surfaceNormal = event angle,
.normal!.clone() color: `#${color.getHexString()}`,
.applyQuaternion( editingPlate: editStatic,
event.object.getWorldQuaternion(new Quaternion()), };
); });
const angle = surfaceNormal.angleTo(cameraNormal); }}
const thicknessAngled = thickness / Math.sin(Math.PI / 2 - angle); />
);
mutateTankopediaEphemeralStore((draft) => {
draft.highlightArmor = {
type: props.type,
name,
point,
thickness,
thicknessAngled,
angle,
color: `#${color.getHexString()}`,
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,29 +114,35 @@ 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
// group(group) { {...props}
// return { key={key}
// visible: onPointerDown={onPointerDown}
// group.name !== 'ST_base' && castShadow
// group.name !== 'MR_Base2' && />
// (!group.name.includes('state_entity') || );
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive && group.name.endsWith('shields'))),
// };
// },
}, },
node.uuid,
); // group(group) {
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive && group.name.endsWith('shields'))),
// };
// },
});
})} })}
<group ref={turretContainer}> <group ref={turretContainer}>
@@ -217,30 +223,36 @@ 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
// group(group) { {...props}
// return { key={key}
// visible: onPointerDown={onPointerDown}
// group.name !== 'ST_base' && castShadow
// group.name !== 'MR_Base2' && />
// (!group.name.includes('state_entity') || );
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
}, },
node.uuid,
); // group(group) {
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
});
})} })}
<group ref={gunContainer}> <group ref={gunContainer}>
@@ -324,30 +336,36 @@ 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
// group(group) { {...props}
// return { key={key}
// visible: onPointerDown={onPointerDown}
// group.name !== 'ST_base' && castShadow
// group.name !== 'MR_Base2' && />
// (!group.name.includes('state_entity') || );
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
}, },
node.uuid,
); // group(group) {
// return {
// visible:
// group.name !== 'ST_base' &&
// group.name !== 'MR_Base2' &&
// (!group.name.includes('state_entity') ||
// (isDynamicArmorActive &&
// group.name.includes('state_entity_01')) ||
// (!isDynamicArmorActive &&
// group.name.includes('state_entity_00'))) &&
// (!group.name.includes('shields') ||
// (isDynamicArmorActive &&
// group.name.includes('shields_rev')) ||
// (!isDynamicArmorActive &&
// group.name.endsWith('shields'))),
// };
// },
});
})} })}
</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),
);
}); });
}); });
}, []); }, []);