From 077beae2261158ba9c0b18264a2b836a6ea73494 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Tue, 11 Mar 2025 10:37:32 -0700
Subject: [PATCH 1/7] add 3D point and weed creation interactions
---
.../__tests__/garden_model_test.tsx | 11 ++
.../three_d_garden/bed/__tests__/bed_test.tsx | 39 ++++++-
frontend/three_d_garden/bed/bed.tsx | 94 +++++++++++----
.../garden/__tests__/plants_test.tsx | 1 +
.../garden/__tests__/point_test.tsx | 48 +++++++-
.../garden/__tests__/sky_test.tsx | 8 +-
.../garden/__tests__/weed_test.tsx | 2 +-
frontend/three_d_garden/garden/plants.tsx | 3 +-
frontend/three_d_garden/garden/point.tsx | 108 ++++++++++++++----
frontend/three_d_garden/garden/sky.tsx | 56 +++------
frontend/three_d_garden/garden/weed.tsx | 39 +++++--
frontend/three_d_garden/garden_model.tsx | 47 +++++---
12 files changed, 333 insertions(+), 123 deletions(-)
diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx
index 89697546d4..4797de768d 100644
--- a/frontend/three_d_garden/__tests__/garden_model_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx
@@ -15,6 +15,7 @@ import {
} from "../../__test_support__/fake_state/resources";
import { fakeAddPlantProps } from "../../__test_support__/fake_props";
import { ASSETS } from "../constants";
+import { Path } from "../../internal_urls";
describe("", () => {
const fakeProps = (): GardenModelProps => ({
@@ -58,6 +59,16 @@ describe("", () => {
expect(container).toContainHTML(ASSETS.other.weed);
});
+ it("renders drawn point", () => {
+ location.pathname = Path.mock(Path.points("add"));
+ const p = fakeProps();
+ const addPlantProps = fakeAddPlantProps([]);
+ addPlantProps.designer.drawnPoint = { cx: 1, cy: 2, z: 3, color: "red", r: 25 };
+ p.addPlantProps = addPlantProps;
+ const { container } = render();
+ expect(container).toContainHTML("drawn-point");
+ });
+
it("doesn't render bot", () => {
const p = fakeProps();
p.addPlantProps = fakeAddPlantProps([]);
diff --git a/frontend/three_d_garden/bed/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
index 3fc3b06494..1c588bf6fa 100644
--- a/frontend/three_d_garden/bed/__tests__/bed_test.tsx
+++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
@@ -28,6 +28,7 @@ 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";
+import { Actions } from "../../../constants";
describe("", () => {
const fakeProps = (): BedProps => ({
@@ -63,6 +64,42 @@ describe("", () => {
}));
});
+ it("adds a drawn point", () => {
+ location.pathname = Path.mock(Path.points("add"));
+ const p = fakeProps();
+ const addPlantProps = fakeAddPlantProps([]);
+ addPlantProps.designer.drawnPoint = {
+ cx: 1, cy: 2, z: 3, r: 150, color: "green",
+ };
+ p.addPlantProps = addPlantProps;
+ render();
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.click(soil);
+ expect(p.addPlantProps.dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_DRAWN_POINT_DATA,
+ payload: { cx: 1360, cy: 660, r: 150 },
+ });
+ });
+
+ it("adds a drawn weed", () => {
+ location.pathname = Path.mock(Path.weeds("add"));
+ mockIsMobile = false;
+ mockRef.current = { position: { set: mockSetPosition } };
+ const p = fakeProps();
+ const addPlantProps = fakeAddPlantProps([]);
+ addPlantProps.designer.drawnWeed = {
+ cx: 1, cy: 2, z: 3, r: 25, color: "red",
+ };
+ p.addPlantProps = addPlantProps;
+ render();
+ const soil = screen.getAllByText("soil")[0];
+ fireEvent.click(soil);
+ expect(p.addPlantProps.dispatch).toHaveBeenCalledWith({
+ type: Actions.SET_DRAWN_WEED_DATA,
+ payload: { cx: 1360, cy: 660, r: 25 },
+ });
+ });
+
it("updates pointer plant position", () => {
location.pathname = Path.mock(Path.cropSearch("mint"));
mockIsMobile = false;
@@ -72,7 +109,7 @@ describe("", () => {
render();
const soil = screen.getAllByText("soil")[0];
fireEvent.pointerMove(soil);
- expect(mockSetPosition).toHaveBeenCalledWith(0, 0, -75);
+ expect(mockSetPosition).toHaveBeenCalledWith(0, 0, 0);
});
it("handles missing ref", () => {
diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx
index 2bc7543fa1..259a959b54 100644
--- a/frontend/three_d_garden/bed/bed.tsx
+++ b/frontend/three_d_garden/bed/bed.tsx
@@ -1,6 +1,7 @@
import React from "react";
import {
Billboard, Box, Detailed, Extrude, useTexture, Image,
+ Line,
} from "@react-three/drei";
import {
DoubleSide, Path as LinePath, Shape, RepeatWrapping, Group as GroupType,
@@ -25,6 +26,11 @@ import { ThreeEvent } from "@react-three/fiber";
import { Path } from "../../internal_urls";
import { findIcon } from "../../crops/find";
import { DEFAULT_PLANT_RADIUS } from "../../farm_designer/plant";
+import { DrawnPoint, getDrawnPointData } from "../garden";
+import { Actions } from "../../constants";
+
+const HOVER_OBJECT_MODES = [Mode.clickToAdd, Mode.createPoint, Mode.createWeed];
+export const DRAW_POINT_MODES = [Mode.createPoint, Mode.createWeed];
const soil = (
Type: typeof LinePath | typeof Shape,
@@ -162,28 +168,37 @@ export const Bed = (props: BedProps) => {
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,
- });
+ if (addPlantProps) {
+ if (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,
+ });
+ }
+ if (DRAW_POINT_MODES.includes(getMode())) {
+ const center = getGardenPosition(e);
+ const point = getDrawnPointData(addPlantProps.designer, props.config);
+ addPlantProps.dispatch({
+ type: getMode() == Mode.createWeed
+ ? Actions.SET_DRAWN_WEED_DATA
+ : Actions.SET_DRAWN_POINT_DATA,
+ payload: { cx: center.x, cy: center.y, r: point.radius },
+ });
+ }
}
}}
onPointerMove={(e: ThreeEvent) => {
if (addPlantProps
- && getMode() == Mode.clickToAdd
+ && HOVER_OBJECT_MODES.includes(getMode())
&& !isMobile()
&& pointerPlantRef.current) {
const position = get3DPosition(getGardenPosition(e));
- pointerPlantRef.current.position.set(
- position.x,
- position.y,
- zZero(props.config) - props.config.soilHeight + iconSize / 2);
+ pointerPlantRef.current.position.set(position.x, position.y, 0);
}
}}
castShadow={true}
@@ -201,6 +216,10 @@ export const Bed = (props: BedProps) => {
;
};
+ const drawnPoint = props.addPlantProps &&
+ getDrawnPointData(props.addPlantProps.designer, props.config);
+ const soilZ = zZero(props.config) - props.config.soilHeight;
+
return
@@ -274,16 +293,41 @@ export const Bed = (props: BedProps) => {
]}>
- {getMode() == Mode.clickToAdd && !isMobile() &&
-
-
- }
+ {HOVER_OBJECT_MODES.includes(getMode()) &&
+ !isMobile() &&
+
+
+ {DRAW_POINT_MODES.includes(getMode()) && props.addPlantProps &&
+ }
+ {getMode() == Mode.clickToAdd &&
+
+
+ }
+
+ }
+ {DRAW_POINT_MODES.includes(getMode()) &&
+ !isMobile() &&
+ pointerPlantRef.current &&
+ drawnPoint &&
+ }
", () => {
i: 0,
config: config,
hoveredPlant: undefined,
+ visible: true,
};
};
diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx
index b145453945..25752e74a0 100644
--- a/frontend/three_d_garden/garden/__tests__/point_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx
@@ -1,12 +1,14 @@
import React from "react";
import { fireEvent, render } from "@testing-library/react";
-import { Point, PointProps } from "../point";
+import { DrawnPoint, DrawnPointProps, getDrawnPointData, Point, PointProps } from "../point";
import { INITIAL } from "../../config";
import { clone } from "lodash";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { Path } from "../../../internal_urls";
import { Actions } from "../../../constants";
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
+import { fakeDesignerState } from "../../../__test_support__/fake_designer_state";
+import { Vector3 } from "three";
describe("", () => {
const fakeProps = (): PointProps => ({
@@ -43,3 +45,47 @@ describe("", () => {
expect(mockNavigate).not.toHaveBeenCalled();
});
});
+
+describe("getDrawnPointData()", () => {
+ it("returns point data", () => {
+ location.pathname = Path.mock(Path.weeds("add"));
+ const designer = fakeDesignerState();
+ designer.drawnWeed = {
+ cx: 1, cy: 2, z: 3, color: "red", r: 25,
+ };
+ const config = clone(INITIAL);
+ const expectedPosition = new Vector3(-1360 + 1, -660 + 2, 400 + 3);
+ expect(getDrawnPointData(designer, config)).toEqual({
+ position: expectedPosition, radius: 25, color: "red",
+ });
+ });
+});
+
+describe("", () => {
+ const fakeProps = (): DrawnPointProps => {
+ const designer = fakeDesignerState();
+ designer.drawnWeed = {
+ cx: 1, cy: 2, z: 3, color: "red", r: 25,
+ };
+ designer.drawnPoint = {
+ cx: 10, cy: 20, z: 30, color: "green", r: 15,
+ };
+ const config = clone(INITIAL);
+ return {
+ designer,
+ usePosition: false,
+ config,
+ };
+ };
+
+ it("draws weed", () => {
+ location.pathname = Path.mock(Path.weeds("add"));
+ const p = fakeProps();
+ const { container } = render();
+ expect(container).toContainHTML("generic-weed");
+ expect(container).toContainHTML("position=\"0,0,0\"");
+ expect(container).toContainHTML("scale=\"25\"");
+ expect(container).toContainHTML("color=\"red\"");
+ expect(container).toContainHTML("opacity=\"0.25\"");
+ });
+});
diff --git a/frontend/three_d_garden/garden/__tests__/sky_test.tsx b/frontend/three_d_garden/garden/__tests__/sky_test.tsx
index 98ebf5bc36..841c700da8 100644
--- a/frontend/three_d_garden/garden/__tests__/sky_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/sky_test.tsx
@@ -1,13 +1,15 @@
import React from "react";
-import { mount } from "enzyme";
+import { render } from "@testing-library/react";
import { Sky, SkyProps } from "../sky";
+import { Vector3 } from "three";
describe("", () => {
const fakeProps = (): SkyProps => ({
+ sunPosition: new Vector3(),
});
it("renders", () => {
- const wrapper = mount();
- expect(wrapper.html()).toContain("primitive");
+ const { container } = render();
+ expect(container).toContainHTML("primitive");
});
});
diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx
index c2effdea4a..b70a085816 100644
--- a/frontend/three_d_garden/garden/__tests__/weed_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx
@@ -25,7 +25,7 @@ describe("", () => {
p.dispatch = mockDispatch(dispatch);
p.weed.body.id = 1;
const { container } = render();
- const weed = container.querySelector("[name='weed'");
+ const weed = container.querySelector("[name='weed-1'");
weed && fireEvent.click(weed);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_PANEL_OPEN, payload: true,
diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx
index bb18384e9f..9c459b40ef 100644
--- a/frontend/three_d_garden/garden/plants.tsx
+++ b/frontend/three_d_garden/garden/plants.tsx
@@ -91,6 +91,7 @@ export interface ThreeDPlantProps {
config: Config;
hoveredPlant: number | undefined;
dispatch?: Function;
+ visible?: boolean;
}
export const ThreeDPlant = (props: ThreeDPlantProps) => {
@@ -114,7 +115,7 @@ export const ThreeDPlant = (props: ThreeDPlantProps) => {
: {
- if (plant.id && !isUndefined(props.dispatch)) {
+ if (plant.id && !isUndefined(props.dispatch) && props.visible) {
props.dispatch(setPanelOpen(true));
navigate(Path.plants(plant.id));
}
diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx
index 792506ea97..45573dc714 100644
--- a/frontend/three_d_garden/garden/point.tsx
+++ b/frontend/three_d_garden/garden/point.tsx
@@ -3,12 +3,16 @@ import { TaggedGenericPointer } from "farmbot";
import { Config } from "../config";
import { Group, MeshPhongMaterial } from "../components";
import { Cylinder, Sphere } from "@react-three/drei";
-import { DoubleSide } from "three";
+import { DoubleSide, Vector3 } from "three";
import { zero as zeroFunc, threeSpace } from "../helpers";
import { useNavigate } from "react-router";
import { Path } from "../../internal_urls";
import { isUndefined } from "lodash";
import { setPanelOpen } from "../../farm_designer/panel_header";
+import { DesignerState } from "../../farm_designer/interfaces";
+import { getMode } from "../../farm_designer/map/util";
+import { Mode } from "../../farm_designer/map/interfaces";
+import { WeedBase } from ".";
export interface PointProps {
point: TaggedGenericPointer;
@@ -18,50 +22,112 @@ export interface PointProps {
export const Point = (props: PointProps) => {
const { point, config } = props;
- const RADIUS = 25;
- const HEIGHT = 100;
const navigate = useNavigate();
- return
+ )}
+ onClick={() => {
+ if (point.body.id && !isUndefined(props.dispatch)) {
+ props.dispatch(setPanelOpen(true));
+ navigate(Path.points(point.body.id));
+ }
+ }}
+ color={point.body.meta.color}
+ radius={point.body.radius}
+ />;
+};
+
+export const getDrawnPointData = (designer: DesignerState, config: Config) => {
+ const { drawnPoint, drawnWeed } = designer;
+ const point = getMode() == Mode.createWeed ? drawnWeed : drawnPoint;
+ const xyz = {
+ x: point?.cx || 0,
+ y: point?.cy || 0,
+ z: point?.z || -config.soilHeight,
+ };
+ const color = point?.color || "green";
+ const radius = point?.r || 15;
+ const position = new Vector3(
+ threeSpace(xyz.x, config.bedLengthOuter) + config.bedXOffset,
+ threeSpace(xyz.y, config.bedWidthOuter) + config.bedYOffset,
+ zeroFunc(config).z + xyz.z,
+ );
+ const data = {
+ position,
+ color,
+ radius,
+ };
+ return data;
+};
+
+export interface DrawnPointProps {
+ designer: DesignerState;
+ usePosition: boolean;
+ config: Config;
+}
+
+export const DrawnPoint = (props: DrawnPointProps) => {
+ const { config } = props;
+ const data = getDrawnPointData(props.designer, config);
+ const Base = getMode() == Mode.createWeed ? WeedBase : PointBase;
+ return ;
+};
+
+interface PointBaseProps {
+ pointName: string;
+ position?: Vector3;
+ onClick?: () => void;
+ color: string | undefined;
+ radius: number;
+ alpha: number;
+}
+
+const PointBase = (props: PointBaseProps) => {
+ const RADIUS = 25;
+ const HEIGHT = 100;
+
+ return
{
- if (point.body.id && !isUndefined(props.dispatch)) {
- props.dispatch(setPanelOpen(true));
- navigate(Path.points(point.body.id));
- }
- }}>
+ onClick={props.onClick}>
+ opacity={1 * props.alpha} />
+ opacity={1 * props.alpha} />
+ args={[props.radius, props.radius, 100, 32, 32, true]}>
+ opacity={0.5 * props.alpha} />
;
};
diff --git a/frontend/three_d_garden/garden/sky.tsx b/frontend/three_d_garden/garden/sky.tsx
index c2eb96b49d..ccd02ff53c 100644
--- a/frontend/three_d_garden/garden/sky.tsx
+++ b/frontend/three_d_garden/garden/sky.tsx
@@ -1,51 +1,23 @@
-// Forked from https://github.com/pmndrs/drei/blob/master/src/core/Sky.tsx
-
import React from "react";
-import { Vector3 as Vector3Type } from "@react-three/fiber";
import { Sky as SkyImpl } from "three-stdlib";
import { Vector3 } from "three";
-import { ForwardRefComponent } from "@react-three/drei/helpers/ts-utils";
import { Primitive } from "../components";
export type SkyProps = {
- distance?: number
- sunPosition?: Vector3Type
- mieCoefficient?: number
- mieDirectionalG?: number
- rayleigh?: number
- turbidity?: number
- up?: Vector3Type
+ sunPosition: Vector3;
}
-export const Sky: ForwardRefComponent =
- /* @__PURE__ */
- React.forwardRef((
- {
- distance = 1000,
- mieCoefficient = 0.005,
- mieDirectionalG = 0.8,
- rayleigh = 0.5,
- turbidity = 10,
- sunPosition = [0, 5000, 0],
- up = [0, 0, 1],
- ...props
- }: SkyProps,
- ref,
- ) => {
- const scale = React.useMemo(() => new Vector3()
- .setScalar(distance), [distance]);
- const [sky] = React.useState(() => new SkyImpl());
+export const Sky = (props: SkyProps) => {
+ const [sky] = React.useState(() => new SkyImpl());
- return ;
- });
+ return ;
+};
diff --git a/frontend/three_d_garden/garden/weed.tsx b/frontend/three_d_garden/garden/weed.tsx
index 17e1f9882f..2401cd9f8c 100644
--- a/frontend/three_d_garden/garden/weed.tsx
+++ b/frontend/three_d_garden/garden/weed.tsx
@@ -4,7 +4,7 @@ import { Config } from "../config";
import { ASSETS } from "../constants";
import { Group, MeshPhongMaterial } from "../components";
import { Image, Billboard, Sphere } from "@react-three/drei";
-import { DoubleSide } from "three";
+import { DoubleSide, Vector3 } from "three";
import { zero as zeroFunc, threeSpace } from "../helpers";
import { useNavigate } from "react-router";
import { Path } from "../../internal_urls";
@@ -20,34 +20,55 @@ export interface WeedProps {
export const Weed = (props: WeedProps) => {
const { weed, config } = props;
const navigate = useNavigate();
- return {
if (weed.body.id && !isUndefined(props.dispatch)) {
props.dispatch(setPanelOpen(true));
navigate(Path.weeds(weed.body.id));
}
}}
- position={[
+ position={new Vector3(
threeSpace(weed.body.x, config.bedLengthOuter) + config.bedXOffset,
threeSpace(weed.body.y, config.bedWidthOuter) + config.bedYOffset,
zeroFunc(config).z - config.soilHeight,
- ]}>
+ )}
+ color={weed.body.meta.color}
+ radius={weed.body.radius} />;
+};
+
+interface WeedBaseProps {
+ pointName: string;
+ position?: Vector3;
+ onClick?: () => void;
+ color: string | undefined;
+ radius: number;
+ alpha: number;
+}
+
+export const WeedBase = (props: WeedBaseProps) => {
+ return
+ position={[0, 0, props.radius / 2]}>
+ opacity={0.5 * props.alpha} />
;
};
diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx
index 20d0d5307f..4860c7ae4c 100644
--- a/frontend/three_d_garden/garden_model.tsx
+++ b/frontend/three_d_garden/garden_model.tsx
@@ -8,11 +8,12 @@ import {
} from "@react-three/drei";
import { BackSide } from "three";
import { Bot } from "./bot";
-import { AddPlantProps, Bed } from "./bed";
+import { AddPlantProps, Bed, DRAW_POINT_MODES } from "./bed";
import {
Sky, Solar, Sun, sunPosition, ZoomBeacons,
calculatePlantPositions, convertPlants, ThreeDPlant,
Point, Grid, Clouds, Ground, Weed,
+ DrawnPoint,
} from "./garden";
import { Config } from "./config";
import { useSpring, animated } from "@react-spring/three";
@@ -27,6 +28,7 @@ import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot";
import { BooleanSetting } from "../session_keys";
import { SlotWithTool } from "../resources/interfaces";
import { cameraInit } from "./camera";
+import { getMode } from "../farm_designer/map/util";
const AnimatedGroup = animated(Group);
@@ -41,14 +43,15 @@ export interface GardenModelProps {
mountedToolName?: string | undefined;
}
+// eslint-disable-next-line complexity
export const GardenModel = (props: GardenModelProps) => {
- const { config } = props;
- const dispatch = props.addPlantProps?.dispatch;
+ const { config, addPlantProps } = props;
+ const dispatch = addPlantProps?.dispatch;
const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera;
- const plants = isUndefined(props.addPlantProps)
+ const plants = isUndefined(addPlantProps)
? calculatePlantPositions(config)
- : convertPlants(config, props.addPlantProps.plants);
+ : convertPlants(config, addPlantProps.plants);
const [hoveredPlant, setHoveredPlant] =
React.useState(undefined);
@@ -76,6 +79,14 @@ export const GardenModel = (props: GardenModelProps) => {
const camera = getCamera(config, props.activeFocus, cameraInit());
+ const showPlants = !addPlantProps
+ || !!addPlantProps.getConfigValue(BooleanSetting.show_plants);
+ const plantsVisible = props.activeFocus != "Planter bed" && showPlants;
+ const showFarmbot = !addPlantProps
+ || !!addPlantProps.getConfigValue(BooleanSetting.show_farmbot);
+ const showPoints = !!addPlantProps?.getConfigValue(BooleanSetting.show_points);
+ const showWeeds = !!addPlantProps?.getConfigValue(BooleanSetting.show_weeds);
+
// eslint-disable-next-line no-null/no-null
return {
config={config}
activeFocus={props.activeFocus}
setActiveFocus={props.setActiveFocus} />}
-
+
@@ -117,9 +123,8 @@ export const GardenModel = (props: GardenModelProps) => {
- {(!props.addPlantProps
- || !!props.addPlantProps.getConfigValue(BooleanSetting.show_farmbot)) &&
+ addPlantProps={addPlantProps} />
+ {showFarmbot &&
{
{plants.map((plant, i) =>
)}
+ visible={showPoints}>
{props.mapPoints?.map(point =>
)}
+ {addPlantProps && DRAW_POINT_MODES.includes(getMode()) &&
+ }
+ visible={showWeeds}>
{props.weeds?.map(weed =>
Date: Tue, 11 Mar 2025 10:40:59 -0700
Subject: [PATCH 2/7] upgrade deps
---
Gemfile.lock | 10 +++++-----
package.json | 14 +++++++-------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 5a7745dc89..a9bd18e878 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -141,12 +141,12 @@ GEM
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.50.0)
google-apis-core (>= 0.15.0, < 2.a)
- google-cloud-core (1.7.1)
+ google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a)
- google-cloud-errors (1.4.0)
+ google-cloud-errors (1.5.0)
google-cloud-storage (1.55.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
@@ -248,7 +248,7 @@ GEM
hashie (~> 4.1)
multi_json (~> 1.15)
racc (1.8.1)
- rack (2.2.11)
+ rack (2.2.13)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
@@ -376,7 +376,7 @@ GEM
rails
warden (1.2.9)
rack (>= 2.0.9)
- webmock (3.25.0)
+ webmock (3.25.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -439,4 +439,4 @@ RUBY VERSION
ruby 3.3.7p123
BUNDLED WITH
- 2.6.2
+ 2.6.5
diff --git a/package.json b/package.json
index 88f9a498e8..eaff4167ce 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,8 @@
"@parcel/watcher": "2.1.0"
},
"dependencies": {
- "@blueprintjs/core": "5.17.3",
- "@blueprintjs/select": "5.3.15",
+ "@blueprintjs/core": "5.17.5",
+ "@blueprintjs/select": "5.3.17",
"@monaco-editor/react": "4.7.0",
"@parcel/transformer-sass": "2.13.3",
"@parcel/transformer-typescript-tsc": "2.13.3",
@@ -48,15 +48,15 @@
"@rollbar/react": "0.12.1",
"@types/lodash": "4.17.16",
"@types/markdown-it": "14.1.2",
- "@types/node": "22.13.9",
+ "@types/node": "22.13.10",
"@types/promise-timeout": "1.3.3",
"@types/react": "19.0.10",
"@types/react-color": "3.0.13",
"@types/react-dom": "19.0.4",
"@types/three": "0.174.0",
- "@types/ws": "8.5.14",
+ "@types/ws": "8.18.0",
"@xterm/xterm": "5.5.0",
- "axios": "1.8.1",
+ "axios": "1.8.2",
"bowser": "2.11.0",
"browser-speech": "1.1.1",
"events": "3.3.0",
@@ -68,7 +68,7 @@
"moment": "2.30.1",
"monaco-editor": "0.52.2",
"mqtt": "5.10.4",
- "npm": "11.1.0",
+ "npm": "11.2.0",
"parcel": "2.13.3",
"process": "0.11.10",
"promise-timeout": "1.3.0",
@@ -78,7 +78,7 @@
"react-color": "2.19.3",
"react-dom": "18.3.1",
"react-redux": "9.2.0",
- "react-router": "7.2.0",
+ "react-router": "7.3.0",
"redux": "5.0.1",
"redux-immutable-state-invariant": "2.1.0",
"redux-thunk": "3.1.0",
From 752e79604eb3bcf3fd2f6b0ed1f4222316fb722a Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Tue, 11 Mar 2025 13:05:21 -0700
Subject: [PATCH 3/7] adjust test worker count to 3 on local (keep 6 on ci)
---
.circleci/config.yml | 2 +-
package.json | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 68ce3361d0..a719f31e0b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -69,7 +69,7 @@ commands:
- run:
name: Run JS tests
command: |
- sudo docker compose run web npm run test-slow -- -c .circleci/jest-ci.config.js
+ sudo docker compose run web npm run test-slow -- -c .circleci/jest-ci.config.js -w 6
echo 'export COVERAGE_AVAILABLE=true' >> $BASH_ENV
lint-commands:
steps:
diff --git a/package.json b/package.json
index eaff4167ce..bf201559a1 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,8 @@
},
"scripts": {
"test-very-slow": "node --expose-gc ./node_modules/.bin/jest -i --colors --coverage",
- "test-slow": "./node_modules/.bin/jest -w 6 --colors",
- "test": "./node_modules/.bin/jest -w 5 --no-coverage",
+ "test-slow": "./node_modules/.bin/jest -w 3 --colors",
+ "test": "./node_modules/.bin/jest -w 3 --no-coverage",
"graph-modules-dot": "./node_modules/.bin/madge --dot ./frontend > module_graph.dot",
"graph-modules-svg": "dot -Tsvg module_graph.dot -o module_graph.svg",
"typecheck": "./node_modules/typescript/bin/tsc --noEmit",
From 2cd2432a9d7275fcde2862b9feb3d944853fd338 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Fri, 14 Mar 2025 16:23:44 -0700
Subject: [PATCH 4/7] rework point and weed creation interactions
---
.../__test_support__/fake_designer_state.ts | 13 +-
frontend/__tests__/hotkeys_test.tsx | 51 +++-
frontend/app.tsx | 4 +-
frontend/constants.ts | 1 -
.../farm_designer/farm_designer_panels.scss | 14 +-
.../__tests__/location_info_test.tsx | 5 +-
.../farm_designer/__tests__/reducer_test.ts | 71 +----
frontend/farm_designer/interfaces.ts | 20 +-
frontend/farm_designer/location_info.tsx | 18 +-
.../map/__tests__/garden_map_test.tsx | 73 ++---
.../__tests__/drawn_point_actions_test.tsx | 33 +--
.../__tests__/drawn_point_test.tsx | 14 +-
.../drawn_point/__tests__/drawn_weed_test.tsx | 25 +-
.../map/drawn_point/drawn_point.tsx | 7 +-
.../map/drawn_point/drawn_point_actions.tsx | 25 +-
.../map/drawn_point/drawn_weed.tsx | 12 +-
frontend/farm_designer/map/garden_map.tsx | 34 +--
.../layers/spread/spread_overlap_helper.tsx | 8 +-
frontend/farm_designer/map/util.ts | 14 +
frontend/farm_designer/reducer.ts | 15 -
frontend/hotkeys.tsx | 33 ++-
.../plants/grid/__tests__/plant_grid_test.tsx | 11 +
frontend/plants/grid/interfaces.ts | 2 +
frontend/plants/grid/plant_grid.tsx | 87 +++---
.../points/__tests__/create_points_test.tsx | 244 ++++++++--------
frontend/points/create_points.tsx | 270 +++++++-----------
.../__tests__/garden_model_test.tsx | 3 +-
.../three_d_garden/bed/__tests__/bed_test.tsx | 172 +++++++++--
frontend/three_d_garden/bed/bed.tsx | 140 ++++++---
.../three_d_garden/bot/components/tools.tsx | 6 +-
frontend/three_d_garden/constants.ts | 11 +
.../garden/__tests__/point_test.tsx | 55 ++--
frontend/three_d_garden/garden/plants.tsx | 6 +-
frontend/three_d_garden/garden/point.tsx | 69 ++---
frontend/three_d_garden/garden/weed.tsx | 38 ++-
frontend/three_d_garden/garden_model.tsx | 3 +-
36 files changed, 890 insertions(+), 717 deletions(-)
diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts
index d4953a1dbf..1ffae18e72 100644
--- a/frontend/__test_support__/fake_designer_state.ts
+++ b/frontend/__test_support__/fake_designer_state.ts
@@ -1,4 +1,4 @@
-import { DesignerState } from "../farm_designer/interfaces";
+import { DesignerState, DrawnPointPayl } from "../farm_designer/interfaces";
import { HelpState } from "../help/reducer";
import { RunButtonMenuOpen } from "../sequences/interfaces";
@@ -20,7 +20,6 @@ export const fakeDesignerState = (): DesignerState => ({
bulkPlantSlug: undefined,
chosenLocation: { x: undefined, y: undefined, z: undefined },
drawnPoint: undefined,
- drawnWeed: undefined,
openedSavedGarden: undefined,
tryGroupSortType: undefined,
editGroupAreaInMap: false,
@@ -63,3 +62,13 @@ export const fakeMenuOpenState = (): RunButtonMenuOpen => ({
component: undefined,
uuid: undefined,
});
+
+export const fakeDrawnPoint = (): DrawnPointPayl => ({
+ name: "Fake Point",
+ cx: 10,
+ cy: 20,
+ r: 30,
+ color: "green",
+ z: 0,
+ at_soil_level: false,
+});
diff --git a/frontend/__tests__/hotkeys_test.tsx b/frontend/__tests__/hotkeys_test.tsx
index 18b81c9ab3..1794c6f00d 100644
--- a/frontend/__tests__/hotkeys_test.tsx
+++ b/frontend/__tests__/hotkeys_test.tsx
@@ -12,58 +12,80 @@ jest.mock("../api/crud", () => ({ save: jest.fn() }));
import React from "react";
import { shallow } from "enzyme";
import {
- HotKey, HotKeys, HotKeysProps, hotkeysWithActions, toggleHotkeyHelpOverlay,
+ HotKey, HotKeys, HotKeysProps, hotkeysWithActions, HotkeysWithActionsProps,
+ toggleHotkeyHelpOverlay,
} from "../hotkeys";
import { sync } from "../devices/actions";
import { save } from "../api/crud";
import { Actions } from "../constants";
import { Path } from "../internal_urls";
import { mockDispatch } from "../__test_support__/fake_dispatch";
+import {
+ fakeDesignerState, fakeDrawnPoint,
+} from "../__test_support__/fake_designer_state";
+import { resetDrawnPointDataAction } from "../points/create_points";
describe("hotkeysWithActions()", () => {
beforeEach(() => {
location.pathname = Path.mock(Path.designer());
});
+ const fakeProps = (): HotkeysWithActionsProps => ({
+ navigate: jest.fn(),
+ dispatch: jest.fn(),
+ designer: fakeDesignerState(),
+ slug: "",
+ });
+
it("has key bindings", () => {
- const dispatch = jest.fn();
- const navigate = jest.fn();
- const hotkeys = hotkeysWithActions(navigate, dispatch, "");
+ const p = fakeProps();
+ const hotkeys = hotkeysWithActions(p);
expect(Object.values(hotkeys).length).toBe(8);
const e = {} as KeyboardEvent;
hotkeys[HotKey.save].onKeyDown?.(e);
expect(save).not.toHaveBeenCalled();
mockState.resources.consumers.sequences.current = "uuid";
- const hotkeysSettingsPath = hotkeysWithActions(navigate, dispatch, "settings");
+ p.slug = "settings";
+ const hotkeysSettingsPath = hotkeysWithActions(p);
hotkeysSettingsPath[HotKey.save].onKeyDown?.(e);
expect(save).not.toHaveBeenCalled();
- const hotkeysSequencesPath = hotkeysWithActions(
- navigate, dispatch, "sequences");
+ p.slug = "sequences";
+ const hotkeysSequencesPath = hotkeysWithActions(p);
hotkeysSequencesPath[HotKey.save].onKeyDown?.(e);
expect(save).toHaveBeenCalledWith("uuid");
hotkeys[HotKey.sync].onKeyDown?.(e);
- expect(dispatch).toHaveBeenCalledWith(sync());
+ expect(p.dispatch).toHaveBeenCalledWith(sync());
hotkeys[HotKey.navigateRight].onKeyDown?.(e);
- expect(navigate).toHaveBeenCalledWith(Path.plants());
+ expect(p.navigate).toHaveBeenCalledWith(Path.plants());
hotkeys[HotKey.navigateLeft].onKeyDown?.(e);
- expect(navigate).toHaveBeenCalledWith(Path.settings());
+ expect(p.navigate).toHaveBeenCalledWith(Path.settings());
hotkeys[HotKey.addPlant].onKeyDown?.(e);
- expect(navigate).toHaveBeenCalledWith(Path.cropSearch());
+ expect(p.navigate).toHaveBeenCalledWith(Path.cropSearch());
hotkeys[HotKey.addEvent].onKeyDown?.(e);
- expect(navigate).toHaveBeenCalledWith(Path.farmEvents("add"));
+ expect(p.navigate).toHaveBeenCalledWith(Path.farmEvents("add"));
- const hotkeysWithDispatch =
- hotkeysWithActions(navigate, mockDispatch(dispatch), "");
+ p.slug = "";
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ const hotkeysWithDispatch = hotkeysWithActions(p);
hotkeysWithDispatch[HotKey.closePanel].onKeyDown?.(e);
expect(dispatch).toHaveBeenCalledWith({
type: Actions.SET_PANEL_OPEN, payload: false,
});
+
+ p.dispatch = jest.fn();
+ const point = fakeDrawnPoint();
+ point.cx = 1;
+ p.designer.drawnPoint = point;
+ const hotkeysWithDrawnPoint = hotkeysWithActions(p);
+ hotkeysWithDrawnPoint[HotKey.closePanel].onKeyDown?.(e);
+ expect(p.dispatch).toHaveBeenCalledWith(resetDrawnPointDataAction());
});
});
@@ -81,6 +103,7 @@ describe("", () => {
const fakeProps = (): HotKeysProps => ({
dispatch: jest.fn(),
hotkeyGuide: false,
+ designer: fakeDesignerState(),
});
it("renders", () => {
diff --git a/frontend/app.tsx b/frontend/app.tsx
index 3c198dfbe2..a4211f8f45 100644
--- a/frontend/app.tsx
+++ b/frontend/app.tsx
@@ -185,7 +185,9 @@ export class RawApp extends React.Component {
{(Path.equals("") || Path.equals(Path.app())) && isString(landingPage) &&
}
{!syncLoaded && }
-
+
{syncLoaded && ", () => {
wrapper.find(".add-point").simulate("click");
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SET_DRAWN_POINT_DATA,
- payload: { cx: 1, cy: 1 }
+ payload: {
+ name: "Location Point", cx: 1, cy: 1, color: "gray", r: 0, z: 0,
+ at_soil_level: false,
+ },
});
expect(mockNavigate).toHaveBeenCalledWith(Path.points("add"));
});
diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts
index e4bc45b2a9..cdbf437c45 100644
--- a/frontend/farm_designer/__tests__/reducer_test.ts
+++ b/frontend/farm_designer/__tests__/reducer_test.ts
@@ -1,9 +1,11 @@
import { designer } from "../reducer";
import { Actions } from "../../constants";
import { ReduxAction } from "../../redux/interfaces";
-import { HoveredPlantPayl, DrawnPointPayl, DrawnWeedPayl } from "../interfaces";
+import { HoveredPlantPayl, DrawnPointPayl } from "../interfaces";
import { BotPosition } from "../../devices/interfaces";
-import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
+import {
+ fakeDesignerState, fakeDrawnPoint,
+} from "../../__test_support__/fake_designer_state";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { PlantStage, PointType } from "farmbot";
import { UUID } from "../../resources/interfaces";
@@ -200,71 +202,10 @@ describe("designer reducer", () => {
it("sets current point data", () => {
const action: ReduxAction = {
type: Actions.SET_DRAWN_POINT_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30, color: "red" }
+ payload: fakeDrawnPoint(),
};
const newState = designer(oldState(), action);
- expect(newState.drawnPoint).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "red"
- });
- });
-
- it("uses current point color", () => {
- const action: ReduxAction = {
- type: Actions.SET_DRAWN_POINT_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30 }
- };
- const state = oldState();
- state.drawnPoint = { cx: 0, cy: 0, z: 0, r: 0, color: "red" };
- const newState = designer(state, action);
- expect(newState.drawnPoint).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "red"
- });
- });
-
- it("uses default point color", () => {
- const action: ReduxAction = {
- type: Actions.SET_DRAWN_POINT_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30 }
- };
- const newState = designer(oldState(), action);
- expect(newState.drawnPoint).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "green"
- });
- });
-
- it("sets current weed data", () => {
- const action: ReduxAction = {
- type: Actions.SET_DRAWN_WEED_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30, color: "red" }
- };
- const newState = designer(oldState(), action);
- expect(newState.drawnWeed).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "red"
- });
- });
-
- it("uses current weed color", () => {
- const action: ReduxAction = {
- type: Actions.SET_DRAWN_WEED_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30 }
- };
- const state = oldState();
- state.drawnWeed = { cx: 0, cy: 0, z: 0, r: 0, color: "red" };
- const newState = designer(state, action);
- expect(newState.drawnWeed).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "red"
- });
- });
-
- it("uses default weed color", () => {
- const action: ReduxAction = {
- type: Actions.SET_DRAWN_WEED_DATA,
- payload: { cx: 10, cy: 20, z: 0, r: 30 }
- };
- const newState = designer(oldState(), action);
- expect(newState.drawnWeed).toEqual({
- cx: 10, cy: 20, z: 0, r: 30, color: "red"
- });
+ expect(newState.drawnPoint).toEqual(fakeDrawnPoint());
});
it("sets opened saved garden", () => {
diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts
index 433bf5aebc..dac57756b1 100644
--- a/frontend/farm_designer/interfaces.ts
+++ b/frontend/farm_designer/interfaces.ts
@@ -146,7 +146,6 @@ export interface DesignerState {
bulkPlantSlug: string | undefined;
chosenLocation: BotPosition;
drawnPoint: DrawnPointPayl | undefined;
- drawnWeed: DrawnWeedPayl | undefined;
openedSavedGarden: number | undefined;
tryGroupSortType: ExtendedPointGroupSortType | undefined;
editGroupAreaInMap: boolean;
@@ -359,20 +358,11 @@ export interface CameraCalibrationData {
}
export interface DrawnPointPayl {
- name?: string;
- cx: number;
- cy: number;
+ name: string;
+ cx: number | undefined;
+ cy: number | undefined;
z: number;
r: number;
- color?: string;
- at_soil_level?: boolean;
-}
-
-export interface DrawnWeedPayl {
- name?: string;
- cx: number;
- cy: number;
- z: number;
- r: number;
- color?: string;
+ color: string;
+ at_soil_level: boolean;
}
diff --git a/frontend/farm_designer/location_info.tsx b/frontend/farm_designer/location_info.tsx
index f0e1534124..b550716af9 100644
--- a/frontend/farm_designer/location_info.tsx
+++ b/frontend/farm_designer/location_info.tsx
@@ -45,6 +45,7 @@ import { ImageFlipper } from "../photos/images/image_flipper";
import { PhotoFooter } from "../photos/images/photos";
import { Path } from "../internal_urls";
import { NavigationContext } from "../routes_helpers";
+import { DrawnPointPayl } from "./interfaces";
export const mapStateToProps = (props: Everything): LocationInfoProps => ({
chosenLocation: props.resources.consumers.farm_designer.chosenLocation,
@@ -482,13 +483,16 @@ const LocationActions = (props: LocationActionsProps) => {
{props.currentBotLocation.z})
}