diff --git a/frontend/three_d_garden/__tests__/bed_test.tsx b/frontend/three_d_garden/__tests__/bed_test.tsx
index 9bbf78e895..a4dbc2278b 100644
--- a/frontend/three_d_garden/__tests__/bed_test.tsx
+++ b/frontend/three_d_garden/__tests__/bed_test.tsx
@@ -1,8 +1,33 @@
+jest.mock("../../farm_designer/map/layers/plants/plant_actions", () => ({
+ dropPlant: jest.fn(),
+}));
+
+let mockIsMobile = false;
+jest.mock("../../screen_size", () => ({
+ isMobile: () => mockIsMobile,
+}));
+
+const mockSetPosition = jest.fn();
+interface MockRefCurrent {
+ position: { set: Function; };
+}
+interface MockRef {
+ current: MockRefCurrent | undefined;
+}
+const mockRef: MockRef = { current: undefined };
+jest.mock("react", () => ({
+ ...jest.requireActual("react"),
+ useRef: () => mockRef,
+}));
+
import React from "react";
-import { mount } from "enzyme";
import { INITIAL } from "../config";
import { Bed, BedProps } from "../bed";
import { clone } from "lodash";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { dropPlant } from "../../farm_designer/map/layers/plants/plant_actions";
+import { Path } from "../../internal_urls";
+import { fakeAddPlantProps } from "../../__test_support__/fake_props";
describe("
", () => {
const fakeProps = (): BedProps => ({
@@ -13,8 +38,8 @@ describe("
", () => {
it("renders bed", () => {
const p = fakeProps();
p.config.extraLegsX = 0;
- const wrapper = mount(
);
- expect(wrapper.html()).toContain("bed-group");
+ const { container } = render(
);
+ expect(container).toContainHTML("bed-group");
});
it("renders bed with extra legs", () => {
@@ -22,7 +47,55 @@ describe("
", () => {
p.config.extraLegsX = 2;
p.config.extraLegsY = 2;
p.config.legsFlush = false;
- const wrapper = mount(
);
- expect(wrapper.html()).toContain("bed-group");
+ const { container } = render(
);
+ expect(container).toContainHTML("bed-group");
+ });
+
+ it("adds a plant", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ const p = fakeProps();
+ p.addPlantProps = fakeAddPlantProps([]);
+ render(
);
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.click(soil);
+ expect(dropPlant).toHaveBeenCalledWith(expect.objectContaining({
+ gardenCoords: { x: 1360, y: 620 },
+ }));
+ });
+
+ it("updates pointer plant position", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ mockIsMobile = false;
+ mockRef.current = { position: { set: mockSetPosition } };
+ const p = fakeProps();
+ p.addPlantProps = fakeAddPlantProps([]);
+ render(
);
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.pointerMove(soil);
+ expect(mockSetPosition).toHaveBeenCalledWith(0, 0, -75);
+ });
+
+ it("handles missing ref", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ mockIsMobile = false;
+ mockRef.current = undefined;
+ const p = fakeProps();
+ p.addPlantProps = fakeAddPlantProps([]);
+ render(
);
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.pointerMove(soil);
+ expect(mockSetPosition).not.toHaveBeenCalled();
+ });
+
+ it("doesn't update pointer plant position: mobile", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ mockIsMobile = true;
+ mockRef.current = { position: { set: mockSetPosition } };
+ const p = fakeProps();
+ p.addPlantProps = fakeAddPlantProps([]);
+ render(
);
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.pointerMove(soil);
+ expect(mockSetPosition).not.toHaveBeenCalled();
});
});
diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx
index 122e8ce72f..29415b2435 100644
--- a/frontend/three_d_garden/__tests__/garden_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_test.tsx
@@ -1,6 +1,7 @@
let mockIsDesktop = false;
jest.mock("../../screen_size", () => ({
isDesktop: () => mockIsDesktop,
+ isMobile: jest.fn(),
}));
import React from "react";
@@ -9,25 +10,27 @@ import { GardenModelProps, GardenModel } from "../garden";
import { clone } from "lodash";
import { INITIAL } from "../config";
import { render, screen } from "@testing-library/react";
-import { ASSETS } from "../constants";
+import { fakePlant } from "../../__test_support__/fake_state/resources";
+import { fakeAddPlantProps } from "../../__test_support__/fake_props";
describe("
", () => {
const fakeProps = (): GardenModelProps => ({
config: clone(INITIAL),
activeFocus: "",
setActiveFocus: jest.fn(),
+ addPlantProps: fakeAddPlantProps([]),
});
it("renders", () => {
- const wrapper = mount(
);
- expect(wrapper.html()).toContain("zoom-beacons");
- expect(wrapper.html()).not.toContain("stats");
- expect(wrapper.html()).toContain("darkgreen");
+ const { container } = render(
);
+ expect(container).toContainHTML("zoom-beacons");
+ expect(container).not.toContainHTML("stats");
+ expect(container).toContainHTML("darkgreen");
});
it("renders no user plants", () => {
const p = fakeProps();
- p.plants = [];
+ p.addPlantProps = fakeAddPlantProps([]);
render(
);
const plantLabels = screen.queryAllByText("Beet");
expect(plantLabels.length).toEqual(0);
@@ -35,16 +38,9 @@ describe("
", () => {
it("renders user plant", () => {
const p = fakeProps();
- p.plants = [
- {
- label: "Beet",
- icon: ASSETS.icons.beet,
- spread: 175,
- size: 150,
- x: 0,
- y: 0,
- },
- ];
+ const plant = fakePlant();
+ plant.body.name = "Beet";
+ p.addPlantProps = fakeAddPlantProps([plant]);
render(
);
const plantLabels = screen.queryAllByText("Beet");
expect(plantLabels.length).toEqual(1);
@@ -52,7 +48,7 @@ describe("
", () => {
it("renders promo plants", () => {
const p = fakeProps();
- p.plants = undefined;
+ p.addPlantProps = undefined;
render(
);
const plantLabels = screen.queryAllByText("Beet");
expect(plantLabels.length).toEqual(7);
@@ -70,9 +66,10 @@ describe("
", () => {
p.config.viewCube = true;
p.config.lab = true;
p.activeFocus = "plant";
- const wrapper = mount(
);
- expect(wrapper.html()).toContain("gray");
- expect(wrapper.html()).toContain("stats");
+ p.addPlantProps = undefined;
+ const { container } = render(
);
+ expect(container).toContainHTML("gray");
+ expect(container).toContainHTML("stats");
});
it("sets hover", () => {
diff --git a/frontend/three_d_garden/__tests__/index_test.tsx b/frontend/three_d_garden/__tests__/index_test.tsx
index a910b5ec12..ee85f796cf 100644
--- a/frontend/three_d_garden/__tests__/index_test.tsx
+++ b/frontend/three_d_garden/__tests__/index_test.tsx
@@ -5,11 +5,12 @@ import {
import React from "react";
import { INITIAL } from "../config";
import { clone } from "lodash";
+import { fakeAddPlantProps } from "../../__test_support__/fake_props";
describe("
", () => {
const fakeProps = (): ThreeDGardenProps => ({
config: clone(INITIAL),
- plants: [],
+ addPlantProps: fakeAddPlantProps([]),
});
it("renders", () => {
diff --git a/frontend/three_d_garden/__tests__/plants_test.tsx b/frontend/three_d_garden/__tests__/plants_test.tsx
new file mode 100644
index 0000000000..32bc15c344
--- /dev/null
+++ b/frontend/three_d_garden/__tests__/plants_test.tsx
@@ -0,0 +1,121 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { clone } from "lodash";
+import { fakePlant } from "../../__test_support__/fake_state/resources";
+import { INITIAL } from "../config";
+import {
+ calculatePlantPositions,
+ convertPlants,
+ ThreeDPlant,
+ ThreeDPlantProps,
+} from "../plants";
+import { CROPS } from "../../crops/constants";
+
+describe("calculatePlantPositions()", () => {
+ it("calculates plant positions", () => {
+ const config = clone(INITIAL);
+ config.plants = "Spring";
+ const positions = calculatePlantPositions(config);
+ expect(positions).toContainEqual({
+ icon: CROPS.beet.icon,
+ label: "Beet",
+ size: 150,
+ spread: 175,
+ x: 350,
+ y: 680,
+ });
+ expect(positions.length).toEqual(65);
+ });
+
+ it("returns no plants", () => {
+ const config = clone(INITIAL);
+ config.plants = "";
+ const positions = calculatePlantPositions(config);
+ expect(positions.length).toEqual(0);
+ });
+});
+
+describe("convertPlants()", () => {
+ it("converts plants", () => {
+ const config = clone(INITIAL);
+ config.bedXOffset = 10;
+ config.bedYOffset = 1;
+
+ const plant0 = fakePlant();
+ plant0.body.name = "Spinach";
+ plant0.body.openfarm_slug = "spinach";
+ plant0.body.x = 100;
+ plant0.body.y = 200;
+
+ const plant1 = fakePlant();
+ plant1.body.name = "Unknown";
+ plant1.body.openfarm_slug = "not-set";
+ plant1.body.x = 1000;
+ plant1.body.y = 2000;
+
+ const plants = [plant0, plant1];
+
+ const convertedPlants = convertPlants(config, plants);
+
+ expect(convertedPlants).toEqual([{
+ icon: CROPS.spinach.icon,
+ label: "Spinach",
+ size: 50,
+ spread: 0,
+ x: 110,
+ y: 201,
+ },
+ {
+ icon: CROPS["generic-plant"].icon,
+ label: "Unknown",
+ size: 50,
+ spread: 0,
+ x: 1010,
+ y: 2001,
+ },
+ ]);
+ });
+});
+
+describe("
", () => {
+ const fakeProps = (): ThreeDPlantProps => {
+ const config = clone(INITIAL);
+ const plant = fakePlant();
+ plant.body.name = "Beet";
+ return {
+ plant: convertPlants(config, [plant])[0],
+ i: 0,
+ config: config,
+ hoveredPlant: undefined,
+ };
+ };
+
+ it("renders label", () => {
+ const p = fakeProps();
+ p.config.labels = true;
+ p.config.labelsOnHover = false;
+ p.labelOnly = true;
+ render(
);
+ expect(screen.getByText("Beet")).toBeInTheDocument();
+ });
+
+ it("renders hovered label", () => {
+ const p = fakeProps();
+ p.config.labels = true;
+ p.config.labelsOnHover = true;
+ p.hoveredPlant = 0;
+ p.labelOnly = true;
+ render(
);
+ expect(screen.getByText("Beet")).toBeInTheDocument();
+ });
+
+ it("renders plant", () => {
+ const p = fakeProps();
+ p.config.labels = false;
+ p.config.labelsOnHover = false;
+ p.labelOnly = false;
+ render(
);
+ const { container } = render(
);
+ expect(container).toContainHTML("image");
+ });
+});
diff --git a/frontend/three_d_garden/bed.tsx b/frontend/three_d_garden/bed.tsx
index 5ab3ef5fcd..295aa883c8 100644
--- a/frontend/three_d_garden/bed.tsx
+++ b/frontend/three_d_garden/bed.tsx
@@ -1,6 +1,10 @@
import React from "react";
-import { Box, Detailed, Extrude, useTexture } from "@react-three/drei";
-import { DoubleSide, Path, Shape, RepeatWrapping } from "three";
+import {
+ Billboard, Box, Detailed, Extrude, useTexture, Image,
+} from "@react-three/drei";
+import {
+ DoubleSide, Path as LinePath, Shape, RepeatWrapping, Group as GroupType,
+} from "three";
import { range } from "lodash";
import { threeSpace, zZero, getColorFromBrightness } from "./helpers";
import { Config, detailLevels } from "./config";
@@ -11,11 +15,24 @@ import { Packaging } from "./packaging";
import { Caster } from "./caster";
import { UtilitiesPost } from "./utilities_post";
import { Group, MeshPhongMaterial } from "./components";
+import { getMode, round } from "../farm_designer/map/util";
+import {
+ AxisNumberProperty, Mode, TaggedPlant,
+} from "../farm_designer/map/interfaces";
+import { dropPlant } from "../farm_designer/map/layers/plants/plant_actions";
+import { TaggedCurve } from "farmbot";
+import { GetWebAppConfigValue } from "../config_storage/actions";
+import { DesignerState } from "../farm_designer/interfaces";
+import { isMobile } from "../screen_size";
+import { ThreeEvent } from "@react-three/fiber";
+import { Path } from "../internal_urls";
+import { findIcon } from "../crops/find";
+import { DEFAULT_PLANT_RADIUS } from "../farm_designer/plant";
const soil = (
- Type: typeof Path | typeof Shape,
+ Type: typeof LinePath | typeof Shape,
botSize: Record<"x" | "y" | "z" | "thickness", number>,
-): Path | Shape => {
+): LinePath | Shape => {
const { x, y, thickness } = botSize;
const hole = new Type();
@@ -41,19 +58,30 @@ const bedStructure2D = (
shape.lineTo(0, 0);
// inner edge
- shape.holes.push(soil(Path, botSize));
+ shape.holes.push(soil(LinePath, botSize));
return shape;
};
+export interface AddPlantProps {
+ gridSize: AxisNumberProperty;
+ dispatch: Function;
+ getConfigValue: GetWebAppConfigValue;
+ plants: TaggedPlant[];
+ curves: TaggedCurve[];
+ designer: DesignerState;
+}
+
export interface BedProps {
config: Config;
activeFocus: string;
+ addPlantProps?: AddPlantProps;
}
export const Bed = (props: BedProps) => {
const {
- bedWidthOuter, bedLengthOuter, botSizeZ, bedHeight, bedZOffset,
+ bedWidthOuter, bedLengthOuter, botSizeZ, bedHeight,
+ bedXOffset, bedYOffset, bedZOffset,
legSize, legsFlush, extraLegsX, extraLegsY, bedBrightness, soilBrightness,
soilHeight, ccSupportSize, axes, xyDimensions,
} = props.config;
@@ -109,9 +137,58 @@ export const Bed = (props: BedProps) => {
{children}
;
- const Soil = ({ children }: { children: React.ReactElement }) => {
+ // eslint-disable-next-line no-null/no-null
+ const pointerPlantRef = React.useRef
(null);
+
+ type XY = AxisNumberProperty;
+
+ const getGardenPosition = (e: ThreeEvent): XY => ({
+ x: round(threeSpace(e.point.x, -bedLengthOuter) - bedXOffset),
+ y: round(threeSpace(e.point.y, -bedWidthOuter) - bedYOffset),
+ });
+
+ const get3DPosition = (gardenPosition: XY): XY => ({
+ x: threeSpace(gardenPosition.x + bedXOffset, bedLengthOuter),
+ y: threeSpace(gardenPosition.y + bedYOffset, bedWidthOuter),
+ });
+
+ const iconSize =
+ (props.addPlantProps?.designer.cropRadius || DEFAULT_PLANT_RADIUS) * 2;
+
+ interface SoilProps {
+ children: React.ReactElement;
+ addPlantProps?: AddPlantProps;
+ }
+
+ const Soil = ({ children, addPlantProps }: SoilProps) => {
const soilDepth = bedHeight + zZero(props.config) - soilHeight;
return {
+ e.stopPropagation();
+ if (addPlantProps && getMode() == Mode.clickToAdd) {
+ dropPlant({
+ gardenCoords: getGardenPosition(e),
+ gridSize: addPlantProps.gridSize,
+ dispatch: addPlantProps.dispatch,
+ getConfigValue: addPlantProps.getConfigValue,
+ plants: addPlantProps.plants,
+ curves: addPlantProps.curves,
+ designer: addPlantProps.designer,
+ });
+ }
+ }}
+ onPointerMove={e => {
+ if (addPlantProps
+ && getMode() == Mode.clickToAdd
+ && !isMobile()
+ && pointerPlantRef.current) {
+ const position = get3DPosition(getGardenPosition(e));
+ pointerPlantRef.current.position.set(
+ position.x,
+ position.y,
+ zZero(props.config) - props.config.soilHeight + iconSize / 2);
+ }
+ }}
castShadow={true}
receiveShadow={true}
args={[
@@ -130,7 +207,8 @@ export const Bed = (props: BedProps) => {
return
-
+
@@ -199,12 +277,22 @@ export const Bed = (props: BedProps) => {
]}>
+ {getMode() == Mode.clickToAdd && !isMobile() &&
+
+
+ }
-
+
-
+
diff --git a/frontend/three_d_garden/bot.tsx b/frontend/three_d_garden/bot.tsx
index 4b399c00b2..9e193171aa 100644
--- a/frontend/three_d_garden/bot.tsx
+++ b/frontend/three_d_garden/bot.tsx
@@ -618,8 +618,7 @@ export const Bot = (props: FarmbotModelProps) => {
depth: zAxisLength - 350,
bevelEnabled: false,
},
- )}
- >
+ )}>
@@ -841,8 +840,7 @@ export const Bot = (props: FarmbotModelProps) => {
depth: botSizeY - 30,
bevelEnabled: false,
},
- )}
- >
+ )}>
diff --git a/frontend/three_d_garden/constants.ts b/frontend/three_d_garden/constants.ts
index 470d4a7802..96889e1921 100644
--- a/frontend/three_d_garden/constants.ts
+++ b/frontend/three_d_garden/constants.ts
@@ -16,50 +16,6 @@ export const ASSETS: Record> = {
concrete: "/3D/textures/concrete.avif",
screen: "/3D/textures/screen.avif",
},
- icons: {
- anaheimPepper: "/3D/icons/anaheim_pepper.avif",
- arugula: "/3D/icons/arugula.avif",
- basil: "/3D/icons/basil.avif",
- beet: "/3D/icons/beet.avif",
- bibbLettuce: "/3D/icons/bibb_lettuce.avif",
- bokChoy: "/3D/icons/bok_choy.avif",
- broccoli: "/3D/icons/broccoli.avif",
- brusselsSprout: "/3D/icons/brussels_sprout.avif",
- carrot: "/3D/icons/carrot.avif",
- cauliflower: "/3D/icons/cauliflower.avif",
- celery: "/3D/icons/celery.avif",
- chard: "/3D/icons/swiss_chard.avif",
- cherryBelleRadish: "/3D/icons/cherry_belle_radish.avif",
- cilantro: "/3D/icons/cilantro.avif",
- collardGreens: "/3D/icons/collard_greens.avif",
- cucumber: "/3D/icons/cucumber.avif",
- eggplant: "/3D/icons/eggplant.avif",
- frenchBreakfastRadish: "/3D/icons/french_breakfast_radish.avif",
- garlic: "/3D/icons/garlic.avif",
- goldenBeet: "/3D/icons/golden_beet.avif",
- hillbillyTomato: "/3D/icons/hillbilly_tomato.avif",
- icicleRadish: "/3D/icons/icicle_radish.avif",
- lacinatoKale: "/3D/icons/lacinato_kale.avif",
- leek: "/3D/icons/leek.avif",
- napaCabbage: "/3D/icons/napa_cabbage.avif",
- okra: "/3D/icons/okra.avif",
- parsnip: "/3D/icons/parsnip.avif",
- rainbowChard: "/3D/icons/rainbow_chard.avif",
- redBellPepper: "/3D/icons/red_bell_pepper.avif",
- redCurlyKale: "/3D/icons/red_curly_kale.avif",
- redRussianKale: "/3D/icons/red_russian_kale.avif",
- runnerBean: "/3D/icons/runner_bean.avif",
- rutabaga: "/3D/icons/rutabaga.avif",
- savoyCabbage: "/3D/icons/savoy_cabbage.avif",
- shallot: "/3D/icons/shallot.avif",
- snapPea: "/3D/icons/snap_pea.avif",
- spinach: "/3D/icons/spinach.avif",
- sweetPotato: "/3D/icons/sweet_potato.avif",
- turmeric: "/3D/icons/turmeric.avif",
- turnip: "/3D/icons/turnip.avif",
- yellowOnion: "/3D/icons/yellow_onion.avif",
- zucchini: "/3D/icons/zucchini.avif",
- },
shapes: {
track: "/3D/shapes/track.svg",
column: "/3D/shapes/column.svg",
@@ -111,7 +67,6 @@ export const ASSETS: Record> = {
interface Plant {
label: string;
- icon: string;
spread: number;
size: number;
}
@@ -123,253 +78,211 @@ interface Gardens {
export const PLANTS: Record = {
anaheimPepper: {
label: "Anaheim Pepper",
- icon: ASSETS.icons.anaheimPepper,
spread: 400,
size: 150,
},
arugula: {
label: "Arugula",
- icon: ASSETS.icons.arugula,
spread: 250,
size: 180,
},
basil: {
label: "Basil",
- icon: ASSETS.icons.basil,
spread: 250,
size: 160,
},
beet: {
label: "Beet",
- icon: ASSETS.icons.beet,
spread: 175,
size: 150,
},
bibbLettuce: {
label: "Bibb Lettuce",
- icon: ASSETS.icons.bibbLettuce,
spread: 250,
size: 200,
},
bokChoy: {
label: "Bok Choy",
- icon: ASSETS.icons.bokChoy,
spread: 210,
size: 160,
},
broccoli: {
label: "Broccoli",
- icon: ASSETS.icons.broccoli,
spread: 375,
size: 250,
},
brusselsSprout: {
label: "Brussels Sprout",
- icon: ASSETS.icons.brusselsSprout,
spread: 300,
size: 250,
},
carrot: {
label: "Carrot",
- icon: ASSETS.icons.carrot,
spread: 150,
size: 125,
},
cauliflower: {
label: "Cauliflower",
- icon: ASSETS.icons.cauliflower,
spread: 400,
size: 250,
},
celery: {
label: "Celery",
- icon: ASSETS.icons.celery,
spread: 350,
size: 200,
},
chard: {
label: "Swiss Chard",
- icon: ASSETS.icons.chard,
spread: 300,
size: 300,
},
cherryBelleRadish: {
- label: "Cherry Belle Radish",
- icon: ASSETS.icons.cherryBelleRadish,
+ label: "Cherry Bell Radish",
spread: 100,
size: 100,
},
cilantro: {
label: "Cilantro",
- icon: ASSETS.icons.cilantro,
spread: 180,
size: 150,
},
collardGreens: {
label: "Collard Greens",
- icon: ASSETS.icons.collardGreens,
spread: 230,
size: 230,
},
cucumber: {
label: "Cucumber",
- icon: ASSETS.icons.cucumber,
spread: 400,
size: 200,
},
eggplant: {
label: "Eggplant",
- icon: ASSETS.icons.eggplant,
spread: 400,
size: 200,
},
frenchBreakfastRadish: {
label: "French Breakfast Radish",
- icon: ASSETS.icons.frenchBreakfastRadish,
spread: 100,
size: 100,
},
garlic: {
label: "Garlic",
- icon: ASSETS.icons.garlic,
spread: 175,
size: 100,
},
goldenBeet: {
label: "Golden Beet",
- icon: ASSETS.icons.goldenBeet,
spread: 175,
size: 150,
},
hillbillyTomato: {
label: "Hillbilly Tomato",
- icon: ASSETS.icons.hillbillyTomato,
spread: 400,
size: 200,
},
icicleRadish: {
label: "Icicle Radish",
- icon: ASSETS.icons.icicleRadish,
spread: 100,
size: 100,
},
- lacinatoKale: {
- label: "Lacinato Kale",
- icon: ASSETS.icons.lacinatoKale,
+ laciantoKale: {
+ label: "Lacianto Kale",
spread: 250,
size: 220,
},
leek: {
label: "Leek",
- icon: ASSETS.icons.leek,
spread: 200,
size: 200,
},
napaCabbage: {
label: "Napa Cabbage",
- icon: ASSETS.icons.napaCabbage,
spread: 400,
size: 220,
},
okra: {
label: "Okra",
- icon: ASSETS.icons.okra,
spread: 400,
size: 200,
},
parsnip: {
label: "Parsnip",
- icon: ASSETS.icons.parsnip,
spread: 180,
size: 120,
},
rainbowChard: {
label: "Rainbow Chard",
- icon: ASSETS.icons.rainbowChard,
spread: 250,
size: 250,
},
redBellPepper: {
label: "Red Bell Pepper",
- icon: ASSETS.icons.redBellPepper,
spread: 350,
size: 200,
},
redCurlyKale: {
label: "Red Curly Kale",
- icon: ASSETS.icons.redCurlyKale,
spread: 350,
size: 220,
},
redRussianKale: {
label: "Red Russian Kale",
- icon: ASSETS.icons.redRussianKale,
spread: 250,
size: 200,
},
runnerBean: {
label: "Runner Bean",
- icon: ASSETS.icons.runnerBean,
spread: 350,
size: 200,
},
rutabaga: {
label: "Rutabaga",
- icon: ASSETS.icons.rutabaga,
spread: 200,
size: 150,
},
savoyCabbage: {
label: "Savoy Cabbage",
- icon: ASSETS.icons.savoyCabbage,
spread: 400,
size: 250,
},
shallot: {
label: "Shallot",
- icon: ASSETS.icons.shallot,
spread: 200,
size: 140,
},
snapPea: {
label: "Snap Pea",
- icon: ASSETS.icons.snapPea,
spread: 200,
size: 150,
},
spinach: {
label: "Spinach",
- icon: ASSETS.icons.spinach,
spread: 250,
size: 200,
},
sweetPotato: {
label: "Sweet Potato",
- icon: ASSETS.icons.sweetPotato,
spread: 400,
size: 180,
},
turmeric: {
label: "Turmeric",
- icon: ASSETS.icons.turmeric,
spread: 250,
size: 150,
},
turnip: {
label: "Turnip",
- icon: ASSETS.icons.turnip,
spread: 175,
size: 150,
},
yellowOnion: {
label: "Yellow Onion",
- icon: ASSETS.icons.yellowOnion,
spread: 200,
size: 150,
},
zucchini: {
label: "Zucchini",
- icon: ASSETS.icons.zucchini,
spread: 400,
size: 250,
},
@@ -386,7 +299,7 @@ export const GARDENS: Gardens = {
],
"Fall": [
"arugula", "cherryBelleRadish", "cilantro", "collardGreens", "garlic",
- "goldenBeet", "leek", "lacinatoKale", "turnip", "yellowOnion",
+ "goldenBeet", "leek", "laciantoKale", "turnip", "yellowOnion",
],
"Winter": [
"frenchBreakfastRadish", "napaCabbage", "parsnip", "redCurlyKale",
diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx
index 40d2114e1b..aaccede1b6 100644
--- a/frontend/three_d_garden/garden.tsx
+++ b/frontend/three_d_garden/garden.tsx
@@ -3,23 +3,18 @@ import { ThreeEvent } from "@react-three/fiber";
import {
GizmoHelper, GizmoViewcube,
OrbitControls, PerspectiveCamera,
- Circle, Stats, Billboard, Image, Clouds, Cloud, OrthographicCamera,
+ Circle, Stats, Image, Clouds, Cloud, OrthographicCamera,
Detailed, Sphere,
useTexture,
Line,
} from "@react-three/drei";
-import { RepeatWrapping, Vector3, BackSide } from "three";
+import { RepeatWrapping, BackSide } from "three";
import { Bot } from "./bot";
-import { Bed } from "./bed";
-import {
- threeSpace,
- zZero as zZeroFunc,
- zero as zeroFunc,
- extents as extentsFunc,
-} from "./helpers";
+import { AddPlantProps, Bed } from "./bed";
+import { zero as zeroFunc, extents as extentsFunc } from "./helpers";
import { Sky } from "./sky";
import { Config, detailLevels, seasonProperties } from "./config";
-import { ASSETS, GARDENS, PLANTS } from "./constants";
+import { ASSETS } from "./constants";
import { useSpring, animated } from "@react-spring/three";
import { Solar } from "./solar";
import { Sun, sunPosition } from "./sun";
@@ -30,8 +25,11 @@ import {
AmbientLight, AxesHelper, Group, MeshBasicMaterial, MeshPhongMaterial,
} from "./components";
import { isDesktop } from "../screen_size";
-import { Text } from "./text";
import { isUndefined, range } from "lodash";
+import {
+ calculatePlantPositions, convertPlants, ThreeDPlant,
+} from "./plants";
+import { ICON_URLS } from "../crops/constants";
const AnimatedGroup = animated(Group);
@@ -39,69 +37,18 @@ export interface GardenModelProps {
config: Config;
activeFocus: string;
setActiveFocus(focus: string): void;
- plants?: Plant[];
-}
-
-interface Plant {
- label: string;
- icon: string;
- size: number;
- spread: number;
- x: number;
- y: number;
+ addPlantProps?: AddPlantProps;
}
-export interface ThreeDGardenPlant extends Plant { }
-
+// eslint-disable-next-line complexity
export const GardenModel = (props: GardenModelProps) => {
const { config } = props;
const groundZ = config.bedZOffset + config.bedHeight;
const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera;
- const gardenPlants = GARDENS[config.plants] || [];
- const calculatePlantPositions = (): Plant[] => {
- const positions: Plant[] = [];
- const startX = 350;
- let nextX = startX;
- let index = 0;
- while (nextX <= config.bedLengthOuter - 100) {
- const plantKey = gardenPlants[index];
- const plant = PLANTS[plantKey];
- if (!plant) { return []; }
- positions.push({
- ...plant,
- x: nextX,
- y: config.bedWidthOuter / 2,
- });
- const plantsPerHalfRow =
- Math.ceil((config.bedWidthOuter - plant.spread) / 2 / plant.spread);
- for (let i = 1; i < plantsPerHalfRow; i++) {
- positions.push({
- ...plant,
- x: nextX,
- y: config.bedWidthOuter / 2 + plant.spread * i,
- });
- positions.push({
- ...plant,
- x: nextX,
- y: config.bedWidthOuter / 2 - plant.spread * i,
- });
- }
- if (index + 1 < gardenPlants.length) {
- const nextPlant = PLANTS[gardenPlants[index + 1]];
- nextX += (plant.spread / 2) + (nextPlant.spread / 2);
- index++;
- } else {
- index = 0;
- const nextPlant = PLANTS[gardenPlants[0]];
- nextX += (plant.spread / 2) + (nextPlant.spread / 2);
- }
- }
- return positions;
- };
- const plants = isUndefined(props.plants)
- ? calculatePlantPositions()
- : props.plants;
+ const plants = isUndefined(props.addPlantProps)
+ ? calculatePlantPositions(config)
+ : convertPlants(config, props.addPlantProps.plants);
const [hoveredPlant, setHoveredPlant] =
React.useState(undefined);
@@ -118,35 +65,6 @@ export const GardenModel = (props: GardenModelProps) => {
: undefined;
};
- interface PlantProps {
- plant: Plant;
- i: number;
- labelOnly?: boolean;
- }
-
- const Plant = (props: PlantProps) => {
- const { i, plant, labelOnly } = props;
- const alwaysShowLabels = config.labels && !config.labelsOnHover;
- return
- {labelOnly
- ?
- {plant.label}
-
- : }
- ;
- };
const isXL = config.sizePreset == "Genesis XL";
const { scale } = useSpring({
scale: isXL ? 1.75 : 1,
@@ -181,7 +99,7 @@ export const GardenModel = (props: GardenModelProps) => {
const camera = getCamera(config, props.activeFocus, initCamera);
const zero = zeroFunc(config);
- const gridZ = zero.z - config.soilHeight;
+ const gridZ = zero.z - config.soilHeight + 5;
const extents = extentsFunc(config);
// eslint-disable-next-line no-null/no-null
@@ -250,15 +168,21 @@ export const GardenModel = (props: GardenModelProps) => {
.cloudOpacity}
fade={5000} />
-
+
- {Object.values(PLANTS).map((plant, i) =>
- )}
+ {ICON_URLS.map((url, i) => )}
{plants.map((plant, i) =>
- )}
+ )}
{range(0, config.botSizeX + 100, 100).map(x =>
@@ -282,7 +206,10 @@ export const GardenModel = (props: GardenModelProps) => {
onPointerMove={setHover(true)}
onPointerLeave={setHover(false)}>
{plants.map((plant, i) =>
- )}
+ )}
diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx
index d19150b03b..8c5e90b503 100644
--- a/frontend/three_d_garden/index.tsx
+++ b/frontend/three_d_garden/index.tsx
@@ -1,21 +1,24 @@
import { Canvas } from "@react-three/fiber";
import React from "react";
import { Config } from "./config";
-import { GardenModel, ThreeDGardenPlant } from "./garden";
+import { GardenModel } from "./garden";
import { noop } from "lodash";
+import { AddPlantProps } from "./bed";
export interface ThreeDGardenProps {
config: Config;
- plants: ThreeDGardenPlant[];
+ addPlantProps: AddPlantProps;
}
export const ThreeDGarden = (props: ThreeDGardenProps) => {
return ;
diff --git a/frontend/three_d_garden/plants.tsx b/frontend/three_d_garden/plants.tsx
new file mode 100644
index 0000000000..d251917c50
--- /dev/null
+++ b/frontend/three_d_garden/plants.tsx
@@ -0,0 +1,112 @@
+import { TaggedPlant } from "../farm_designer/map/interfaces";
+import { Config } from "./config";
+import { GARDENS, PLANTS } from "./constants";
+import { Billboard, Image } from "@react-three/drei";
+import React from "react";
+import { Vector3 } from "three";
+import { threeSpace, zZero as zZeroFunc } from "./helpers";
+import { Text } from "./text";
+import { findIcon } from "../crops/find";
+import { kebabCase } from "lodash";
+
+interface Plant {
+ label: string;
+ icon: string;
+ size: number;
+ spread: number;
+ x: number;
+ y: number;
+}
+
+export interface ThreeDGardenPlant extends Plant { }
+
+export const convertPlants = (config: Config, plants: TaggedPlant[]): Plant[] => {
+ return plants.map(plant => {
+ return {
+ label: plant.body.name,
+ icon: findIcon(plant.body.openfarm_slug),
+ size: plant.body.radius * 2,
+ spread: 0,
+ x: plant.body.x + config.bedXOffset,
+ y: plant.body.y + config.bedYOffset,
+ };
+ });
+};
+
+export const calculatePlantPositions = (config: Config): Plant[] => {
+ const gardenPlants = GARDENS[config.plants] || [];
+ const positions: Plant[] = [];
+ const startX = 350;
+ let nextX = startX;
+ let index = 0;
+ while (nextX <= config.bedLengthOuter - 100) {
+ const plantKey = gardenPlants[index];
+ const plant = PLANTS[plantKey];
+ if (!plant) { return []; }
+ const icon = findIcon(kebabCase(plant.label));
+ positions.push({
+ ...plant,
+ icon,
+ x: nextX,
+ y: config.bedWidthOuter / 2,
+ });
+ const plantsPerHalfRow =
+ Math.ceil((config.bedWidthOuter - plant.spread) / 2 / plant.spread);
+ for (let i = 1; i < plantsPerHalfRow; i++) {
+ positions.push({
+ ...plant,
+ icon,
+ x: nextX,
+ y: config.bedWidthOuter / 2 + plant.spread * i,
+ });
+ positions.push({
+ ...plant,
+ icon,
+ x: nextX,
+ y: config.bedWidthOuter / 2 - plant.spread * i,
+ });
+ }
+ if (index + 1 < gardenPlants.length) {
+ const nextPlant = PLANTS[gardenPlants[index + 1]];
+ nextX += (plant.spread / 2) + (nextPlant.spread / 2);
+ index++;
+ } else {
+ index = 0;
+ const nextPlant = PLANTS[gardenPlants[0]];
+ nextX += (plant.spread / 2) + (nextPlant.spread / 2);
+ }
+ }
+ return positions;
+};
+
+export interface ThreeDPlantProps {
+ plant: Plant;
+ i: number;
+ labelOnly?: boolean;
+ config: Config;
+ hoveredPlant: number | undefined;
+}
+
+export const ThreeDPlant = (props: ThreeDPlantProps) => {
+ const { i, plant, labelOnly, config, hoveredPlant } = props;
+ const alwaysShowLabels = config.labels && !config.labelsOnHover;
+ return
+ {labelOnly
+ ?
+ {plant.label}
+
+ : }
+ ;
+};
diff --git a/frontend/weeds/weed_inventory_item.tsx b/frontend/weeds/weed_inventory_item.tsx
index 858ef3492f..d3da001ae9 100644
--- a/frontend/weeds/weed_inventory_item.tsx
+++ b/frontend/weeds/weed_inventory_item.tsx
@@ -3,8 +3,7 @@ import { TaggedWeedPointer } from "farmbot";
import { Actions } from "../constants";
import { useNavigate } from "react-router";
import { t } from "../i18next_wrapper";
-import { svgToUrl } from "../open_farm/icons";
-import { genericWeedIcon } from "../point_groups/point_group_item";
+import { genericWeedIcon, svgToUrl } from "../point_groups/point_group_item";
import { getMode } from "../farm_designer/map/util";
import { Mode } from "../farm_designer/map/interfaces";
import { mapPointClickAction, selectPoint } from "../farm_designer/map/actions";
diff --git a/frontend/weeds/weeds_edit.tsx b/frontend/weeds/weeds_edit.tsx
index 40ac055a6b..fda1ded663 100644
--- a/frontend/weeds/weeds_edit.tsx
+++ b/frontend/weeds/weeds_edit.tsx
@@ -98,7 +98,7 @@ export const RawEditWeed = (props: EditWeedProps) => {
{weed
- ?