Skip to content

Commit 9017aa8

Browse files
use spritemap for 3D plant icons
1 parent 7727b3c commit 9017aa8

11 files changed

Lines changed: 2622 additions & 19 deletions

File tree

frontend/three_d_garden/__tests__/garden_model_test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ let mockIsDesktop = false;
22
let mockIsMobile = false;
33

44
import React from "react";
5+
import { useTexture } from "@react-three/drei";
56
import { GardenModelProps, GardenModel } from "../garden_model";
67
import { clone } from "lodash";
78
import { INITIAL, INITIAL_POSITION, SurfaceDebugOption } from "../config";
@@ -20,6 +21,7 @@ import {
2021
createRenderer,
2122
unmountRenderer,
2223
} from "../../__test_support__/test_renderer";
24+
import { PLANT_ICON_ATLAS } from "../garden/plant_icon_atlas";
2325

2426
let isDesktopSpy: jest.SpyInstance;
2527
let isMobileSpy: jest.SpyInstance;
@@ -56,6 +58,7 @@ describe("<GardenModel />", () => {
5658
useStateSpy.mockRestore();
5759
isDesktopSpy.mockRestore();
5860
isMobileSpy.mockRestore();
61+
delete PLANT_ICON_ATLAS["/crops/icons/beet.avif"];
5962
location.pathname = originalPathname;
6063
});
6164

@@ -109,6 +112,26 @@ describe("<GardenModel />", () => {
109112
expect(plantLabels.length).toEqual(1);
110113
});
111114

