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 (
<>
{jsxTree(
node,
{ mesh: { renderOrder: 0, material: excludeMaterial } },
`${node.uuid}-exclude`,
)}
{jsxTree(
node,
{ mesh: { renderOrder: 1, material } },
`${node.uuid}-include`,
)}
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={0}
material={excludeMaterial}
/>
);
},
})}
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh {...props} key={key} renderOrder={1} material={material} />
);
},
})}
</>
);
}
@@ -114,33 +114,40 @@ export function SpacedArmorSubExternal({
return (
<>
{jsxTree(
node,
{
mesh: {
renderOrder: 3,
material: new MeshBasicMaterial({
colorWrite: false,
depthTest: true,
depthWrite: true,
...(clip ? { clippingPlanes: [clip] } : {}),
}),
onClick() {},
userData: {
type: ArmorType.External,
thickness,
variant,
} satisfies ArmorUserData,
},
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={3}
material={
new MeshBasicMaterial({
colorWrite: false,
depthTest: true,
depthWrite: true,
...(clip ? { clippingPlanes: [clip] } : {}),
})
}
userData={
{
type: ArmorType.External,
thickness,
variant,
} satisfies ArmorUserData
}
/>
);
},
`${node.uuid}-exclude`,
)}
})}
{jsxTree(
node,
{ mesh: { renderOrder: 4, material } },
`${node.uuid}-include`,
)}
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh {...props} key={key} renderOrder={4} material={material} />
);
},
})}
</>
);
}
@@ -124,32 +124,38 @@ export function SpacedArmorSubSpaced({
return (
<>
{jsxTree(
node,
{
mesh: {
renderOrder: 2,
material,
onClick() {},
userData: {
type: ArmorType.Spaced,
thickness,
} satisfies ArmorUserData,
},
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={2}
material={material}
onClick={() => {}}
userData={
{
type: ArmorType.Spaced,
thickness,
} satisfies ArmorUserData
}
/>
);
},
`${node.uuid}-exclude`,
)}
})}
{jsxTree(
node,
{
mesh: {
renderOrder: 5,
material: depthWriteMaterial,
},
{jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={5}
material={depthWriteMaterial}
/>
);
},
`${node.uuid}-include`,
)}
})}
</>
);
}
@@ -372,29 +372,33 @@ export function SpacedArmorSceneComponent({
return (
<>
{props.type === ArmorType.Primary &&
jsxTree(
node,
{
mesh: {
renderOrder: 0,
material: omitMaterial,
userData: {
type: ArmorType.Primary,
thickness,
} satisfies ArmorUserData,
async onClick(event) {
event.stopPropagation();
const shot = (await shoot(
event.point,
event.intersections,
true,
))!;
tankopediaEphemeralStore.setState({ shot });
},
},
jsxTree(node, {
mesh(mesh, props, key) {
return (
<mesh
{...props}
key={key}
renderOrder={0}
material={omitMaterial}
userData={
{
type: ArmorType.Primary,
thickness,
} satisfies ArmorUserData
}
onClick={async (event) => {
event.stopPropagation();
const shot = (await shoot(
event.point,
event.intersections,
true,
))!;
tankopediaEphemeralStore.setState({ shot });
}}
/>
);
},
node.uuid,
)}
})}
{props.type === ArmorType.Spaced && (
<SpacedArmorSubSpaced node={node} thickness={thickness} />
@@ -215,50 +215,61 @@ export function StaticArmorSceneComponent({
return (
<>
{jsxTree(node, {
mesh: {
material: surfaceMaterial,
onPointerDown,
userData: {
type: props.type,
variant: props.type === ArmorType.External ? props.variant : 'gun',
thickness,
} satisfies ArmorUserData,
mesh(_, meshProps, key) {
return (
<mesh
{...meshProps}
key={key}
material={surfaceMaterial}
userData={
{
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) {
if (discardClippingPlane(event.object, event.point)) return;
event.stopPropagation();
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);
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);
mutateTankopediaEphemeralStore((draft) => {
draft.highlightArmor = {
type: props.type,
name,
point,
thickness,
thicknessAngled,
angle,
color: `#${color.getHexString()}`,
editingPlate: editStatic,
};
});
},
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 { Button, Dialog, Flex, SegmentedControl } from '@radix-ui/themes';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale';
@@ -15,7 +16,10 @@ interface ControlsProps {
onAddTankDialogOpenChange: (open: boolean) => void;
}
const provisionDefinitions = await awaitableProvisionDefinitions;
const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function Controls({
addTankDialogOpen,
@@ -56,9 +60,11 @@ export function Controls({
<TankSearch
compact
onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => {
draft.members.push(
tankToCompareMember(tank, provisionDefinitions),
tankToCompareMember(tank, model, provisionDefinitions),
);
draft.sorting = undefined;
});
@@ -67,9 +73,15 @@ export function Controls({
onSelectAll={(tanks) => {
mutateCompareEphemeral((draft) => {
draft.members.push(
...tanks.map((tank) =>
tankToCompareMember(tank, provisionDefinitions),
),
...tanks.map((tank) => {
const model = modelDefinitions.models[tank.id];
return tankToCompareMember(
tank,
model,
provisionDefinitions,
);
}),
);
draft.sorting = undefined;
});
@@ -1,6 +1,7 @@
import { ExternalLinkIcon, LoopIcon, TrashIcon } from '@radix-ui/react-icons';
import { Dialog, Flex, IconButton } from '@radix-ui/themes';
import { useState } from 'react';
import { awaitableModelDefinitions } from '../../core/awaitables/modelDefinitions';
import { awaitableProvisionDefinitions } from '../../core/awaitables/provisionDefinitions';
import { tankToCompareMember } from '../../core/blitzkit/tankToCompareMember';
import { useLocale } from '../../hooks/useLocale';
@@ -13,7 +14,10 @@ interface TankControlProps {
id: number;
}
const provisionDefinitions = await awaitableProvisionDefinitions;
const [provisionDefinitions, modelDefinitions] = await Promise.all([
awaitableProvisionDefinitions,
awaitableModelDefinitions,
]);
export function TankControl({ index, id }: TankControlProps) {
const { locale } = useLocale();
@@ -57,9 +61,12 @@ export function TankControl({ index, id }: TankControlProps) {
<TankSearch
compact
onSelect={(tank) => {
const model = modelDefinitions.models[tank.id];
mutateCompareEphemeral((draft) => {
draft.members[index] = tankToCompareMember(
tank,
model,
provisionDefinitions,
);
draft.sorting = undefined;
@@ -114,29 +114,35 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: { onPointerDown, castShadow: true },
// 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'))),
// };
// },
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
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}>
@@ -217,30 +223,36 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: { onPointerDown, castShadow: true },
// 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'))),
// };
// },
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
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}>
@@ -324,30 +336,36 @@ export function TankModel() {
window.removeEventListener('pointerup', handlePointerUp);
}
return jsxTree(
node,
{
mesh: { onPointerDown, castShadow: true },
// 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'))),
// };
// },
return jsxTree(node, {
mesh(_, props, key) {
return (
<mesh
{...props}
key={key}
onPointerDown={onPointerDown}
castShadow
/>
);
},
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>
+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';
export function jsxTree(
node: Object3D,
mergers?: {
mesh?: MeshProps | ((mesh: Mesh) => MeshProps);
group?: GroupProps | ((object3d: Object3D) => GroupProps);
mesh?: (mesh: Mesh, props: MeshProps, key: string) => ReactNode;
group?: (
object3d: Object3D,
props: Object3DProps,
key: string,
) => ReactNode;
},
key?: string,
) {
): ReactNode {
if (node instanceof Mesh) {
return (
<mesh
key={key}
geometry={node.geometry}
material={node.material}
position={node.position}
rotation={node.rotation}
scale={node.scale}
{...(mergers?.mesh &&
(typeof mergers.mesh === 'function'
? mergers.mesh(node)
: mergers.mesh))}
/>
const props = {
geometry: node.geometry,
material: node.material,
position: node.position,
rotation: node.rotation,
scale: node.scale,
};
return mergers?.mesh ? (
mergers.mesh(node, props, node.uuid)
) : (
<mesh key={node.uuid} {...props} />
);
} else if (node instanceof Object3D) {
return (
<group
key={key}
position={node.position}
rotation={node.rotation}
scale={node.scale}
{...(mergers?.group &&
(typeof mergers.group === 'function'
? mergers.group(node)
: mergers.group))}
>
{node.children.map((child) => jsxTree(child, mergers, child.uuid))}
</group>
const props = {
position: node.position,
rotation: node.rotation,
scale: node.scale,
children: node.children.map((child) => jsxTree(child, mergers)),
};
return mergers?.group ? (
mergers.group(node, props, node.uuid)
) : (
<group key={node.uuid} {...props} />
);
}
@@ -113,7 +113,11 @@ function Content() {
if (tank === undefined) return;
draft.members.push(tankToCompareMember(tank, provisionDefinitions));
const model = modelDefinitions.models[tank.id];
draft.members.push(
tankToCompareMember(tank, model, provisionDefinitions),
);
});
});
}, []);