115+
it("preloads the atlas texture for mapped plant icons", () => {
116+
PLANT_ICON_ATLAS["/crops/icons/beet.avif"] = {
117+
atlasUrl: "/crops/icons/atlas.avif",
118+
textureWidth: 256,
119+
textureHeight: 256,
120+
x: 0,
121+
y: 0,
122+
width: 64,
123+
height: 64,
124+
};
125+
const p = fakeProps();
126+
const plant = fakePlant();
127+
plant.body.name = "Beet";
128+
p.threeDPlants = convertPlants(p.config, [plant]);
129+
130+
render(<GardenModel {...p} />);
131+
132+
expect(useTexture).toHaveBeenCalledWith("/crops/icons/atlas.avif");
133+
});
134+
112135
it("doesn't render hover labels without a hovered plant", () => {
113136
const p = fakeProps();
114137
const plant = fakePlant();

frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
let mockIsMobile = false;
22
import React from "react";
3+
import { useTexture } from "@react-three/drei";
34
import {
45
ActivePositionRef,
56
BillboardRef,
@@ -22,6 +23,7 @@ import { Vector3 } from "three";
2223
import { ThreeEvent } from "@react-three/fiber";
2324
import * as plantActions from "../../../../farm_designer/map/layers/plants/plant_actions";
2425
import * as screenSize from "../../../../screen_size";
26+
import { PLANT_ICON_ATLAS } from "../../../garden/plant_icon_atlas";
2527

2628
let dropPlantSpy: jest.SpyInstance;
2729
let isMobileSpy: jest.SpyInstance;
@@ -44,6 +46,7 @@ afterEach(() => {
4446
dropPlantSpy.mockRestore();
4547
isMobileSpy.mockRestore();
4648
requestAnimationFrameSpy.mockRestore();
49+
delete PLANT_ICON_ATLAS["/crops/icons/mint.avif"];
4750
});
4851

4952
describe("<PointerObjects />", () => {
@@ -65,7 +68,25 @@ describe("<PointerObjects />", () => {
6568
location.pathname = Path.mock(Path.cropSearch("mint"));
6669
mockIsMobile = false;
6770
const { container } = render(<PointerObjects {...fakeProps()} />);
68-
expect(container).toContainHTML("mint");
71+
expect(container).toContainHTML("pointerPlant");
72+
});
73+
74+
it("loads the atlas texture for the pointer plant preview", () => {
75+
PLANT_ICON_ATLAS["/crops/icons/mint.avif"] = {
76+
atlasUrl: "/crops/icons/atlas.avif",
77+
textureWidth: 256,
78+
textureHeight: 256,
79+
x: 0,
80+
y: 0,
81+
width: 64,
82+
height: 64,
83+
};
84+
location.pathname = Path.mock(Path.cropSearch("mint"));
85+
mockIsMobile = false;
86+
87+
render(<PointerObjects {...fakeProps()} />);
88+
89+
expect(useTexture).toHaveBeenCalledWith("/crops/icons/atlas.avif");
6990
});
7091
});
7192

frontend/three_d_garden/bed/objects/pointer_objects.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from "react";
2-
import { Group, MeshPhongMaterial } from "../../components";
3-
import { Billboard, Line, Image, Sphere } from "@react-three/drei";
2+
import {
3+
Group, MeshPhongMaterial, Mesh, PlaneGeometry, MeshBasicMaterial,
4+
} from "../../components";
5+
import { Billboard, Line, Sphere, useTexture } from "@react-three/drei";
46
import { findCrop, findIcon } from "../../../crops/find";
57
import { Mode } from "../../../farm_designer/map/interfaces";
68
import { getMode, round, xyDistance } from "../../../farm_designer/map/util";
@@ -35,6 +37,10 @@ import { Actions } from "../../../constants";
3537
import { NavigateFunction } from "react-router";
3638
import { DrawnPointPayl } from "../../../farm_designer/interfaces";
3739
import { Line2 } from "three/examples/jsm/lines/Line2.js";
40+
import {
41+
getPlantIconTexture,
42+
getPlantIconTextureUrl,
43+
} from "../../garden/plant_icon_atlas";
3844

3945
export type PointerPlantRef = React.RefObject<GroupType | null>;
4046
export type RadiusRef = React.RefObject<MeshType | null>;
@@ -71,6 +77,11 @@ export const PointerObjects = (props: PointerObjectsProps) => {
7177
const zero = zeroFunc(config);
7278
const extents = extentsFunc(config);
7379
const iconSize = (addPlantProps.designer.cropRadius || DEFAULT_PLANT_RADIUS) * 2;
80+
const icon = findIcon(Path.getCropSlug());
81+
const baseTexture = useTexture(getPlantIconTextureUrl(icon));
82+
const plantIconTexture = React.useMemo(
83+
() => getPlantIconTexture(baseTexture, icon),
84+
[baseTexture, icon]);
7485

7586
const { drawnPoint } = addPlantProps.designer;
7687
const settingRadius =
@@ -127,12 +138,15 @@ export const PointerObjects = (props: PointerObjectsProps) => {
127138
{getMode() == Mode.clickToAdd &&
128139
<Group>
129140
<Billboard follow={true} position={[0, 0, iconSize / 2]}>
130-
<Image
141+
<Mesh
131142
name={"pointerPlant"}
132-
url={findIcon(Path.getCropSlug())}
133-
scale={iconSize}
134-
transparent={true}
135-
renderOrder={RenderOrder.pointerPlant} />
143+
renderOrder={RenderOrder.pointerPlant}>
144+
<PlaneGeometry args={[iconSize, iconSize]} />
145+
<MeshBasicMaterial
146+
map={plantIconTexture}
147+
alphaTest={0.1}
148+
transparent={true} />
149+
</Mesh>
136150
</Billboard>
137151
<Sphere args={[findCrop(Path.getCropSlug()).spread / 2 * 10, 32, 32]}>
138152
<MeshPhongMaterial
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
PLANT_ICON_ATLAS,
3+
getPlantIconTexture,
4+
getPlantIconTextureTransform,
5+
getPlantIconTextureUrl,
6+
} from "../plant_icon_atlas";
7+
import { Texture } from "three";
8+
9+
describe("plant icon atlas helpers", () => {
10+
const icon = "/crops/icons/mint.avif";
11+
12+
afterEach(() => {
13+
delete PLANT_ICON_ATLAS[icon];
14+
});
15+
16+
it("falls back to the original icon url", () => {
17+
delete PLANT_ICON_ATLAS[icon];
18+
expect(getPlantIconTextureUrl(icon)).toEqual(icon);
19+
expect(getPlantIconTextureTransform(icon)).toBeUndefined();
20+
});
21+
22+
it("resolves atlas url and UV transform", () => {
23+
PLANT_ICON_ATLAS[icon] = {
24+
atlasUrl: "/crops/icons/atlas.avif",
25+
textureWidth: 256,
26+
textureHeight: 128,
27+
x: 64,
28+
y: 16,
29+
width: 32,
30+
height: 48,
31+
};
32+
33+
expect(getPlantIconTextureUrl(icon)).toEqual("/crops/icons/atlas.avif");
34+
expect(getPlantIconTextureTransform(icon)).toEqual({
35+
offset: [0.25, 0.5],
36+
repeat: [0.125, 0.375],
37+
});
38+
});
39+
40+
it("gets atlas textures per base texture and icon", () => {
41+
PLANT_ICON_ATLAS[icon] = {
42+
atlasUrl: "/crops/icons/atlas.avif",
43+
textureWidth: 256,
44+
textureHeight: 128,
45+
x: 64,
46+
y: 16,
47+
width: 32,
48+
height: 48,
49+
};
50+
const baseTexture = new Texture();
51+
52+
const texture = getPlantIconTexture(baseTexture, icon);
53+
54+
expect(texture).not.toBe(baseTexture);
55+
});
56+
});

frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React from "react";
2020
import { fireEvent, render } from "@testing-library/react";
2121
import { clone } from "lodash";
2222
import { useFrame } from "@react-three/fiber";
23+
import { useTexture } from "@react-three/drei";
2324
import { Quaternion } from "three";
2425
import { fakePlant } from "../../../__test_support__/fake_state/resources";
2526
import { INITIAL } from "../../config";
@@ -33,6 +34,7 @@ import { Actions } from "../../../constants";
3334
import { convertPlants } from "../../../farm_designer/three_d_garden_map";
3435
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
3536
import { setMockInstanceId } from "../../../__test_support__/three_d_mocks";
37+
import { PLANT_ICON_ATLAS } from "../plant_icon_atlas";
3638

3739
describe("<PlantInstances />", () => {
3840
let reactUseRefSpy: jest.SpyInstance;
@@ -42,6 +44,7 @@ describe("<PlantInstances />", () => {
4244
.mockImplementation(() => mockRefImpl() as never);
4345
location.pathname = Path.mock(Path.designer());
4446
(useFrame as jest.Mock).mockClear();
47+
(useTexture as unknown as jest.Mock).mockClear();
4548
(useFrame as jest.Mock).mockImplementation((frameFn: Function) => frameFn({
4649
clock: { getElapsedTime: jest.fn(() => 0) },
4750
camera: { quaternion: new Quaternion() },
@@ -50,6 +53,7 @@ describe("<PlantInstances />", () => {
5053

5154
afterEach(() => {
5255
reactUseRefSpy.mockRestore();
56+
delete PLANT_ICON_ATLAS["/crops/icons/beet.avif"];
5357
});
5458

5559
const fakeProps = (): PlantInstancesProps => {
@@ -76,6 +80,20 @@ describe("<PlantInstances />", () => {
7680
expect(meshes.length).toBe(2);
7781
});
7882

83+
it("loads the atlas texture when an icon is mapped", () => {
84+
PLANT_ICON_ATLAS["/crops/icons/beet.avif"] = {
85+
atlasUrl: "/crops/icons/atlas.avif",
86+
textureWidth: 256,
87+
textureHeight: 256,
88+
x: 0,
89+
y: 0,
90+
width: 64,
91+
height: 64,
92+
};
93+
render(<PlantInstances {...fakeProps()} />);
94+
expect(useTexture).toHaveBeenCalledWith("/crops/icons/atlas.avif");
95+
});
96+
7997
it("clamps plant icon brightness", () => {
8098
expect(plantIconBrightness(undefined)).toEqual(1);
8199
expect(plantIconBrightness(0)).toEqual(0.25);

0 commit comments

Comments
 (0)