From 66f5fd4b2567ba2ea03fdd7b5975247d0b4d8102 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Thu, 16 Apr 2026 15:03:21 -0700
Subject: [PATCH 01/11] add ruby-check to agents file
---
spec/AGENTS.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/spec/AGENTS.md b/spec/AGENTS.md
index ab556ccaae..ee1f362fcf 100644
--- a/spec/AGENTS.md
+++ b/spec/AGENTS.md
@@ -5,6 +5,10 @@ For changes to files in the `app/` or `spec/` directories.
- Do not terminate test commands before they are completed.
### For the files you change
+- Make sure all checks and linters pass:
+ ```
+ bun run ruby-check
+ ```
- Run tests via `rspec FILES`
where `FILES` is a space-separated list of spec files for the app files you changed.
For example, `rspec spec/file_0_spec.rb spec/file_1_spec.rb`.
From 42bb2d410aea6058a75ebdd7613f86a3cc075e45 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Thu, 16 Apr 2026 15:04:19 -0700
Subject: [PATCH 02/11] avoid long test failure output
---
frontend/__test_support__/setup_tests.ts | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/frontend/__test_support__/setup_tests.ts b/frontend/__test_support__/setup_tests.ts
index 15dd9b291b..43f87b4568 100644
--- a/frontend/__test_support__/setup_tests.ts
+++ b/frontend/__test_support__/setup_tests.ts
@@ -1,2 +1,17 @@
import "@testing-library/jest-dom";
import "./customMatchers";
+
+expect.extend({
+ toContainHTML(received: Element | { innerHTML?: string }, expected: string) {
+ const actual = received?.innerHTML ?? "";
+ const pass = actual.includes(expected);
+
+ return {
+ pass,
+ message: () =>
+ `expected html to${pass ? " not" : ""} contain ` +
+ `${this.utils.printExpected(expected)}\n` +
+ `received: ${this.utils.printReceived(actual)}`,
+ };
+ },
+});
From 73dc542e47c68ff6f3e0e8fab24da35cae77dcb8 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Thu, 16 Apr 2026 15:04:40 -0700
Subject: [PATCH 03/11] add 3D mirroring option
---
.../__tests__/three_d_garden_map_test.tsx | 12 +-
frontend/farm_designer/three_d_garden_map.tsx | 24 +-
.../three_d_garden/__tests__/helpers_test.ts | 45 ++
.../three_d_garden/bed/__tests__/bed_test.tsx | 40 +-
frontend/three_d_garden/bed/bed.tsx | 47 +-
.../__tests__/pointer_objects_test.tsx | 36 +
.../bed/objects/pointer_objects.tsx | 10 +-
frontend/three_d_garden/bot/bot.tsx | 145 ++--
.../bot/components/__tests__/tools_test.tsx | 66 ++
.../three_d_garden/bot/components/bounds.tsx | 36 +-
.../bot/components/cable_carriers.tsx | 79 +--
.../bot/components/camera_view.tsx | 26 +-
.../bot/components/electronics_box.tsx | 13 +-
.../bot/components/gantry_beam.tsx | 13 +-
.../bot/components/solenoid.tsx | 45 +-
.../three_d_garden/bot/components/tools.tsx | 76 ++-
.../bot/components/watering_animations.tsx | 11 +-
frontend/three_d_garden/config.ts | 8 +-
frontend/three_d_garden/config_overlays.tsx | 2 +
.../garden/__tests__/images_test.tsx | 40 +-
.../garden/__tests__/plant_instances_test.tsx | 43 +-
.../garden/__tests__/plants_test.tsx | 10 +
.../garden/__tests__/point_test.tsx | 12 +
.../garden/__tests__/weed_test.tsx | 12 +
frontend/three_d_garden/garden/grid.tsx | 31 +-
frontend/three_d_garden/garden/images.tsx | 63 +-
.../garden/moisture_texture.tsx | 1 +
.../three_d_garden/garden/plant_instances.tsx | 10 +-
frontend/three_d_garden/garden/plants.tsx | 32 +-
frontend/three_d_garden/garden/point.tsx | 9 +-
frontend/three_d_garden/garden/weed.tsx | 9 +-
.../three_d_garden/group_order_visual.tsx | 20 +-
frontend/three_d_garden/helpers.ts | 29 +-
frontend/three_d_garden/triangles.ts | 2 +-
frontend/three_d_garden/visualization.tsx | 24 +-
.../three_d_garden/zoom_beacons_constants.tsx | 627 +++++++++---------
36 files changed, 1103 insertions(+), 605 deletions(-)
diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
index d8d0d0fe51..67f8c99d1b 100644
--- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
+++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
@@ -146,8 +146,8 @@ describe(" ", () => {
seed: 0,
size: 50,
spread: 30,
- x: 101,
- y: 201,
+ x: 100,
+ y: 200,
}],
addPlantProps: expect.any(Object),
...EMPTY_PROPS,
@@ -336,8 +336,8 @@ describe("convertPlants()", () => {
seed: 0,
size: 50,
spread: 20,
- x: 110,
- y: 201,
+ x: 100,
+ y: 200,
},
{
icon: CROPS["generic-plant"].icon,
@@ -347,8 +347,8 @@ describe("convertPlants()", () => {
seed: 0,
size: 50,
spread: 0,
- x: 1010,
- y: 2001,
+ x: 1000,
+ y: 2000,
},
]);
});
diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx
index 60950c908a..6fade06eda 100644
--- a/frontend/farm_designer/three_d_garden_map.tsx
+++ b/frontend/farm_designer/three_d_garden_map.tsx
@@ -75,11 +75,25 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => {
config.negativeZ = props.negativeZ;
config.exaggeratedZ = props.designer.threeDExaggeratedZ;
+ const quadrant = props.mapTransformProps.quadrant;
+ config.mirrorX = props.mapTransformProps.xySwap
+ ? [3, 4].includes(quadrant)
+ : [1, 4].includes(quadrant);
+ config.mirrorY = props.mapTransformProps.xySwap
+ ? [1, 4].includes(quadrant)
+ : [3, 4].includes(quadrant);
+
+ const getValue = props.get3DConfigValue;
+ config.bedXOffset = getValue("bedXOffset");
+ config.bedYOffset = getValue("bedYOffset");
+ config.bedZOffset = getValue("bedZOffset");
const position = clone(INITIAL_POSITION);
position.x = props.botPosition.x || 0;
position.y = props.botPosition.y || 0;
position.z = props.botPosition.z || 0;
+ if (config.mirrorY) { position.y = gridSize.y - position.y; }
+ if (config.mirrorX) { position.x = gridSize.x - position.x; }
const { designer } = props;
config.distanceIndicator = designer.distanceIndicator;
@@ -89,16 +103,12 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => {
config.zGantryOffset = fbosConfig("gantry_height");
config.soilHeight = Math.abs(fbosConfig("soil_height"));
- const getValue = props.get3DConfigValue;
config.bedWallThickness = getValue("bedWallThickness");
config.bedHeight = getValue("bedHeight");
config.ccSupportSize = getValue("ccSupportSize");
config.beamLength = getValue("beamLength");
config.columnLength = getValue("columnLength");
config.zAxisLength = getValue("zAxisLength");
- config.bedXOffset = getValue("bedXOffset");
- config.bedYOffset = getValue("bedYOffset");
- config.bedZOffset = getValue("bedZOffset");
config.legSize = getValue("legSize");
config.legsFlush = !!getValue("legsFlush");
config.extraLegsX = getValue("extraLegsX");
@@ -214,15 +224,15 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => {
};
export const convertPlants =
- (config: Config, plants: TaggedPlant[]): ThreeDGardenPlant[] =>
+ (_config: Config, plants: TaggedPlant[]): ThreeDGardenPlant[] =>
plants.map(plant => ({
id: plant.body.id,
label: plant.body.name,
icon: findIcon(plant.body.openfarm_slug),
size: plant.body.radius * 2,
spread: findCrop(plant.body.openfarm_slug).spread,
- x: plant.body.x + config.bedXOffset,
- y: plant.body.y + config.bedYOffset,
+ x: plant.body.x,
+ y: plant.body.y,
key: "",
seed: 0,
}));
diff --git a/frontend/three_d_garden/__tests__/helpers_test.ts b/frontend/three_d_garden/__tests__/helpers_test.ts
index cb3cbbbd3f..211fd3fc2e 100644
--- a/frontend/three_d_garden/__tests__/helpers_test.ts
+++ b/frontend/three_d_garden/__tests__/helpers_test.ts
@@ -2,6 +2,9 @@ import { clone } from "lodash";
import {
easyCubicBezierCurve3,
getColorFromBrightness,
+ get3DPositionFunc,
+ getGardenPositionFunc,
+ getWorldPositionFunc,
threeSpace,
zDir,
zZero,
@@ -53,3 +56,45 @@ describe("easyCubicBezierCurve3()", () => {
expect(result).toEqual(expected);
});
});
+
+describe("mirror-aware position helpers", () => {
+ const fakeConfig = () => {
+ const config = clone(INITIAL);
+ config.botSizeX = 1000;
+ config.botSizeY = 500;
+ config.bedLengthOuter = 1200;
+ config.bedWidthOuter = 700;
+ config.bedXOffset = 100;
+ config.bedYOffset = 50;
+ return config;
+ };
+
+ it("round trips garden coordinates without mirroring", () => {
+ const config = fakeConfig();
+ const get3DPosition = get3DPositionFunc(config);
+ const getGardenPosition = getGardenPositionFunc(config, false);
+ const world = get3DPosition({ x: 125, y: 250 });
+ expect(getGardenPosition(world)).toEqual({ x: 125, y: 250 });
+ });
+
+ it("round trips garden coordinates with x and y mirroring", () => {
+ const config = fakeConfig();
+ config.mirrorX = true;
+ config.mirrorY = true;
+ const get3DPosition = get3DPositionFunc(config);
+ const getGardenPosition = getGardenPositionFunc(config, false);
+ const world = get3DPosition({ x: 125, y: 250 });
+ expect(world).toEqual({ x: 375, y: 50 });
+ expect(getGardenPosition(world)).toEqual({ x: 125, y: 250 });
+ });
+
+ it("returns world position with mirrored x and y", () => {
+ const config = fakeConfig();
+ config.columnLength = 200;
+ config.zGantryOffset = 100;
+ config.mirrorX = true;
+ config.mirrorY = true;
+ const getWorldPosition = getWorldPositionFunc(config);
+ expect(getWorldPosition({ x: 125, y: 250, z: 10 })).toEqual([375, 50, 150]);
+ });
+});
diff --git a/frontend/three_d_garden/bed/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
index 4b037a0ea6..98a709e9d3 100644
--- a/frontend/three_d_garden/bed/__tests__/bed_test.tsx
+++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
@@ -77,7 +77,7 @@ import { fakeDrawnPoint } from "../../../__test_support__/fake_designer_state";
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { SpecialStatus } from "farmbot";
-import { BufferGeometry } from "three";
+import { BufferGeometry, Float32BufferAttribute } from "three";
import { ActivePositionRef } from "../objects/pointer_objects";
import { Mode } from "../../../farm_designer/map/interfaces";
import * as mapUtil from "../../../farm_designer/map/util";
@@ -443,4 +443,42 @@ describe(" ", () => {
expect(mockSetBillboardPosition).not.toHaveBeenCalled();
expect(mockSetImageScale).not.toHaveBeenCalled();
});
+
+ it("mirrors the rendered soil surface geometry", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.bedLengthOuter = 1000;
+ p.config.bedWidthOuter = 800;
+ p.config.bedWallThickness = 100;
+ p.config.bedXOffset = 50;
+ p.config.bedYOffset = 25;
+ const geometry = new BufferGeometry();
+ geometry.setAttribute("position", new Float32BufferAttribute([
+ 150, 200, 10,
+ 300, 400, 20,
+ ], 3));
+ geometry.setAttribute("normal", new Float32BufferAttribute([
+ 1, 2, 3,
+ 4, 5, 6,
+ ], 3));
+ p.soilSurfaceGeometry = geometry;
+ const cloneSpy = jest.spyOn(geometry, "clone");
+
+ render( );
+
+ const mirroredGeometry = cloneSpy.mock.results[0]?.value as BufferGeometry;
+ const position = mirroredGeometry.getAttribute("position");
+ const normal = mirroredGeometry.getAttribute("normal");
+ expect(position.getX(0)).toEqual(750);
+ expect(position.getY(0)).toEqual(550);
+ expect(position.getX(1)).toEqual(600);
+ expect(position.getY(1)).toEqual(350);
+ expect(normal.getX(0)).toEqual(-1);
+ expect(normal.getY(0)).toEqual(-2);
+ expect(normal.getZ(0)).toEqual(3);
+ expect(normal.getX(1)).toEqual(-4);
+ expect(normal.getY(1)).toEqual(-5);
+ expect(normal.getZ(1)).toEqual(6);
+ });
});
diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx
index 5ba8611a2d..768e912201 100644
--- a/frontend/three_d_garden/bed/bed.tsx
+++ b/frontend/three_d_garden/bed/bed.tsx
@@ -10,6 +10,7 @@ import {
BufferGeometry,
Mesh as MeshType,
BackSide,
+ FrontSide,
Color,
} from "three";
import { range } from "lodash";
@@ -44,6 +45,7 @@ import { ImageTexture } from "../garden";
import { VertexNormalsHelper } from "three/examples/jsm/Addons.js";
import { MoistureSurface } from "../garden/moisture_texture";
import { HeightMaterial } from "../garden/height_material";
+import { soilSurfaceExtents } from "../triangles";
const soil = (
Type: typeof LinePath | typeof Shape,
@@ -89,7 +91,11 @@ const Surface = (props: SurfaceProps) => {
// eslint-disable-next-line no-null/no-null
const ref = React.useRef(null) as React.RefObject;
useHelper(ref, VertexNormalsHelper, 1000);
- return
+ const enableHelper = [
+ SurfaceDebugOption.normals,
+ SurfaceDebugOption.height,
+ ].includes(props.config.surfaceDebug);
+ return
{props.children}
;
};
@@ -279,6 +285,41 @@ export const Bed = (props: BedProps) => {
const SurfaceMaterial = getSurfaceMaterial();
const surfaceTexture = soilTexture;
+ const mirroredAxesCount =
+ Number(props.config.mirrorX) + Number(props.config.mirrorY);
+ const soilSurfaceSide = mirroredAxesCount % 2 == 1 ? FrontSide : BackSide;
+ const renderSoilSurfaceGeometry = React.useMemo(() => {
+ if (!props.config.mirrorX && !props.config.mirrorY) {
+ return props.soilSurfaceGeometry;
+ }
+ const geometry = props.soilSurfaceGeometry.clone();
+ const position = geometry.getAttribute("position");
+ const normal = geometry.getAttribute("normal");
+ const extents = soilSurfaceExtents(props.config);
+ const xMid = (extents.x.min + extents.x.max) / 2;
+ const yMid = (extents.y.min + extents.y.max) / 2;
+ for (let i = 0; i < position.count; i++) {
+ if (props.config.mirrorX) {
+ position.setX(i, 2 * xMid - position.getX(i));
+ }
+ if (props.config.mirrorY) {
+ position.setY(i, 2 * yMid - position.getY(i));
+ }
+ if (normal) {
+ if (props.config.mirrorX) {
+ normal.setX(i, -normal.getX(i));
+ }
+ if (props.config.mirrorY) {
+ normal.setY(i, -normal.getY(i));
+ }
+ }
+ }
+ position.needsUpdate = true;
+ if (normal) { normal.needsUpdate = true; }
+ geometry.computeBoundingBox();
+ geometry.computeBoundingSphere();
+ return geometry;
+ }, [props.soilSurfaceGeometry, props.config]);
const soilPosition: [number, number, number] = [
threeSpace(0, bedLengthOuter) + bedXOffset,
threeSpace(0, bedWidthOuter) + bedYOffset,
@@ -320,7 +361,7 @@ export const Bed = (props: BedProps) => {
]);
const commonSoilLayerProps = {
config: props.config,
- geometry: props.soilSurfaceGeometry,
+ geometry: renderSoilSurfaceGeometry,
position: soilPosition,
onClick: onSoilClick,
onPointerMove: onSoilPointerMove,
@@ -434,7 +475,7 @@ export const Bed = (props: BedProps) => {
{surfaceTexture}
diff --git a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
index 77821c3204..2ab254637d 100644
--- a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
+++ b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
@@ -113,6 +113,24 @@ describe("soilClick()", () => {
gardenCoords: { x: 1360, y: 660 },
}));
});
+
+ it("creates plant with mirrored garden coordinates", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ mockIsMobile = false;
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.botSizeX = 2000;
+ p.config.botSizeY = 1000;
+ const e = {
+ stopPropagation: jest.fn(),
+ point: { x: 1, y: 2 },
+ } as unknown as ThreeEvent;
+ soilClick(p)(e);
+ expect(dropPlantSpy).toHaveBeenCalledWith(expect.objectContaining({
+ gardenCoords: { x: 1360, y: 660 },
+ }));
+ });
});
describe("soilPointerMove()", () => {
@@ -179,6 +197,24 @@ describe("soilPointerMove()", () => {
.toHaveBeenCalledWith(110, 210, 0);
});
+ it("updates plant position with mirrored world coordinates", () => {
+ location.pathname = Path.mock(Path.cropSearch("mint"));
+ mockIsMobile = false;
+ const p = fakeProps();
+ p.config.columnLength = 100;
+ p.config.botSizeX = 1000;
+ p.config.botSizeY = 800;
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ const e = {
+ stopPropagation: jest.fn(),
+ point: { x: 100, y: 200 },
+ } as unknown as ThreeEvent;
+ soilPointerMove(p)(e);
+ expect(p.pointerPlantRef.current?.position.set)
+ .toHaveBeenCalledWith(100, 200, 0);
+ });
+
it("skips re-rendering the same pointer position", () => {
location.pathname = Path.mock(Path.cropSearch("mint"));
mockIsMobile = false;
diff --git a/frontend/three_d_garden/bed/objects/pointer_objects.tsx b/frontend/three_d_garden/bed/objects/pointer_objects.tsx
index 2acefb01d0..ca8a53c77b 100644
--- a/frontend/three_d_garden/bed/objects/pointer_objects.tsx
+++ b/frontend/three_d_garden/bed/objects/pointer_objects.tsx
@@ -19,9 +19,9 @@ import {
import {
zero as zeroFunc,
extents as extentsFunc,
- zZero,
getGardenPositionFunc,
get3DPositionFunc,
+ getWorldPositionFunc,
} from "../../helpers";
import { Config } from "../../config";
import { SpecialStatus, TaggedGenericPointer } from "farmbot";
@@ -158,6 +158,8 @@ export const PointerObjects = (props: PointerObjectsProps) => {
shader.uniforms.uHalfSize = { value: halfSize };
shader.uniforms.uInside = { value: new Color("white") };
shader.uniforms.uOutside = { value: new Color("red") };
+ shader.uniforms.uMirrorX = { value: config.mirrorX ? -1 : 1 };
+ shader.uniforms.uMirrorY = { value: config.mirrorY ? -1 : 1 };
outOfBoundsShaderModification(shader);
}}
depthWrite={false} />
@@ -246,6 +248,7 @@ export const soilPointerMove = (props: SoilPointerMoveProps) =>
} = props;
const getGardenPosition = getGardenPositionFunc(config);
const get3DPosition = get3DPositionFunc(config);
+ const getWorldPosition = getWorldPositionFunc(config);
let frame = 0;
let pendingGardenPosition: ReturnType | undefined;
let lastRenderedPosition: { x: number, y: number } | undefined;
@@ -261,7 +264,10 @@ export const soilPointerMove = (props: SoilPointerMoveProps) =>
|| isMobile()
|| !pointerPlantRef.current) { return; }
const { x, y } = get3DPosition(gardenPosition);
- const z = zZero(config) + props.getZ(gardenPosition.x, gardenPosition.y);
+ const [, , z] = getWorldPosition({
+ ...gardenPosition,
+ z: props.getZ(gardenPosition.x, gardenPosition.y),
+ });
if (lastRenderedPosition?.x === x && lastRenderedPosition.y === y) {
return;
}
diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx
index 8c3d15a9b0..cc5e2fb3bd 100644
--- a/frontend/three_d_garden/bot/bot.tsx
+++ b/frontend/three_d_garden/bot/bot.tsx
@@ -6,7 +6,7 @@ import {
} from "@react-three/drei";
import { DoubleSide, Shape, RepeatWrapping } from "three";
import {
- easyCubicBezierCurve3, threeSpace,
+ easyCubicBezierCurve3, get3DPositionNoMirrorFunc,
zDir as zDirFunc,
zZero as zZeroFunc,
} from "../helpers";
@@ -106,12 +106,19 @@ export interface FarmbotModelProps {
export const Bot = (props: FarmbotModelProps) => {
const config = props.config;
const { botSizeX, botSizeY, botSizeZ, trail, laser,
- bedXOffset, bedYOffset, bedLengthOuter, bedWidthOuter, tracks,
+ bedYOffset, bedWidthOuter, tracks,
columnLength, zAxisLength, zGantryOffset,
} = props.config;
const { x, y, z } = props.configPosition;
const zZero = zZeroFunc(config);
const zDir = zDirFunc(config);
+ const get3DPosition = get3DPositionNoMirrorFunc(config);
+ const gardenXY = (gardenX: number, gardenY: number): [number, number] => {
+ const position = get3DPosition({ x: gardenX, y: gardenY });
+ return [position.x, position.y];
+ };
+ const outerXY = (gardenX: number, outerY: number): [number, number] =>
+ gardenXY(gardenX, outerY - bedYOffset);
const gantryWheelPlate =
useGLTF(ASSETS.models.gantryWheelPlate, LIB_DIR) as unknown as GantryWheelPlateFull;
const GantryWheelPlateComponent = GantryWheelPlate(gantryWheelPlate);
@@ -210,18 +217,10 @@ export const Bot = (props: FarmbotModelProps) => {
const airTubeEndPosition = (kitVersion: string): [number, number, number] => {
switch (kitVersion) {
case "v1.7":
- return [
- threeSpace(x + 80, bedLengthOuter) + bedXOffset,
- threeSpace(y + 100, bedWidthOuter) + bedYOffset,
- zZero - zDir * z + 245,
- ];
+ return [...gardenXY(x + 80, y + 100), zZero - zDir * z + 245];
case "v1.8":
default:
- return [
- threeSpace(x + 35, bedLengthOuter) + bedXOffset,
- threeSpace(y, bedWidthOuter) + bedYOffset,
- zZero - zDir * z + 245,
- ];
+ return [...gardenXY(x + 35, y), zZero - zDir * z + 245];
}
};
@@ -238,24 +237,15 @@ export const Bot = (props: FarmbotModelProps) => {
const vacuumPumpCoverPosition = (kitVersion: string): [number, number, number] => {
switch (kitVersion) {
case "v1.7":
- return [
- threeSpace(x + 12, bedLengthOuter) + bedXOffset,
- threeSpace(y + 55, bedWidthOuter) + bedYOffset,
- zZero - zDir * z + 490,
- ];
+ return [...gardenXY(x + 12, y + 55), zZero - zDir * z + 490];
case "v1.8":
default:
- return [
- threeSpace(x + 2, bedLengthOuter) + bedXOffset,
- threeSpace(y + 110, bedWidthOuter) + bedYOffset,
- zZero + columnLength + 25,
- ];
+ return [...gardenXY(x + 2, y + 110), zZero + columnLength + 25];
}
};
const cameraMountPosition = new THREE.Vector3(
- threeSpace(x + cameraMountOffset.x, bedLengthOuter) + bedXOffset,
- threeSpace(y + cameraMountOffset.y, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x + cameraMountOffset.x, y + cameraMountOffset.y),
zZero - zDir * z - 140 + zGantryOffset + 20,
);
@@ -272,8 +262,7 @@ export const Bot = (props: FarmbotModelProps) => {
{ steps: 1, depth: columnLength, bevelEnabled: false },
]}
position={[
- threeSpace(x - extrusionWidth - 12, bedLengthOuter) + bedXOffset,
- threeSpace(y + bedColumnYOffset, bedWidthOuter),
+ ...outerXY(x - extrusionWidth - 12, y + bedColumnYOffset),
30,
]}
rotation={[0, 0, Math.PI / 2]}>
@@ -284,9 +273,9 @@ export const Bot = (props: FarmbotModelProps) => {
{
{
material={undefined} />
{
@@ -338,10 +327,11 @@ export const Bot = (props: FarmbotModelProps) => {
{ steps: 1, depth: botSizeX + xTrackPadding, bevelEnabled: false },
]}
position={[
- threeSpace(index == 0
- ? botSizeX + xTrackPadding / 2
- : -xTrackPadding / 2, bedLengthOuter) + bedXOffset,
- threeSpace(y + (index == 0 ? 2.5 : 17.5), bedWidthOuter),
+ ...outerXY(
+ index == 0
+ ? botSizeX + xTrackPadding / 2
+ : -xTrackPadding / 2,
+ y + (index == 0 ? 2.5 : 17.5)),
2,
]}
rotation={[
@@ -356,8 +346,7 @@ export const Bot = (props: FarmbotModelProps) => {
{
{
{
})}
{
{
{ steps: 1, depth: zAxisLength, bevelEnabled: false },
]}
position={[
- threeSpace(x, bedLengthOuter) + bedXOffset,
- threeSpace(y + utmRadius, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x, y + utmRadius),
zZero - zDir * z,
]}
rotation={[0, 0, 0]}>
@@ -435,8 +420,7 @@ export const Bot = (props: FarmbotModelProps) => {
{
{
material={undefined} />
{
@@ -478,8 +459,7 @@ export const Bot = (props: FarmbotModelProps) => {
{
@@ -501,8 +480,7 @@ export const Bot = (props: FarmbotModelProps) => {
material-color={"#555"}
args={[4, 4, zAxisLength - 200]}
position={[
- threeSpace(x + 6, bedLengthOuter) + bedXOffset,
- threeSpace(y - 30, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x + 6, y - 30),
zZero - zDir * z + zAxisLength / 2,
]}
rotation={[Math.PI / 2, 0, 0]} />
@@ -512,8 +490,7 @@ export const Bot = (props: FarmbotModelProps) => {
{
{
{
receiveShadow={true}
args={[easyCubicBezierCurve3(
[
- threeSpace(x + 28, bedLengthOuter) + bedXOffset,
- threeSpace(y, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x + 28, y),
zZero - zDir * z + 35,
],
[0, 0, 100],
@@ -598,8 +572,7 @@ export const Bot = (props: FarmbotModelProps) => {
interval={1}>
{
material-color={"red"}
args={[5, 5, distanceToSoil]}
position={[
- threeSpace(x, bedLengthOuter) + bedXOffset,
- threeSpace(y, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x, y),
zZero - zDir * z - distanceToSoil / 2,
]}
rotation={[Math.PI / 2, 0, 0]} />
@@ -632,8 +604,7 @@ export const Bot = (props: FarmbotModelProps) => {
{
{ steps: 1, depth: 6, bevelEnabled: false },
]}
position={[
- threeSpace(x - 14.5, bedLengthOuter) + bedXOffset,
- threeSpace(-100, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x - 14.5, -100),
columnLength + 100,
]}
rotation={[0, -Math.PI / 2, 0]}>
@@ -656,8 +626,7 @@ export const Bot = (props: FarmbotModelProps) => {
", () => {
expect(container).not.toContainHTML("toolbay3");
});
+ it("uses mirrored xy position for tool slots", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.botSizeX = 1000;
+ p.config.botSizeY = 500;
+ const tool = fakeTool();
+ tool.body.name = "soil sensor";
+ tool.body.id = 2;
+ const toolSlot = fakeToolSlot();
+ toolSlot.body.x = 100;
+ toolSlot.body.y = 200;
+ toolSlot.body.id = 1;
+ toolSlot.body.tool_id = tool.body.id;
+ p.toolSlots = [{ toolSlot, tool }];
+ const { container } = render( );
+ expect(container).toContainHTML("position=\"1265,460,391\"");
+ });
+
+ it("flips rendered pullout direction for mirrored axis", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ const tool = fakeTool();
+ tool.body.name = "soil sensor";
+ tool.body.id = 2;
+ const toolSlot = fakeToolSlot();
+ toolSlot.body.id = 1;
+ toolSlot.body.tool_id = tool.body.id;
+ toolSlot.body.pullout_direction = ToolPulloutDirection.POSITIVE_X;
+ p.toolSlots = [{ toolSlot, tool }];
+ const { container } = render( );
+ expect(container).toContainHTML(`rotation="0,0,${Math.PI / 2}"`);
+ });
+
+ it("uses mirrored bot x for gantry-mounted tools when mirrorX is active", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.configPosition.x = p.config.botSizeX - p.configPosition.x;
+ const tool = fakeTool();
+ tool.body.name = "soil sensor";
+ tool.body.id = 2;
+ const toolSlot = fakeToolSlot();
+ toolSlot.body.id = 1;
+ toolSlot.body.tool_id = tool.body.id;
+ toolSlot.body.gantry_mounted = true;
+ p.toolSlots = [{ toolSlot, tool }];
+ const { container } = render( );
+ expect(container).toContainHTML("position=\"1065,-680,391\"");
+ });
+
+ it("doesn't mirror gantry-mounted tool y when mirrorY is active", () => {
+ const p = fakeProps();
+ p.config.mirrorY = true;
+ p.configPosition.y = p.config.botSizeY - p.configPosition.y;
+ const tool = fakeTool();
+ tool.body.name = "soil sensor";
+ tool.body.id = 2;
+ const toolSlot = fakeToolSlot();
+ toolSlot.body.id = 1;
+ toolSlot.body.tool_id = tool.body.id;
+ toolSlot.body.gantry_mounted = true;
+ p.toolSlots = [{ toolSlot, tool }];
+ const { container } = render( );
+ expect(container).toContainHTML("position=\"-1055,-680,391\"");
+ });
+
it("renders vacuum animation when not in toolbay and vacuum", () => {
const p = fakeProps();
p.config.vacuum = true;
diff --git a/frontend/three_d_garden/bot/components/bounds.tsx b/frontend/three_d_garden/bot/components/bounds.tsx
index 64d9262d2d..0521e868a7 100644
--- a/frontend/three_d_garden/bot/components/bounds.tsx
+++ b/frontend/three_d_garden/bot/components/bounds.tsx
@@ -3,6 +3,7 @@ import { Config, PositionConfig } from "../../config";
import { Box, Edges } from "@react-three/drei";
import { Group, MeshBasicMaterial } from "../../components";
import {
+ get3DPositionNoMirrorFunc,
threeSpace,
zero as zeroFunc,
zDir as zDirFunc,
@@ -19,11 +20,12 @@ export const Bounds = (props: BoundsProps) => {
const {
bedLengthOuter, bedWidthOuter,
zAxisLength, columnLength, beamLength, bounds,
- bedXOffset, bedYOffset, botSizeX, botSizeY, botSizeZ,
+ bedYOffset, botSizeX, botSizeY, botSizeZ,
} = props.config;
const { x, y, z } = props.configPosition;
const zDir = zDirFunc(props.config);
const zero = zeroFunc(props.config);
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
return
{
diff --git a/frontend/three_d_garden/bot/components/cable_carriers.tsx b/frontend/three_d_garden/bot/components/cable_carriers.tsx
index 7ee38be500..f7639b5264 100644
--- a/frontend/three_d_garden/bot/components/cable_carriers.tsx
+++ b/frontend/three_d_garden/bot/components/cable_carriers.tsx
@@ -3,7 +3,7 @@ import * as THREE from "three";
import { Extrude, useGLTF } from "@react-three/drei";
import { Shape } from "three";
import {
- threeSpace,
+ get3DPositionNoMirrorFunc,
zDir as zDirFunc,
zZero as zZeroFunc,
} from "../../helpers";
@@ -62,11 +62,15 @@ interface CableCarrierXProps {
export const CableCarrierX = (props: CableCarrierXProps) => {
const {
- bedHeight, cableCarriers, botSizeX, bedLengthOuter, bedXOffset,
- tracks, bedWidthOuter
+ bedHeight, cableCarriers, botSizeX, tracks, bedYOffset,
} = props.config;
const { x } = props.configPosition;
const bedCCSupportHeight = Math.min(150, bedHeight / 2);
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
+ const position = get3DPosition({
+ x: botSizeX / 2,
+ y: (tracks ? 0 : extrusionWidth) - 15 - bedYOffset,
+ });
return {
{ steps: 1, depth: 22, bevelEnabled: false },
]}
position={[
- threeSpace(botSizeX / 2, bedLengthOuter) + bedXOffset,
- threeSpace((tracks ? 0 : extrusionWidth) - 15, bedWidthOuter),
+ position.x,
+ position.y,
-40,
]}
rotation={[-Math.PI / 2, -Math.PI, 0 * Math.PI]}>
@@ -93,10 +97,10 @@ interface CableCarrierYProps {
export const CableCarrierY = (props: CableCarrierYProps) => {
const {
- columnLength, cableCarriers, botSizeY, bedLengthOuter, bedYOffset,
- bedXOffset, bedWidthOuter, kitVersion,
+ columnLength, cableCarriers, botSizeY, kitVersion,
} = props.config;
const { x, y } = props.configPosition;
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
const ccDepth = (kitVersion: string) => {
switch (kitVersion) {
case "v1.7":
@@ -106,17 +110,17 @@ export const CableCarrierY = (props: CableCarrierYProps) => {
return 40;
}
};
+ const getPosition = (): [number, number, number] => {
+ const position = get3DPosition({ x: x - 28, y: 20 });
+ return [position.x, position.y, columnLength + 150];
+ };
return
;
@@ -129,12 +133,13 @@ interface CableCarrierZProps {
export const CableCarrierZ = (props: CableCarrierZProps) => {
const {
- cableCarriers, botSizeZ, zGantryOffset, bedLengthOuter, bedYOffset,
- bedXOffset, bedWidthOuter,
+ cableCarriers, botSizeZ, zGantryOffset,
} = props.config;
const { x, y, z } = props.configPosition;
const zZero = zZeroFunc(props.config);
const zDir = zDirFunc(props.config);
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
+ const position = get3DPosition({ x: x - 41, y: y - 25 });
return {
{ steps: 1, depth: 60, bevelEnabled: false },
]}
position={[
- threeSpace(x - 41, bedLengthOuter) + bedXOffset,
- threeSpace(y - 25, bedWidthOuter) + bedYOffset,
+ position.x,
+ position.y,
zZero - zDir * z + 125,
]}
rotation={[Math.PI / 2, Math.PI, Math.PI / 2]}>
@@ -159,12 +164,12 @@ export interface CableCarrierSupportVerticalProps {
export const CableCarrierSupportVertical =
(props: CableCarrierSupportVerticalProps) => {
const {
- bedLengthOuter, bedYOffset, bedXOffset, bedWidthOuter, zAxisLength,
- kitVersion,
+ zAxisLength, kitVersion,
} = props.config;
const { x, y, z } = props.configPosition;
const zZero = zZeroFunc(props.config);
const zDir = zDirFunc(props.config);
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
const ccSupportVertical =
useGLTF(ASSETS.models.ccSupportVertical, LIB_DIR) as unknown as CCSupportVertical;
const verticalInstances = React.useMemo(() => range((zAxisLength - 350) / 200), [zAxisLength]);
@@ -173,9 +178,10 @@ export const CableCarrierSupportVertical =
if (!verticalRef.current || verticalInstances.length === 0) { return; }
const temp = new THREE.Object3D();
verticalInstances.forEach((i, index) => {
+ const position = get3DPosition({ x: x + 20, y: y + 55 });
temp.position.set(
- threeSpace(x + 20, bedLengthOuter) + bedXOffset,
- threeSpace(y + 55, bedWidthOuter) + bedYOffset,
+ position.x,
+ position.y,
zZero - zDir * z + i * 200 + 125,
);
temp.rotation.set(0, 0, Math.PI / 2);
@@ -185,14 +191,11 @@ export const CableCarrierSupportVertical =
});
verticalRef.current.instanceMatrix.needsUpdate = true;
}, [
- bedLengthOuter,
- bedXOffset,
- bedYOffset,
- bedWidthOuter,
verticalInstances,
x,
y,
z,
+ get3DPosition,
zDir,
zZero,
]);
@@ -211,13 +214,13 @@ export const CableCarrierSupportVertical =
}
;
case "v1.8":
+ const getPosition = (): [number, number, number] => {
+ const position = get3DPosition({ x: x + 20, y: y + 35 });
+ return [position.x, position.y, zZero - zDir * z + 125];
+ };
return
{
@@ -256,10 +259,10 @@ export interface CableCarrierSupportHorizontalProps {
export const CableCarrierSupportHorizontal =
(props: CableCarrierSupportHorizontalProps) => {
const {
- bedLengthOuter, bedYOffset, bedXOffset, bedWidthOuter, botSizeY,
- columnLength, kitVersion,
+ botSizeY, columnLength, kitVersion,
} = props.config;
const { x } = props.configPosition;
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
const ccSupportHorizontal =
useGLTF(ASSETS.models.ccSupportHorizontal, LIB_DIR) as unknown as CCSupportHorizontal;
const horizontalInstances = React.useMemo(() => range((botSizeY - 10) / 300), [botSizeY]);
@@ -269,9 +272,10 @@ export const CableCarrierSupportHorizontal =
if (!horizontalRef.current || horizontalInstances.length === 0) { return; }
const temp = new THREE.Object3D();
horizontalInstances.forEach((i, index) => {
+ const position = get3DPosition({ x: x - 28, y: 50 + i * 300 });
temp.position.set(
- threeSpace(x - 28, bedLengthOuter) + bedXOffset,
- threeSpace(50 + i * 300, bedWidthOuter) + bedYOffset,
+ position.x,
+ position.y,
columnLength + 60,
);
temp.rotation.set(Math.PI / 2, 0, 0);
@@ -281,13 +285,10 @@ export const CableCarrierSupportHorizontal =
});
horizontalRef.current.instanceMatrix.needsUpdate = true;
}, [
- bedLengthOuter,
- bedXOffset,
- bedYOffset,
- bedWidthOuter,
columnLength,
horizontalInstances,
x,
+ get3DPosition,
]);
switch (kitVersion) {
case "v1.7":
@@ -307,8 +308,8 @@ export const CableCarrierSupportHorizontal =
return
{
+export const getCameraViewPoints = (props: CameraViewProps) => {
const { config, distanceToSoil, cameraMountPosition } = props;
const cameraLensPosition = cameraMountPosition.clone()
.add(cameraMountToLensOffset);
@@ -59,9 +59,10 @@ export const CameraView = (props: CameraViewProps) => {
const offset = toV([config.imgOffsetX, config.imgOffsetY, 0]);
const rotation = config.imgRotation + extraRotation(config);
- const rotateTop = (point: V3) => rotatePoint(point, rotation, topCenter);
- const rotateBottom = (point: V3) => rotatePoint(point, rotation, bottomCenter)
- .add(offset);
+ const rotateTop = (point: V3) =>
+ rotatePoint(point, rotation, topCenter);
+ const rotateBottom = (point: V3) =>
+ rotatePoint(point, rotation, bottomCenter).add(offset);
const TUL = [-lensSize, -lensSize, 0];
const TUR = [-lensSize, lensSize, 0];
@@ -75,13 +76,20 @@ export const CameraView = (props: CameraViewProps) => {
const BLR = [xCenter + xEdgeAtSoil, yCenter + yEdgeAtSoil, -distanceToSoil];
const BOTTOM = ([BUL, BUR, BLL, BLR] as V3[]).map(rotateBottom);
- const VERTICES = [
- ...TOP,
- ...BOTTOM,
- ];
+ return {
+ cameraLensPosition,
+ points: [
+ ...TOP,
+ ...BOTTOM,
+ ],
+ };
+};
+export const CameraView = (props: CameraViewProps) => {
+ const { config } = props;
+ const { cameraLensPosition, points } = getCameraViewPoints(props);
return config.cameraView
- ?
+ ?
: <>>;
};
diff --git a/frontend/three_d_garden/bot/components/electronics_box.tsx b/frontend/three_d_garden/bot/components/electronics_box.tsx
index c7752dc66d..600d897443 100644
--- a/frontend/three_d_garden/bot/components/electronics_box.tsx
+++ b/frontend/three_d_garden/bot/components/electronics_box.tsx
@@ -1,7 +1,7 @@
import React from "react";
import * as THREE from "three";
import { Cylinder, useGLTF } from "@react-three/drei";
-import { threeSpace } from "../../helpers";
+import { get3DPositionNoMirrorFunc } from "../../helpers";
import { Config, PositionConfig } from "../../config";
import type { GLTF } from "three-stdlib";
import { ASSETS, ElectronicsBoxMaterial, LIB_DIR, PartName } from "../../constants";
@@ -80,8 +80,13 @@ export interface ElectronicsBoxProps {
}
export const ElectronicsBox = (props: ElectronicsBoxProps) => {
- const { bedXOffset, bedLengthOuter, bedWidthOuter, columnLength } = props.config;
+ const { bedYOffset, columnLength } = props.config;
const { x } = props.configPosition;
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
+ const position = get3DPosition({
+ x: x - 62,
+ y: -20 - bedYOffset,
+ });
const box = useGLTF(ASSETS.models.box, LIB_DIR) as unknown as Box;
const btn = useGLTF(ASSETS.models.btn, LIB_DIR) as unknown as Btn;
@@ -91,8 +96,8 @@ export const ElectronicsBox = (props: ElectronicsBoxProps) => {
return
{
const {
- beamLength, columnLength, bedXOffset, bedLengthOuter, bedWidthOuter,
+ beamLength, columnLength, bedYOffset, bedWidthOuter,
} = props.config;
const { x } = props.configPosition;
+ const get3DPosition = get3DPositionNoMirrorFunc(props.config);
+ const position = get3DPosition({
+ x: x - extrusionWidth - 8,
+ y: (bedWidthOuter + beamLength) / 2 - 50 - bedYOffset,
+ });
return
diff --git a/frontend/three_d_garden/bot/components/solenoid.tsx b/frontend/three_d_garden/bot/components/solenoid.tsx
index 12d19633bd..33520248e7 100644
--- a/frontend/three_d_garden/bot/components/solenoid.tsx
+++ b/frontend/three_d_garden/bot/components/solenoid.tsx
@@ -3,7 +3,9 @@ import * as THREE from "three";
import { Config, PositionConfig } from "../../config";
import { Group, Mesh } from "../../components";
import { WaterTube } from "./water_tube";
-import { easyCubicBezierCurve3, threeSpace, zDir as zDirFunc } from "../../helpers";
+import {
+ easyCubicBezierCurve3, get3DPositionNoMirrorFunc, zDir as zDirFunc,
+} from "../../helpers";
import type { GLTF } from "three-stdlib";
import { useGLTF } from "@react-three/drei";
import { ASSETS, LIB_DIR, PartName } from "../../constants";
@@ -20,27 +22,31 @@ export interface SolenoidProps {
export const Solenoid = (props: SolenoidProps) => {
const { config } = props;
- const {
- bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset,
- columnLength, zGantryOffset,
- } = config;
+ const { bedYOffset, columnLength, zGantryOffset } = config;
const { x, y, z } = props.configPosition;
const zDir = zDirFunc(config);
+ const get3DPosition = get3DPositionNoMirrorFunc(config);
+ const outerXY = (gardenX: number, outerY: number): [number, number] => {
+ const position = get3DPosition({ x: gardenX, y: outerY - bedYOffset });
+ return [position.x, position.y];
+ };
+ const gardenXY = (gardenX: number, gardenY: number): [number, number] => {
+ const position = get3DPosition({ x: gardenX, y: gardenY });
+ return [position.x, position.y];
+ };
const solenoid = useGLTF(ASSETS.models.solenoid, LIB_DIR) as unknown as SolenoidPart;
return
{
radialSegments={8} />
{
waterFlow={config.waterFlow}
tubePath={easyCubicBezierCurve3(
[
- threeSpace(x - 104.25, bedLengthOuter) + bedXOffset,
- threeSpace(20, bedWidthOuter),
+ ...outerXY(x - 104.25, 20),
columnLength - 98,
],
[0, 0, 100],
[0, -75, 5],
[
- threeSpace(x - 70, bedLengthOuter) + bedXOffset,
- threeSpace(35, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x - 70, 35),
columnLength + 90,
],
)}
@@ -80,15 +83,13 @@ export const Solenoid = (props: SolenoidProps) => {
waterFlow={config.waterFlow}
tubePath={easyCubicBezierCurve3(
[
- threeSpace(x - 70, bedLengthOuter) + bedXOffset,
- threeSpace(y + 80, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x - 70, y + 80),
columnLength + 140,
],
[0, -50, 0],
[0, 0, -50],
[
- threeSpace(x - 32.5, bedLengthOuter) + bedXOffset,
- threeSpace(y - 10, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x - 32.5, y - 10),
columnLength + 180,
],
)}
@@ -99,15 +100,13 @@ export const Solenoid = (props: SolenoidProps) => {
waterFlow={config.waterFlow}
tubePath={easyCubicBezierCurve3(
[
- threeSpace(x + 32.5, bedLengthOuter) + bedXOffset,
- threeSpace(y - 10, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x + 32.5, y - 10),
columnLength - zDir * z - zGantryOffset + 200,
],
[0, 0, -50],
[0, 0, 50],
[
- threeSpace(x + 2, bedLengthOuter) + bedXOffset,
- threeSpace(y + 15, bedWidthOuter) + bedYOffset,
+ ...gardenXY(x + 2, y + 15),
columnLength - zDir * z - zGantryOffset + 75,
],
)}
diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx
index 725ebd6f0c..53e816bd82 100644
--- a/frontend/three_d_garden/bot/components/tools.tsx
+++ b/frontend/three_d_garden/bot/components/tools.tsx
@@ -1,7 +1,13 @@
import React from "react";
import * as THREE from "three";
import { useGLTF } from "@react-three/drei";
-import { threeSpace, zDir as zDirFunc, zZero as zZeroFunc } from "../../helpers";
+import {
+ get3DPositionFunc,
+ get3DPositionNoMirrorFunc,
+ threeSpace,
+ zDir as zDirFunc,
+ zZero as zZeroFunc,
+} from "../../helpers";
import { Config, PositionConfig } from "../../config";
import type { GLTF } from "three-stdlib";
import {
@@ -110,13 +116,13 @@ export const convertSlotsWithTools =
export const Tools = (props: ToolsProps) => {
const {
- bedXOffset, bedYOffset, bedLengthOuter, bedWidthOuter, bedWallThickness,
+ bedLengthOuter, bedWidthOuter, bedWallThickness,
} = props.config;
- const botPosition = {
- x: props.configPosition.x,
- y: props.configPosition.y,
- z: props.configPosition.z,
- };
+ const get3DPosition = get3DPositionFunc(props.config);
+ const get3DPositionNoMirror = get3DPositionNoMirrorFunc(props.config);
+ const mirroredBotX = props.config.mirrorX
+ ? props.config.botSizeX - props.configPosition.x
+ : props.configPosition.x;
const mountedToolName = isUndefined(props.toolSlots)
? props.config.tool
: reduceToolName(props.mountedToolName);
@@ -145,6 +151,31 @@ export const Tools = (props: ToolsProps) => {
const wateringNozzle = useGLTF(
ASSETS.models.wateringNozzle, LIB_DIR) as unknown as WateringNozzle;
+ const displayedPulloutDirection = (
+ toolPulloutDirection: ToolPulloutDirection,
+ ): ToolPulloutDirection => {
+ switch (toolPulloutDirection) {
+ case ToolPulloutDirection.POSITIVE_X:
+ return props.config.mirrorX
+ ? ToolPulloutDirection.NEGATIVE_X
+ : ToolPulloutDirection.POSITIVE_X;
+ case ToolPulloutDirection.NEGATIVE_X:
+ return props.config.mirrorX
+ ? ToolPulloutDirection.POSITIVE_X
+ : ToolPulloutDirection.NEGATIVE_X;
+ case ToolPulloutDirection.POSITIVE_Y:
+ return props.config.mirrorY
+ ? ToolPulloutDirection.NEGATIVE_Y
+ : ToolPulloutDirection.POSITIVE_Y;
+ case ToolPulloutDirection.NEGATIVE_Y:
+ return props.config.mirrorY
+ ? ToolPulloutDirection.POSITIVE_Y
+ : ToolPulloutDirection.NEGATIVE_Y;
+ default:
+ return toolPulloutDirection;
+ }
+ };
+
const rotationFactor = (toolPulloutDirection: ToolPulloutDirection) => {
switch (toolPulloutDirection) {
case ToolPulloutDirection.POSITIVE_X: return 3;
@@ -165,7 +196,8 @@ export const Tools = (props: ToolsProps) => {
const ToolbaySlot = (slotProps: ToolbaySlotProps) => {
const { position, children, toolPulloutDirection, mounted } = slotProps;
- const rotationMultiplier = rotationFactor(toolPulloutDirection);
+ const rotationMultiplier =
+ rotationFactor(displayedPulloutDirection(toolPulloutDirection));
const navigate = useNavigate();
return {
const Tool = (toolProps: ToolProps) => {
const { toolPulloutDirection, inToolbay, id } = toolProps;
const mounted = inToolbay && toolProps.toolName == mountedToolName;
+ const mirroredPosition = get3DPosition({ x: toolProps.x, y: toolProps.y });
+ const noMirrorPosition = get3DPositionNoMirror({
+ x: toolProps.x,
+ y: toolProps.y,
+ });
const position = {
- x: threeSpace(toolProps.x, bedLengthOuter) + bedXOffset,
- y: threeSpace(toolProps.y, bedWidthOuter) + bedYOffset,
+ x: inToolbay ? mirroredPosition.x : noMirrorPosition.x,
+ y: inToolbay && !toolProps.gantryMounted
+ ? mirroredPosition.y
+ : noMirrorPosition.y,
z: zZero - zDir * toolProps.z + (inToolbay ? 0 : (utmHeight / 2 - 15)),
};
const common: ToolbaySlotProps = {
@@ -336,14 +375,14 @@ export const Tools = (props: ToolsProps) => {
return
{toolProps.firstTrough
?
@@ -351,7 +390,7 @@ export const Tools = (props: ToolsProps) => {
: {
return
@@ -399,7 +438,10 @@ export const Tools = (props: ToolsProps) => {
{tools.map((tool, i) =>
)}
;
};
diff --git a/frontend/three_d_garden/bot/components/watering_animations.tsx b/frontend/three_d_garden/bot/components/watering_animations.tsx
index 113fc0b581..7ed48c0791 100644
--- a/frontend/three_d_garden/bot/components/watering_animations.tsx
+++ b/frontend/three_d_garden/bot/components/watering_animations.tsx
@@ -4,7 +4,9 @@ import { Group } from "../../components";
import { ASSETS } from "../../constants";
import { Cloud, Clouds } from "@react-three/drei";
import { WaterStream } from "./water_stream";
-import { easyCubicBezierCurve3, threeSpace, zDir, zZero } from "../../helpers";
+import {
+ easyCubicBezierCurve3, get3DPositionNoMirrorFunc, zDir, zZero,
+} from "../../helpers";
import { Config, PositionConfig } from "../../config";
import { utmHeight } from "../bot";
@@ -17,8 +19,8 @@ export interface WateringAnimationsProps {
export const WateringAnimations = (props: WateringAnimationsProps) => {
const { waterFlow, getZ, config } = props;
- const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config;
const { x, y, z } = props.configPosition;
+ const get3DPosition = get3DPositionNoMirrorFunc(config);
const utmZ = -zDir(config) * z + utmHeight / 2 - 15;
const nozzleToSoil = getZ(x, y) - utmZ;
const [visible, setVisible] = React.useState(false);
@@ -28,11 +30,12 @@ export const WateringAnimations = (props: WateringAnimationsProps) => {
}, 50);
return () => clearTimeout(timer);
}, []);
+ const position = get3DPosition({ x, y });
return
{range(16).map(i => {
diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts
index 3a25002b26..a4e7552b9f 100644
--- a/frontend/three_d_garden/config.ts
+++ b/frontend/three_d_garden/config.ts
@@ -100,6 +100,8 @@ export interface Config {
promoSpread: boolean;
cameraView: boolean;
lastImageCapture: number;
+ mirrorX: boolean;
+ mirrorY: boolean;
}
export interface PositionConfig {
@@ -114,6 +116,7 @@ export enum SurfaceDebugOption {
none,
normals,
height,
+ blank,
}
export const INITIAL: ConfigWithPosition = {
@@ -221,6 +224,8 @@ export const INITIAL: ConfigWithPosition = {
promoSpread: false,
cameraView: false,
lastImageCapture: 0,
+ mirrorX: false,
+ mirrorY: false,
};
export const INITIAL_POSITION: PositionConfig = {
@@ -256,7 +261,7 @@ export const BOOLEAN_KEYS = [
"animate", "animateSeasons", "negativeZ",
"waterFlow", "exaggeratedZ", "showSoilPoints", "urlParamAutoAdd",
"light", "vacuum", "north", "desk", "interpolationUseNearest", "promoSpread",
- "cameraView",
+ "cameraView", "mirrorX", "mirrorY",
];
export const PRESETS: Record = {
@@ -476,6 +481,7 @@ const OTHER_CONFIG_KEYS: (keyof Config)[] = [
"imgScale", "imgRotation", "imgOffsetX", "imgOffsetY", "imgOrigin", "imgCalZ",
"imgCenterX", "imgCenterY", "interpolationStepSize", "interpolationUseNearest",
"interpolationPower", "promoSpread", "cameraView", "lastImageCapture",
+ "mirrorX", "mirrorY",
];
export const modifyConfig =
diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx
index f55559ba50..23ee3537db 100644
--- a/frontend/three_d_garden/config_overlays.tsx
+++ b/frontend/three_d_garden/config_overlays.tsx
@@ -342,6 +342,8 @@ export const PrivateOverlay = (props: OverlayProps) => {
+
+
diff --git a/frontend/three_d_garden/garden/__tests__/images_test.tsx b/frontend/three_d_garden/garden/__tests__/images_test.tsx
index bece391809..cd565c5b16 100644
--- a/frontend/three_d_garden/garden/__tests__/images_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/images_test.tsx
@@ -1,7 +1,10 @@
let mockDemo = false;
import React from "react";
import { render, screen } from "@testing-library/react";
-import { extraRotation, ImageTexture, ImageTextureProps } from "../images";
+import {
+ extraRotation, getImagePosition, getImageScale, getMirrorTextureProps,
+ ImageTexture, ImageTextureProps,
+} from "../images";
import { INITIAL } from "../../config";
import { clone } from "lodash";
import {
@@ -168,3 +171,38 @@ describe("extraRotation()", () => {
expect(extraRotation(config)).toEqual(result);
});
});
+
+describe("getImageScale()", () => {
+ it("returns mirrored image scale", () => {
+ const config = clone(INITIAL);
+ config.mirrorX = true;
+ config.mirrorY = true;
+ expect(getImageScale(config, 100, 200)).toEqual([-100, -200, 1000]);
+ });
+});
+
+describe("getMirrorTextureProps()", () => {
+ it("returns mirrored repeat and offset", () => {
+ const config = clone(INITIAL);
+ config.mirrorX = true;
+ config.mirrorY = true;
+ expect(getMirrorTextureProps(config)).toEqual({
+ repeat: [-1, -1],
+ offset: [1, 1],
+ });
+ });
+});
+
+describe("getImagePosition()", () => {
+ it("pre-mirrors image position while keeping offsets", () => {
+ const config = clone(INITIAL);
+ config.botSizeX = 1000;
+ config.botSizeY = 500;
+ config.imgOffsetX = 10;
+ config.imgOffsetY = 20;
+ config.mirrorX = true;
+ config.mirrorY = true;
+ expect(getImagePosition(config, 100, 200, 30, 40, 5))
+ .toEqual([940, 360, 5]);
+ });
+});
diff --git a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
index 755d7fcac3..4b86b01ba5 100644
--- a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
@@ -15,6 +15,7 @@ let mockRefImpl = (): MockRef => ({
instanceMatrix: { needsUpdate: false },
}
});
+let allRefs: MockRef[] = [];
import React from "react";
import { fireEvent, render } from "@testing-library/react";
@@ -40,8 +41,21 @@ describe(" ", () => {
let reactUseRefSpy: jest.SpyInstance;
beforeEach(() => {
+ mockRefImpl = () => ({
+ current: {
+ scale: { set: jest.fn() },
+ position: { z: 0 },
+ setMatrixAt: jest.fn(),
+ instanceMatrix: { needsUpdate: false },
+ }
+ });
+ allRefs = [];
reactUseRefSpy = jest.spyOn(React, "useRef")
- .mockImplementation(() => mockRefImpl() as never);
+ .mockImplementation(() => {
+ const ref = mockRefImpl();
+ allRefs.push(ref);
+ return ref as never;
+ });
location.pathname = Path.mock(Path.designer());
(useFrame as jest.Mock).mockClear();
(useTexture as unknown as jest.Mock).mockClear();
@@ -164,6 +178,33 @@ describe(" ", () => {
expect(container).toBeTruthy();
});
+ it("uses garden coordinates for getZ", () => {
+ const getZ = jest.fn(() => 0);
+ const p = fakeProps();
+ p.getZ = getZ;
+ p.plants = [p.plants[0]];
+ render( );
+ expect(getZ).toHaveBeenCalledWith(100, 200);
+ });
+
+ it("uses mirrored world placement for plant icons", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.botSizeX = 1000;
+ p.config.botSizeY = 500;
+ p.plants = [p.plants[0]];
+ render( );
+ (useFrame as jest.Mock).mock.calls.forEach(([frameFn]) =>
+ frameFn({ camera: { quaternion: new Quaternion() } }));
+ const instancedRef = allRefs.find(ref => !!ref.current?.setMatrixAt);
+ expect(instancedRef?.current?.setMatrixAt).toHaveBeenCalled();
+ const matrix = (instancedRef?.current?.setMatrixAt as jest.Mock)
+ .mock.calls[0][1];
+ expect(matrix.elements[12]).toBeCloseTo(1260);
+ expect(matrix.elements[13]).toBeCloseTo(460);
+ });
+
it("updates material brightness when changed", () => {
const setScalar = jest.fn();
const instancedRef = {
diff --git a/frontend/three_d_garden/garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
index 32e4154ce7..93f4f910cc 100644
--- a/frontend/three_d_garden/garden/__tests__/plants_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
@@ -121,6 +121,12 @@ describe(" ", () => {
render( );
expect(screen.getByText("Beet")).toBeInTheDocument();
});
+
+ it("keeps plant coordinates in garden space", () => {
+ const p = fakeProps();
+ expect(p.plant.x).toEqual(100);
+ expect(p.plant.y).toEqual(200);
+ });
});
describe(" ", () => {
@@ -335,6 +341,10 @@ describe("outOfBoundsShaderModification", () => {
} as unknown as WebGLProgramParametersWithUniforms;
outOfBoundsShaderModification(shader, false);
expect(shader.fragmentShader).toContain("uInside");
+ expect(shader.fragmentShader).toContain("uMirrorX");
+ expect(shader.fragmentShader).toContain("uMirrorY");
+ expect(shader.fragmentShader).toContain("p.x *= uMirrorX");
+ expect(shader.fragmentShader).toContain("p.y *= uMirrorY");
expect(shader.vertexShader).not.toContain("vInstanceColor");
});
});
diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx
index 56afab7ae2..7326c664bc 100644
--- a/frontend/three_d_garden/garden/__tests__/point_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx
@@ -30,6 +30,18 @@ describe(" ", () => {
expect(container).toContainHTML("opacity=\"1\"");
});
+ it("renders mirrored position", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.botSizeX = 1000;
+ p.config.botSizeY = 500;
+ p.point.body.x = 100;
+ p.point.body.y = 200;
+ const { container } = render( );
+ expect(container).toContainHTML("position=\"1260,460,400\"");
+ });
+
it("renders: unsaved", () => {
const p = fakeProps();
p.point.specialStatus = SpecialStatus.DIRTY;
diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx
index cf2124a918..d321f52d97 100644
--- a/frontend/three_d_garden/garden/__tests__/weed_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx
@@ -33,6 +33,18 @@ describe(" ", () => {
expect(container).toContainHTML("weed");
});
+ it("renders mirrored position", () => {
+ const p = fakeProps();
+ p.config.mirrorX = true;
+ p.config.mirrorY = true;
+ p.config.botSizeX = 1000;
+ p.config.botSizeY = 500;
+ p.weed.body.x = 100;
+ p.weed.body.y = 200;
+ const { container } = render( );
+ expect(container).toContainHTML("position=\"1260,460,400\"");
+ });
+
it("navigates to weed info", () => {
const p = fakeProps();
const dispatch = jest.fn();
diff --git a/frontend/three_d_garden/garden/grid.tsx b/frontend/three_d_garden/garden/grid.tsx
index 70ff6988d3..a853aace2e 100644
--- a/frontend/three_d_garden/garden/grid.tsx
+++ b/frontend/three_d_garden/garden/grid.tsx
@@ -2,7 +2,7 @@ import React from "react";
import { Config } from "../config";
import { Group, Primitive } from "../components";
import {
- zero as zeroFunc, extents as extentsFunc, getGardenPositionFunc,
+ get3DPositionFunc, zero as zeroFunc,
} from "../helpers";
import { chain, floor, range } from "lodash";
import { useThree } from "@react-three/fiber";
@@ -29,13 +29,15 @@ const lineSegmentsFor = (
config: Config,
) => {
const positions: number[] = [];
- const getGardenPosition = getGardenPositionFunc(config, false);
+ const get3DPosition = get3DPositionFunc(config);
let prev: { x: number, y: number, z: number } | undefined;
range(101).forEach(i => {
const t = i / 100;
- const x = start.x + (end.x - start.x) * t;
- const y = start.y + (end.y - start.y) * t;
- const gardenPosition = getGardenPosition({ x, y });
+ const gardenPosition = {
+ x: start.x + (end.x - start.x) * t,
+ y: start.y + (end.y - start.y) * t,
+ };
+ const { x, y } = get3DPosition(gardenPosition);
const z = getZ(gardenPosition.x, gardenPosition.y);
if (prev) {
positions.push(prev.x, prev.y, prev.z, x, y, z);
@@ -93,7 +95,6 @@ export interface GridProps {
export const Grid = (props: GridProps) => {
const { config } = props;
const zero = zeroFunc(config);
- const extents = extentsFunc(config);
const { outerPositions, innerPositions } = React.useMemo(() => {
const result = {
outerPositions: [] as number[],
@@ -102,11 +103,11 @@ export const Grid = (props: GridProps) => {
gridLineOffsets(config.botSizeX).forEach(xOffset => {
const isOuterLine = xOffset === 0 || xOffset === config.botSizeX;
const positions = lineSegmentsFor({
- x: zero.x + xOffset,
- y: zero.y,
+ x: xOffset,
+ y: 0,
}, {
- x: zero.x + xOffset,
- y: extents.y,
+ x: xOffset,
+ y: config.botSizeY,
}, props.getZ, config);
if (isOuterLine) {
result.outerPositions.push(...positions);
@@ -117,11 +118,11 @@ export const Grid = (props: GridProps) => {
gridLineOffsets(config.botSizeY).forEach(yOffset => {
const isOuterLine = yOffset === 0 || yOffset === config.botSizeY;
const positions = lineSegmentsFor({
- x: zero.x,
- y: zero.y + yOffset,
+ x: 0,
+ y: yOffset,
}, {
- x: extents.x,
- y: zero.y + yOffset,
+ x: config.botSizeX,
+ y: yOffset,
}, props.getZ, config);
if (isOuterLine) {
result.outerPositions.push(...positions);
@@ -130,7 +131,7 @@ export const Grid = (props: GridProps) => {
}
});
return result;
- }, [config, extents.x, extents.y, props.getZ, zero.x, zero.y]);
+ }, [config, props.getZ]);
return
diff --git a/frontend/three_d_garden/garden/images.tsx b/frontend/three_d_garden/garden/images.tsx
index f44c358e2f..75f71155e5 100644
--- a/frontend/three_d_garden/garden/images.tsx
+++ b/frontend/three_d_garden/garden/images.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { TaggedImage, TaggedSensor, TaggedSensorReading } from "farmbot";
-import { Config } from "../config";
+import { Config, SurfaceDebugOption } from "../config";
import { isNumber } from "lodash";
import {
Decal, OrthographicCamera, Plane, RenderTexture, useTexture,
@@ -48,6 +48,47 @@ const PlaneWrapper = (props: PlaneWrapperProps) =>
{props.children}
;
+export const getImageScale = (
+ config: Pick,
+ width: number,
+ height: number,
+): [number, number, number] =>
+ [
+ config.mirrorY ? -width : width,
+ config.mirrorX ? -height : height,
+ 1000,
+ ];
+
+export const getMirrorTextureProps =
+ (config: Pick) => ({
+ repeat: [
+ config.mirrorX ? -1 : 1,
+ config.mirrorY ? -1 : 1,
+ ] as [number, number],
+ offset: [
+ config.mirrorX ? 1 : 0,
+ config.mirrorY ? 1 : 0,
+ ] as [number, number],
+ });
+
+export const getImagePosition = (
+ config: Pick,
+ x: number,
+ y: number,
+ xOffset: number,
+ yOffset: number,
+ z: number,
+): [number, number, number] => {
+ const baseX = config.mirrorX ? config.botSizeX - x : x;
+ const baseY = config.mirrorY ? config.botSizeY - y : y;
+ return [
+ baseX + config.imgOffsetX + xOffset,
+ baseY + config.imgOffsetY + yOffset,
+ z,
+ ];
+};
+
export interface ImageTextureProps extends BaseProps {
images?: TaggedImage[];
addPlantProps?: AddPlantProps;
@@ -79,7 +120,10 @@ export const ImageTexture = (props: ImageTextureProps) => {
const lastImageArray = filteredImages.filter(img => img.highlighted);
const highlightActive = lastImageArray[0]?.highlighted;
const commonProps = { width, height, bedWallThickness };
- return
+ const mirrorTextureProps = getMirrorTextureProps(props.config);
+ return
{
position={[bedXOffset, bedYOffset, 4000]}
rotation={[0, 0, 0]}
zoom={1}
- scale={[1, 1, 1]}
up={[0, 0, 1]} />
-
+
{highlightActive &&
@@ -169,7 +215,7 @@ const ImageWrapper = (props: ImageWrapperProps) => {
if (!props.image.highlighted &&
!imageSizeCheck({ width: i.width, height: i.height },
{ x: "" + config.imgCenterX, y: "" + config.imgCenterY })) { return; }
- const scale: [number, number, number] = [width, height, 1000];
+ const scale = getImageScale(config, width, height);
const alreadyRotated = isRotated(props.image.body.meta.name);
const initialRotation = alreadyRotated ? 0 : config.imgRotation;
@@ -178,11 +224,8 @@ const ImageWrapper = (props: ImageWrapperProps) => {
return {
: [0, 0, 0]}>
{props.readings.map(reading =>
{
const tempPosition = React.useMemo(() => new Vector3(), []);
const tempScale = React.useMemo(() => new Vector3(), []);
const tempQuaternion = React.useMemo(() => new Quaternion(), []);
+ const get3DPosition = React.useMemo(() => get3DPositionFunc(config), [config]);
const getPlantZ = React.useCallback((size: number, plant: ThreeDGardenPlant) =>
zZeroFunc(config)
- + getZ(plant.x - config.bedXOffset, plant.y - config.bedYOffset)
+ + getZ(plant.x, plant.y)
+ size / 2, [config, getZ]);
useFrame(state => {
@@ -84,9 +85,10 @@ const PlantIconInstances = (props: PlantIconInstancesProps) => {
const scale = (config.animateSeasons && startTimeRef)
? plant.size * getSizeAtTime(plant, config.plants, t)
: plant.size;
+ const position = get3DPosition({ x: plant.x, y: plant.y });
tempPosition.set(
- threeSpace(plant.x, config.bedLengthOuter),
- threeSpace(plant.y, config.bedWidthOuter),
+ position.x,
+ position.y,
getPlantZ(scale, plant),
);
tempScale.set(scale, scale, scale);
diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx
index 4583a58106..fa5269c72d 100644
--- a/frontend/three_d_garden/garden/plants.tsx
+++ b/frontend/three_d_garden/garden/plants.tsx
@@ -14,9 +14,9 @@ import {
} from "three";
import {
getGardenPositionFunc,
- threeSpace,
zZero,
zZero as zZeroFunc,
+ get3DPositionFunc,
} from "../helpers";
import { Text } from "../elements";
import { isUndefined } from "lodash";
@@ -58,16 +58,18 @@ export const ThreeDPlantLabel = (props: ThreeDPlantLabelProps) => {
const alwaysShowLabels = config.labels && !config.labelsOnHover;
// eslint-disable-next-line no-null/no-null
const billboardRef = React.useRef(null);
+ const get3DPosition = React.useMemo(() => get3DPositionFunc(config), [config]);
const getPlantZ = (size: number) =>
zZeroFunc(config)
- + props.getZ(plant.x - config.bedXOffset, plant.y - config.bedYOffset)
+ + props.getZ(plant.x, plant.y)
+ size / 2;
+ const position = get3DPosition({ x: plant.x, y: plant.y });
return
{
const tempScale = React.useMemo(() => new Vector3(), []);
const tempQuaternion = React.useMemo(() => new Quaternion(), []);
const tempColor = React.useMemo(() => new Color(), []);
+ const get3DPosition = React.useMemo(() => get3DPositionFunc(config), [config]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const boundsCenter = React.useMemo(getBoundsCenter(config), []);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -121,7 +124,7 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
plants.map((_, index) => index), [plants]);
const getPlantZ = React.useCallback((size: number, plant: ThreeDGardenPlant) =>
zZeroFunc(config)
- + getZ(plant.x - config.bedXOffset, plant.y - config.bedYOffset)
+ + getZ(plant.x, plant.y)
+ size / 2, [config, getZ]);
const editPlantMode =
Path.getSlug(Path.designer()) == "plants" && Path.lastChunkIsNum();
@@ -171,8 +174,8 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
y: currentPlant?.y || -10000,
}
: {
- x: activePointer.x + config.bedXOffset,
- y: activePointer.y + config.bedYOffset,
+ x: activePointer.x,
+ y: activePointer.y,
};
const clickToAddMode = getMode() == Mode.clickToAdd;
plants.forEach((plant, index) => {
@@ -184,9 +187,10 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
const scale = (spreadVisible || !plant.id || editPlantMode)
? spreadRadii.inactive
: 0;
+ const position = get3DPosition({ x: plant.x, y: plant.y });
tempPosition.set(
- threeSpace(plant.x, config.bedLengthOuter),
- threeSpace(plant.y, config.bedWidthOuter),
+ position.x,
+ position.y,
getPlantZ(plant.size, plant),
);
tempScale.set(scale, scale, scale);
@@ -246,6 +250,8 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
shader.uniforms.uBoundsCenter = { value: boundsCenter };
shader.uniforms.uHalfSize = { value: halfSize };
shader.uniforms.uOutside = { value: new Color("red") };
+ shader.uniforms.uMirrorX = { value: config.mirrorX ? -1 : 1 };
+ shader.uniforms.uMirrorY = { value: config.mirrorY ? -1 : 1 };
outOfBoundsShaderModification(shader, true);
}}
depthWrite={false} />
@@ -283,11 +289,15 @@ export const outOfBoundsShaderModification =
? `uniform vec3 uBoundsCenter;
uniform vec3 uHalfSize;
uniform vec3 uOutside;
+ uniform float uMirrorX;
+ uniform float uMirrorY;
varying vec3 vInstanceColor;`
: `uniform vec3 uBoundsCenter;
uniform vec3 uHalfSize;
uniform vec3 uInside;
- uniform vec3 uOutside;`;
+ uniform vec3 uOutside;
+ uniform float uMirrorX;
+ uniform float uMirrorY;`;
const insideColor = useInstanceColor ? "vInstanceColor" : "uInside";
shader.vertexShader = shader.vertexShader.replace(
"#include ",
@@ -308,6 +318,8 @@ export const outOfBoundsShaderModification =
"#include ",
`#include
vec3 p = vWorldPosition - uBoundsCenter;
+ p.x *= uMirrorX;
+ p.y *= uMirrorY;
bool inside =
p.x > -uHalfSize.x &&
abs(p.y) <= uHalfSize.y &&
diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx
index 012e459fed..d60892698d 100644
--- a/frontend/three_d_garden/garden/point.tsx
+++ b/frontend/three_d_garden/garden/point.tsx
@@ -4,7 +4,7 @@ import { Config } from "../config";
import { Group, MeshPhongMaterial } from "../components";
import { Cylinder, Sphere, Torus } from "@react-three/drei";
import { DoubleSide } from "three";
-import { zero as zeroFunc, threeSpace } from "../helpers";
+import { getWorldPositionFunc } from "../helpers";
import { useNavigate } from "react-router";
import { Path } from "../../internal_urls";
import { isUndefined, round } from "lodash";
@@ -109,16 +109,13 @@ const PointBase = (props: PointBaseProps) => {
const {
pointName, position, onClick, color, alpha, config, radius, torusRef,
} = props;
+ const getWorldPosition = getWorldPositionFunc(config);
return
diff --git a/frontend/three_d_garden/garden/weed.tsx b/frontend/three_d_garden/garden/weed.tsx
index c346b91f91..fc8bc6b67a 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, HOVER_OBJECT_MODES, RenderOrder } from "../constants";
import { Group, MeshPhongMaterial } from "../components";
import { Image, Billboard, Sphere } from "@react-three/drei";
-import { zero as zeroFunc, threeSpace } from "../helpers";
+import { getWorldPositionFunc } from "../helpers";
import { useNavigate } from "react-router";
import { Path } from "../../internal_urls";
import { isUndefined } from "lodash";
@@ -63,16 +63,13 @@ export const WeedBase = (props: WeedBaseProps) => {
pointName, position, onClick, color, radius, alpha, config,
radiusRef, billboardRef, imageRef,
} = props;
+ const getWorldPosition = getWorldPositionFunc(config);
const weedSize = radius == 0 ? 50 : radius;
const iconSize = weedSize * WEED_IMG_SIZE_FRACTION;
return
{
const { sortType, groupPoints, config, getZ, tryGroupSortType } = props;
const sortedPoints = sortGroupBy(tryGroupSortType || sortType, groupPoints);
+ const getWorldPosition = getWorldPositionFunc(config);
const positions: [number, number, number][] = sortedPoints
.map(p => {
- const x = threeSpace(p.body.x, config.bedLengthOuter) + config.bedXOffset;
- const y = threeSpace(p.body.y, config.bedWidthOuter) + config.bedYOffset;
- const zZero = zZeroFunc(config);
if (p.body.pointer_type == "ToolSlot") {
- return [x, y, zZero + p.body.z + 25];
+ return getWorldPosition({ x: p.body.x, y: p.body.y, z: p.body.z + 25 });
}
if (p.body.pointer_type == "GenericPointer") {
- return [x, y, zZero + getZ(p.body.x, p.body.y) + 75];
+ return getWorldPosition({
+ x: p.body.x,
+ y: p.body.y,
+ z: getZ(p.body.x, p.body.y) + 75,
+ });
}
- return [x, y, zZero + getZ(p.body.x, p.body.y) + p.body.radius + 10];
+ return getWorldPosition({
+ x: p.body.x,
+ y: p.body.y,
+ z: getZ(p.body.x, p.body.y) + p.body.radius + 10,
+ });
});
return
(threeDPosition: XY): XY => {
const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config;
+ const unmirroredPosition = {
+ x: config.mirrorX ? -threeDPosition.x : threeDPosition.x,
+ y: config.mirrorY ? -threeDPosition.y : threeDPosition.y,
+ };
const position = {
- x: threeSpace(threeDPosition.x, -bedLengthOuter) - bedXOffset,
- y: threeSpace(threeDPosition.y, -bedWidthOuter) - bedYOffset,
+ x: threeSpace(unmirroredPosition.x, -bedLengthOuter) - bedXOffset,
+ y: threeSpace(unmirroredPosition.y, -bedWidthOuter) - bedYOffset,
};
return snap
? { x: round(position.x), y: round(position.y) }
@@ -71,6 +75,15 @@ export const getGardenPositionFunc = (config: Config, snap = true) =>
};
export const get3DPositionFunc = (config: Config) =>
+ (gardenPosition: XY): XY => {
+ const position = get3DPositionNoMirrorFunc(config)(gardenPosition);
+ return {
+ x: config.mirrorX ? -position.x : position.x,
+ y: config.mirrorY ? -position.y : position.y,
+ };
+ };
+
+export const get3DPositionNoMirrorFunc = (config: Config) =>
(gardenPosition: XY): XY => {
const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config;
return {
@@ -78,3 +91,15 @@ export const get3DPositionFunc = (config: Config) =>
y: threeSpace(gardenPosition.y + bedYOffset, bedWidthOuter),
};
};
+
+type XYZ = Record<"x" | "y" | "z", number>;
+
+export const getWorldPositionFunc = (config: Config) =>
+ (gardenPosition: XYZ): [number, number, number] => {
+ const position = get3DPositionFunc(config)(gardenPosition);
+ return [
+ position.x,
+ position.y,
+ zZero(config) + gardenPosition.z,
+ ];
+ };
diff --git a/frontend/three_d_garden/triangles.ts b/frontend/three_d_garden/triangles.ts
index f0eea32f92..6a1298cefb 100644
--- a/frontend/three_d_garden/triangles.ts
+++ b/frontend/three_d_garden/triangles.ts
@@ -23,7 +23,7 @@ export const filterMoisturePoints = (props: FilterMoisturePointsProps) => {
.filter(p =>
!isUndefined(p.body.x) &&
!isUndefined(p.body.y))
- .map(p => [p.body.x, p.body.y, p.body.value]) as [number, number, number][];
+ .map(p => [p.body.x, p.body.y, p.body.value]);
const params = boundaryPoints(props.config);
const outerPoints = [
{ x: params.outer.x.min, y: params.outer.y.min },
diff --git a/frontend/three_d_garden/visualization.tsx b/frontend/three_d_garden/visualization.tsx
index e65da854ce..379d427661 100644
--- a/frontend/three_d_garden/visualization.tsx
+++ b/frontend/three_d_garden/visualization.tsx
@@ -4,7 +4,7 @@ import { collectDemoSequenceActions } from "../demo/lua_runner";
import { store } from "../redux/store";
import { findSequence } from "../resources/selectors_by_kind";
import { expandActions } from "../demo/lua_runner/actions";
-import { threeSpace, zZero as zZeroFunc } from "./helpers";
+import { getWorldPositionFunc } from "./helpers";
import { Config, PositionConfig } from "./config";
export interface VisualizationProps {
@@ -15,9 +15,8 @@ export interface VisualizationProps {
export const Visualization = (props: VisualizationProps) => {
const { visualizedSequenceUUID, config } = props;
- const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config;
const { x, y, z } = props.configPosition;
- const zZero = zZeroFunc(config);
+ const getWorldPosition = getWorldPositionFunc(config);
const visualizationPoints = React.useMemo(() => {
if (!visualizedSequenceUUID) { return []; }
const resources = store.getState().resources.index;
@@ -26,20 +25,23 @@ export const Visualization = (props: VisualizationProps) => {
const stashedPos = { x, y, z };
const actions =
collectDemoSequenceActions(0, resources, sequence.body.id, []);
- const points = [[stashedPos.x, stashedPos.y, stashedPos.z]]
+ const points = [[
+ stashedPos.x + config.bedXOffset - config.bedLengthOuter / 2,
+ stashedPos.y + config.bedYOffset - config.bedWidthOuter / 2,
+ z + config.columnLength + 40 - config.zGantryOffset,
+ ] as [number, number, number]]
.concat(expandActions(actions, [], stashedPos)
.filter(action => action.type == "expanded_move_absolute")
.map(action => action.args as [number, number, number]))
- .map(coordinate => [
- threeSpace(coordinate[0], bedLengthOuter) + bedXOffset,
- threeSpace(coordinate[1], bedWidthOuter) + bedYOffset,
- zZero + coordinate[2],
- ] as [number, number, number]);
+ .map(coordinate => getWorldPosition({
+ x: coordinate[0],
+ y: coordinate[1],
+ z: coordinate[2],
+ }));
return points;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visualizedSequenceUUID,
- bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset,
- zZero]);
+ config, getWorldPosition, x, y, z]);
return visualizationPoints.length > 0 &&
[
- {
- label: "What you can grow",
- info: {
- description:
-
- FarmBot is well suited to growing a polycrop of many common
- garden veggies at the same time. Crops we've had success with include
- Bok Choy, Lettuces, Radish, Beets, Chard, Arugula, Broccoli, and much
- more.
+export const FOCI = (config: Config, configPosition: PositionConfig): Focus[] => {
+ const get3DPosition = get3DPositionNoMirrorFunc(config);
+ const utmPosition = get3DPosition({
+ x: configPosition.x,
+ y: configPosition.y + 150,
+ });
+ const electronicsPosition = get3DPosition({
+ x: configPosition.x - 50,
+ y: -200 - config.bedYOffset,
+ });
+ return [
+ {
+ label: "What you can grow",
+ info: {
+ description:
+ FarmBot is well suited to growing a polycrop of many common
+ garden veggies at the same time. Crops we've had success with include
+ Bok Choy, Lettuces, Radish, Beets, Chard, Arugula, Broccoli, and much
+ more.
+
+
+ By placing vining and other indeterminate crops near the ends
+ of the bed and training them outwards, you can easily double or
+ triple the area your plants can utilize while still being maintained
+ by the FarmBot.
- By placing vining and other indeterminate crops near the ends
- of the bed and training them outwards, you can easily double or
- triple the area your plants can utilize while still being maintained
- by the FarmBot.
-
-
,
- position: [
- 0,
- config.bedWidthOuter * .8,
- 300,
- ],
- scale: config.sizePreset == "Genesis XL" ? 6000 : 3000,
- },
- position: [
- threeSpace(config.bedLengthOuter / 2, config.bedLengthOuter),
- threeSpace(config.bedWidthOuter / 2, config.bedWidthOuter),
- 150,
- ],
- camera: {
- narrow: {
+
,
position: [
0,
- -1000,
- config.sizePreset == "Genesis XL" ? 16000 : 8000,
- ],
- target: [
- 0,
- -1000,
- 0,
+ config.bedWidthOuter * .8,
+ 300,
],
+ scale: config.sizePreset == "Genesis XL" ? 6000 : 3000,
},
- wide: {
- position: [
- 0,
- 0,
- config.sizePreset == "Genesis XL" ? 10000 : 5000,
- ],
- target: [
- 0,
- 0,
- 0,
- ],
+ position: [
+ threeSpace(config.bedLengthOuter / 2, config.bedLengthOuter),
+ threeSpace(config.bedWidthOuter / 2, config.bedWidthOuter),
+ 150,
+ ],
+ camera: {
+ narrow: {
+ position: [
+ 0,
+ -1000,
+ config.sizePreset == "Genesis XL" ? 16000 : 8000,
+ ],
+ target: [
+ 0,
+ -1000,
+ 0,
+ ],
+ },
+ wide: {
+ position: [
+ 0,
+ 0,
+ config.sizePreset == "Genesis XL" ? 10000 : 5000,
+ ],
+ target: [
+ 0,
+ 0,
+ 0,
+ ],
+ },
},
},
- },
- {
- label: "Included tools",
- info: {
- description:
-
- FarmBot comes with four tools to cover the basics of food production:
- the Seed Injector, Watering Nozzle, Soil Sensor, and Rotary Tool.
-
-
- Also included are the Seed Bin, Seed Tray, Seed Troughs, and the Camera
- which remains permanently mounted to the Z-axis.
-
-
,
- position: [
- 0,
- -300,
- 175,
- ],
- scale: 550,
- },
- position: [
- threeSpace(0, config.bedLengthOuter),
- threeSpace(config.bedWidthOuter / 2, config.bedWidthOuter),
- 100,
- ],
- camera: {
- narrow: {
+ referrerPolicy="strict-origin-when-cross-origin"
+ allowFullScreen>
+
+ ,
position: [
- 1100,
- -1150,
- 500,
- ],
- target: [
- 0,
0,
- -325,
+ -300,
+ 175,
],
+ scale: 550,
},
- wide: {
- position: [
- 850,
- -650,
- 350,
- ],
- target: [
- 0,
- 0,
- 0,
- ],
+ position: [
+ threeSpace(0, config.bedLengthOuter),
+ threeSpace(config.bedWidthOuter / 2, config.bedWidthOuter),
+ 100,
+ ],
+ camera: {
+ narrow: {
+ position: [
+ 1100,
+ -1150,
+ 500,
+ ],
+ target: [
+ 0,
+ 0,
+ -325,
+ ],
+ },
+ wide: {
+ position: [
+ 850,
+ -650,
+ 350,
+ ],
+ target: [
+ 0,
+ 0,
+ 0,
+ ],
+ },
},
},
- },
- {
- label: "Universal Tool Mounting",
- info: {
- description:
-
The Universal Tool Mount (UTM) allows FarmBot to automatically
- switch between a variety of lightweight
- tools — whichever one is appropriate for the task at hand (seeding,
- watering, weeding, etc). Using three neodymium ring magnets, tools
- are magnetically held in place during operation, but can be
- automatically dismounted in a toolbay when not in use.
+ {
+ label: "Universal Tool Mounting",
+ info: {
+ description:
+
The Universal Tool Mount (UTM) allows FarmBot to automatically
+ switch between a variety of lightweight
+ tools — whichever one is appropriate for the task at hand (seeding,
+ watering, weeding, etc). Using three neodymium ring magnets, tools
+ are magnetically held in place during operation, but can be
+ automatically dismounted in a toolbay when not in use.
-
Once a tool has been mounted, FarmBot can power it up and communicate
- with it using the 12 gold-plated pogo pins inside the UTM. The stock
- connections include ground, 5v, 24v, as well as analog and digital I/O.
- Meanwhile, the remaining electrical connections are available for
- custom tooling such as specialized sensors or low power motorized
- implements. Additionally, the three liquid/gas ports provide water,
- vacuum air, and an expansion port for custom applications.
+
Once a tool has been mounted, FarmBot can power it up and communicate
+ with it using the 12 gold-plated pogo pins inside the UTM. The stock
+ connections include ground, 5v, 24v, as well as analog and digital I/O.
+ Meanwhile, the remaining electrical connections are available for
+ custom tooling such as specialized sensors or low power motorized
+ implements. Additionally, the three liquid/gas ports provide water,
+ vacuum air, and an expansion port for custom applications.
-
Because FarmBot is 100% open-source, you can download our
- CAD models to start designing your own compatible creations.
- Tools can be 3D printed and wired up with common electrical hardware in
- just an afternoon.
-
,
- position: [
- 0,
- 75,
- 0,
- ],
- scale: 400,
- },
- position: [
- threeSpace(configPosition.x, config.bedLengthOuter) + config.bedXOffset,
- threeSpace(configPosition.y + 150, config.bedWidthOuter) + config.bedYOffset,
- zZero(config) - zDir(config) * configPosition.z,
- ],
- camera: {
- narrow: {
+
Because FarmBot is 100% open-source, you can download our
+ CAD models to start designing your own compatible creations.
+ Tools can be 3D printed and wired up with common electrical hardware in
+ just an afternoon.
+
,
position: [
- 500,
- -300,
- 225,
- ],
- target: [
0,
- -150,
- -100,
- ],
- },
- wide: {
- position: [
- 500,
- -300,
- 225,
- ],
- target: [
+ 75,
0,
- -75,
- -25,
],
+ scale: 400,
},
- },
- },
- {
- label: "Electronics",
- info: {
- description:
-
- FarmBot is powered by the workhorses of the DIY movement:
- the Raspberry Pi and a custom designed Arduino we call the
- Farmduino .
-
-
- This custom circuit board includes Trinamic TMC2130 stepper drivers
- with built-in quiet mode, an STM32 co-processor for monitoring the
- rotary encoders, five 24v peripheral outputs, and an H-bridge for
- reversible DC motor control at the UTM.
-
-
,
position: [
- 0,
- 200,
- 200,
+ utmPosition.x,
+ utmPosition.y,
+ zZero(config) - zDir(config) * configPosition.z,
],
- scale: 550,
+ camera: {
+ narrow: {
+ position: [
+ 500,
+ -300,
+ 225,
+ ],
+ target: [
+ 0,
+ -150,
+ -100,
+ ],
+ },
+ wide: {
+ position: [
+ 500,
+ -300,
+ 225,
+ ],
+ target: [
+ 0,
+ -75,
+ -25,
+ ],
+ },
+ },
},
- position: [
- threeSpace(configPosition.x, config.bedLengthOuter) + config.bedXOffset - 50,
- threeSpace(-200, config.bedWidthOuter),
- config.columnLength - 150,
- ],
- camera: {
- narrow: {
+ {
+ label: "Electronics",
+ info: {
+ description:
+
+ FarmBot is powered by the workhorses of the DIY movement:
+ the Raspberry Pi and a custom designed Arduino we call the
+ Farmduino .
+
+
+ This custom circuit board includes Trinamic TMC2130 stepper drivers
+ with built-in quiet mode, an STM32 co-processor for monitoring the
+ rotary encoders, five 24v peripheral outputs, and an H-bridge for
+ reversible DC motor control at the UTM.
+
+
,
position: [
- -200,
- -550,
- 400,
- ],
- target: [
0,
- 100,
- -150,
+ 200,
+ 200,
],
+ scale: 550,
},
- wide: {
- position: [
- -200,
- -550,
- 400,
- ],
- target: [
- 0,
- 100,
- 100,
- ],
+ position: [electronicsPosition.x, electronicsPosition.y, config.columnLength - 150],
+ camera: {
+ narrow: {
+ position: [
+ -200,
+ -550,
+ 400,
+ ],
+ target: [
+ 0,
+ 100,
+ -150,
+ ],
+ },
+ wide: {
+ position: [
+ -200,
+ -550,
+ 400,
+ ],
+ target: [
+ 0,
+ 100,
+ 100,
+ ],
+ },
},
},
- },
- {
- label: "What you need to provide",
- info: {
- description:
-
- FarmBot must be plugged into a standard 100-265V AC outlet.
- The power cable comes with a standard US 3-prong plug (NEMA 5-15P), which
- can be used with a plug adapter for installations outside the US.
-
-
- FarmBot can optionally be powered by
- a solar
- system with the appropriate battery and inverter. These
- components may be purchased from a 3rd party.
-
-
- FarmBot's water system has a 3/4″ female Garden Hose Thread (GHT)
- connection, meaning you can take a standard US garden hose and
- screw it into your FarmBot. You will need to provide a hose of
- the appropriate length.
-
-
- FarmBot requires an internet connection and supports both WiFi
- and Ethernet. You may need to reposition your WiFi router or
- install a repeater to ensure a reliable connection.
-
-
,
- position: [
- 300,
- -300,
- 0,
- ],
- scale: 1000,
- },
- position: [
- threeSpace(config.bedLengthOuter + 700, config.bedLengthOuter),
- threeSpace(config.legSize / 2, config.bedWidthOuter),
- 250 - config.bedZOffset - config.bedHeight,
- ],
- camera: {
- narrow: {
- position: [
- -1200,
- -1000,
- 450,
- ],
- target: [
- -150,
- -150,
- -150,
- ],
- },
- wide: {
+ {
+ label: "What you need to provide",
+ info: {
+ description:
+
+ FarmBot must be plugged into a standard 100-265V AC outlet.
+ The power cable comes with a standard US 3-prong plug (NEMA 5-15P), which
+ can be used with a plug adapter for installations outside the US.
+
+
+ FarmBot can optionally be powered by
+ a solar
+ system with the appropriate battery and inverter. These
+ components may be purchased from a 3rd party.
+
+
+ FarmBot's water system has a 3/4″ female Garden Hose Thread (GHT)
+ connection, meaning you can take a standard US garden hose and
+ screw it into your FarmBot. You will need to provide a hose of
+ the appropriate length.
+
+
+ FarmBot requires an internet connection and supports both WiFi
+ and Ethernet. You may need to reposition your WiFi router or
+ install a repeater to ensure a reliable connection.
+
+
,
position: [
- -1000,
- -800,
- 600,
- ],
- target: [
- 0,
- -150,
+ 300,
+ -300,
0,
],
+ scale: 1000,
},
- },
- },
- {
- label: "Planter bed",
- info: {
- description:
-
- All FarmBots must be mounted to a raised
- bed or similar infrastructure. Neither materials for the bed nor
- soil are included with the kits because every installation will be
- different, and shipping lumber and soil would be prohibitively expensive.
-
-
,
position: [
- 0,
- -config.bedWidthOuter / 2,
- config.sizePreset == "Genesis XL" ? 1000 : 800,
+ threeSpace(config.bedLengthOuter + 700, config.bedLengthOuter),
+ threeSpace(config.legSize / 2, config.bedWidthOuter),
+ 250 - config.bedZOffset - config.bedHeight,
],
- scale: 1500,
+ camera: {
+ narrow: {
+ position: [
+ -1200,
+ -1000,
+ 450,
+ ],
+ target: [
+ -150,
+ -150,
+ -150,
+ ],
+ },
+ wide: {
+ position: [
+ -1000,
+ -800,
+ 600,
+ ],
+ target: [
+ 0,
+ -150,
+ 0,
+ ],
+ },
+ },
},
- position: [
- threeSpace(config.bedLengthOuter + 50, config.bedLengthOuter),
- 0,
- -config.bedHeight / 2,
- ],
- camera: {
- narrow: {
+ {
+ label: "Planter bed",
+ info: {
+ description:
+
+ All FarmBots must be mounted to a raised
+ bed or similar infrastructure. Neither materials for the bed nor
+ soil are included with the kits because every installation will be
+ different, and shipping lumber and soil would be prohibitively expensive.
+
+
,
position: [
- config.sizePreset == "Genesis XL" ? 9000 : 4500,
- config.sizePreset == "Genesis XL"
- ? -7000
- : -2500 - config.bedWidthOuter / 2,
- 1500,
- ],
- target: [
0,
-config.bedWidthOuter / 2,
- 0,
+ config.sizePreset == "Genesis XL" ? 1000 : 800,
],
+ scale: 1500,
},
- wide: {
- position: [
- 2000,
- config.sizePreset == "Genesis XL" ? -3000 : -2000,
- 800,
- ],
- target: [
- 0,
- -config.bedWidthOuter / 2,
- 500 - config.bedZOffset / 2,
- ],
+ position: [
+ threeSpace(config.bedLengthOuter + 50, config.bedLengthOuter),
+ 0,
+ -config.bedHeight / 2,
+ ],
+ camera: {
+ narrow: {
+ position: [
+ config.sizePreset == "Genesis XL" ? 9000 : 4500,
+ config.sizePreset == "Genesis XL"
+ ? -7000
+ : -2500 - config.bedWidthOuter / 2,
+ 1500,
+ ],
+ target: [
+ 0,
+ -config.bedWidthOuter / 2,
+ 0,
+ ],
+ },
+ wide: {
+ position: [
+ 2000,
+ config.sizePreset == "Genesis XL" ? -3000 : -2000,
+ 800,
+ ],
+ target: [
+ 0,
+ -config.bedWidthOuter / 2,
+ 500 - config.bedZOffset / 2,
+ ],
+ },
},
},
- },
-];
+ ];
+};
export const getFocus = (
config: Config,
From 83b2592de16be65dbcc45748b53fdb6c1ccdff6a Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Fri, 17 Apr 2026 14:54:59 -0700
Subject: [PATCH 04/11] fix 3D mirroring issues
---
.../__tests__/three_d_garden_map_test.tsx | 8 +++++---
.../farm_designer/map/legend/garden_map_legend.tsx | 14 ++++++++------
frontend/farm_designer/three_d_garden_map.tsx | 9 ++-------
.../garden/__tests__/images_test.tsx | 11 +----------
frontend/three_d_garden/garden/images.tsx | 14 +-------------
5 files changed, 17 insertions(+), 39 deletions(-)
diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
index 67f8c99d1b..098e0c1cc5 100644
--- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
+++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
@@ -133,11 +133,13 @@ describe(" ", () => {
expectedConfig.imgScale = 0.6;
expectedConfig.imgCenterX = 0;
expectedConfig.imgCenterY = 0;
+ expectedConfig.mirrorX = true;
+ expectedConfig.mirrorY = true;
const call = lastThreeDGardenProps();
expect(call).toEqual(expect.objectContaining({
config: expectedConfig,
- configPosition: { x: 1, y: 2, z: 3 },
+ configPosition: { x: 2999, y: 1498, z: 3 },
threeDPlants: [{
id: expect.any(Number),
icon: expect.any(String),
@@ -161,7 +163,7 @@ describe(" ", () => {
render( );
const call = lastThreeDGardenProps();
expect(call).toEqual(expect.objectContaining({
- configPosition: { x: 0, y: 0, z: 0 },
+ configPosition: { x: 3000, y: 1500, z: 0 },
threeDPlants: [],
addPlantProps: expect.any(Object),
...EMPTY_PROPS,
@@ -177,7 +179,7 @@ describe(" ", () => {
const call = lastThreeDGardenProps();
expect(call).toEqual(expect.objectContaining({
config: expect.objectContaining({ negativeZ: true }),
- configPosition: { x: 0, y: 0, z: -100 },
+ configPosition: { x: 3000, y: 1500, z: -100 },
threeDPlants: [],
addPlantProps: expect.any(Object),
...EMPTY_PROPS,
diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx
index bc3b6f013d..9c461aedaf 100644
--- a/frontend/farm_designer/map/legend/garden_map_legend.tsx
+++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx
@@ -215,8 +215,9 @@ const LayerToggles = (props: LayerTogglesProps) => {
;
};
-export const MapSettingsContent = (props: SettingsSubMenuProps) =>
-
+export const MapSettingsContent = (props: SettingsSubMenuProps) => {
+ const is3D = props.getConfigValue(BooleanSetting.three_d_garden);
+ return
helpText={Content.MAP_SIZE}>
-
- }
+ {!is3D &&
-
+ }
;
+};
const MapSettings = (props: SettingsSubMenuProps) =>
diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx
index 6fade06eda..404e47dbdb 100644
--- a/frontend/farm_designer/three_d_garden_map.tsx
+++ b/frontend/farm_designer/three_d_garden_map.tsx
@@ -75,15 +75,10 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => {
config.negativeZ = props.negativeZ;
config.exaggeratedZ = props.designer.threeDExaggeratedZ;
- const quadrant = props.mapTransformProps.quadrant;
- config.mirrorX = props.mapTransformProps.xySwap
- ? [3, 4].includes(quadrant)
- : [1, 4].includes(quadrant);
- config.mirrorY = props.mapTransformProps.xySwap
- ? [1, 4].includes(quadrant)
- : [3, 4].includes(quadrant);
const getValue = props.get3DConfigValue;
+ config.mirrorX = !!getValue("mirrorX");
+ config.mirrorY = !!getValue("mirrorY");
config.bedXOffset = getValue("bedXOffset");
config.bedYOffset = getValue("bedYOffset");
config.bedZOffset = getValue("bedZOffset");
diff --git a/frontend/three_d_garden/garden/__tests__/images_test.tsx b/frontend/three_d_garden/garden/__tests__/images_test.tsx
index cd565c5b16..b09bb4a218 100644
--- a/frontend/three_d_garden/garden/__tests__/images_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/images_test.tsx
@@ -2,7 +2,7 @@ let mockDemo = false;
import React from "react";
import { render, screen } from "@testing-library/react";
import {
- extraRotation, getImagePosition, getImageScale, getMirrorTextureProps,
+ extraRotation, getImagePosition, getMirrorTextureProps,
ImageTexture, ImageTextureProps,
} from "../images";
import { INITIAL } from "../../config";
@@ -172,15 +172,6 @@ describe("extraRotation()", () => {
});
});
-describe("getImageScale()", () => {
- it("returns mirrored image scale", () => {
- const config = clone(INITIAL);
- config.mirrorX = true;
- config.mirrorY = true;
- expect(getImageScale(config, 100, 200)).toEqual([-100, -200, 1000]);
- });
-});
-
describe("getMirrorTextureProps()", () => {
it("returns mirrored repeat and offset", () => {
const config = clone(INITIAL);
diff --git a/frontend/three_d_garden/garden/images.tsx b/frontend/three_d_garden/garden/images.tsx
index 75f71155e5..3ee0b11341 100644
--- a/frontend/three_d_garden/garden/images.tsx
+++ b/frontend/three_d_garden/garden/images.tsx
@@ -48,17 +48,6 @@ const PlaneWrapper = (props: PlaneWrapperProps) =>
{props.children}
;
-export const getImageScale = (
- config: Pick,
- width: number,
- height: number,
-): [number, number, number] =>
- [
- config.mirrorY ? -width : width,
- config.mirrorX ? -height : height,
- 1000,
- ];
-
export const getMirrorTextureProps =
(config: Pick) => ({
repeat: [
@@ -215,7 +204,6 @@ const ImageWrapper = (props: ImageWrapperProps) => {
if (!props.image.highlighted &&
!imageSizeCheck({ width: i.width, height: i.height },
{ x: "" + config.imgCenterX, y: "" + config.imgCenterY })) { return; }
- const scale = getImageScale(config, width, height);
const alreadyRotated = isRotated(props.image.body.meta.name);
const initialRotation = alreadyRotated ? 0 : config.imgRotation;
@@ -230,7 +218,7 @@ const ImageWrapper = (props: ImageWrapperProps) => {
material-side={DoubleSide}
depthTest={true}
rotation={[0, 0, rotation]}
- scale={scale} />;
+ scale={[width, height, 1000]} />;
};
export const extraRotation = (config: Config) => {
From 54db42e7804a776a6747f3a1c58a1759da7f355e Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Fri, 17 Apr 2026 14:55:15 -0700
Subject: [PATCH 05/11] add 3D camera selector
---
...d_new_3d_view_options_to_web_app_config.rb | 11 +++
.../__test_support__/fake_designer_state.ts | 3 +-
frontend/constants.ts | 9 +++
.../farm_designer/__tests__/reducer_test.ts | 9 +++
.../__tests__/three_d_garden_map_test.tsx | 3 +-
frontend/farm_designer/index.tsx | 2 +
frontend/farm_designer/interfaces.ts | 3 +-
frontend/farm_designer/map/interfaces.ts | 2 +
.../__tests__/garden_map_legend_test.tsx | 53 +++++++++++-
.../map/legend/garden_map_legend.tsx | 31 ++++++-
frontend/farm_designer/reducer.ts | 7 +-
frontend/farm_designer/three_d_garden_map.tsx | 11 ++-
frontend/session_keys.ts | 6 +-
frontend/settings/__tests__/index_test.tsx | 46 ++++++++---
frontend/settings/default_values.ts | 2 +
frontend/settings/farm_designer_settings.tsx | 28 +++++++
.../__tests__/camera_selection_ui_test.tsx | 81 +++++++++++++++++++
.../three_d_garden/__tests__/camera_test.ts | 55 +++++++++++--
.../three_d_garden/__tests__/config_test.ts | 8 ++
.../__tests__/garden_model_test.tsx | 20 +++--
.../three_d_garden/__tests__/helpers_test.ts | 17 ++++
.../three_d_garden/__tests__/index_test.tsx | 13 +++
frontend/three_d_garden/camera.ts | 43 ++++++++--
.../three_d_garden/camera_selection_ui.tsx | 63 +++++++++++++++
frontend/three_d_garden/config.ts | 20 +++--
frontend/three_d_garden/config_overlays.tsx | 3 +
frontend/three_d_garden/garden_model.tsx | 20 +++--
frontend/three_d_garden/helpers.ts | 12 +++
frontend/three_d_garden/index.tsx | 6 +-
.../scenes/props/__tests__/people_test.tsx | 18 ++++-
.../three_d_garden/scenes/props/people.tsx | 33 +++++---
31 files changed, 573 insertions(+), 65 deletions(-)
create mode 100644 db/migrate/20260417190743_add_new_3d_view_options_to_web_app_config.rb
create mode 100644 frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
create mode 100644 frontend/three_d_garden/camera_selection_ui.tsx
diff --git a/db/migrate/20260417190743_add_new_3d_view_options_to_web_app_config.rb b/db/migrate/20260417190743_add_new_3d_view_options_to_web_app_config.rb
new file mode 100644
index 0000000000..4ccfc37cd1
--- /dev/null
+++ b/db/migrate/20260417190743_add_new_3d_view_options_to_web_app_config.rb
@@ -0,0 +1,11 @@
+class AddNew3dViewOptionsToWebAppConfig < ActiveRecord::Migration[8.1]
+ def up
+ add_column :web_app_configs, :top_down_view, :boolean, default: false
+ add_column :web_app_configs, :viewpoint_heading, :integer, default: 0
+ end
+
+ def down
+ remove_column :web_app_configs, :top_down_view
+ remove_column :web_app_configs, :viewpoint_heading
+ end
+end
diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts
index 3edbefd273..53f5c3f3ed 100644
--- a/frontend/__test_support__/fake_designer_state.ts
+++ b/frontend/__test_support__/fake_designer_state.ts
@@ -51,7 +51,8 @@ export const fakeDesignerState = (): DesignerState => ({
cropRadius: undefined,
distanceIndicator: "",
panelOpen: true,
- threeDTopDownView: false,
+ threeDTopDownView: undefined,
+ threeDCameraSelection: false,
threeDExaggeratedZ: false,
threeDTime: undefined,
});
diff --git a/frontend/constants.ts b/frontend/constants.ts
index 3796bee82b..6c5855efde 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -1015,6 +1015,12 @@ export namespace Content {
trim(`Select a map origin by clicking on one of the four quadrants to
adjust the garden map to your viewing angle.`);
+ export const TOP_DOWN_VIEW =
+ trim(`Upon open, display the 3D garden map from a top-down perspective.`);
+
+ export const VIEWPOINT_HEADING =
+ trim(`Heading of the camera when the 3D garden map is opened (in degrees).`);
+
export const CROP_MAP_IMAGES =
trim(`Crop images displayed in the garden map to remove black borders
from image rotation. Crop amount determined by CAMERA ROTATION value.`);
@@ -2234,6 +2240,8 @@ export enum DeviceSetting {
mapSize = `Map size`,
rotateMap = `Rotate map`,
mapOrigin = `Map origin`,
+ openInTopDownView = `Open in top-down view`,
+ cameraLocationUponOpen = `Camera location upon open`,
cropMapImages = `Crop map images`,
clipPhotosOutOfBounds = `Clip photos out of bounds`,
cameraView = `Camera view`,
@@ -2562,6 +2570,7 @@ export enum Actions {
// 3D
SET_DISTANCE_INDICATOR = "SET_DISTANCE_INDICATOR",
TOGGLE_3D_TOP_DOWN_VIEW = "TOGGLE_3D_TOP_DOWN_VIEW",
+ TOGGLE_3D_CAMERA_SELECTION = "TOGGLE_3D_CAMERA_SELECTION",
TOGGLE_3D_EXAGGERATED_Z = "TOGGLE_3D_EXAGGERATED_Z",
SET_3D_TIME = "RESET_3D_TIME",
diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts
index 9d449acf06..43dbcc1579 100644
--- a/frontend/farm_designer/__tests__/reducer_test.ts
+++ b/frontend/farm_designer/__tests__/reducer_test.ts
@@ -135,6 +135,15 @@ describe("designer reducer", () => {
expect(newState.threeDTopDownView).toEqual(true);
});
+ it("sets camera", () => {
+ const action: ReduxAction = {
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: true,
+ };
+ const newState = designer(oldState(), action);
+ expect(newState.threeDCameraSelection).toEqual(true);
+ });
+
it("sets exaggerated z", () => {
const action: ReduxAction = {
type: Actions.TOGGLE_3D_EXAGGERATED_Z,
diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
index 098e0c1cc5..534e04d763 100644
--- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
+++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
@@ -59,7 +59,7 @@ describe(" ", () => {
designer: fakeDesignerState(),
plants: [fakePlant()],
dispatch: jest.fn(),
- getWebAppConfigValue: jest.fn(),
+ getWebAppConfigValue: () => 0,
curves: [],
mapPoints: [],
weeds: [],
@@ -135,6 +135,7 @@ describe(" ", () => {
expectedConfig.imgCenterY = 0;
expectedConfig.mirrorX = true;
expectedConfig.mirrorY = true;
+ expectedConfig.viewpointHeading = 0;
const call = lastThreeDGardenProps();
expect(call).toEqual(expect.objectContaining({
diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx
index ea8cad8f6a..cea3dcfaf0 100755
--- a/frontend/farm_designer/index.tsx
+++ b/frontend/farm_designer/index.tsx
@@ -188,6 +188,7 @@ export class RawFarmDesigner
showZones={show_zones}
showSensorReadings={show_sensor_readings}
showMoistureInterpolationMap={show_moisture_interpolation_map}
+ designer={this.props.designer}
dispatch={this.props.dispatch}
timeSettings={this.props.timeSettings}
getConfigValue={this.props.getConfigValue}
@@ -316,6 +317,7 @@ export class RawFarmDesigner
dispatch={this.props.dispatch}
device={this.props.device}
designer={this.props.designer}
+ getConfigValue={this.props.getConfigValue}
threeDGarden={threeDGarden} />
;
}
diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts
index 54ff2e0c34..e5b0a9dba4 100644
--- a/frontend/farm_designer/interfaces.ts
+++ b/frontend/farm_designer/interfaces.ts
@@ -179,7 +179,8 @@ export interface DesignerState {
cropRadius: number | undefined;
distanceIndicator: string;
panelOpen: boolean;
- threeDTopDownView: boolean;
+ threeDTopDownView: boolean | undefined;
+ threeDCameraSelection: boolean;
threeDExaggeratedZ: boolean;
threeDTime: string | undefined;
}
diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts
index b606eded99..ec528b425a 100644
--- a/frontend/farm_designer/map/interfaces.ts
+++ b/frontend/farm_designer/map/interfaces.ts
@@ -9,6 +9,7 @@ import type {
} from "farmbot";
import type {
State, BotOriginQuadrant, MountedToolInfo, CameraCalibrationData,
+ DesignerState,
} from "../interfaces";
import type {
BotPosition, BotLocationData, SourceFbosConfig,
@@ -64,6 +65,7 @@ export interface GardenMapLegendProps {
firmwareConfig: McuParams;
botLocationData: BotLocationData;
botSize: BotSize;
+ designer: DesignerState;
}
export type MapTransformProps = {
diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
index c229ecd091..c9ca3b9000 100644
--- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
+++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
@@ -9,7 +9,7 @@ import {
ZoomControlsProps,
} from "../garden_map_legend";
import { GardenMapLegendProps } from "../../interfaces";
-import { BooleanSetting } from "../../../../session_keys";
+import { BooleanSetting, NumericSetting } from "../../../../session_keys";
import * as zoom from "../../zoom";
import {
fakeTimeSettings,
@@ -21,6 +21,8 @@ import {
import {
fakeFirmwareConfig,
} from "../../../../__test_support__/fake_state/resources";
+import { fakeDesignerState } from "../../../../__test_support__/fake_designer_state";
+import { Actions } from "../../../../constants";
let atMaxZoomSpy: jest.SpyInstance;
let atMinZoomSpy: jest.SpyInstance;
@@ -67,6 +69,7 @@ describe("
", () => {
firmwareConfig: fakeFirmwareConfig().body,
botLocationData: fakeBotLocationData(),
botSize: fakeBotSize(),
+ designer: fakeDesignerState(),
});
it("renders", () => {
@@ -137,6 +140,7 @@ const fakeProps = (): SettingsSubMenuProps => ({
dispatch: jest.fn(),
getConfigValue: () => true,
firmwareConfig: fakeFirmwareConfig().body,
+ designer: fakeDesignerState(),
});
describe("
", () => {
@@ -185,4 +189,51 @@ describe("
", () => {
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
BooleanSetting.dynamic_map, false);
});
+
+ it("shows 2D-only controls", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => key != BooleanSetting.three_d_garden;
+ const { container } = render(
);
+ expect(container.textContent).toContain("Rotate map");
+ expect(container.textContent).toContain("Map origin");
+ expect(container.textContent).not.toContain("Camera location upon open");
+ });
+
+ it("shows 3D-only controls", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => key == BooleanSetting.three_d_garden;
+ const { container } = render(
);
+ expect(container.textContent).toContain("Open in top-down view");
+ expect(container.textContent).toContain("Camera location upon open");
+ expect(container.textContent).toContain("Enable camera heading selection view");
+ expect(container.textContent).not.toContain("Rotate map");
+ });
+
+ it("changes viewpoint heading in 3D settings", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => {
+ if (key == BooleanSetting.three_d_garden) { return true; }
+ if (key == NumericSetting.viewpoint_heading) { return 0; }
+ return false;
+ };
+ const { container } = render(
);
+ const quadrants = container.querySelectorAll(".quadrant");
+ fireEvent.click(quadrants[quadrants.length - 1]);
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ NumericSetting.viewpoint_heading, 270);
+ });
+
+ it("toggles camera selection view", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => key == BooleanSetting.three_d_garden;
+ const { container } = render(
);
+ const toggleBtn =
+ container.querySelector("button[title='Enable camera heading selection view']");
+ if (!toggleBtn) { throw new Error("Missing camera selection toggle"); }
+ fireEvent.click(toggleBtn);
+ expect(p.dispatch).toHaveBeenCalledWith({
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: undefined,
+ });
+ });
});
diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx
index 9c461aedaf..c3d1f8c6e7 100644
--- a/frontend/farm_designer/map/legend/garden_map_legend.tsx
+++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx
@@ -13,7 +13,7 @@ import {
import { BooleanSetting } from "../../../session_keys";
import { t } from "../../../i18next_wrapper";
import { SelectModeLink } from "../../../plants/select_plants";
-import { DeviceSetting, Content } from "../../../constants";
+import { DeviceSetting, Content, Actions } from "../../../constants";
import { Help, Popover, ToggleButton } from "../../../ui";
import {
BooleanConfigKey as WebAppBooleanConfigKey,
@@ -22,8 +22,9 @@ import { ZDisplay, ZDisplayToggle } from "./z_display";
import { getModifiedClassName } from "../../../settings/default_values";
import { Position } from "@blueprintjs/core";
import { MapSizeInputs } from "../../map_size_setting";
-import { OriginSelector } from "../../../settings/farm_designer_settings";
+import { HeadingSelector, OriginSelector } from "../../../settings/farm_designer_settings";
import { McuParams } from "farmbot";
+import { DesignerState } from "../../interfaces";
export interface ZoomControlsProps {
zoom(value: number): () => void;
@@ -89,6 +90,7 @@ export interface SettingsSubMenuProps {
dispatch: Function;
getConfigValue: GetWebAppConfigValue;
firmwareConfig: McuParams;
+ designer: DesignerState;
}
export const PointsSubMenu = (props: SettingsSubMenuProps) =>
@@ -127,8 +129,8 @@ export const FarmbotSubMenu = (props: SettingsSubMenuProps) =>
interface LayerTogglesProps extends GardenMapLegendProps { }
const LayerToggles = (props: LayerTogglesProps) => {
- const { toggle, getConfigValue, dispatch, firmwareConfig } = props;
- const subMenuProps = { dispatch, getConfigValue, firmwareConfig };
+ const { toggle, getConfigValue, dispatch, firmwareConfig, designer } = props;
+ const subMenuProps = { dispatch, getConfigValue, firmwareConfig, designer };
const is3D = getConfigValue(BooleanSetting.three_d_garden);
const only2DClass = is3D ? "disabled" : "";
return
@@ -236,6 +238,26 @@ export const MapSettingsContent = (props: SettingsSubMenuProps) => {
helpText={Content.MAP_ORIGIN}>
}
+ {is3D &&
}
+ {is3D &&
+
+ }
+ {is3D &&
+ {t("Enable camera heading selection view")}
+ props.dispatch({
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: undefined,
+ })}
+ toggleValue={props.designer.threeDCameraSelection} />
+
}
;
};
@@ -272,6 +294,7 @@ export function GardenMapLegend(props: GardenMapLegendProps) {
diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts
index ffb0d1c413..b3e1ceb29c 100644
--- a/frontend/farm_designer/reducer.ts
+++ b/frontend/farm_designer/reducer.ts
@@ -59,7 +59,8 @@ export const initialState: DesignerState = {
cropRadius: undefined,
distanceIndicator: "",
panelOpen: true,
- threeDTopDownView: false,
+ threeDTopDownView: undefined,
+ threeDCameraSelection: false,
threeDExaggeratedZ: false,
threeDTime: undefined,
};
@@ -246,6 +247,10 @@ export const designer = generateReducer
(initialState)
s.threeDTopDownView = payload;
return s;
})
+ .add(Actions.TOGGLE_3D_CAMERA_SELECTION, (s) => {
+ s.threeDCameraSelection = !s.threeDCameraSelection;
+ return s;
+ })
.add(Actions.TOGGLE_3D_EXAGGERATED_Z, (s, { payload }) => {
s.threeDExaggeratedZ = payload;
return s;
diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx
index 404e47dbdb..43fee1869d 100644
--- a/frontend/farm_designer/three_d_garden_map.tsx
+++ b/frontend/farm_designer/three_d_garden_map.tsx
@@ -13,7 +13,7 @@ import {
} from "farmbot";
import { CameraCalibrationData, DesignerState } from "./interfaces";
import { GetWebAppConfigValue } from "../config_storage/actions";
-import { BooleanSetting } from "../session_keys";
+import { BooleanSetting, NumericSetting } from "../session_keys";
import { SlotWithTool } from "../resources/interfaces";
import { calcSunCoordinate, ThreeDGardenPlant } from "../three_d_garden/garden";
import { findCrop, findIcon } from "../crops/find";
@@ -25,6 +25,7 @@ import { get3DTime, latLng } from "../three_d_garden/time_travel";
import { parseCalibrationData } from "./map/layers/images/map_image";
import { fetchInterpolationOptions } from "./map/layers/points/interpolation_map";
import { unpackUUID } from "../util";
+import { isTopDown } from "../three_d_garden/helpers";
export interface ThreeDGardenMapProps {
botSize: BotSize;
@@ -180,10 +181,14 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => {
config.interpolationUseNearest = options.useNearest;
config.interpolationPower = options.power;
+ config.topDown = isTopDown(props.designer, props.getWebAppConfigValue);
config.zoom = true;
config.pan = true;
- config.rotate = !props.designer.threeDTopDownView;
- config.perspective = !props.designer.threeDTopDownView;
+ config.rotate = !config.topDown;
+ config.perspective = !config.topDown;
+ config.viewpointHeading =
+ parseInt("" + props.getWebAppConfigValue(NumericSetting.viewpoint_heading));
+ config.cameraSelectionView = props.designer.threeDCameraSelection;
const lastCaptureTime = React.useMemo(() => {
const localIds = props.logs
diff --git a/frontend/session_keys.ts b/frontend/session_keys.ts
index e87b666d11..f1b3cb2150 100644
--- a/frontend/session_keys.ts
+++ b/frontend/session_keys.ts
@@ -4,8 +4,8 @@ import {
StringConfigKey as WebAppStringConfigKey,
} from "farmbot/dist/resources/configs/web_app";
-type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey;
-type WebAppNumberConfigKeyAll = WebAppNumberConfigKey;
+type WebAppBooleanConfigKeyAll = WebAppBooleanConfigKey | "top_down_view";
+type WebAppNumberConfigKeyAll = WebAppNumberConfigKey | "viewpoint_heading";
type WebAppStringConfigKeyAll = WebAppStringConfigKey;
type BooleanSettings = Record;
@@ -50,6 +50,7 @@ export const BooleanSetting: BooleanSettings = {
clip_image_layer: "clip_image_layer",
highlight_modified_settings: "highlight_modified_settings",
three_d_garden: "three_d_garden",
+ top_down_view: "top_down_view" as WebAppBooleanConfigKey,
/** Sequence settings */
confirm_step_deletion: "confirm_step_deletion",
@@ -96,6 +97,7 @@ export const NumericSetting: NumericSettings = {
map_size_y: "map_size_y",
bot_origin_quadrant: "bot_origin_quadrant",
default_plant_depth: "default_plant_depth",
+ viewpoint_heading: "viewpoint_heading" as WebAppNumberConfigKey,
/** App settings */
beep_verbosity: "beep_verbosity",
diff --git a/frontend/settings/__tests__/index_test.tsx b/frontend/settings/__tests__/index_test.tsx
index 345a1d88f8..3872ae2508 100644
--- a/frontend/settings/__tests__/index_test.tsx
+++ b/frontend/settings/__tests__/index_test.tsx
@@ -1,5 +1,6 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
+import { changeBlurableInputRTL } from "../../__test_support__/helpers";
import { RawDesignerSettings as DesignerSettings } from "../index";
import { DesignerSettingsProps } from "../interfaces";
import { BooleanSetting, NumericSetting } from "../../session_keys";
@@ -25,12 +26,14 @@ import * as bootSequenceSelector from "../fbos_settings/boot_sequence_selector";
const EMPTY_RESOURCE_INDEX = buildResourceIndex([]).index;
-const getSetting =
- (container: HTMLElement, position: number, containsString: string) => {
- const setting = container.querySelectorAll(".designer-setting")[position] as HTMLElement;
- expect(setting.textContent?.toLowerCase())
- .toContain(containsString.toLowerCase());
- return setting;
+const getSettingByText =
+ (container: HTMLElement, containsString: string) => {
+ const settings = Array.from(container.querySelectorAll(".designer-setting"))
+ .filter((setting): setting is HTMLElement => setting instanceof HTMLElement);
+ const setting = settings.find(setting =>
+ setting.textContent?.toLowerCase().includes(containsString.toLowerCase()));
+ expect(setting).toBeTruthy();
+ return setting as HTMLElement;
};
describe(" ", () => {
@@ -171,7 +174,7 @@ describe(" ", () => {
config.body.confirm_plant_deletion = undefined as never;
p.getConfigValue = key => config.body[key];
const { container } = render( );
- const confirmDeletion = getSetting(container, 12, "confirm plant");
+ const confirmDeletion = getSettingByText(container, "confirm plant");
expect(confirmDeletion.querySelector("button")?.textContent).toEqual("on");
});
@@ -179,7 +182,7 @@ describe(" ", () => {
const p = fakeProps();
p.settingsPanelState.farm_designer = true;
const { container } = render( );
- const trailSetting = getSetting(container, 1, "trail");
+ const trailSetting = getSettingByText(container, "trail");
const button = trailSetting.querySelector("button");
if (!button) { throw new Error("Expected trail toggle button"); }
fireEvent.click(button);
@@ -192,13 +195,38 @@ describe(" ", () => {
p.settingsPanelState.farm_designer = true;
p.getConfigValue = () => 2;
const { container } = render( );
- const originSetting = getSetting(container, 6, "origin");
+ const originSetting = getSettingByText(container, "origin");
const quadrants = originSetting.querySelectorAll(".quadrant");
fireEvent.click(quadrants[quadrants.length - 1]);
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
NumericSetting.bot_origin_quadrant, 4);
});
+ it("toggles top down view", () => {
+ const p = fakeProps();
+ p.settingsPanelState.farm_designer = true;
+ const { container } = render( );
+ const topDownSetting = getSettingByText(container, "open in top-down view");
+ const button = topDownSetting.querySelector("button");
+ if (!button) { throw new Error("Expected top down toggle button"); }
+ fireEvent.click(button);
+ expect(setWebAppConfigValueSpy)
+ .toHaveBeenCalledWith(BooleanSetting.top_down_view, true);
+ });
+
+ it("changes viewpoint heading", () => {
+ const p = fakeProps();
+ p.settingsPanelState.farm_designer = true;
+ p.getConfigValue = key => key == NumericSetting.viewpoint_heading ? 0 : 2;
+ const { container } = render( );
+ const headingSetting = getSettingByText(container, "camera location upon open");
+ const input = headingSetting.querySelector("input");
+ if (!input) { throw new Error("Expected viewpoint heading input"); }
+ changeBlurableInputRTL(input, "270");
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ NumericSetting.viewpoint_heading, 270);
+ });
+
it("renders env editor", () => {
const p = fakeProps();
p.searchTerm = "env";
diff --git a/frontend/settings/default_values.ts b/frontend/settings/default_values.ts
index 0f3f874c7d..6fd01aa839 100644
--- a/frontend/settings/default_values.ts
+++ b/frontend/settings/default_values.ts
@@ -89,6 +89,8 @@ const DEFAULT_WEB_APP_CONFIG_VALUES: Record = {
show_advanced_settings: false,
three_d_garden: false,
dark_mode: true,
+ ["top_down_view" as Key]: false,
+ ["viewpoint_heading" as Key]: 0,
};
const DEFAULT_EXPRESS_WEB_APP_CONFIG_VALUES =
diff --git a/frontend/settings/farm_designer_settings.tsx b/frontend/settings/farm_designer_settings.tsx
index 1db46429ba..a4791ade59 100644
--- a/frontend/settings/farm_designer_settings.tsx
+++ b/frontend/settings/farm_designer_settings.tsx
@@ -128,6 +128,16 @@ const DESIGNER_SETTINGS =
description: Content.MAP_ORIGIN,
children:
},
+ {
+ title: DeviceSetting.openInTopDownView,
+ description: Content.TOP_DOWN_VIEW,
+ setting: BooleanSetting.top_down_view,
+ },
+ {
+ title: DeviceSetting.cameraLocationUponOpen,
+ description: Content.VIEWPOINT_HEADING,
+ numberSetting: NumericSetting.viewpoint_heading,
+ },
{
title: DeviceSetting.cropMapImages,
description: Content.CROP_MAP_IMAGES,
@@ -180,3 +190,21 @@ export const OriginSelector = (props: DesignerSettingsPropsBase) => {
;
};
+
+export const HeadingSelector = (props: DesignerSettingsPropsBase) => {
+ const settingKey = NumericSetting.viewpoint_heading;
+ const heading = props.getConfigValue(settingKey);
+ const update = (value: number) => () =>
+ props.dispatch(setWebAppConfigValue(settingKey, value));
+ return
+
+ {[180, 0, 90, 270].map(angle =>
+
)}
+
+
;
+};
diff --git a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
new file mode 100644
index 0000000000..a97a678677
--- /dev/null
+++ b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
@@ -0,0 +1,81 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import { CameraSelectionUI } from "../camera_selection_ui";
+import { clone } from "lodash";
+import { INITIAL } from "../config";
+import * as configStorageActions from "../../config_storage/actions";
+import { NumericSetting } from "../../session_keys";
+import {
+ actRenderer,
+ createRenderer,
+ unmountRenderer,
+} from "../../__test_support__/test_renderer";
+
+describe(" ", () => {
+ let setWebAppConfigValueSpy: jest.SpyInstance;
+ const mountedWrappers: ReturnType[] = [];
+
+ beforeEach(() => {
+ setWebAppConfigValueSpy = jest.spyOn(configStorageActions, "setWebAppConfigValue")
+ .mockImplementation(jest.fn());
+ });
+
+ afterEach(() => {
+ mountedWrappers.splice(0).forEach(wrapper =>
+ unmountRenderer(wrapper));
+ setWebAppConfigValueSpy.mockRestore();
+ });
+
+ const fakeConfig = () => {
+ const config = clone(INITIAL);
+ config.bedHeight = 100;
+ return config;
+ };
+
+ it("renders hidden by default", () => {
+ const wrapper = createRenderer(
+ );
+ mountedWrappers.push(wrapper);
+ const group = wrapper.root.findAll(node =>
+ node.props.name == "camera-selection")[0];
+ expect(group?.props.visible).toEqual(false);
+ });
+
+ it("renders unique heading marker", () => {
+ const config = fakeConfig();
+ config.cameraSelectionView = true;
+ config.viewpointHeading = 45;
+ const { container } = render(
+ );
+ expect(container.querySelectorAll(".spherehead").length).toEqual(5);
+ });
+
+ it("dispatches heading update", () => {
+ const config = fakeConfig();
+ config.cameraSelectionView = true;
+ const dispatch = jest.fn();
+ const wrapper = createRenderer(
+ );
+ mountedWrappers.push(wrapper);
+ const groups = wrapper.root.findAll(node => node.props.onClick);
+ actRenderer(() => {
+ groups[0]?.props.onClick();
+ });
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ NumericSetting.viewpoint_heading, 0);
+ expect(dispatch).toHaveBeenCalled();
+ });
+
+ it("handles missing dispatch", () => {
+ const config = fakeConfig();
+ config.cameraSelectionView = true;
+ const wrapper = createRenderer(
+ );
+ mountedWrappers.push(wrapper);
+ const groups = wrapper.root.findAll(node => node.props.onClick);
+ actRenderer(() => {
+ groups[0]?.props.onClick();
+ });
+ expect(setWebAppConfigValueSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/frontend/three_d_garden/__tests__/camera_test.ts b/frontend/three_d_garden/__tests__/camera_test.ts
index 0b49df5267..1e2fe6d0f6 100644
--- a/frontend/three_d_garden/__tests__/camera_test.ts
+++ b/frontend/three_d_garden/__tests__/camera_test.ts
@@ -1,7 +1,9 @@
let mockDev: string | undefined = undefined;
let mockIsDesktop = true;
-import { cameraInit } from "../camera";
+import {
+ cameraInit, CameraInitProps, getDefaultCameraPosition,
+} from "../camera";
import * as devSupport from "../../settings/dev/dev_support";
import * as screenSize from "../../screen_size";
@@ -10,7 +12,8 @@ let isDesktopSpy: jest.SpyInstance;
beforeEach(() => {
get3dCameraSpy = jest.spyOn(devSupport.DevSettings, "get3dCamera")
- .mockImplementation((() => mockDev || "") as typeof devSupport.DevSettings.get3dCamera);
+ .mockImplementation((() =>
+ mockDev || "") as typeof devSupport.DevSettings.get3dCamera);
isDesktopSpy = jest.spyOn(screenSize, "isDesktop")
.mockImplementation(() => mockIsDesktop);
});
@@ -21,10 +24,15 @@ afterEach(() => {
});
describe("cameraInit()", () => {
+ const fakeProps = (): CameraInitProps => ({
+ topDown: false,
+ viewpointHeading: 0,
+ });
+
it("initializes camera", () => {
mockDev = undefined;
mockIsDesktop = true;
- expect(cameraInit(false)).toEqual({
+ expect(cameraInit(fakeProps())).toEqual({
position: [2000, -4000, 2500],
target: [0, 0, 0],
});
@@ -33,13 +41,16 @@ describe("cameraInit()", () => {
it("initializes camera: dev", () => {
mockDev = JSON.stringify({ position: [0, 0, 0], target: [0, 0, 0] });
mockIsDesktop = true;
- expect(cameraInit(false)).toEqual({ position: [0, 0, 0], target: [0, 0, 0] });
+ expect(cameraInit(fakeProps())).toEqual({
+ position: [0, 0, 0],
+ target: [0, 0, 0],
+ });
});
it("handles invalid dev camera setting", () => {
mockDev = "{";
mockIsDesktop = true;
- expect(cameraInit(false)).toEqual({
+ expect(cameraInit(fakeProps())).toEqual({
position: [2000, -4000, 2500],
target: [0, 0, 0],
});
@@ -48,7 +59,7 @@ describe("cameraInit()", () => {
it("initializes camera: mobile", () => {
mockDev = undefined;
mockIsDesktop = false;
- expect(cameraInit(false)).toEqual({
+ expect(cameraInit(fakeProps())).toEqual({
position: [5400, -2500, 3400],
target: [0, 0, 0],
});
@@ -57,9 +68,39 @@ describe("cameraInit()", () => {
it("initializes camera: top-down", () => {
mockDev = undefined;
mockIsDesktop = false;
- expect(cameraInit(true)).toEqual({
+ const p = fakeProps();
+ p.topDown = true;
+ expect(cameraInit(p)).toEqual({
position: [0, 0, 5000],
target: [0, 0, 0],
});
});
+
+ it("initializes camera from heading", () => {
+ mockDev = undefined;
+ mockIsDesktop = true;
+ const p = fakeProps();
+ p.viewpointHeading = 90;
+ expect(cameraInit(p)).toEqual({
+ position: [2000, 4000, 2500],
+ target: [0, 0, 0],
+ });
+ });
+});
+
+describe("getDefaultCameraPosition()", () => {
+ it("returns desktop position", () => {
+ mockIsDesktop = true;
+ expect(getDefaultCameraPosition(180)).toEqual([-2000, 4000, 2500]);
+ });
+
+ it("returns mobile position", () => {
+ mockIsDesktop = false;
+ expect(getDefaultCameraPosition(270)).toEqual([-5400, -2500, 3400]);
+ });
+
+ it("returns top down position", () => {
+ mockIsDesktop = true;
+ expect(getDefaultCameraPosition(90, true)).toEqual([5657, 0, 5000]);
+ });
});
diff --git a/frontend/three_d_garden/__tests__/config_test.ts b/frontend/three_d_garden/__tests__/config_test.ts
index fffb3c9f78..acc38bbc81 100644
--- a/frontend/three_d_garden/__tests__/config_test.ts
+++ b/frontend/three_d_garden/__tests__/config_test.ts
@@ -54,6 +54,14 @@ describe("modifyConfig()", () => {
const result = modifyConfig(initial, { otherPreset: "Initial" });
expect(result.bedHeight).toEqual(300);
});
+
+ it("modifies config: top down", () => {
+ const initial = clone(INITIAL);
+ const result = modifyConfig(initial, { topDown: true });
+ expect(result.topDown).toEqual(true);
+ expect(result.perspective).toEqual(false);
+ expect(result.rotate).toEqual(false);
+ });
});
describe("modifyConfigsFromUrlParams()", () => {
diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx
index 0c1020b74a..59cf406a63 100644
--- a/frontend/three_d_garden/__tests__/garden_model_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx
@@ -2,7 +2,7 @@ let mockIsDesktop = false;
let mockIsMobile = false;
import React from "react";
-import { useTexture } from "@react-three/drei";
+import { OrbitControls, useTexture } from "@react-three/drei";
import { GardenModelProps, GardenModel } from "../garden_model";
import { clone } from "lodash";
import { INITIAL, INITIAL_POSITION, SurfaceDebugOption } from "../config";
@@ -87,11 +87,21 @@ describe(" ", () => {
it("renders top down view", () => {
mockIsMobile = true;
const p = fakeProps();
- const addPlantProps = fakeAddPlantProps();
- addPlantProps.designer.threeDTopDownView = true;
- p.addPlantProps = addPlantProps;
+ p.config.topDown = true;
+ p.config.viewpointHeading = 90;
+ const wrapper = createWrapper(p);
+ const orbitControls = wrapper.root.findByType(OrbitControls);
+ expect(orbitControls.props.minAzimuthAngle).toEqual(Math.PI / 2);
+ expect(orbitControls.props.maxAzimuthAngle).toEqual(Math.PI / 2);
+ });
+
+ it("renders camera selection view", () => {
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ p.config.viewpointHeading = 45;
const { container } = render( );
- expect(container.innerHTML).toContain("darkgreen");
+ expect(container.innerHTML).toContain("camera-selection");
+ expect(container.innerHTML).toContain("person_2.avif");
});
it("renders no user plants", () => {
diff --git a/frontend/three_d_garden/__tests__/helpers_test.ts b/frontend/three_d_garden/__tests__/helpers_test.ts
index 211fd3fc2e..c5a00da49d 100644
--- a/frontend/three_d_garden/__tests__/helpers_test.ts
+++ b/frontend/three_d_garden/__tests__/helpers_test.ts
@@ -4,6 +4,7 @@ import {
getColorFromBrightness,
get3DPositionFunc,
getGardenPositionFunc,
+ isTopDown,
getWorldPositionFunc,
threeSpace,
zDir,
@@ -11,6 +12,7 @@ import {
} from "../helpers";
import { INITIAL } from "../config";
import * as THREE from "three";
+import { fakeDesignerState } from "../../__test_support__/fake_designer_state";
describe("threeSpace()", () => {
it("returns adjusted position", () => {
@@ -98,3 +100,18 @@ describe("mirror-aware position helpers", () => {
expect(getWorldPosition({ x: 125, y: 250, z: 10 })).toEqual([375, 50, 150]);
});
});
+
+describe("isTopDown()", () => {
+ it("returns local state when defined", () => {
+ const designer = fakeDesignerState();
+ designer.threeDTopDownView = false;
+ expect(isTopDown(designer, () => true)).toEqual(false);
+ });
+
+ it("falls back to saved config when local state is undefined", () => {
+ const designer = fakeDesignerState();
+ designer.threeDTopDownView = undefined;
+ expect(isTopDown(designer, () => true)).toEqual(true);
+ expect(isTopDown(designer, () => false)).toEqual(false);
+ });
+});
diff --git a/frontend/three_d_garden/__tests__/index_test.tsx b/frontend/three_d_garden/__tests__/index_test.tsx
index b5235f1610..2937660acc 100644
--- a/frontend/three_d_garden/__tests__/index_test.tsx
+++ b/frontend/three_d_garden/__tests__/index_test.tsx
@@ -43,6 +43,7 @@ describe(" ", () => {
device: fakeDevice().body,
designer: fakeDesignerState(),
threeDGarden: true,
+ getConfigValue: jest.fn(),
});
it("renders off", () => {
@@ -75,6 +76,18 @@ describe(" ", () => {
});
});
+ it("uses saved top down setting", () => {
+ const p = fakeProps();
+ p.getConfigValue = () => true;
+ render( );
+ const isoViewButton = screen.getByTitle("3D View");
+ fireEvent.click(isoViewButton);
+ expect(p.dispatch).toHaveBeenCalledWith({
+ type: Actions.TOGGLE_3D_TOP_DOWN_VIEW,
+ payload: false,
+ });
+ });
+
it("enables top down view", () => {
const p = fakeProps();
render( );
diff --git a/frontend/three_d_garden/camera.ts b/frontend/three_d_garden/camera.ts
index 0b176c9bb4..3cc54a69b0 100644
--- a/frontend/three_d_garden/camera.ts
+++ b/frontend/three_d_garden/camera.ts
@@ -1,8 +1,15 @@
+import { round } from "lodash";
import { isDesktop } from "../screen_size";
import { DevSettings } from "../settings/dev/dev_support";
import { Camera } from "./zoom_beacons_constants";
-export const cameraInit = (topDown: boolean): Camera => {
+export interface CameraInitProps {
+ topDown: boolean;
+ viewpointHeading: number;
+}
+
+export const cameraInit = (props: CameraInitProps): Camera => {
+ const { topDown, viewpointHeading } = props;
const devCameraString = DevSettings.get3dCamera();
let devCamera;
try {
@@ -11,13 +18,10 @@ export const cameraInit = (topDown: boolean): Camera => {
devCamera = undefined;
}
- const defaultCameraPosition = isDesktop()
- ? [2000, -4000, 2500]
- : [5400, -2500, 3400];
const topDownCameraPosition = topDown ? [0, 0, 5000] : undefined;
const cameraPositionInit = topDownCameraPosition
|| devCamera?.position
- || defaultCameraPosition;
+ || getDefaultCameraPosition(viewpointHeading);
const defaultCameraTarget = [0, 0, 0];
const topDownCameraTarget = topDown ? [0, 0, 0] : undefined;
@@ -31,3 +35,32 @@ export const cameraInit = (topDown: boolean): Camera => {
};
return initCamera;
};
+
+export const getDefaultCameraPosition = (
+ heading: number,
+ topDown = false,
+): [number, number, number] => {
+ const radians = heading * Math.PI / 180;
+
+ if (topDown) {
+ const phase = Math.PI / 2;
+ return [
+ round(4000 * Math.SQRT2 * Math.cos(radians - phase)),
+ round(2000 * Math.SQRT2 * Math.sin(radians - phase)),
+ 5000,
+ ];
+ }
+
+ const phase = Math.PI / 4;
+ return isDesktop()
+ ? [
+ round(2000 * Math.SQRT2 * Math.cos(radians - phase)),
+ round(4000 * Math.SQRT2 * Math.sin(radians - phase)),
+ 2500,
+ ]
+ : [
+ round(5400 * Math.SQRT2 * Math.cos(radians - phase)),
+ round(2500 * Math.SQRT2 * Math.sin(radians - phase)),
+ 3400,
+ ];
+};
diff --git a/frontend/three_d_garden/camera_selection_ui.tsx b/frontend/three_d_garden/camera_selection_ui.tsx
new file mode 100644
index 0000000000..5f4a2fd2aa
--- /dev/null
+++ b/frontend/three_d_garden/camera_selection_ui.tsx
@@ -0,0 +1,63 @@
+import React from "react";
+import { Config } from "./config";
+import { getDefaultCameraPosition } from "./camera";
+import { Cylinder, Sphere } from "@react-three/drei";
+import { Group, MeshPhongMaterial } from "./components";
+import { uniq } from "lodash";
+import { setWebAppConfigValue } from "../config_storage/actions";
+import { NumericSetting } from "../session_keys";
+import { Person } from "./scenes/props";
+import { ASSETS } from "./constants";
+
+export interface CameraSelectionUIProps {
+ config: Config;
+ dispatch: Function | undefined;
+}
+
+export const CameraSelectionUI = (props: CameraSelectionUIProps) => {
+ const { config } = props;
+ const [hovered, setHovered] = React.useState(undefined);
+ return
+ {uniq([0, 90, 180, 270, config.viewpointHeading]).map(angle => {
+ const selected = angle == config.viewpointHeading;
+ const baseColor = selected ? "blue" : "orange";
+ const color = hovered == angle ? "cyan" : baseColor;
+ const position = getDefaultCameraPosition(angle, config.topDown);
+ const scaledPosition = position.map(p => p * 0.33) as [number, number, number];
+ const height = scaledPosition[2] + config.bedHeight;
+ return setHovered(angle)}
+ onPointerOut={() => setHovered(undefined)}
+ onClick={() => props.dispatch &&
+ props.dispatch(setWebAppConfigValue(
+ NumericSetting.viewpoint_heading, angle))}>
+
+
+
+
+
+
+
+ ;
+ })}
+ ;
+};
diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts
index a4e7552b9f..c8de3d920b 100644
--- a/frontend/three_d_garden/config.ts
+++ b/frontend/three_d_garden/config.ts
@@ -42,6 +42,9 @@ export interface Config {
sunAzimuth: number;
heading: number;
perspective: boolean;
+ topDown: boolean;
+ viewpointHeading: number;
+ cameraSelectionView: boolean;
bot: boolean;
laser: boolean;
tool: string;
@@ -166,6 +169,9 @@ export const INITIAL: ConfigWithPosition = {
sunAzimuth: 230,
heading: 0,
perspective: true,
+ topDown: false,
+ viewpointHeading: 0,
+ cameraSelectionView: false,
bot: true,
laser: false,
tool: "rotaryTool",
@@ -248,12 +254,12 @@ export const NUMBER_KEYS = [
"soilSurfacePointCount", "soilSurfaceVariance", "sun", "ambient", "rotary",
"imgScale", "imgRotation", "imgOffsetX", "imgOffsetY", "imgCalZ",
"imgCenterX", "imgCenterY", "surfaceDebug", "interpolationStepSize",
- "interpolationPower", "lastImageCapture",
+ "interpolationPower", "lastImageCapture", "viewpointHeading",
];
export const BOOLEAN_KEYS = [
"legsFlush", "labels", "labelsOnHover", "ground", "grid", "axes", "trail",
- "tracks", "clouds", "perspective", "bot", "laser", "cableCarriers",
+ "tracks", "clouds", "perspective", "topDown", "bot", "laser", "cableCarriers",
"viewCube", "stats", "config", "zoom", "pan", "rotate", "bounds", "threeAxes",
"xyDimensions", "zDimension", "promoInfo", "settingsBar", "zoomBeacons",
"solar", "utilitiesPost", "packaging", "people", "lowDetail",
@@ -261,7 +267,7 @@ export const BOOLEAN_KEYS = [
"animate", "animateSeasons", "negativeZ",
"waterFlow", "exaggeratedZ", "showSoilPoints", "urlParamAutoAdd",
"light", "vacuum", "north", "desk", "interpolationUseNearest", "promoSpread",
- "cameraView", "mirrorX", "mirrorY",
+ "cameraView", "mirrorX", "mirrorY", "cameraSelectionView",
];
export const PRESETS: Record = {
@@ -467,7 +473,7 @@ const OTHER_CONFIG_KEYS: (keyof Config)[] = [
"ccSupportSize", "legSize", "legsFlush",
"bedBrightness", "soilBrightness", "plants", "labels", "ground", "grid", "axes",
"trail", "clouds", "sunInclination", "sunAzimuth", "heading",
- "perspective", "bot", "laser",
+ "perspective", "topDown", "bot", "laser", "viewpointHeading",
"tool", "cableCarriers", "viewCube", "stats", "config", "zoom", "bounds",
"threeAxes", "xyDimensions", "zDimension", "labelsOnHover", "promoInfo",
"settingsBar", "zoomBeacons", "pan", "rotate",
@@ -481,7 +487,7 @@ const OTHER_CONFIG_KEYS: (keyof Config)[] = [
"imgScale", "imgRotation", "imgOffsetX", "imgOffsetY", "imgOrigin", "imgCalZ",
"imgCenterX", "imgCenterY", "interpolationStepSize", "interpolationUseNearest",
"interpolationPower", "promoSpread", "cameraView", "lastImageCapture",
- "mirrorX", "mirrorY",
+ "mirrorX", "mirrorY", "cameraSelectionView",
];
export const modifyConfig =
@@ -508,6 +514,10 @@ export const modifyConfig =
newConfig.bedZOffset = newConfig.bedType == "Mobile" ? 500 : 0;
newConfig.legsFlush = newConfig.bedType != "Mobile";
}
+ if (Object.keys(update).includes("topDown")) {
+ newConfig.perspective = !update.topDown;
+ newConfig.rotate = !update.topDown;
+ }
if (update.otherPreset) {
if (update.otherPreset == "Reset all") {
Object.keys(config).map(key => {
diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx
index 23ee3537db..9c19cfd961 100644
--- a/frontend/three_d_garden/config_overlays.tsx
+++ b/frontend/three_d_garden/config_overlays.tsx
@@ -405,6 +405,9 @@ export const PrivateOverlay = (props: OverlayProps) => {
+
+
+
{"Environment"}
{
},
});
- const topDown = addPlantProps?.designer.threeDTopDownView;
- const topDownMobile = topDown && isMobile();
+ const baseAngle = 0;
+ const topDownCameraAngle = config.topDown
+ ? baseAngle + config.viewpointHeading * Math.PI / 180
+ : undefined;
const camera = getCamera(
config,
props.configPosition,
props.activeFocus,
- cameraInit(!!topDown));
+ cameraInit({
+ topDown: config.topDown,
+ viewpointHeading: config.viewpointHeading,
+ }));
const [controlsCamera, setControlsCamera] =
// eslint-disable-next-line no-null/no-null
React.useState(null);
@@ -232,15 +237,15 @@ export const GardenModel = (props: GardenModelProps) => {
fov={40} near={10} far={BigDistance.far}
position={camera.position}
rotation={[0, 0, 0]}
- zoom={topDown ? 0.25 : 1}
+ zoom={config.topDown ? 0.25 : 1}
up={[0, 0, 1]} />
{controlsCamera &&
{
+
;
};
diff --git a/frontend/three_d_garden/helpers.ts b/frontend/three_d_garden/helpers.ts
index e084518d03..2009b98c83 100644
--- a/frontend/three_d_garden/helpers.ts
+++ b/frontend/three_d_garden/helpers.ts
@@ -2,6 +2,9 @@ import { Config } from "./config";
import * as THREE from "three";
import { AxisNumberProperty } from "../farm_designer/map/interfaces";
import { round } from "../farm_designer/map/util";
+import { DesignerState } from "../farm_designer/interfaces";
+import { BooleanSetting } from "../session_keys";
+import { GetWebAppConfigValue } from "../config_storage/actions";
export const threeSpace = (position: number, max: number): number =>
position - max / 2;
@@ -103,3 +106,12 @@ export const getWorldPositionFunc = (config: Config) =>
zZero(config) + gardenPosition.z,
];
};
+
+export const isTopDown = (
+ designer: DesignerState,
+ getWebAppConfigValue: GetWebAppConfigValue,
+) => {
+ const state = designer.threeDTopDownView;
+ const db = !!getWebAppConfigValue(BooleanSetting.top_down_view);
+ return state ?? db;
+};
diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx
index 3985486bfd..722dec76cd 100644
--- a/frontend/three_d_garden/index.tsx
+++ b/frontend/three_d_garden/index.tsx
@@ -19,11 +19,12 @@ import { isMobile } from "../screen_size";
import { Help } from "../ui";
import { BooleanSetting } from "../session_keys";
import { LayerToggle } from "../farm_designer/map/legend/layer_toggle";
-import { setWebAppConfigValue } from "../config_storage/actions";
+import { GetWebAppConfigValue, setWebAppConfigValue } from "../config_storage/actions";
import { DesignerState } from "../farm_designer/interfaces";
import { setPanelOpen } from "../farm_designer/panel_header";
import { ThreeDGardenPlant } from "./garden";
import { DeviceAccountSettings } from "farmbot/dist/resources/api_resources";
+import { isTopDown } from "./helpers";
export interface ThreeDGardenProps {
config: Config;
@@ -87,12 +88,13 @@ export interface ThreeDGardenToggleProps {
designer: DesignerState;
threeDGarden: boolean;
device: DeviceAccountSettings;
+ getConfigValue: GetWebAppConfigValue;
}
// eslint-disable-next-line complexity
export const ThreeDGardenToggle = (props: ThreeDGardenToggleProps) => {
const { navigate, dispatch, threeDGarden } = props;
- const topDown = props.designer.threeDTopDownView;
+ const topDown = isTopDown(props.designer, props.getConfigValue);
const exaggeratedZ = props.designer.threeDExaggeratedZ;
const description = isMobile()
? Content.SHOW_3D_VIEW_DESCRIPTION_MOBILE
diff --git a/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx
index 0494efaba6..8b345f8113 100644
--- a/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx
+++ b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx
@@ -1,8 +1,9 @@
import React from "react";
import { render } from "@testing-library/react";
-import { People, PeopleProps } from "../people";
-import { INITIAL } from "../../../config";
+import { People, PeopleProps, Person } from "../people";
+import { ASSETS } from "../../../constants";
import { clone } from "lodash";
+import { INITIAL } from "../../../config";
describe(" ", () => {
const fakeProps = (): PeopleProps => ({
@@ -18,3 +19,16 @@ describe(" ", () => {
expect(container).toContainHTML("people");
});
});
+
+describe(" ", () => {
+ it("renders image with transform props", () => {
+ const { container } = render(
+ );
+ expect(container.innerHTML).toContain(ASSETS.people.person2);
+ expect(container.innerHTML).toContain("1,2,3");
+ expect(container.innerHTML).toContain("4,5,6");
+ });
+});
diff --git a/frontend/three_d_garden/scenes/props/people.tsx b/frontend/three_d_garden/scenes/props/people.tsx
index 3a1ce34f42..fca581d33c 100644
--- a/frontend/three_d_garden/scenes/props/people.tsx
+++ b/frontend/three_d_garden/scenes/props/people.tsx
@@ -3,7 +3,7 @@ import { Billboard, Image } from "@react-three/drei";
import { Group } from "../../components";
import { Config } from "../../config";
import { threeSpace } from "../../helpers";
-import { Vector3 } from "three";
+import { Vector3, DoubleSide } from "three";
import { ASSETS, RenderOrder } from "../../constants";
export interface PeopleProps {
@@ -18,7 +18,6 @@ export const People = (props: PeopleProps) => {
return
{people.map((person, i) => {
- const scalingData = SCALING_DATA[person.url];
const offset = new Vector3(...person.offset);
return {
threeSpace(offset.y, config.bedWidthOuter),
groundZ,
]}>
-
+
;
})}
;
@@ -53,3 +46,25 @@ const SCALING_DATA: Record = {
[ASSETS.people.person4]: { scale: [580, 1700], position: [0, 850, 0] },
[ASSETS.people.person4Flipped]: { scale: [580, 1700], position: [0, 850, 0] },
};
+
+export interface PersonProps {
+ url: string;
+ position?: [number, number, number];
+ rotation?: [number, number, number];
+}
+
+export const Person = (props: PersonProps) => {
+ const scalingData = SCALING_DATA[props.url];
+ return
+
+ ;
+};
From c70de930ae2c4f7205019ffc6d912ecfbc8f1d6b Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Tue, 21 Apr 2026 17:47:19 -0700
Subject: [PATCH 06/11] improve 3D camera selection UI
---
frontend/__test_support__/bun_test_setup.ts | 4 +
frontend/__test_support__/three_d_mocks.tsx | 8 +
frontend/constants.ts | 12 +-
.../farm_designer/map/__tests__/util_test.ts | 12 ++
frontend/farm_designer/map/interfaces.ts | 1 +
.../__tests__/garden_map_legend_test.tsx | 24 +--
.../map/legend/garden_map_legend.tsx | 25 +--
frontend/farm_designer/map/util.ts | 3 +
frontend/settings/__tests__/index_test.tsx | 26 ---
frontend/settings/farm_designer_settings.tsx | 49 ++---
.../__tests__/camera_selection_ui_test.tsx | 204 ++++++++++++++++--
.../three_d_garden/__tests__/camera_test.ts | 40 +++-
.../__tests__/garden_model_test.tsx | 29 +++
frontend/three_d_garden/camera.ts | 77 ++++---
.../three_d_garden/camera_selection_ui.tsx | 191 +++++++++++++---
frontend/three_d_garden/fps_probe.tsx | 5 +-
.../garden/__tests__/plant_instances_test.tsx | 18 ++
.../garden/__tests__/plants_test.tsx | 14 ++
.../three_d_garden/garden/plant_instances.tsx | 3 +-
frontend/three_d_garden/garden/plants.tsx | 2 +-
frontend/three_d_garden/garden_model.tsx | 15 +-
.../three_d_garden/scenes/props/people.tsx | 1 +
frontend/wizard/__tests__/checks_test.tsx | 22 ++
frontend/wizard/__tests__/data_test.ts | 23 ++
frontend/wizard/checks.tsx | 17 +-
frontend/wizard/data.ts | 37 ++--
26 files changed, 653 insertions(+), 209 deletions(-)
diff --git a/frontend/__test_support__/bun_test_setup.ts b/frontend/__test_support__/bun_test_setup.ts
index 7bbc62a2e2..040e58b2a9 100644
--- a/frontend/__test_support__/bun_test_setup.ts
+++ b/frontend/__test_support__/bun_test_setup.ts
@@ -229,6 +229,10 @@ const defaultThreeFiberState = () => ({
scene: { traverse: jest.fn() },
size: { width: 800, height: 600 },
pointer: { x: 0, y: 0 },
+ raycaster: {
+ setFromCamera: jest.fn(),
+ intersectObjects: jest.fn(() => []),
+ },
});
type MockLike = {
diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx
index 38079a3e97..9eee54e122 100644
--- a/frontend/__test_support__/three_d_mocks.tsx
+++ b/frontend/__test_support__/three_d_mocks.tsx
@@ -203,6 +203,10 @@ jest.mock("@react-three/fiber", () => ({
scene: { traverse: jest.fn() },
size: { width: 800, height: 600 },
pointer: { x: 0, y: 0 },
+ raycaster: {
+ setFromCamera: jest.fn(),
+ intersectObjects: jest.fn(() => []),
+ },
})),
useThree: jest.fn(() => ({
gl: {
@@ -214,6 +218,10 @@ jest.mock("@react-three/fiber", () => ({
scene: { traverse: jest.fn() },
pointer: { x: 0, y: 0 },
camera: new THREE.PerspectiveCamera(),
+ raycaster: {
+ setFromCamera: jest.fn(),
+ intersectObjects: jest.fn(() => []),
+ },
size: { width: 800, height: 600 },
})),
extend: jest.fn(),
diff --git a/frontend/constants.ts b/frontend/constants.ts
index 6c5855efde..c4b9f8c3e8 100644
--- a/frontend/constants.ts
+++ b/frontend/constants.ts
@@ -1018,8 +1018,8 @@ export namespace Content {
export const TOP_DOWN_VIEW =
trim(`Upon open, display the 3D garden map from a top-down perspective.`);
- export const VIEWPOINT_HEADING =
- trim(`Heading of the camera when the 3D garden map is opened (in degrees).`);
+ export const CAMERA_STARTING_LOCATION =
+ trim(`Location of the camera when the 3D garden map is opened.`);
export const CROP_MAP_IMAGES =
trim(`Crop images displayed in the garden map to remove black borders
@@ -1779,6 +1779,10 @@ export namespace SetupWizardContent {
map to your real life FarmBot. The relevant controls are available
below the video for your convenience.`);
+ export const SET_CAMERA_LOCATION =
+ trim(`Press the "SET" button to show camera location options.
+ Select the correct camera angle by clicking on the camera location.`);
+
export const PRESS_RIGHT_JOG_BUTTON =
trim(`Standing from where you will normally view the FarmBot,
**press the right arrow button**.`);
@@ -2240,8 +2244,8 @@ export enum DeviceSetting {
mapSize = `Map size`,
rotateMap = `Rotate map`,
mapOrigin = `Map origin`,
- openInTopDownView = `Open in top-down view`,
- cameraLocationUponOpen = `Camera location upon open`,
+ topDownView = `Top down view`,
+ setCameraStartingLocation = `Set camera starting location`,
cropMapImages = `Crop map images`,
clipPhotosOutOfBounds = `Clip photos out of bounds`,
cameraView = `Camera view`,
diff --git a/frontend/farm_designer/map/__tests__/util_test.ts b/frontend/farm_designer/map/__tests__/util_test.ts
index a3b2f47ba9..010c7e7fcd 100644
--- a/frontend/farm_designer/map/__tests__/util_test.ts
+++ b/frontend/farm_designer/map/__tests__/util_test.ts
@@ -448,6 +448,18 @@ describe("getMode()", () => {
location.pathname = Path.mock(Path.app());
expect(getMode()).toEqual(Mode.none);
});
+
+ it("returns camera selection mode", () => {
+ const state = fakeState();
+ state.resources.consumers.farm_designer = fakeDesignerState();
+ state.resources.consumers.farm_designer.threeDCameraSelection = true;
+ const originalGetState = store.getState;
+ (store as unknown as { getState: () => typeof state }).getState = () => state;
+ location.pathname = Path.mock(Path.app());
+ expect(getMode()).toEqual(Mode.cameraSelection);
+ (store as unknown as { getState: typeof originalGetState }).getState =
+ originalGetState;
+ });
});
describe("savedGardenOpen", () => {
diff --git a/frontend/farm_designer/map/interfaces.ts b/frontend/farm_designer/map/interfaces.ts
index ec528b425a..64ff0e8b57 100644
--- a/frontend/farm_designer/map/interfaces.ts
+++ b/frontend/farm_designer/map/interfaces.ts
@@ -202,4 +202,5 @@ export enum Mode {
templateView = "templateView",
editGroup = "editGroup",
profile = "profile",
+ cameraSelection = "cameraSelection",
}
diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
index c9ca3b9000..97944428e8 100644
--- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
+++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx
@@ -9,7 +9,7 @@ import {
ZoomControlsProps,
} from "../garden_map_legend";
import { GardenMapLegendProps } from "../../interfaces";
-import { BooleanSetting, NumericSetting } from "../../../../session_keys";
+import { BooleanSetting } from "../../../../session_keys";
import * as zoom from "../../zoom";
import {
fakeTimeSettings,
@@ -196,39 +196,23 @@ describe(" ", () => {
const { container } = render( );
expect(container.textContent).toContain("Rotate map");
expect(container.textContent).toContain("Map origin");
- expect(container.textContent).not.toContain("Camera location upon open");
+ expect(container.textContent).not.toContain("Set camera starting location");
});
it("shows 3D-only controls", () => {
const p = fakeProps();
p.getConfigValue = key => key == BooleanSetting.three_d_garden;
const { container } = render( );
- expect(container.textContent).toContain("Open in top-down view");
- expect(container.textContent).toContain("Camera location upon open");
- expect(container.textContent).toContain("Enable camera heading selection view");
+ expect(container.textContent).toContain("Set camera starting location");
expect(container.textContent).not.toContain("Rotate map");
});
- it("changes viewpoint heading in 3D settings", () => {
- const p = fakeProps();
- p.getConfigValue = key => {
- if (key == BooleanSetting.three_d_garden) { return true; }
- if (key == NumericSetting.viewpoint_heading) { return 0; }
- return false;
- };
- const { container } = render( );
- const quadrants = container.querySelectorAll(".quadrant");
- fireEvent.click(quadrants[quadrants.length - 1]);
- expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
- NumericSetting.viewpoint_heading, 270);
- });
-
it("toggles camera selection view", () => {
const p = fakeProps();
p.getConfigValue = key => key == BooleanSetting.three_d_garden;
const { container } = render( );
const toggleBtn =
- container.querySelector("button[title='Enable camera heading selection view']");
+ container.querySelector("button[title='Set camera starting location']");
if (!toggleBtn) { throw new Error("Missing camera selection toggle"); }
fireEvent.click(toggleBtn);
expect(p.dispatch).toHaveBeenCalledWith({
diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx
index c3d1f8c6e7..ba1caab37a 100644
--- a/frontend/farm_designer/map/legend/garden_map_legend.tsx
+++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx
@@ -13,7 +13,7 @@ import {
import { BooleanSetting } from "../../../session_keys";
import { t } from "../../../i18next_wrapper";
import { SelectModeLink } from "../../../plants/select_plants";
-import { DeviceSetting, Content, Actions } from "../../../constants";
+import { DeviceSetting, Content } from "../../../constants";
import { Help, Popover, ToggleButton } from "../../../ui";
import {
BooleanConfigKey as WebAppBooleanConfigKey,
@@ -22,7 +22,9 @@ import { ZDisplay, ZDisplayToggle } from "./z_display";
import { getModifiedClassName } from "../../../settings/default_values";
import { Position } from "@blueprintjs/core";
import { MapSizeInputs } from "../../map_size_setting";
-import { HeadingSelector, OriginSelector } from "../../../settings/farm_designer_settings";
+import {
+ CameraStartingLocationButton, OriginSelector,
+} from "../../../settings/farm_designer_settings";
import { McuParams } from "farmbot";
import { DesignerState } from "../../interfaces";
@@ -238,25 +240,10 @@ export const MapSettingsContent = (props: SettingsSubMenuProps) => {
helpText={Content.MAP_ORIGIN}>
}
- {is3D && }
- {is3D &&
-
- }
{is3D &&
- {t("Enable camera heading selection view")}
- props.dispatch({
- type: Actions.TOGGLE_3D_CAMERA_SELECTION,
- payload: undefined,
- })}
- toggleValue={props.designer.threeDCameraSelection} />
+ {t(DeviceSetting.setCameraStartingLocation)}
+
}
;
};
diff --git a/frontend/farm_designer/map/util.ts b/frontend/farm_designer/map/util.ts
index a8f5d827bf..95030f196c 100644
--- a/frontend/farm_designer/map/util.ts
+++ b/frontend/farm_designer/map/util.ts
@@ -305,6 +305,9 @@ export const getMode = (): Mode => {
if (store.getState().resources.consumers.farm_designer.profileOpen) {
return Mode.profile;
}
+ if (store.getState().resources.consumers.farm_designer.threeDCameraSelection) {
+ return Mode.cameraSelection;
+ }
const panelSlug = Path.getSlug(Path.designer());
if ((panelSlug === "groups" || panelSlug === "zones")
&& Path.getSlug(Path.groups())) { return Mode.editGroup; }
diff --git a/frontend/settings/__tests__/index_test.tsx b/frontend/settings/__tests__/index_test.tsx
index 3872ae2508..7c2f81faf2 100644
--- a/frontend/settings/__tests__/index_test.tsx
+++ b/frontend/settings/__tests__/index_test.tsx
@@ -1,6 +1,5 @@
import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
-import { changeBlurableInputRTL } from "../../__test_support__/helpers";
import { RawDesignerSettings as DesignerSettings } from "../index";
import { DesignerSettingsProps } from "../interfaces";
import { BooleanSetting, NumericSetting } from "../../session_keys";
@@ -202,31 +201,6 @@ describe(" ", () => {
NumericSetting.bot_origin_quadrant, 4);
});
- it("toggles top down view", () => {
- const p = fakeProps();
- p.settingsPanelState.farm_designer = true;
- const { container } = render( );
- const topDownSetting = getSettingByText(container, "open in top-down view");
- const button = topDownSetting.querySelector("button");
- if (!button) { throw new Error("Expected top down toggle button"); }
- fireEvent.click(button);
- expect(setWebAppConfigValueSpy)
- .toHaveBeenCalledWith(BooleanSetting.top_down_view, true);
- });
-
- it("changes viewpoint heading", () => {
- const p = fakeProps();
- p.settingsPanelState.farm_designer = true;
- p.getConfigValue = key => key == NumericSetting.viewpoint_heading ? 0 : 2;
- const { container } = render( );
- const headingSetting = getSettingByText(container, "camera location upon open");
- const input = headingSetting.querySelector("input");
- if (!input) { throw new Error("Expected viewpoint heading input"); }
- changeBlurableInputRTL(input, "270");
- expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
- NumericSetting.viewpoint_heading, 270);
- });
-
it("renders env editor", () => {
const p = fakeProps();
p.searchTerm = "env";
diff --git a/frontend/settings/farm_designer_settings.tsx b/frontend/settings/farm_designer_settings.tsx
index a4791ade59..a117f6cbde 100644
--- a/frontend/settings/farm_designer_settings.tsx
+++ b/frontend/settings/farm_designer_settings.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Content, DeviceSetting } from "../constants";
+import { Actions, Content, DeviceSetting } from "../constants";
import { t } from "../i18next_wrapper";
import { setWebAppConfigValue } from "../config_storage/actions";
import { Help, ToggleButton, BlurableInput } from "../ui";
@@ -123,20 +123,20 @@ const DESIGNER_SETTINGS =
description: Content.MAP_SWAP_XY,
setting: BooleanSetting.xy_swap,
},
+ {
+ title: DeviceSetting.topDownView,
+ description: Content.TOP_DOWN_VIEW,
+ setting: BooleanSetting.top_down_view,
+ },
{
title: DeviceSetting.mapOrigin,
description: Content.MAP_ORIGIN,
children:
},
{
- title: DeviceSetting.openInTopDownView,
- description: Content.TOP_DOWN_VIEW,
- setting: BooleanSetting.top_down_view,
- },
- {
- title: DeviceSetting.cameraLocationUponOpen,
- description: Content.VIEWPOINT_HEADING,
- numberSetting: NumericSetting.viewpoint_heading,
+ title: DeviceSetting.setCameraStartingLocation,
+ description: Content.CAMERA_STARTING_LOCATION,
+ children: ,
},
{
title: DeviceSetting.cropMapImages,
@@ -191,20 +191,17 @@ export const OriginSelector = (props: DesignerSettingsPropsBase) => {
;
};
-export const HeadingSelector = (props: DesignerSettingsPropsBase) => {
- const settingKey = NumericSetting.viewpoint_heading;
- const heading = props.getConfigValue(settingKey);
- const update = (value: number) => () =>
- props.dispatch(setWebAppConfigValue(settingKey, value));
- return
-
- {[180, 0, 90, 270].map(angle =>
-
)}
-
-
;
-};
+export interface CameraStartingLocationButtonProps {
+ dispatch: Function;
+}
+
+export const CameraStartingLocationButton =
+ (props: CameraStartingLocationButtonProps) => props.dispatch({
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: undefined,
+ })}>
+ {t("Set")}
+ ;
diff --git a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
index a97a678677..5ab1b13ba7 100644
--- a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
+++ b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
@@ -1,29 +1,72 @@
import React from "react";
import { render } from "@testing-library/react";
-import { CameraSelectionUI } from "../camera_selection_ui";
+import { CameraSelectionUI, CameraSelectionUIProps } from "../camera_selection_ui";
import { clone } from "lodash";
+import * as lodash from "lodash";
import { INITIAL } from "../config";
import * as configStorageActions from "../../config_storage/actions";
-import { NumericSetting } from "../../session_keys";
+import { BooleanSetting, NumericSetting } from "../../session_keys";
import {
actRenderer,
createRenderer,
unmountRenderer,
} from "../../__test_support__/test_renderer";
+import * as threeFiber from "@react-three/fiber";
+import { Object3D, PerspectiveCamera } from "three";
describe(" ", () => {
let setWebAppConfigValueSpy: jest.SpyInstance;
+ let debounceSpy: jest.SpyInstance;
+ let useFrameSpy: jest.SpyInstance;
+ let useThreeSpy: jest.SpyInstance;
+ let useStateSpy: jest.SpyInstance;
+ let frameHandler: threeFiber.RenderCallback | undefined;
+ let intersectObjects: jest.Mock;
+ let setFromCamera: jest.Mock;
const mountedWrappers: ReturnType[] = [];
beforeEach(() => {
setWebAppConfigValueSpy = jest.spyOn(configStorageActions, "setWebAppConfigValue")
.mockImplementation(jest.fn());
+ debounceSpy = jest.spyOn(lodash, "debounce")
+ .mockImplementation((fn => fn) as typeof lodash.debounce);
+ frameHandler = undefined;
+ intersectObjects = jest.fn(() => []);
+ setFromCamera = jest.fn();
+ useFrameSpy = jest.spyOn(threeFiber, "useFrame")
+ .mockImplementation((callback: threeFiber.RenderCallback) => {
+ frameHandler = callback;
+ // eslint-disable-next-line no-null/no-null
+ return null;
+ });
+ useThreeSpy = jest.spyOn(threeFiber, "useThree")
+ .mockReturnValue({
+ camera: new PerspectiveCamera(),
+ gl: {
+ info: {
+ render: { calls: 0, triangles: 0, points: 0, lines: 0 },
+ memory: { geometries: 0, textures: 0 },
+ },
+ },
+ pointer: { x: 0, y: 0 },
+ raycaster: {
+ setFromCamera,
+ intersectObjects,
+ },
+ scene: { traverse: jest.fn() },
+ size: { width: 800, height: 600 },
+ });
+ useStateSpy = jest.spyOn(React, "useState");
});
afterEach(() => {
mountedWrappers.splice(0).forEach(wrapper =>
unmountRenderer(wrapper));
setWebAppConfigValueSpy.mockRestore();
+ debounceSpy.mockRestore();
+ useFrameSpy.mockRestore();
+ useThreeSpy.mockRestore();
+ useStateSpy.mockRestore();
});
const fakeConfig = () => {
@@ -32,50 +75,167 @@ describe(" ", () => {
return config;
};
+ const fakeProps = (): CameraSelectionUIProps => ({
+ config: fakeConfig(),
+ dispatch: jest.fn(),
+ topDownAtStart: false,
+ });
+
+ const attachMeshRefs = (wrapper: ReturnType) => {
+ wrapper.root.findAll(node => node.props.ref)
+ .forEach((node, index) => {
+ node.props.ref({
+ userData: node.props.userData,
+ uuid: `mesh-${index}`,
+ } as Object3D);
+ });
+ };
+
it("renders hidden by default", () => {
- const wrapper = createRenderer(
- );
+ const wrapper = createRenderer( );
mountedWrappers.push(wrapper);
const group = wrapper.root.findAll(node =>
node.props.name == "camera-selection")[0];
expect(group?.props.visible).toEqual(false);
});
+ it("doesn't raycast when camera selection is hidden", () => {
+ const wrapper = createRenderer( );
+ mountedWrappers.push(wrapper);
+ actRenderer(() => {
+ frameHandler?.({} as never, 0);
+ });
+ expect(setFromCamera).not.toHaveBeenCalled();
+ expect(intersectObjects).not.toHaveBeenCalled();
+ });
+
it("renders unique heading marker", () => {
- const config = fakeConfig();
- config.cameraSelectionView = true;
- config.viewpointHeading = 45;
- const { container } = render(
- );
- expect(container.querySelectorAll(".spherehead").length).toEqual(5);
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ p.config.viewpointHeading = 45;
+ const { container } = render( );
+ expect(container.querySelectorAll(".spherehead").length).toEqual(12);
});
it("dispatches heading update", () => {
- const config = fakeConfig();
- config.cameraSelectionView = true;
- const dispatch = jest.fn();
- const wrapper = createRenderer(
- );
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ const wrapper = createRenderer( );
mountedWrappers.push(wrapper);
const groups = wrapper.root.findAll(node => node.props.onClick);
actRenderer(() => {
- groups[0]?.props.onClick();
+ groups[0]?.props.onClick({ stopPropagation: jest.fn() });
});
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
NumericSetting.viewpoint_heading, 0);
- expect(dispatch).toHaveBeenCalled();
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ BooleanSetting.top_down_view, true);
+ expect(p.dispatch).toHaveBeenCalled();
});
it("handles missing dispatch", () => {
- const config = fakeConfig();
- config.cameraSelectionView = true;
- const wrapper = createRenderer(
- );
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ p.dispatch = undefined;
+ const wrapper = createRenderer( );
mountedWrappers.push(wrapper);
const groups = wrapper.root.findAll(node => node.props.onClick);
actRenderer(() => {
- groups[0]?.props.onClick();
+ groups[0]?.props.onClick({ stopPropagation: jest.fn() });
});
expect(setWebAppConfigValueSpy).not.toHaveBeenCalled();
});
+
+ it("updates marker color from raycast hover state", () => {
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ const wrapper = createRenderer( );
+ mountedWrappers.push(wrapper);
+ attachMeshRefs(wrapper);
+ intersectObjects.mockReturnValue([{
+ object: { userData: { hovered: { angle: 45, topDown: false } } },
+ }]);
+
+ actRenderer(() => {
+ frameHandler?.({} as never, 0);
+ });
+
+ const hoveredSphere = wrapper.root.findAll(node =>
+ node.props.name == "head"
+ && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.topDown === false)[0];
+ expect(hoveredSphere).toBeTruthy();
+ const material = hoveredSphere?.findAll(node =>
+ node.props.color !== undefined)[0];
+ expect(material?.props.color).toEqual("cyan");
+ });
+
+ it("keeps default color when raycast finds no hovered marker", () => {
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ const wrapper = createRenderer( );
+ mountedWrappers.push(wrapper);
+ attachMeshRefs(wrapper);
+ intersectObjects.mockReturnValue([]);
+
+ actRenderer(() => {
+ frameHandler?.({} as never, 0);
+ });
+
+ const sphere = wrapper.root.findAll(node =>
+ node.props.name == "head"
+ && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.topDown === false)[0];
+ const material = sphere?.findAll(node =>
+ node.props.color !== undefined)[0];
+ expect(material?.props.color).toEqual("orange");
+ });
+
+ it("avoids repeated hover state updates for the same marker", () => {
+ const setHovered = jest.fn();
+ useStateSpy.mockImplementationOnce(initial =>
+ [initial as unknown, setHovered]);
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ const wrapper = createRenderer( );
+ mountedWrappers.push(wrapper);
+ attachMeshRefs(wrapper);
+ intersectObjects.mockReturnValue([{
+ object: { userData: { hovered: { angle: 45, topDown: false } } },
+ }]);
+
+ actRenderer(() => {
+ frameHandler?.({} as never, 0);
+ frameHandler?.({} as never, 0);
+ });
+
+ expect(setHovered).toHaveBeenCalledTimes(1);
+ expect(setHovered).toHaveBeenCalledWith({ angle: 45, topDown: false });
+ });
+
+ it("renders debug camera markers", () => {
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ p.config.lightsDebug = true;
+ const { container } = render( );
+ expect(container.querySelectorAll(".line").length).toEqual(8);
+ });
+
+ it("dispatches non-top-down heading update", () => {
+ const p = fakeProps();
+ p.config.cameraSelectionView = true;
+ const wrapper = createRenderer( );
+ mountedWrappers.push(wrapper);
+ const body = wrapper.root.findAll(node =>
+ node.props.name == "body"
+ && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.topDown === false)[0];
+ actRenderer(() => {
+ body?.props.onClick({ stopPropagation: jest.fn() });
+ });
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ NumericSetting.viewpoint_heading, 45);
+ expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
+ BooleanSetting.top_down_view, false);
+ });
});
diff --git a/frontend/three_d_garden/__tests__/camera_test.ts b/frontend/three_d_garden/__tests__/camera_test.ts
index 1e2fe6d0f6..8024241316 100644
--- a/frontend/three_d_garden/__tests__/camera_test.ts
+++ b/frontend/three_d_garden/__tests__/camera_test.ts
@@ -3,6 +3,7 @@ let mockIsDesktop = true;
import {
cameraInit, CameraInitProps, getDefaultCameraPosition,
+ GetDefaultCameraPositionProps,
} from "../camera";
import * as devSupport from "../../settings/dev/dev_support";
import * as screenSize from "../../screen_size";
@@ -26,14 +27,15 @@ afterEach(() => {
describe("cameraInit()", () => {
const fakeProps = (): CameraInitProps => ({
topDown: false,
- viewpointHeading: 0,
+ viewpointHeading: 45,
+ bedSize: { x: 3000, y: 1500 },
});
it("initializes camera", () => {
mockDev = undefined;
mockIsDesktop = true;
expect(cameraInit(fakeProps())).toEqual({
- position: [2000, -4000, 2500],
+ position: [2192, -2192, 2500],
target: [0, 0, 0],
});
});
@@ -51,7 +53,7 @@ describe("cameraInit()", () => {
mockDev = "{";
mockIsDesktop = true;
expect(cameraInit(fakeProps())).toEqual({
- position: [2000, -4000, 2500],
+ position: [2192, -2192, 2500],
target: [0, 0, 0],
});
});
@@ -60,7 +62,7 @@ describe("cameraInit()", () => {
mockDev = undefined;
mockIsDesktop = false;
expect(cameraInit(fakeProps())).toEqual({
- position: [5400, -2500, 3400],
+ position: [2475, -2475, 3400],
target: [0, 0, 0],
});
});
@@ -82,25 +84,47 @@ describe("cameraInit()", () => {
const p = fakeProps();
p.viewpointHeading = 90;
expect(cameraInit(p)).toEqual({
- position: [2000, 4000, 2500],
+ position: [3100, 0, 2500],
target: [0, 0, 0],
});
});
});
describe("getDefaultCameraPosition()", () => {
+ const fakeProps = (): GetDefaultCameraPositionProps => ({
+ heading: 0,
+ bedSize: { x: 3000, y: 1500 },
+ topDown: false,
+ visual: false,
+ });
+
it("returns desktop position", () => {
mockIsDesktop = true;
- expect(getDefaultCameraPosition(180)).toEqual([-2000, 4000, 2500]);
+ const p = fakeProps();
+ p.heading = 180;
+ expect(getDefaultCameraPosition(p)).toEqual([0, 3100, 2500]);
});
it("returns mobile position", () => {
mockIsDesktop = false;
- expect(getDefaultCameraPosition(270)).toEqual([-5400, -2500, 3400]);
+ const p = fakeProps();
+ p.heading = 270;
+ expect(getDefaultCameraPosition(p)).toEqual([-3500, 0, 3400]);
});
it("returns top down position", () => {
mockIsDesktop = true;
- expect(getDefaultCameraPosition(90, true)).toEqual([5657, 0, 5000]);
+ const p = fakeProps();
+ p.heading = 90;
+ p.topDown = true;
+ expect(getDefaultCameraPosition(p)).toEqual([3100, 0, 5000]);
+ });
+
+ it("returns camera location visual location", () => {
+ mockIsDesktop = true;
+ const p = fakeProps();
+ p.heading = 180;
+ p.visual = true;
+ expect(getDefaultCameraPosition(p)).toEqual([0, 1600, 2500]);
});
});
diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx
index 59cf406a63..fa19452233 100644
--- a/frontend/three_d_garden/__tests__/garden_model_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx
@@ -95,6 +95,35 @@ describe(" ", () => {
expect(orbitControls.props.maxAzimuthAngle).toEqual(Math.PI / 2);
});
+ it("rounds top down heading up to the nearest 90 degrees", () => {
+ mockIsMobile = true;
+ const p = fakeProps();
+ p.config.topDown = true;
+ p.config.viewpointHeading = 1;
+ const wrapper = createWrapper(p);
+ const orbitControls = wrapper.root.findByType(OrbitControls);
+ expect(orbitControls.props.minAzimuthAngle).toEqual(Math.PI / 2);
+ expect(orbitControls.props.maxAzimuthAngle).toEqual(Math.PI / 2);
+ });
+
+ it("scales top down zoom by bed length", () => {
+ const p = fakeProps();
+ p.config.topDown = true;
+ p.config.bedLengthOuter = 6000;
+ const wrapper = createWrapper(p);
+ const camera = wrapper.root.findAll(node => node.props.name == "camera")[0];
+ expect(camera?.props.zoom).toEqual(0.125);
+ });
+
+ it("increases top down zoom for shorter beds", () => {
+ const p = fakeProps();
+ p.config.topDown = true;
+ p.config.bedLengthOuter = 1500;
+ const wrapper = createWrapper(p);
+ const camera = wrapper.root.findAll(node => node.props.name == "camera")[0];
+ expect(camera?.props.zoom).toEqual(0.5);
+ });
+
it("renders camera selection view", () => {
const p = fakeProps();
p.config.cameraSelectionView = true;
diff --git a/frontend/three_d_garden/camera.ts b/frontend/three_d_garden/camera.ts
index 3cc54a69b0..91e0d11c4b 100644
--- a/frontend/three_d_garden/camera.ts
+++ b/frontend/three_d_garden/camera.ts
@@ -2,14 +2,16 @@ import { round } from "lodash";
import { isDesktop } from "../screen_size";
import { DevSettings } from "../settings/dev/dev_support";
import { Camera } from "./zoom_beacons_constants";
+import { AxisNumberProperty } from "../farm_designer/map/interfaces";
export interface CameraInitProps {
topDown: boolean;
viewpointHeading: number;
+ bedSize: AxisNumberProperty;
}
export const cameraInit = (props: CameraInitProps): Camera => {
- const { topDown, viewpointHeading } = props;
+ const { topDown, viewpointHeading, bedSize } = props;
const devCameraString = DevSettings.get3dCamera();
let devCamera;
try {
@@ -21,7 +23,12 @@ export const cameraInit = (props: CameraInitProps): Camera => {
const topDownCameraPosition = topDown ? [0, 0, 5000] : undefined;
const cameraPositionInit = topDownCameraPosition
|| devCamera?.position
- || getDefaultCameraPosition(viewpointHeading);
+ || getDefaultCameraPosition({
+ heading: viewpointHeading,
+ bedSize,
+ topDown: false,
+ visual: false,
+ });
const defaultCameraTarget = [0, 0, 0];
const topDownCameraTarget = topDown ? [0, 0, 0] : undefined;
@@ -36,31 +43,45 @@ export const cameraInit = (props: CameraInitProps): Camera => {
return initCamera;
};
-export const getDefaultCameraPosition = (
- heading: number,
- topDown = false,
-): [number, number, number] => {
- const radians = heading * Math.PI / 180;
+const SMALL_FACTOR = 100;
+const BIG_FACTOR = 500;
- if (topDown) {
- const phase = Math.PI / 2;
- return [
- round(4000 * Math.SQRT2 * Math.cos(radians - phase)),
- round(2000 * Math.SQRT2 * Math.sin(radians - phase)),
- 5000,
- ];
- }
+export interface GetDefaultCameraPositionProps {
+ heading: number;
+ bedSize: AxisNumberProperty;
+ topDown: boolean;
+ visual: boolean;
+}
- const phase = Math.PI / 4;
- return isDesktop()
- ? [
- round(2000 * Math.SQRT2 * Math.cos(radians - phase)),
- round(4000 * Math.SQRT2 * Math.sin(radians - phase)),
- 2500,
- ]
- : [
- round(5400 * Math.SQRT2 * Math.cos(radians - phase)),
- round(2500 * Math.SQRT2 * Math.sin(radians - phase)),
- 3400,
- ];
-};
+export const getDefaultCameraPosition =
+ (props: GetDefaultCameraPositionProps): [number, number, number] => {
+ const { heading, bedSize, topDown, visual } = props;
+ const angle = topDown ? heading : (heading - 45) % 360;
+ const radians = angle * Math.PI / 180;
+ const smallX = bedSize.x + SMALL_FACTOR;
+ const smallY = visual ? bedSize.y + SMALL_FACTOR : smallX;
+ const bigX = bedSize.x + BIG_FACTOR;
+ const bigY = visual ? bedSize.y + BIG_FACTOR : bigX;
+
+ if (topDown) {
+ const phase = Math.PI / 2;
+ return [
+ round(smallX * Math.cos(radians - phase)),
+ round(smallY * Math.sin(radians - phase)),
+ 5000,
+ ];
+ }
+
+ const phase = Math.PI / 4;
+ return isDesktop()
+ ? [
+ round(smallX * Math.cos(radians - phase)),
+ round(smallY * Math.sin(radians - phase)),
+ 2500,
+ ]
+ : [
+ round(bigX * Math.cos(radians - phase)),
+ round(bigY * Math.sin(radians - phase)),
+ 3400,
+ ];
+ };
diff --git a/frontend/three_d_garden/camera_selection_ui.tsx b/frontend/three_d_garden/camera_selection_ui.tsx
index 5f4a2fd2aa..2e85981a80 100644
--- a/frontend/three_d_garden/camera_selection_ui.tsx
+++ b/frontend/three_d_garden/camera_selection_ui.tsx
@@ -1,63 +1,190 @@
import React from "react";
import { Config } from "./config";
import { getDefaultCameraPosition } from "./camera";
-import { Cylinder, Sphere } from "@react-three/drei";
+import { ThreeEvent, useFrame, useThree } from "@react-three/fiber";
+import { Cylinder, Line, Sphere } from "@react-three/drei";
import { Group, MeshPhongMaterial } from "./components";
-import { uniq } from "lodash";
+import { debounce, uniq } from "lodash";
import { setWebAppConfigValue } from "../config_storage/actions";
-import { NumericSetting } from "../session_keys";
+import { BooleanSetting, NumericSetting } from "../session_keys";
import { Person } from "./scenes/props";
import { ASSETS } from "./constants";
+import { Actions } from "../constants";
+import { Object3D } from "three";
export interface CameraSelectionUIProps {
config: Config;
dispatch: Function | undefined;
+ topDownAtStart: boolean;
+}
+
+interface Hovered {
+ angle: number;
+ topDown: boolean;
}
export const CameraSelectionUI = (props: CameraSelectionUIProps) => {
const { config } = props;
- const [hovered, setHovered] = React.useState(undefined);
+ const [hovered, setHovered] = React.useState(undefined);
+ const hoveredRef = React.useRef(undefined);
+ const markerRefs =
+ React.useRef>({});
+ const markerRefCallbacks =
+ // eslint-disable-next-line func-call-spacing
+ React.useRef void>>({});
+ const { camera, pointer, raycaster } = useThree();
+ const setMarkerRef = React.useCallback(
+ (markerId: string) => {
+ markerRefCallbacks.current[markerId] ||= (node: Object3D | null) => {
+ markerRefs.current[markerId] = node;
+ };
+ return markerRefCallbacks.current[markerId];
+ },
+ []);
+ useFrame(() => {
+ if (!config.cameraSelectionView) { return; }
+ raycaster.setFromCamera(pointer, camera);
+ const markerNodes = Object.values(markerRefs.current)
+ .filter((node): node is Object3D => !!node);
+ const intersection = raycaster
+ .intersectObjects(markerNodes, false)
+ .find(hit => !!hit.object.userData.hovered);
+ const nextHovered = intersection?.object.userData.hovered as
+ Hovered | undefined;
+ if (hoveredRef.current?.angle == nextHovered?.angle
+ && hoveredRef.current?.topDown == nextHovered?.topDown) {
+ return;
+ }
+ hoveredRef.current = nextHovered;
+ setHovered(nextHovered);
+ });
+ const topDownSelected = props.topDownAtStart;
+ const common = {
+ config: props.config,
+ dispatch: props.dispatch,
+ topDownAtStart: props.topDownAtStart,
+ hovered,
+ setMarkerRef,
+ };
return
- {uniq([0, 90, 180, 270, config.viewpointHeading]).map(angle => {
- const selected = angle == config.viewpointHeading;
- const baseColor = selected ? "blue" : "orange";
- const color = hovered == angle ? "cyan" : baseColor;
- const position = getDefaultCameraPosition(angle, config.topDown);
- const scaledPosition = position.map(p => p * 0.33) as [number, number, number];
- const height = scaledPosition[2] + config.bedHeight;
- return setHovered(angle)}
- onPointerOut={() => setHovered(undefined)}
- onClick={() => props.dispatch &&
- props.dispatch(setWebAppConfigValue(
- NumericSetting.viewpoint_heading, angle))}>
-
-
-
+ {uniq([0, 90, 180, 270,
+ topDownSelected ? config.viewpointHeading : 0])
+ .map(angle =>
+ )}
+ {uniq([0, 90, 180, 270, 45, 135, 225, 315,
+ topDownSelected ? 0 : config.viewpointHeading])
+ .map(angle =>
+ )}
+ {config.lightsDebug &&
+ uniq([0, 90, 180, 270, 45, 135, 225, 315])
+ .map(angle =>
+ )}
+ ;
+};
+
+interface CameraLocationProps extends Hovered {
+ config: Config;
+ dispatch: Function | undefined;
+ topDownAtStart: boolean;
+ hovered: Hovered | undefined;
+ setMarkerRef(markerId: string): (node: Object3D | null) => void;
+ debug: boolean;
+}
+
+const CameraLocation = (props: CameraLocationProps) => {
+ const {
+ config, dispatch, topDownAtStart, hovered,
+ setMarkerRef, angle, topDown, debug,
+ } = props;
+ const markerId = `${topDown}-${debug}-${angle}`;
+ const isSelected = (topDownAtStart == topDown)
+ && angle == (config.viewpointHeading);
+ const isHovered = hovered?.angle == angle && hovered?.topDown == topDown;
+ const baseColor = isSelected ? "blue" : "orange";
+ const color = isHovered ? "cyan" : baseColor;
+ const bedSize = { x: config.bedLengthOuter, y: config.bedWidthOuter };
+ const position = getDefaultCameraPosition({
+ heading: angle,
+ bedSize,
+ topDown,
+ visual: !debug,
+ });
+ const baseScaleXY = debug ? 1 : 0.8;
+ const scale = topDown ? 0.1 : baseScaleXY;
+ const baseScaleZ = debug ? 1 : 0.8 * 0.25;
+ const zScale = topDown ? 0 : baseScaleZ;
+ const scaledPosition: [number, number, number] = [
+ position[0] * scale,
+ position[1] * scale,
+ position[2] * zScale,
+ ];
+ const height = config.bedHeight + scaledPosition[2];
+ const click = debounce(() => {
+ if (dispatch) {
+ dispatch(setWebAppConfigValue(
+ NumericSetting.viewpoint_heading, angle));
+ dispatch(setWebAppConfigValue(
+ BooleanSetting.top_down_view, topDown));
+ dispatch({
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: undefined,
+ });
+ dispatch({
+ type: Actions.TOGGLE_3D_TOP_DOWN_VIEW,
+ payload: topDown,
+ });
+ }
+ });
+ const hoveredData = { angle, topDown };
+ const onClick = (e: ThreeEvent) => {
+ e.stopPropagation();
+ click();
+ };
+ return
+
+
+
+
+ {!topDown &&
+ rotation={[Math.PI / 2, 0, 0]}
+ onClick={onClick}>
-
+ }
+ {!topDown &&
- ;
- })}
+ rotation={[Math.PI / 2, 0, 0]} />}
+
+ {debug &&
+ }
;
};
diff --git a/frontend/three_d_garden/fps_probe.tsx b/frontend/three_d_garden/fps_probe.tsx
index c3e9949da9..3a9960b586 100644
--- a/frontend/three_d_garden/fps_probe.tsx
+++ b/frontend/three_d_garden/fps_probe.tsx
@@ -107,7 +107,8 @@ export const FPSProbe = () => {
const linesToLog = Object.entries(linesToLogObj)
.map(([key, value]) => `${key}: ${value}`)
.join("\n");
- console.log(linesToLog);
+ const doLog = (localStorage.getItem("FPS_LOGS") ?? "true") == "true";
+ doLog && console.log(linesToLog);
reportCount.current += 1;
const doReport = !(reportCount.current % REPORT_EVERY_N);
const average = Math.round(samples.current
@@ -119,7 +120,7 @@ export const FPSProbe = () => {
total: samples.current.length,
};
doReport && window.logStore?.log("3D Garden FPS", report, "info");
- console.log(report);
+ doLog && console.log(report);
frameCount.current = 0;
lastTime.current = now;
}
diff --git a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
index 4b86b01ba5..f3dcde67e1 100644
--- a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
@@ -36,9 +36,12 @@ import { convertPlants } from "../../../farm_designer/three_d_garden_map";
import { mockDispatch } from "../../../__test_support__/fake_dispatch";
import { setMockInstanceId } from "../../../__test_support__/three_d_mocks";
import { PLANT_ICON_ATLAS } from "../plant_icon_atlas";
+import { Mode } from "../../../farm_designer/map/interfaces";
+import * as mapUtil from "../../../farm_designer/map/util";
describe(" ", () => {
let reactUseRefSpy: jest.SpyInstance;
+ let getModeSpy: jest.SpyInstance;
beforeEach(() => {
mockRefImpl = () => ({
@@ -63,10 +66,12 @@ describe(" ", () => {
clock: { getElapsedTime: jest.fn(() => 0) },
camera: { quaternion: new Quaternion() },
}));
+ getModeSpy = jest.spyOn(mapUtil, "getMode").mockReturnValue(Mode.none);
});
afterEach(() => {
reactUseRefSpy.mockRestore();
+ getModeSpy.mockRestore();
delete PLANT_ICON_ATLAS["/crops/icons/beet.avif"];
});
@@ -139,6 +144,19 @@ describe(" ", () => {
expect(mockNavigate).not.toHaveBeenCalled();
});
+ it("doesn't navigate in camera selection mode", () => {
+ getModeSpy.mockReturnValue(Mode.cameraSelection);
+ setMockInstanceId(0);
+ const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ const { container } = render( );
+ const mesh = container.querySelector("instancedmesh");
+ mesh && fireEvent.click(mesh, { instanceId: 0 });
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(mockNavigate).not.toHaveBeenCalled();
+ });
+
it("doesn't navigate with missing instanceId", () => {
setMockInstanceId(undefined);
const p = fakeProps();
diff --git a/frontend/three_d_garden/garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
index 93f4f910cc..202ff91d9e 100644
--- a/frontend/three_d_garden/garden/__tests__/plants_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
@@ -223,6 +223,20 @@ describe(" ", () => {
expect(mockNavigate).toHaveBeenCalledWith(Path.plants("1"));
});
+ it("doesn't navigate on spread click in camera selection mode", () => {
+ setMockInstanceId(0);
+ getModeSpy.mockReturnValue(Mode.cameraSelection);
+ queueMeshRef();
+ const p = fakeProps();
+ const dispatch = jest.fn();
+ p.dispatch = mockDispatch(dispatch);
+ const { container } = render( );
+ const mesh = container.querySelector("instancedmesh");
+ mesh && fireEvent.click(mesh, { instanceId: 0 });
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(mockNavigate).not.toHaveBeenCalled();
+ });
+
it("updates instance colors on frame", () => {
queueMeshRef();
const p = fakeProps();
diff --git a/frontend/three_d_garden/garden/plant_instances.tsx b/frontend/three_d_garden/garden/plant_instances.tsx
index eed8a07c8d..0bb4dfedbc 100644
--- a/frontend/three_d_garden/garden/plant_instances.tsx
+++ b/frontend/three_d_garden/garden/plant_instances.tsx
@@ -23,6 +23,7 @@ import {
getPlantIconTexture,
getPlantIconTextureUrl,
} from "./plant_icon_atlas";
+import { Mode } from "../../farm_designer/map/interfaces";
export interface PlantInstancesProps {
plants: ThreeDGardenPlant[];
@@ -103,7 +104,7 @@ const PlantIconInstances = (props: PlantIconInstancesProps) => {
if (isUndefined(instanceId)) { return; }
const plant = plants[instanceId];
if (plant?.id && dispatch && visible &&
- !HOVER_OBJECT_MODES.includes(getMode())) {
+ ![...HOVER_OBJECT_MODES, Mode.cameraSelection].includes(getMode())) {
dispatch(setPanelOpen(true));
navigate(Path.plants(plant.id));
}
diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx
index fa5269c72d..0b95b0639b 100644
--- a/frontend/three_d_garden/garden/plants.tsx
+++ b/frontend/three_d_garden/garden/plants.tsx
@@ -228,7 +228,7 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
if (isUndefined(instanceId)) { return; }
const plant = plants[instanceId];
if (plant?.id && dispatch && visible &&
- !HOVER_OBJECT_MODES.includes(getMode())) {
+ ![...HOVER_OBJECT_MODES, Mode.cameraSelection].includes(getMode())) {
dispatch(setPanelOpen(true));
navigate(Path.plants(plant.id));
}
diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx
index 25834d299f..8144b365d5 100644
--- a/frontend/three_d_garden/garden_model.tsx
+++ b/frontend/three_d_garden/garden_model.tsx
@@ -118,8 +118,9 @@ export const GardenModel = (props: GardenModelProps) => {
});
const baseAngle = 0;
+ const heading = Math.ceil(config.viewpointHeading / 90) * 90;
const topDownCameraAngle = config.topDown
- ? baseAngle + config.viewpointHeading * Math.PI / 180
+ ? baseAngle + heading * Math.PI / 180
: undefined;
const camera = getCamera(
config,
@@ -128,6 +129,7 @@ export const GardenModel = (props: GardenModelProps) => {
cameraInit({
topDown: config.topDown,
viewpointHeading: config.viewpointHeading,
+ bedSize: { x: config.bedLengthOuter, y: config.bedWidthOuter },
}));
const [controlsCamera, setControlsCamera] =
// eslint-disable-next-line no-null/no-null
@@ -161,6 +163,10 @@ export const GardenModel = (props: GardenModelProps) => {
const showMoistureReadings = !!props.addPlantProps?.getConfigValue(
BooleanSetting.show_sensor_readings);
+ const topDownAtStart = !!props.addPlantProps?.getConfigValue(
+ BooleanSetting.top_down_view);
+ const topDownZoomLevel = 0.25 * 3000 / config.bedLengthOuter;
+
// eslint-disable-next-line no-null/no-null
const skyRef = React.useRef(null);
const sunFactorRef = React.useRef(1);
@@ -237,7 +243,7 @@ export const GardenModel = (props: GardenModelProps) => {
fov={40} near={10} far={BigDistance.far}
position={camera.position}
rotation={[0, 0, 0]}
- zoom={config.topDown ? 0.25 : 1}
+ zoom={config.topDown ? topDownZoomLevel : 1}
up={[0, 0, 1]} />
{controlsCamera &&
@@ -343,6 +349,9 @@ export const GardenModel = (props: GardenModelProps) => {
-
+
;
};
diff --git a/frontend/three_d_garden/scenes/props/people.tsx b/frontend/three_d_garden/scenes/props/people.tsx
index fca581d33c..fbdafbccf1 100644
--- a/frontend/three_d_garden/scenes/props/people.tsx
+++ b/frontend/three_d_garden/scenes/props/people.tsx
@@ -65,6 +65,7 @@ export const Person = (props: PersonProps) => {
transparent={true}
side={DoubleSide}
opacity={0.4}
+ raycast={() => undefined}
renderOrder={RenderOrder.one} />
;
};
diff --git a/frontend/wizard/__tests__/checks_test.tsx b/frontend/wizard/__tests__/checks_test.tsx
index eb9efa00bd..c1ed3c0353 100644
--- a/frontend/wizard/__tests__/checks_test.tsx
+++ b/frontend/wizard/__tests__/checks_test.tsx
@@ -779,6 +779,28 @@ describe(" ", () => {
const { container } = render( );
expect(container.innerHTML).toContain("map-orientation");
});
+
+ it("renders 3D camera selection controls", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => key == "three_d_garden";
+ const { container } = render( );
+ expect(container.textContent).toContain("Set camera starting location");
+ expect(container.textContent).not.toContain("Map origin");
+ });
+
+ it("opens camera selection from 3D wizard orientation", () => {
+ const p = fakeProps();
+ p.getConfigValue = key => key == "three_d_garden";
+ const { container } = render( );
+ const button =
+ container.querySelector("button[title='Set camera starting location']");
+ if (!button) { throw new Error("Expected camera starting location button"); }
+ fireEvent.click(button);
+ expect(p.dispatch).toHaveBeenCalledWith({
+ type: Actions.TOGGLE_3D_CAMERA_SELECTION,
+ payload: undefined,
+ });
+ });
});
describe(" ", () => {
diff --git a/frontend/wizard/__tests__/data_test.ts b/frontend/wizard/__tests__/data_test.ts
index fdef9d6917..a28a7a79a8 100644
--- a/frontend/wizard/__tests__/data_test.ts
+++ b/frontend/wizard/__tests__/data_test.ts
@@ -1,5 +1,6 @@
import { uniq } from "lodash";
import { fakeWizardStepResult } from "../../__test_support__/fake_state/resources";
+import { BooleanSetting } from "../../session_keys";
import {
setupProgressString,
WizardStepSlug, WIZARD_SECTIONS, WIZARD_STEPS, WIZARD_STEP_SLUGS,
@@ -70,4 +71,26 @@ describe("data check", () => {
});
expect(expressSections.length).toEqual(sections.length - 1);
});
+
+ it("uses camera selection outcome for 3D map orientation", () => {
+ const steps = WIZARD_STEPS({
+ firmwareHardware: "farmduino_k18",
+ getConfigValue: key => key == BooleanSetting.three_d_garden,
+ });
+ const mapOrientation = steps.find(step =>
+ step.slug == WizardStepSlug.mapOrientation);
+ expect(mapOrientation?.outcomes.map(outcome => outcome.slug))
+ .toEqual(["cameraSelection"]);
+ });
+
+ it("keeps 2D map orientation outcomes when 3D is disabled", () => {
+ const steps = WIZARD_STEPS({
+ firmwareHardware: "farmduino_k18",
+ getConfigValue: () => false,
+ });
+ const mapOrientation = steps.find(step =>
+ step.slug == WizardStepSlug.mapOrientation);
+ expect(mapOrientation?.outcomes.map(outcome => outcome.slug))
+ .toEqual(["rotated", "incorrectOrigin"]);
+ });
});
diff --git a/frontend/wizard/checks.tsx b/frontend/wizard/checks.tsx
index 04471fda85..d94087803c 100644
--- a/frontend/wizard/checks.tsx
+++ b/frontend/wizard/checks.tsx
@@ -69,7 +69,7 @@ import {
} from "farmbot/dist/resources/configs/web_app";
import { GetWebAppConfigValue, toggleWebAppBool } from "../config_storage/actions";
import { PLACEHOLDER_FARMBOT } from "../photos/images/image_flipper";
-import { OriginSelector } from "../settings/farm_designer_settings";
+import { CameraStartingLocationButton, OriginSelector } from "../settings/farm_designer_settings";
import { Sensors } from "../sensors";
import {
DropdownConfig,
@@ -568,11 +568,18 @@ export const SelectMapOrigin = (props: WizardOutcomeComponentProps) =>
getConfigValue={props.getConfigValue} />
;
-export const MapOrientation = (props: WizardOutcomeComponentProps) =>
-
-
-
+export const MapOrientation = (props: WizardOutcomeComponentProps) => {
+ const is3D = !!props.getConfigValue(BooleanSetting.three_d_garden);
+ return
+ {!is3D &&
}
+ {!is3D &&
}
+ {is3D &&
+ {t(DeviceSetting.setCameraStartingLocation)}
+
+
}
;
+};
export const PeripheralsCheck = (props: WizardStepComponentProps) => {
const peripherals = uniq(selectAllPeripherals(props.resources));
diff --git a/frontend/wizard/data.ts b/frontend/wizard/data.ts
index 9ce087a278..45777e1f52 100644
--- a/frontend/wizard/data.ts
+++ b/frontend/wizard/data.ts
@@ -190,6 +190,7 @@ export enum WizardStepSlug {
// eslint-disable-next-line complexity
export const WIZARD_STEPS = (props: WizardStepDataProps): WizardSteps => {
const { firmwareHardware } = props;
+ const is3D = !!props.getConfigValue?.(BooleanSetting.three_d_garden);
const xySwap = !!props.getConfigValue?.(BooleanSetting.xy_swap);
const positiveMovementInstruction = (swap: boolean) =>
swap
@@ -511,18 +512,30 @@ export const WIZARD_STEPS = (props: WizardStepDataProps): WizardSteps => {
component: MapOrientation,
question: t("Does the virtual FarmBot match your real life FarmBot?"),
outcomes: [
- {
- slug: "rotated",
- description: t("The map is rotated incorrectly"),
- tips: "",
- component: RotateMapToggle,
- },
- {
- slug: "incorrectOrigin",
- description: t("The map origin is in a different corner"),
- tips: t("Select the correct map origin."),
- component: SelectMapOrigin,
- },
+ ...(is3D
+ ? []
+ : [{
+ slug: "rotated",
+ description: t("The map is rotated incorrectly"),
+ tips: "",
+ component: RotateMapToggle,
+ }]),
+ ...(is3D
+ ? []
+ : [{
+ slug: "incorrectOrigin",
+ description: t("The map origin is in a different corner"),
+ tips: t("Select the correct map origin."),
+ component: SelectMapOrigin,
+ }]),
+ ...(is3D
+ ? [{
+ slug: "cameraSelection",
+ description: t("The camera location is incorrect"),
+ tips: SetupWizardContent.SET_CAMERA_LOCATION,
+ component: MapOrientation,
+ }]
+ : []),
],
},
{
From 3b1ab82832a4fdd49557943b06caad748a0b47d9 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Tue, 21 Apr 2026 19:24:45 -0700
Subject: [PATCH 07/11] improve 3D camera angles
---
...22013033_change_viewpoint_heading_default.rb | 5 +++++
db/structure.sql | 6 +++++-
frontend/settings/default_values.ts | 2 +-
.../__tests__/camera_selection_ui_test.tsx | 13 +++++++------
.../three_d_garden/__tests__/camera_test.ts | 16 ++++++++--------
frontend/three_d_garden/camera.ts | 14 ++++++++------
frontend/three_d_garden/camera_selection_ui.tsx | 17 ++++++++++-------
frontend/three_d_garden/config.ts | 2 +-
frontend/three_d_garden/scenes/props/people.tsx | 2 +-
9 files changed, 46 insertions(+), 31 deletions(-)
create mode 100644 db/migrate/20260422013033_change_viewpoint_heading_default.rb
diff --git a/db/migrate/20260422013033_change_viewpoint_heading_default.rb b/db/migrate/20260422013033_change_viewpoint_heading_default.rb
new file mode 100644
index 0000000000..de72f4efc1
--- /dev/null
+++ b/db/migrate/20260422013033_change_viewpoint_heading_default.rb
@@ -0,0 +1,5 @@
+class ChangeViewpointHeadingDefault < ActiveRecord::Migration[8.1]
+ def change
+ change_column_default(:web_app_configs, :viewpoint_heading, from: 0, to: 30)
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 7e7751df36..d9cef71e9b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -2037,7 +2037,9 @@ CREATE TABLE public.web_app_configs (
show_missed_step_plot boolean DEFAULT false,
enable_3d_electronics_box_top boolean DEFAULT true,
three_d_garden boolean DEFAULT false,
- dark_mode boolean DEFAULT true
+ dark_mode boolean DEFAULT true,
+ top_down_view boolean DEFAULT false,
+ viewpoint_heading integer DEFAULT 30
);
@@ -3763,6 +3765,8 @@ ALTER TABLE ONLY public.users
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
+('20260422013033'),
+('20260417190743'),
('20260305192457'),
('20250930204600'),
('20250925195004'),
diff --git a/frontend/settings/default_values.ts b/frontend/settings/default_values.ts
index 6fd01aa839..21efd070bb 100644
--- a/frontend/settings/default_values.ts
+++ b/frontend/settings/default_values.ts
@@ -90,7 +90,7 @@ const DEFAULT_WEB_APP_CONFIG_VALUES: Record = {
three_d_garden: false,
dark_mode: true,
["top_down_view" as Key]: false,
- ["viewpoint_heading" as Key]: 0,
+ ["viewpoint_heading" as Key]: 30,
};
const DEFAULT_EXPRESS_WEB_APP_CONFIG_VALUES =
diff --git a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
index 5ab1b13ba7..7abfd088c8 100644
--- a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
+++ b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
@@ -112,7 +112,7 @@ describe(" ", () => {
it("renders unique heading marker", () => {
const p = fakeProps();
p.config.cameraSelectionView = true;
- p.config.viewpointHeading = 45;
+ p.config.viewpointHeading = 30;
const { container } = render( );
expect(container.querySelectorAll(".spherehead").length).toEqual(12);
});
@@ -153,7 +153,7 @@ describe(" ", () => {
mountedWrappers.push(wrapper);
attachMeshRefs(wrapper);
intersectObjects.mockReturnValue([{
- object: { userData: { hovered: { angle: 45, topDown: false } } },
+ object: { userData: { hovered: { angle: 30, topDown: false } } },
}]);
actRenderer(() => {
@@ -162,7 +162,7 @@ describe(" ", () => {
const hoveredSphere = wrapper.root.findAll(node =>
node.props.name == "head"
- && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.angle == 30
&& node.props.userData?.hovered?.topDown === false)[0];
expect(hoveredSphere).toBeTruthy();
const material = hoveredSphere?.findAll(node =>
@@ -173,6 +173,7 @@ describe(" ", () => {
it("keeps default color when raycast finds no hovered marker", () => {
const p = fakeProps();
p.config.cameraSelectionView = true;
+ p.config.viewpointHeading = 0;
const wrapper = createRenderer( );
mountedWrappers.push(wrapper);
attachMeshRefs(wrapper);
@@ -184,7 +185,7 @@ describe(" ", () => {
const sphere = wrapper.root.findAll(node =>
node.props.name == "head"
- && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.angle == 30
&& node.props.userData?.hovered?.topDown === false)[0];
const material = sphere?.findAll(node =>
node.props.color !== undefined)[0];
@@ -228,13 +229,13 @@ describe(" ", () => {
mountedWrappers.push(wrapper);
const body = wrapper.root.findAll(node =>
node.props.name == "body"
- && node.props.userData?.hovered?.angle == 45
+ && node.props.userData?.hovered?.angle == 30
&& node.props.userData?.hovered?.topDown === false)[0];
actRenderer(() => {
body?.props.onClick({ stopPropagation: jest.fn() });
});
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
- NumericSetting.viewpoint_heading, 45);
+ NumericSetting.viewpoint_heading, 30);
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
BooleanSetting.top_down_view, false);
});
diff --git a/frontend/three_d_garden/__tests__/camera_test.ts b/frontend/three_d_garden/__tests__/camera_test.ts
index 8024241316..399192f681 100644
--- a/frontend/three_d_garden/__tests__/camera_test.ts
+++ b/frontend/three_d_garden/__tests__/camera_test.ts
@@ -35,7 +35,7 @@ describe("cameraInit()", () => {
mockDev = undefined;
mockIsDesktop = true;
expect(cameraInit(fakeProps())).toEqual({
- position: [2192, -2192, 2500],
+ position: [2475, -2475, 2500],
target: [0, 0, 0],
});
});
@@ -53,7 +53,7 @@ describe("cameraInit()", () => {
mockDev = "{";
mockIsDesktop = true;
expect(cameraInit(fakeProps())).toEqual({
- position: [2192, -2192, 2500],
+ position: [2475, -2475, 2500],
target: [0, 0, 0],
});
});
@@ -62,7 +62,7 @@ describe("cameraInit()", () => {
mockDev = undefined;
mockIsDesktop = false;
expect(cameraInit(fakeProps())).toEqual({
- position: [2475, -2475, 3400],
+ position: [4596, -4596, 3400],
target: [0, 0, 0],
});
});
@@ -84,7 +84,7 @@ describe("cameraInit()", () => {
const p = fakeProps();
p.viewpointHeading = 90;
expect(cameraInit(p)).toEqual({
- position: [3100, 0, 2500],
+ position: [3500, 0, 2500],
target: [0, 0, 0],
});
});
@@ -102,14 +102,14 @@ describe("getDefaultCameraPosition()", () => {
mockIsDesktop = true;
const p = fakeProps();
p.heading = 180;
- expect(getDefaultCameraPosition(p)).toEqual([0, 3100, 2500]);
+ expect(getDefaultCameraPosition(p)).toEqual([0, 3500, 2500]);
});
it("returns mobile position", () => {
mockIsDesktop = false;
const p = fakeProps();
p.heading = 270;
- expect(getDefaultCameraPosition(p)).toEqual([-3500, 0, 3400]);
+ expect(getDefaultCameraPosition(p)).toEqual([-6500, 0, 3400]);
});
it("returns top down position", () => {
@@ -117,7 +117,7 @@ describe("getDefaultCameraPosition()", () => {
const p = fakeProps();
p.heading = 90;
p.topDown = true;
- expect(getDefaultCameraPosition(p)).toEqual([3100, 0, 5000]);
+ expect(getDefaultCameraPosition(p)).toEqual([3500, 0, 5000]);
});
it("returns camera location visual location", () => {
@@ -125,6 +125,6 @@ describe("getDefaultCameraPosition()", () => {
const p = fakeProps();
p.heading = 180;
p.visual = true;
- expect(getDefaultCameraPosition(p)).toEqual([0, 1600, 2500]);
+ expect(getDefaultCameraPosition(p)).toEqual([0, 2750, 2500]);
});
});
diff --git a/frontend/three_d_garden/camera.ts b/frontend/three_d_garden/camera.ts
index 91e0d11c4b..801f17ffa5 100644
--- a/frontend/three_d_garden/camera.ts
+++ b/frontend/three_d_garden/camera.ts
@@ -43,8 +43,8 @@ export const cameraInit = (props: CameraInitProps): Camera => {
return initCamera;
};
-const SMALL_FACTOR = 100;
-const BIG_FACTOR = 500;
+const SMALL_FACTOR = 2000;
+const BIG_FACTOR = 5000;
export interface GetDefaultCameraPositionProps {
heading: number;
@@ -58,10 +58,12 @@ export const getDefaultCameraPosition =
const { heading, bedSize, topDown, visual } = props;
const angle = topDown ? heading : (heading - 45) % 360;
const radians = angle * Math.PI / 180;
- const smallX = bedSize.x + SMALL_FACTOR;
- const smallY = visual ? bedSize.y + SMALL_FACTOR : smallX;
- const bigX = bedSize.x + BIG_FACTOR;
- const bigY = visual ? bedSize.y + BIG_FACTOR : bigX;
+ const smallF = Math.min(SMALL_FACTOR, SMALL_FACTOR * (3000 / bedSize.x) ** 2);
+ const bigF = Math.min(BIG_FACTOR, BIG_FACTOR * (3000 / bedSize.x) ** 2);
+ const smallX = bedSize.x / 2 + smallF;
+ const smallY = visual ? bedSize.y / 2 + smallF : smallX;
+ const bigX = bedSize.x / 2 + bigF;
+ const bigY = visual ? bedSize.y / 2 + BIG_FACTOR : bigX;
if (topDown) {
const phase = Math.PI / 2;
diff --git a/frontend/three_d_garden/camera_selection_ui.tsx b/frontend/three_d_garden/camera_selection_ui.tsx
index 2e85981a80..d92258ac26 100644
--- a/frontend/three_d_garden/camera_selection_ui.tsx
+++ b/frontend/three_d_garden/camera_selection_ui.tsx
@@ -23,6 +23,9 @@ interface Hovered {
topDown: boolean;
}
+const ORTHOGONAL_ANGLES = [0, 90, 180, 270];
+const ISO_ANGLES = [30, 150, 210, 330];
+
export const CameraSelectionUI = (props: CameraSelectionUIProps) => {
const { config } = props;
const [hovered, setHovered] = React.useState(undefined);
@@ -69,22 +72,22 @@ export const CameraSelectionUI = (props: CameraSelectionUIProps) => {
return
- {uniq([0, 90, 180, 270,
- topDownSelected ? config.viewpointHeading : 0])
+ {uniq(ORTHOGONAL_ANGLES.concat(
+ topDownSelected ? config.viewpointHeading : 0))
.map(angle =>
)}
- {uniq([0, 90, 180, 270, 45, 135, 225, 315,
- topDownSelected ? 0 : config.viewpointHeading])
+ {uniq(ORTHOGONAL_ANGLES.concat(ISO_ANGLES).concat(
+ topDownSelected ? 0 : config.viewpointHeading))
.map(angle =>
)}
{config.lightsDebug &&
- uniq([0, 90, 180, 270, 45, 135, 225, 315])
+ uniq(ORTHOGONAL_ANGLES.concat(ISO_ANGLES))
.map(angle =>
{
topDown,
visual: !debug,
});
- const baseScaleXY = debug ? 1 : 0.8;
+ const baseScaleXY = debug ? 1 : 0.5;
const scale = topDown ? 0.1 : baseScaleXY;
- const baseScaleZ = debug ? 1 : 0.8 * 0.25;
+ const baseScaleZ = debug ? 1 : 0.5 * 0.25;
const zScale = topDown ? 0 : baseScaleZ;
const scaledPosition: [number, number, number] = [
position[0] * scale,
diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts
index c8de3d920b..84b538c40d 100644
--- a/frontend/three_d_garden/config.ts
+++ b/frontend/three_d_garden/config.ts
@@ -170,7 +170,7 @@ export const INITIAL: ConfigWithPosition = {
heading: 0,
perspective: true,
topDown: false,
- viewpointHeading: 0,
+ viewpointHeading: 30,
cameraSelectionView: false,
bot: true,
laser: false,
diff --git a/frontend/three_d_garden/scenes/props/people.tsx b/frontend/three_d_garden/scenes/props/people.tsx
index fbdafbccf1..eb4fd22cde 100644
--- a/frontend/three_d_garden/scenes/props/people.tsx
+++ b/frontend/three_d_garden/scenes/props/people.tsx
@@ -66,6 +66,6 @@ export const Person = (props: PersonProps) => {
side={DoubleSide}
opacity={0.4}
raycast={() => undefined}
- renderOrder={RenderOrder.one} />
+ renderOrder={RenderOrder.clouds} />
;
};
From b9b81e9e3465122b51f396d70c1f7320309912b5 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Tue, 21 Apr 2026 20:19:13 -0700
Subject: [PATCH 08/11] fix 3D camera selection issues
---
.../__tests__/camera_selection_ui_test.tsx | 6 +++---
.../three_d_garden/__tests__/garden_model_test.tsx | 1 -
frontend/three_d_garden/camera_selection_ui.tsx | 11 ++---------
frontend/three_d_garden/garden_model.tsx | 11 +++++++----
4 files changed, 12 insertions(+), 17 deletions(-)
diff --git a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
index 7abfd088c8..8b1ad11bb3 100644
--- a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
+++ b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
@@ -227,12 +227,12 @@ describe(" ", () => {
p.config.cameraSelectionView = true;
const wrapper = createRenderer( );
mountedWrappers.push(wrapper);
- const body = wrapper.root.findAll(node =>
- node.props.name == "body"
+ const head = wrapper.root.findAll(node =>
+ node.props.name == "head"
&& node.props.userData?.hovered?.angle == 30
&& node.props.userData?.hovered?.topDown === false)[0];
actRenderer(() => {
- body?.props.onClick({ stopPropagation: jest.fn() });
+ head?.props.onClick({ stopPropagation: jest.fn() });
});
expect(setWebAppConfigValueSpy).toHaveBeenCalledWith(
NumericSetting.viewpoint_heading, 30);
diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx
index fa19452233..366979870f 100644
--- a/frontend/three_d_garden/__tests__/garden_model_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx
@@ -130,7 +130,6 @@ describe(" ", () => {
p.config.viewpointHeading = 45;
const { container } = render( );
expect(container.innerHTML).toContain("camera-selection");
- expect(container.innerHTML).toContain("person_2.avif");
});
it("renders no user plants", () => {
diff --git a/frontend/three_d_garden/camera_selection_ui.tsx b/frontend/three_d_garden/camera_selection_ui.tsx
index d92258ac26..5e7fdd923c 100644
--- a/frontend/three_d_garden/camera_selection_ui.tsx
+++ b/frontend/three_d_garden/camera_selection_ui.tsx
@@ -7,8 +7,6 @@ import { Group, MeshPhongMaterial } from "./components";
import { debounce, uniq } from "lodash";
import { setWebAppConfigValue } from "../config_storage/actions";
import { BooleanSetting, NumericSetting } from "../session_keys";
-import { Person } from "./scenes/props";
-import { ASSETS } from "./constants";
import { Actions } from "../constants";
import { Object3D } from "three";
@@ -132,7 +130,7 @@ const CameraLocation = (props: CameraLocationProps) => {
position[1] * scale,
position[2] * zScale,
];
- const height = config.bedHeight + scaledPosition[2];
+ const height = config.bedZOffset + config.bedHeight + scaledPosition[2];
const click = debounce(() => {
if (dispatch) {
dispatch(setWebAppConfigValue(
@@ -167,7 +165,7 @@ const CameraLocation = (props: CameraLocationProps) => {
opacity={1}
color={color} />
- {!topDown &&
+ {!topDown && config.lightsDebug &&
{
opacity={0.9}
color={color} />
}
- {!topDown &&
- }
{debug &&
}
diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx
index 8144b365d5..93a673fa35 100644
--- a/frontend/three_d_garden/garden_model.tsx
+++ b/frontend/three_d_garden/garden_model.tsx
@@ -257,6 +257,8 @@ export const GardenModel = (props: GardenModelProps) => {
enablePan={config.pan}
dampingFactor={0.2}
target={camera.target}
+ minZoom={config.lightsDebug ? 0 : 0.05}
+ maxZoom={10}
minDistance={config.lightsDebug ? 50 : 500}
maxDistance={config.lightsDebug ? BigDistance.devZoom : BigDistance.zoom} />}
@@ -349,9 +351,10 @@ export const GardenModel = (props: GardenModelProps) => {
-
+ {config.cameraSelectionView &&
+ }
;
};
From d0814a03d1424c7e331305a27fac2590b93fb62c Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Wed, 22 Apr 2026 11:59:44 -0700
Subject: [PATCH 09/11] rotate promo more on mobile and fix init
---
frontend/promo/promo.tsx | 98 +++++++++++++----------
frontend/three_d_garden/garden/images.tsx | 17 +++-
2 files changed, 72 insertions(+), 43 deletions(-)
diff --git a/frontend/promo/promo.tsx b/frontend/promo/promo.tsx
index 3f204fbbf3..487a256432 100644
--- a/frontend/promo/promo.tsx
+++ b/frontend/promo/promo.tsx
@@ -16,6 +16,7 @@ import { ThreeDGardenPlant } from "../three_d_garden/garden";
import { TaggedGenericPointer } from "farmbot";
import { calculatePointPositions } from "./points";
import { SEASON_TIMINGS, SEASONS } from "./constants";
+import { isMobile } from "../screen_size";
const PROMO_BED_SIZES = [
{
@@ -29,6 +30,7 @@ const PROMO_BED_SIZES = [
];
type ThreeDPlantsCache = Record;
+const PLANTS_CACHE: ThreeDPlantsCache = {};
const calcCacheKey = (config: Config): string =>
`${config.bedLengthOuter}x${config.bedWidthOuter}: ${config.plants}`;
@@ -41,11 +43,38 @@ const calcPlantsCache = (
if (cache[cacheKey]) {
return cache;
}
- const positions = calculatePlantPositions(config);
- cache[cacheKey] = positions;
- return cache;
+ return {
+ ...cache,
+ [cacheKey]: calculatePlantPositions(config),
+ };
+};
+
+const prewarmPlantsCache = () => {
+ let next = PLANTS_CACHE;
+ PROMO_BED_SIZES.map(({ length, width }) => {
+ SEASONS.map(season => {
+ next = calcPlantsCache(next, {
+ ...INITIAL,
+ bedLengthOuter: length,
+ bedWidthOuter: width,
+ plants: season,
+ });
+ });
+ });
+ Object.assign(PLANTS_CACHE, next);
+};
+
+const getCachedPlants = (config: Config) => {
+ const cacheKey = calcCacheKey(config);
+ const cachedPlants = PLANTS_CACHE[cacheKey];
+ if (cachedPlants) { return cachedPlants; }
+
+ Object.assign(PLANTS_CACHE, calcPlantsCache(PLANTS_CACHE, config));
+ return PLANTS_CACHE[cacheKey] || [];
};
+prewarmPlantsCache();
+
export const getSeasonTimings = (currentSeason: string, step = 0) => {
const seasons = SEASON_TIMINGS.map(s => s.season);
const seasonIndex = seasons.indexOf(currentSeason);
@@ -56,50 +85,32 @@ export const getSeasonTimings = (currentSeason: string, step = 0) => {
};
export const Promo = () => {
- const [config, setConfig] = React.useState(INITIAL);
+ const [config, setConfig] = React.useState(() => {
+ let next = INITIAL;
+ if (isMobile()) {
+ next = { ...next, viewpointHeading: 80 };
+ }
+ next = modifyConfigsFromUrlParams(next);
+ return next;
+ });
const [toolTip, setToolTip] = React.useState({ timeoutId: 0, text: "" });
- const [activeFocus, setActiveFocus] = React.useState("");
+ const [activeFocus, setActiveFocus] = React.useState(() =>
+ getFocusFromUrlParams());
const common = {
config, setConfig,
toolTip, setToolTip,
activeFocus, setActiveFocus,
};
- React.useEffect(() => {
- setConfig(modifyConfigsFromUrlParams(config));
- setActiveFocus(getFocusFromUrlParams());
- PROMO_BED_SIZES.map(({ length, width }) => {
- SEASONS.map(season => {
- const tmpConfig = {
- ...INITIAL,
- bedLengthOuter: length,
- bedWidthOuter: width,
- plants: season,
- };
- setPlantsCache(calcPlantsCache(plantsCache, tmpConfig));
- });
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []); // intentionally empty dependency array
-
- const [plantsCache, setPlantsCache] = React.useState({});
- const [mapPoints, setMapPoints] = React.useState([]);
-
- React.useEffect(() => {
- setPlantsCache(calcPlantsCache(plantsCache, config));
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [config.plants, config.bedLengthOuter, config.bedWidthOuter]);
-
- React.useEffect(() => {
- setMapPoints(calculatePointPositions(config));
+ const mapPoints = React.useMemo(() =>
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
+ calculatePointPositions(config), [
config.soilSurface, config.soilHeight, config.soilSurfacePointCount,
config.soilSurfaceVariance, config.bedXOffset, config.bedYOffset,
config.bedWallThickness, config.bedLengthOuter, config.bedWidthOuter,
]);
- const startTimeRef = React.useRef(performance.now() / 1000);
+ const startTimeRef = React.useRef(0);
React.useEffect(() => {
if (!config.animateSeasons) { return; }
@@ -121,13 +132,16 @@ export const Promo = () => {
startTimeRef.current = performance.now() / 1000;
}, []);
- const getPlants = () => {
- const plants = plantsCache[calcCacheKey(config)] || [];
- if (config.promoSpread) {
- return plants.map(plant => ({ ...plant, id: 0 }));
- }
- return plants;
- };
+ const plants = React.useMemo(() => {
+ return getCachedPlants(config);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [config.plants, config.bedLengthOuter, config.bedWidthOuter]);
+
+ const threeDPlants = React.useMemo(() => {
+ return config.promoSpread
+ ? plants.map(plant => ({ ...plant, id: 0 }))
+ : plants;
+ }, [plants, config.promoSpread]);
return
@@ -140,7 +154,7 @@ export const Promo = () => {
diff --git a/frontend/three_d_garden/garden/images.tsx b/frontend/three_d_garden/garden/images.tsx
index 3ee0b11341..f33acd3a3e 100644
--- a/frontend/three_d_garden/garden/images.tsx
+++ b/frontend/three_d_garden/garden/images.tsx
@@ -91,6 +91,17 @@ export const ImageTexture = (props: ImageTextureProps) => {
const extents = soilSurfaceExtents(props.config);
const width = extents.x.max - extents.x.min;
const height = extents.y.max - extents.y.min;
+ const textureSize = 1024;
+ const textureWidth = width >= height
+ ? textureSize
+ : Math.max(1, Math.round(textureSize * width / height));
+ const textureHeight = height >= width
+ ? textureSize
+ : Math.max(1, Math.round(textureSize * height / width));
+ const textureKey = [
+ extents.x.min, extents.x.max,
+ extents.y.min, extents.y.max,
+ ].join(":");
const { bedXOffset, bedYOffset, bedWallThickness } = props.config;
const soilTexture = useTexture(ASSETS.textures.soil + "?=soilT");
const color = getColorFromBrightness(props.config.soilBrightness);
@@ -110,7 +121,11 @@ export const ImageTexture = (props: ImageTextureProps) => {
const highlightActive = lastImageArray[0]?.highlighted;
const commonProps = { width, height, bedWallThickness };
const mirrorTextureProps = getMirrorTextureProps(props.config);
- return
Date: Wed, 22 Apr 2026 13:50:23 -0700
Subject: [PATCH 10/11] upgrade deps and fix eslint errors
---
Gemfile.lock | 22 +-
bun.lock | 154 ++--
frontend/__test_support__/bun_test_setup.ts | 12 +-
frontend/__test_support__/three_d_mocks.tsx | 2 +-
frontend/__tests__/loading_plant_test.tsx | 2 +-
frontend/__tests__/revert_to_english_test.ts | 2 +-
.../api/__tests__/crud_data_tracking_test.ts | 8 +-
frontend/api/__tests__/crud_destroy_test.ts | 8 +-
frontend/config/__tests__/actions_test.ts | 4 +-
.../__tests__/connect_device/index_test.ts | 2 +-
.../__tests__/pin_form_fields_test.tsx | 3 +-
.../move/__tests__/take_photo_button_test.tsx | 2 +-
frontend/demo/lua_runner/index.ts | 4 +-
frontend/demo/lua_runner/run.ts | 6 +-
frontend/devices/__tests__/actions_test.ts | 8 +-
frontend/devices/actions.ts | 3 +-
.../farm_designer/__tests__/index_test.tsx | 2 +-
.../__tests__/location_info_test.tsx | 2 +-
.../__tests__/three_d_garden_map_test.tsx | 2 +-
.../map/__tests__/garden_map_test.tsx | 24 +-
.../__tests__/selection_box_actions_test.ts | 9 +-
.../plants/__tests__/plant_actions_test.ts | 9 +-
.../map/profile/__tests__/options_test.tsx | 2 +-
.../map/profile/__tests__/viewer_test.tsx | 2 +-
.../__tests__/add_farm_event_test.tsx | 5 +-
.../__tests__/edit_farm_event_test.tsx | 7 +-
.../__tests__/edit_fe_form_test.tsx | 2 +-
.../__tests__/farm_event_repeat_form_test.tsx | 4 +-
.../__tests__/farm_events_test.tsx | 4 +-
frontend/farmware/__tests__/actions_test.ts | 4 +-
.../farmware/panel/__tests__/info_test.tsx | 6 +-
frontend/folders/__tests__/actions_test.ts | 4 +-
frontend/folders/__tests__/component_test.tsx | 6 +-
.../__tests__/demo_login_option_test.tsx | 4 +-
.../front_page/__tests__/front_page_test.tsx | 16 +-
frontend/front_page/__tests__/login_test.tsx | 4 +-
.../__tests__/resend_verification_test.tsx | 2 +-
frontend/help/documentation.tsx | 8 +-
frontend/logs/__tests__/index_test.tsx | 2 +-
.../__tests__/settings_menu_test.tsx | 2 +-
frontend/messages/__tests__/actions_test.ts | 6 +-
frontend/messages/__tests__/cards_test.tsx | 10 +-
frontend/messages/__tests__/reducer_test.ts | 3 +-
.../__tests__/password_reset_test.tsx | 2 +-
.../photos/images/__tests__/photos_test.tsx | 2 +-
.../__tests__/image_filter_menu_test.tsx | 4 +-
frontend/plants/__tests__/crop_info_test.tsx | 2 +-
frontend/plants/crop_info.tsx | 8 +-
.../point_groups/__tests__/actions_test.ts | 2 +-
.../criteria/__tests__/component_test.tsx | 10 +-
.../criteria/__tests__/show_test.tsx | 8 +-
frontend/points/__tests__/point_info_test.tsx | 2 +-
frontend/promo/__tests__/plants_test.ts | 2 +-
frontend/promo/plants.ts | 4 +-
.../__tests__/create_refresh_trigger_test.ts | 2 +-
frontend/redux/__tests__/root_reducer_test.ts | 2 +-
.../editor/__tests__/state_to_props_test.ts | 2 +-
.../regimens/list/__tests__/list_test.tsx | 6 +-
frontend/resources/reducer_support.ts | 2 +-
frontend/resources/selectors_by_id.ts | 2 +-
frontend/resources/sequence_meta.ts | 2 +-
.../__tests__/garden_snapshot_test.tsx | 4 +-
.../sensors/__tests__/sensor_list_test.tsx | 8 +-
frontend/sequences/__tests__/actions_test.ts | 2 +-
.../__tests__/request_auto_generation_test.ts | 4 +-
.../sequences/__tests__/sequences_test.tsx | 2 +-
.../__tests__/default_value_form_test.tsx | 2 +-
.../__tests__/variable_form_test.tsx | 2 +-
.../sequences/panel/__tests__/list_test.tsx | 2 +-
.../panel/__tests__/preview_test.tsx | 4 +-
.../__tests__/step_title_bar_test.tsx | 3 +-
.../__tests__/tile_execute_test.tsx | 2 +-
.../__tests__/tile_lua_support_test.tsx | 4 +-
.../__tests__/tile_send_message_test.tsx | 2 +-
.../pin_support/__tests__/mode_test.tsx | 4 +-
.../__tests__/component_test.tsx | 2 +-
.../tile_mark_as/__tests__/component_test.tsx | 2 +-
.../step_ui/__tests__/step_header_test.tsx | 4 +-
.../step_ui/__tests__/step_radio_test.tsx | 6 +-
.../step_ui/__tests__/step_wrapper_test.tsx | 2 +-
.../__tests__/farm_designer_settings_test.tsx | 4 +-
.../__tests__/account_settings_test.tsx | 2 +-
.../account/__tests__/actions_test.ts | 4 +-
.../__tests__/change_password_test.tsx | 4 +-
.../dangerous_delete_widget_test.tsx | 2 +-
frontend/settings/dev/dev_settings.tsx | 15 +-
.../settings/fbos_settings/farmbot_os_row.tsx | 19 +-
.../__tests__/axis_settings_test.tsx | 3 +-
.../__tests__/parameter_management_test.tsx | 2 +-
frontend/settings/maybe_highlight.tsx | 94 +-
.../pin_bindings/__tests__/model_test.tsx | 6 +-
.../__tests__/change_ownership_form_test.tsx | 2 +-
.../__tests__/create_transfer_cert_test.ts | 2 +-
.../__tests__/transfer_ownership_test.ts | 2 +-
frontend/sync/__tests__/actions_test.ts | 2 +-
.../__tests__/camera_selection_ui_test.tsx | 4 +-
.../three_d_garden/__tests__/camera_test.ts | 2 +-
.../__tests__/components_test.tsx | 2 +-
.../__tests__/garden_model_test.tsx | 10 +-
.../__tests__/group_order_visual_test.tsx | 2 +-
.../three_d_garden/bed/__tests__/bed_test.tsx | 3 +-
frontend/three_d_garden/bed/bed.tsx | 40 +-
.../__tests__/pointer_objects_test.tsx | 5 +-
.../bed/objects/pointer_objects.tsx | 4 +-
frontend/three_d_garden/bot/bot.tsx | 25 +-
.../components/__tests__/gantry_beam_test.tsx | 2 +-
.../__tests__/suction_animation_test.tsx | 11 +-
.../bot/components/__tests__/tools_test.tsx | 10 +-
.../__tests__/water_stream_test.tsx | 2 +-
.../three_d_garden/bot/components/tools.tsx | 576 +++++++------
.../three_d_garden/bot/parts/cross_slide.tsx | 802 +++++++++---------
.../bot/parts/seed_trough_assembly.tsx | 44 +-
.../bot/parts/seed_trough_holder.tsx | 38 +-
.../three_d_garden/bot/parts/soil_sensor.tsx | 292 +++----
.../bot/parts/vacuum_pump_cover.tsx | 36 +-
frontend/three_d_garden/fps_probe.tsx | 2 +-
.../garden/__tests__/plant_instances_test.tsx | 10 +-
.../garden/__tests__/plants_test.tsx | 6 +-
.../three_d_garden/garden/plant_instances.tsx | 14 +-
frontend/three_d_garden/garden/plants.tsx | 4 +-
frontend/three_d_garden/garden/sun.tsx | 20 +-
frontend/three_d_garden/garden_model.tsx | 10 +-
.../tool_slot_edit_components_test.tsx | 6 +-
.../tos_update/__tests__/component_test.tsx | 2 +-
frontend/ui/__tests__/color_picker_test.tsx | 2 +-
frontend/ui/__tests__/delete_button_test.tsx | 2 +-
frontend/ui/__tests__/filter_search_test.tsx | 6 +-
frontend/util/__tests__/errors_test.ts | 2 +-
frontend/util/__tests__/page_test.tsx | 6 +-
.../weeds/__tests__/weeds_inventory_test.tsx | 2 +-
frontend/wizard/__tests__/checks_test.tsx | 6 +-
.../wizard/__tests__/prerequisites_test.tsx | 2 +-
package.json | 54 +-
133 files changed, 1399 insertions(+), 1348 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index f567c01167..9df3375765 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -87,7 +87,7 @@ GEM
base64 (0.3.0)
bcrypt (3.1.22)
benchmark (0.5.0)
- bigdecimal (4.1.1)
+ bigdecimal (4.1.2)
brakeman (8.0.4)
racc
builder (3.3.0)
@@ -133,14 +133,14 @@ GEM
docile (1.4.1)
drb (2.2.3)
e2mmap (0.1.0)
- erb (6.0.2)
+ erb (6.0.4)
erubi (1.13.1)
factory_bot (6.5.6)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
- faker (3.6.1)
+ faker (3.8.0)
i18n (>= 1.8.11, < 2)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
@@ -200,7 +200,7 @@ GEM
prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
- json (2.19.3)
+ json (2.19.4)
jsonapi-renderer (0.2.2)
jwt (3.1.2)
base64
@@ -236,10 +236,10 @@ GEM
marcel (1.1.0)
method_source (1.1.0)
mini_mime (1.1.5)
- minitest (6.0.3)
+ minitest (6.0.5)
drb (~> 2.0)
prism (~> 1.5)
- multi_json (1.19.1)
+ multi_json (1.20.1)
mutations (0.9.2)
activesupport
mutex_m (0.3.0)
@@ -262,7 +262,7 @@ GEM
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.3)
- parallel (1.28.0)
+ parallel (2.0.1)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
@@ -300,7 +300,7 @@ GEM
rack-cors (3.0.0)
logger
rack (>= 3.0.14)
- rack-session (2.1.1)
+ rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
rack-test (2.2.0)
@@ -338,7 +338,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
- rake (13.3.1)
+ rake (13.4.2)
rbtree (0.4.6)
rdoc (7.2.0)
erb
@@ -383,11 +383,11 @@ GEM
rspec-support (3.13.7)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
- rubocop (1.86.0)
+ rubocop (1.86.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
- parallel (~> 1.10)
+ parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
diff --git a/bun.lock b/bun.lock
index ee15fb8f44..edd6ff89b2 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,27 +5,27 @@
"": {
"name": "farmbot-web-frontend",
"dependencies": {
- "@blueprintjs/core": "6.11.3",
- "@blueprintjs/select": "6.1.8",
+ "@blueprintjs/core": "6.12.0",
+ "@blueprintjs/select": "6.1.9",
"@monaco-editor/react": "4.7.0",
"@react-spring/three": "10.0.3",
"@react-three/drei": "10.7.7",
- "@react-three/fiber": "9.5.0",
+ "@react-three/fiber": "9.6.0",
"@rollbar/react": "1.0.0",
- "@types/bun": "^1.3.11",
+ "@types/bun": "1.3.13",
"@types/lodash": "4.17.24",
"@types/markdown-it": "14.1.2",
- "@types/markdown-it-emoji": "^3.0.1",
+ "@types/markdown-it-emoji": "3.0.1",
"@types/promise-timeout": "1.3.3",
"@types/react": "19.2.14",
"@types/react-color": "3.0.13",
"@types/react-dom": "19.2.3",
- "@types/react-test-renderer": "^19.1.0",
- "@types/redux-immutable-state-invariant": "^2.1.4",
- "@types/three": "0.183.1",
+ "@types/react-test-renderer": "19.1.0",
+ "@types/redux-immutable-state-invariant": "2.1.4",
+ "@types/three": "0.184.0",
"@types/ws": "8.18.1",
"@xterm/xterm": "6.0.0",
- "axios": "1.14.0",
+ "axios": "1.15.2",
"bowser": "2.14.1",
"browser-speech": "1.1.1",
"delaunator": "5.1.0",
@@ -33,7 +33,7 @@
"farmbot": "15.9.3",
"fengari": "0.1.5",
"fengari-web": "0.1.4",
- "i18next": "26.0.3",
+ "i18next": "26.0.6",
"lodash": "4.18.1",
"markdown-it": "14.1.1",
"markdown-it-emoji": "3.0.0",
@@ -44,24 +44,24 @@
"promise-timeout": "1.3.0",
"punycode": "2.3.1",
"querystring-es3": "0.2.1",
- "react": "19.2.4",
+ "react": "19.2.5",
"react-color": "2.19.3",
- "react-dom": "19.2.4",
+ "react-dom": "19.2.5",
"react-redux": "9.2.0",
- "react-router": "7.14.0",
+ "react-router": "7.14.2",
"redux": "5.0.1",
"redux-immutable-state-invariant": "2.1.0",
"redux-thunk": "3.1.0",
"rollbar": "3.1.0",
"suncalc": "1.9.0",
"takeme": "0.12.0",
- "three": "0.183.2",
- "typescript": "6.0.2",
+ "three": "0.184.0",
+ "typescript": "6.0.3",
"url": "0.11.4",
},
"devDependencies": {
"@eslint/js": "10.0.1",
- "@happy-dom/global-registrator": "20.8.9",
+ "@happy-dom/global-registrator": "20.9.0",
"@react-three/eslint-plugin": "0.1.2",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
@@ -71,17 +71,17 @@
"@types/jest": "30.0.0",
"@types/readable-stream": "4.0.23",
"@types/suncalc": "1.9.2",
- "@typescript-eslint/eslint-plugin": "8.58.0",
- "@typescript-eslint/parser": "8.58.0",
- "eslint": "10.2.0",
+ "@typescript-eslint/eslint-plugin": "8.59.0",
+ "@typescript-eslint/parser": "8.59.0",
+ "eslint": "10.2.1",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-jest": "29.15.1",
+ "eslint-plugin-jest": "29.15.2",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-react": "7.37.5",
- "eslint-plugin-react-hooks": "7.0.1",
- "happy-dom": "20.8.9",
+ "eslint-plugin-react-hooks": "7.1.1",
+ "happy-dom": "20.9.0",
"jest": "30.3.0",
"jest-canvas-mock": "2.5.2",
"jest-cli": "30.3.0",
@@ -92,14 +92,14 @@
"madge": "8.0.0",
"path-browserify": "1.0.1",
"playwright": "1.59.1",
- "postcss": "^8.5.9",
- "postcss-scss": "^4.0.9",
+ "postcss": "8.5.10",
+ "postcss-scss": "4.0.9",
"raf": "3.4.1",
- "react-test-renderer": "19.2.4",
+ "react-test-renderer": "19.2.5",
"sass": "1.99.0",
"sass-lint": "1.13.1",
- "stylelint": "^17.6.0",
- "stylelint-config-standard-scss": "^17.0.0",
+ "stylelint": "17.8.0",
+ "stylelint-config-standard-scss": "17.0.0",
"ts-jest": "29.4.9",
"tslint": "6.1.3",
},
@@ -186,11 +186,11 @@
"@blueprintjs/colors": ["@blueprintjs/colors@5.1.16", "", { "dependencies": { "tslib": "~2.6.2" } }, "sha512-P9uX0Aj2TP9+6aUcori1iPl4snxM/Vgq0LZbhUl1l5bHTgNxxwm/0+IoS/SlQg93HBRl8KTAM1evEqtPbwV10A=="],
- "@blueprintjs/core": ["@blueprintjs/core@6.11.3", "", { "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/icons": "^6.8.0", "@floating-ui/react": "^0.27.13", "@popperjs/core": "^2.11.8", "classnames": "^2.3.1", "normalize.css": "^8.0.1", "react-popper": "^2.3.0", "react-transition-group": "^4.4.5", "tslib": "~2.6.2", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"], "bin": { "upgrade-blueprint-2.0.0-rename": "scripts/upgrade-blueprint-2.0.0-rename.sh", "upgrade-blueprint-3.0.0-rename": "scripts/upgrade-blueprint-3.0.0-rename.sh" } }, "sha512-pHApk7prF8aQeTUjcuVhoT7gA89BbDO2Qwn/XdD1/ewFawjK53YbYGuerLv2vQ6BmIMeFf2Cs6cM34XCtT0LWw=="],
+ "@blueprintjs/core": ["@blueprintjs/core@6.12.0", "", { "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/icons": "^6.9.0", "@floating-ui/react": "^0.27.13", "@popperjs/core": "^2.11.8", "classnames": "^2.3.1", "normalize.css": "^8.0.1", "react-popper": "^2.3.0", "react-transition-group": "^4.4.5", "tslib": "~2.6.2", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"], "bin": { "upgrade-blueprint-2.0.0-rename": "scripts/upgrade-blueprint-2.0.0-rename.sh", "upgrade-blueprint-3.0.0-rename": "scripts/upgrade-blueprint-3.0.0-rename.sh" } }, "sha512-huBwfAU0/n4XG33C1xl4cWd/f+POtU11AL1wjTG/zWqyV4HRd57TE92z+SdioBbvCm6fmwEjNfjKAu1P1ojWxw=="],
- "@blueprintjs/icons": ["@blueprintjs/icons@6.8.0", "", { "dependencies": { "change-case": "^4.1.2", "classnames": "^2.3.1", "tslib": "~2.6.2" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"] }, "sha512-a281z8VTNesqVunBO2OigrPFxMRWHpYc84fN+xWbdN5hCiqQNv8ha6sqxeXHUsaTdnemiTH07thiZ8u8Uhfj4A=="],
+ "@blueprintjs/icons": ["@blueprintjs/icons@6.9.0", "", { "dependencies": { "change-case": "^4.1.2", "classnames": "^2.3.1", "tslib": "~2.6.2" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"] }, "sha512-pnwnw6dPARk7Q4CZ9ZKeVe9C8PO1OE9cv9DO4mPgNqJBIOrI9aVAWbHsfjqpbOPXx82/O65kNOQweP8foAzPRA=="],
- "@blueprintjs/select": ["@blueprintjs/select@6.1.8", "", { "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/core": "^6.11.3", "@blueprintjs/icons": "^6.8.0", "classnames": "^2.3.1", "tslib": "~2.6.2" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"] }, "sha512-6cSoUDN5bYery4TBVEsHFeoZcQGUB26c8x+HJ+zrrjL1yyE1iK+0VTcW+kB0PW4J/fFN0vhVkveLKlrJQIcJcA=="],
+ "@blueprintjs/select": ["@blueprintjs/select@6.1.9", "", { "dependencies": { "@blueprintjs/colors": "^5.1.16", "@blueprintjs/core": "^6.12.0", "@blueprintjs/icons": "^6.9.0", "classnames": "^2.3.1", "tslib": "~2.6.2" }, "peerDependencies": { "@types/react": "18", "react": "18", "react-dom": "18" }, "optionalPeers": ["@types/react"] }, "sha512-PtHQl0MWr606/PGxvN3xjYYvMc+gLyeHy/FdzK0/SXK4s9dBCBXGbdJEo8yVtDo71jyrJXZfjUkJlf+KpqRibQ=="],
"@cacheable/memory": ["@cacheable/memory@2.0.8", "", { "dependencies": { "@cacheable/utils": "^2.4.0", "@keyv/bigmap": "^1.3.1", "hookified": "^1.15.1", "keyv": "^5.6.0" } }, "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw=="],
@@ -228,19 +228,19 @@
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
- "@eslint/config-array": ["@eslint/config-array@0.23.4", "", { "dependencies": { "@eslint/object-schema": "^3.0.4", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow=="],
+ "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="],
- "@eslint/config-helpers": ["@eslint/config-helpers@0.5.4", "", { "dependencies": { "@eslint/core": "^1.2.0" } }, "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg=="],
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.5.5", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w=="],
- "@eslint/core": ["@eslint/core@1.2.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A=="],
+ "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
- "@eslint/object-schema": ["@eslint/object-schema@3.0.4", "", {}, "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw=="],
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="],
- "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.0", "levn": "^0.4.1" } }, "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg=="],
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
@@ -252,7 +252,7 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
- "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.8.9" } }, "sha512-DtZeRRHY9A/bisTJziUBBPrdnPui7+R185G/hzi6/Boymhqh7/wi53AY+IvQHS1+7OPaqfO/1XNpngNwthLz+A=="],
+ "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.9.0" } }, "sha512-lBW6/m5BIFl3pMuWPNN0lIOYw9LMCmPfix53ExS3FBi4E+NELEljQ3xH6aAV9IYiQRfn9YIIgzzMrD0vIcD7tw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -394,7 +394,7 @@
"@react-three/eslint-plugin": ["@react-three/eslint-plugin@0.1.2", "", { "dependencies": { "@babel/runtime": "^7.17.8", "eslint": "^8.12.0" } }, "sha512-jenNIhvt+/1fb3NDr3M5vwF06U9euX6kI2SuAFltVKdQP2nzUPY+zdati2Rd67ewAKn0jfljQWT7DWIe6siChg=="],
- "@react-three/fiber": ["@react-three/fiber@9.5.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA=="],
+ "@react-three/fiber": ["@react-three/fiber@9.6.0", "", { "dependencies": { "@babel/runtime": "^7.17.8", "@types/webxr": "*", "base64-js": "^1.5.1", "buffer": "^6.0.3", "its-fine": "^2.0.0", "react-use-measure": "^2.1.7", "scheduler": "^0.27.0", "suspend-react": "^0.1.3", "use-sync-external-store": "^1.4.0", "zustand": "^5.0.3" }, "peerDependencies": { "expo": ">=43.0", "expo-asset": ">=8.4", "expo-file-system": ">=11.0", "expo-gl": ">=11.0", "react": ">=19 <19.3", "react-dom": ">=19 <19.3", "react-native": ">=0.78", "three": ">=0.156" }, "optionalPeers": ["expo", "expo-asset", "expo-file-system", "expo-gl", "react-dom", "react-native"] }, "sha512-90abYK2q5/qDM+GACs9zRvc5KhEEpEWqWlHSd64zTPNxg+9wCJvTfyD9x2so7hlQhjRYO1Fa6flR3BC/kpTFkA=="],
"@rollbar/react": ["@rollbar/react@1.0.0", "", { "dependencies": { "tiny-invariant": "^1.1.0" }, "peerDependencies": { "prop-types": "^15.7.2", "react": "16.x || 17.x || 18.x || 19.x", "rollbar": "^2.26.4 || ^3.0.0-alpha.3" } }, "sha512-e3S9K9k1BLNuqAFA/AD8XH4kcypClYWoMBuW5LO7fnu5Jy1/qiRFIgS5cFZpAFWQqJaSPQAqrLP6n41sH08X9A=="],
@@ -444,7 +444,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
- "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
+ "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.7", "", {}, "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q=="],
@@ -508,7 +508,7 @@
"@types/suncalc": ["@types/suncalc@1.9.2", "", {}, "sha512-ATAGBHHfA1TlE2tjfidLyTcysjoT2JHHEAmWRULh73SU9UTn++j5fqHEW16X6Y/2Li87jEQXzgu4R/OOdlDqzw=="],
- "@types/three": ["@types/three@0.183.1", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~1.0.1" } }, "sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw=="],
+ "@types/three": ["@types/three@0.184.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-4mY2tZAu0y0B0567w7013BBXSpsP0+Z48NJvmNo4Y/Pf76yCyz6Jw4P3tUVs10WuYNXXZ+wmHyGWpCek3amJxA=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
@@ -526,25 +526,25 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
- "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/type-utils": "8.59.0", "@typescript-eslint/utils": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw=="],
- "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="],
- "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.0", "@typescript-eslint/types": "^8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw=="],
- "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="],
- "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg=="],
- "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg=="],
- "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="],
- "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="],
- "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="],
- "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -676,7 +676,7 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
- "axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
+ "axios": ["axios@1.15.2", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A=="],
"babel-jest": ["babel-jest@30.3.0", "", { "dependencies": { "@jest/transform": "30.3.0", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", "babel-preset-jest": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ=="],
@@ -722,7 +722,7 @@
"builtin-modules": ["builtin-modules@1.1.1", "", {}, "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ=="],
- "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
+ "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"cache-base": ["cache-base@1.0.1", "", { "dependencies": { "collection-visit": "^1.0.0", "component-emitter": "^1.2.1", "get-value": "^2.0.6", "has-value": "^1.0.0", "isobject": "^3.0.1", "set-value": "^2.0.0", "to-object-path": "^0.3.0", "union-value": "^1.0.0", "unset-value": "^1.0.0" } }, "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ=="],
@@ -984,7 +984,7 @@
"escope": ["escope@3.6.0", "", { "dependencies": { "es6-map": "^0.1.3", "es6-weak-map": "^2.0.1", "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } }, "sha512-75IUQsusDdalQEW/G/2esa87J7raqdJF+Ca0/Xm5C3Q58Nr4yVYjZGp/P1+2xiEVgXRrA39dpRb8LcshajbqDQ=="],
- "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="],
+ "eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
@@ -994,7 +994,7 @@
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
- "eslint-plugin-jest": ["eslint-plugin-jest@29.15.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "jest": "*", "typescript": ">=4.8.4 <7.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin", "jest", "typescript"] }, "sha512-6BjyErCQauz3zfJvzLw/kAez2lf4LEpbHLvWBfEcG4EI0ZiRSwjoH2uZulMouU8kRkBH+S0rhqn11IhTvxKgKw=="],
+ "eslint-plugin-jest": ["eslint-plugin-jest@29.15.2", "", { "dependencies": { "@typescript-eslint/utils": "^8.0.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "jest": "*", "typescript": ">=4.8.4 <7.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin", "jest", "typescript"] }, "sha512-kEN4r9RZl1xcsb4arGq89LrcVdOUFII/JSCwtTPJyv16mDwmPrcuEQwpxqZHeINvcsd7oK5O/rhdGlxFRaZwvQ=="],
"eslint-plugin-no-null": ["eslint-plugin-no-null@1.0.2", "", { "peerDependencies": { "eslint": ">=3.0.0" } }, "sha512-uRDiz88zCO/2rzGfgG15DBjNsgwWtWiSo4Ezy7zzajUgpnFIqd1TjepKeRmJZHEfBGu58o2a8S0D7vglvvhkVA=="],
@@ -1002,7 +1002,7 @@
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
- "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
@@ -1176,7 +1176,7 @@
"handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="],
- "happy-dom": ["happy-dom@20.8.9", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA=="],
+ "happy-dom": ["happy-dom@20.9.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ=="],
"has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="],
@@ -1226,7 +1226,7 @@
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
- "i18next": ["i18next@26.0.3", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg=="],
+ "i18next": ["i18next@26.0.6", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-A4U6eCXodIbrhf8EarRurB9/4ebyaurH4+fu4gig9bqxmpSt+fCAFm/GpRQDcN1Xzu/LdFCx4nYHsnM1edIIbg=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
@@ -1560,7 +1560,7 @@
"meshline": ["meshline@3.3.1", "", { "peerDependencies": { "three": ">=0.137" } }, "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ=="],
- "meshoptimizer": ["meshoptimizer@1.0.1", "", {}, "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g=="],
+ "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
@@ -1730,7 +1730,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="],
- "postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
+ "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"postcss-media-query-parser": ["postcss-media-query-parser@0.2.3", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="],
@@ -1790,23 +1790,23 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": "cli.js" }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
- "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+ "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="],
"react-color": ["react-color@2.19.3", "", { "dependencies": { "@icons/material": "^0.2.4", "lodash": "^4.17.15", "lodash-es": "^4.17.15", "material-colors": "^1.2.1", "prop-types": "^15.5.10", "reactcss": "^1.2.0", "tinycolor2": "^1.4.1" }, "peerDependencies": { "react": "*" } }, "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA=="],
- "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+ "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="],
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
- "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
+ "react-is": ["react-is@19.2.5", "", {}, "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ=="],
"react-popper": ["react-popper@2.3.0", "", { "dependencies": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" }, "peerDependencies": { "@popperjs/core": "^2.0.0", "react": "^16.8.0 || ^17 || ^18", "react-dom": "^16.8.0 || ^17 || ^18" } }, "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q=="],
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" } }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
- "react-router": ["react-router@7.14.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ=="],
+ "react-router": ["react-router@7.14.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw=="],
- "react-test-renderer": ["react-test-renderer@19.2.4", "", { "dependencies": { "react-is": "^19.2.4", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-Ttl5D7Rnmi6JGMUpri4UjB4BAN0FPs4yRDnu2XSsigCWOLm11o8GwRlVsh27ER+4WFqsGtrBuuv5zumUaRCmKw=="],
+ "react-test-renderer": ["react-test-renderer@19.2.5", "", { "dependencies": { "react-is": "^19.2.5", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-kwViRpdISMTpcpy5B6TSewfJzRjnajihRaj57ZmOWKD+SPN6k9LUM13O0pfOuW8ir6B6OOiAXwCRqOoVxRNykA=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
@@ -2020,7 +2020,7 @@
"strip-json-comments": ["strip-json-comments@1.0.4", "", { "bin": "cli.js" }, "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg=="],
- "stylelint": ["stylelint@17.6.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.1", "css-functions-list": "^3.3.3", "css-tree": "^3.2.1", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.1.1", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.1.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.8", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.2.0", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-tokrsMIVAR9vAQ/q3UVEr7S0dGXCi7zkCezPRnS2kqPUulvUh5Vgfwngrk4EoAoW7wnrThqTdnTFN5Ra7CaxIg=="],
+ "stylelint": ["stylelint@17.8.0", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-syntax-patches-for-csstree": "^1.1.2", "@csstools/css-tokenizer": "^4.0.0", "@csstools/media-query-list-parser": "^5.0.0", "@csstools/selector-resolve-nested": "^4.0.0", "@csstools/selector-specificity": "^6.0.0", "colord": "^2.9.3", "cosmiconfig": "^9.0.1", "css-functions-list": "^3.3.3", "css-tree": "^3.2.1", "debug": "^4.4.3", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", "file-entry-cache": "^11.1.2", "global-modules": "^2.0.0", "globby": "^16.2.0", "globjoin": "^0.1.4", "html-tags": "^5.1.0", "ignore": "^7.0.5", "import-meta-resolve": "^4.2.0", "is-plain-object": "^5.0.0", "mathml-tag-names": "^4.0.0", "meow": "^14.1.0", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.5.9", "postcss-safe-parser": "^7.0.1", "postcss-selector-parser": "^7.1.1", "postcss-value-parser": "^4.2.0", "string-width": "^8.2.0", "supports-hyperlinks": "^4.4.0", "svg-tags": "^1.0.0", "table": "^6.9.0", "write-file-atomic": "^7.0.1" }, "bin": { "stylelint": "bin/stylelint.mjs" } }, "sha512-oHkld9T60LDSaUQ4CSVc+tlt9eUoDlxhaGWShsUCKyIL14boZfmK5bSphZqx64aiC5tCqX+BsQMTMoSz8D1zIg=="],
"stylelint-config-recommended": ["stylelint-config-recommended@18.0.0", "", { "peerDependencies": { "stylelint": "^17.0.0" } }, "sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg=="],
@@ -2062,7 +2062,7 @@
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
- "three": ["three@0.183.2", "", {}, "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ=="],
+ "three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="],
"three-mesh-bvh": ["three-mesh-bvh@0.8.3", "", { "peerDependencies": { "three": ">= 0.159.0" } }, "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg=="],
@@ -2134,7 +2134,7 @@
"typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="],
- "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
@@ -2380,6 +2380,8 @@
"@react-three/eslint-plugin/eslint": ["eslint@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.0", "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": "bin/eslint.js" }, "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ=="],
+ "@react-three/fiber/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
+
"@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -2478,7 +2480,7 @@
"eslint-plugin-import/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
- "eslint-plugin-jest/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ=="],
+ "eslint-plugin-jest/@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
"eslint-plugin-promise/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
@@ -2954,11 +2956,11 @@
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1" } }, "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.1", "@typescript-eslint/tsconfig-utils": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
"eslint-plugin-promise/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -3202,17 +3204,15 @@
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="],
-
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.1", "@typescript-eslint/types": "^8.57.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
- "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+ "eslint-plugin-jest/@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
diff --git a/frontend/__test_support__/bun_test_setup.ts b/frontend/__test_support__/bun_test_setup.ts
index 040e58b2a9..b8d34187ea 100644
--- a/frontend/__test_support__/bun_test_setup.ts
+++ b/frontend/__test_support__/bun_test_setup.ts
@@ -28,7 +28,7 @@ const ensureSyntaxError = () => {
writable: true,
});
};
- assign(globalThis as unknown as Record);
+ assign(globalThis);
assign(globalAny.window as unknown as Record | undefined);
const windowCtor = globalAny.window?.constructor as
| { prototype?: Record }
@@ -74,13 +74,11 @@ const withAxiosDefaultExport = (factory: () => unknown) => () => {
if (globalAny.jest?.mock) {
const originalMock = globalAny.jest.mock.bind(globalAny.jest);
- globalAny.jest.mock = ((specifier: string, factory?: unknown) => {
- const moduleFactory =
- typeof factory === "function" ? factory as () => unknown : undefined;
+ globalAny.jest.mock = ((specifier: string, factory?: () => unknown) => {
return specifier === "axios" && typeof factory === "function"
? originalMock(specifier,
- withAxiosDefaultExport(moduleFactory as () => unknown))
- : originalMock(specifier, factory as never);
+ withAxiosDefaultExport(factory))
+ : originalMock(specifier, factory);
}) as typeof globalAny.jest.mock;
}
@@ -278,7 +276,7 @@ beforeEach(() => {
resetMutableFixture(globalAny.globalConfig, globalConfigBaseline);
} else {
globalAny.globalConfig =
- cloneForReset(globalConfigBaseline) as Record;
+ cloneForReset(globalConfigBaseline);
}
globalThis.localStorage?.clear();
globalThis.sessionStorage?.clear();
diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx
index 9eee54e122..6ec691e255 100644
--- a/frontend/__test_support__/three_d_mocks.tsx
+++ b/frontend/__test_support__/three_d_mocks.tsx
@@ -119,7 +119,7 @@ jest.mock("../three_d_garden/components", () => ({
instanceColor: { needsUpdate: false },
}) as unknown as THREE.InstancedMesh);
return }
+ ref={ref}
{...rest} />;
},
),
diff --git a/frontend/__tests__/loading_plant_test.tsx b/frontend/__tests__/loading_plant_test.tsx
index 612d277a89..39b1effce2 100644
--- a/frontend/__tests__/loading_plant_test.tsx
+++ b/frontend/__tests__/loading_plant_test.tsx
@@ -26,7 +26,7 @@ describe(" ", () => {
});
it("clears initial loading text", () => {
- const el = { outerHTML: "hidden" } as Pick;
+ const el = { outerHTML: "hidden" };
const collection =
[el as unknown as Element] as unknown as HTMLCollectionOf;
jest.spyOn(document, "getElementsByClassName")
diff --git a/frontend/__tests__/revert_to_english_test.ts b/frontend/__tests__/revert_to_english_test.ts
index 00d67b40fe..1099e08e32 100644
--- a/frontend/__tests__/revert_to_english_test.ts
+++ b/frontend/__tests__/revert_to_english_test.ts
@@ -4,7 +4,7 @@ import { revertToEnglish } from "../revert_to_english";
describe("revertToEnglish", () => {
it("runs without throwing", async () => {
jest.spyOn(I18n, "detectLanguage")
- .mockResolvedValue({ lng: "en" } as never);
+ .mockResolvedValue({ lng: "en" });
await expect(Promise.resolve(revertToEnglish() as unknown))
.resolves.toBeUndefined();
diff --git a/frontend/api/__tests__/crud_data_tracking_test.ts b/frontend/api/__tests__/crud_data_tracking_test.ts
index 821d9adbcf..eca611c877 100644
--- a/frontend/api/__tests__/crud_data_tracking_test.ts
+++ b/frontend/api/__tests__/crud_data_tracking_test.ts
@@ -56,7 +56,7 @@ describe("AJAX data tracking", () => {
if (!destroy) { return; }
const thunk = destroy(uuid);
if (typeof thunk !== "function") { return; }
- await thunk(dispatch as unknown as Function, () =>
+ await thunk(dispatch, () =>
({ resources: { index: resourceIndex() } }));
expect(maybeStartTrackingModule.maybeStartTracking).toHaveBeenCalled();
});
@@ -69,7 +69,7 @@ describe("AJAX data tracking", () => {
});
const saveAllAction = loadCrud().saveAll?.(r);
if (typeof saveAllAction !== "function") { return; }
- await saveAllAction(dispatch as unknown as Function);
+ await saveAllAction(dispatch);
expect(maybeStartTrackingModule.maybeStartTracking).toHaveBeenCalled();
});
@@ -94,7 +94,7 @@ describe("AJAX data tracking", () => {
email: "test@test.com"
});
if (typeof initSaveGetIdAction !== "function") { return; }
- const result = initSaveGetIdAction(statefulDispatch as unknown as Function);
+ const result = initSaveGetIdAction(statefulDispatch);
if (result && typeof result === "object" && result && "catch" in result) {
await (result as Promise).catch(() => { });
}
@@ -107,7 +107,7 @@ describe("AJAX data tracking", () => {
email: "test@test.com"
});
if (typeof action !== "function") { return; }
- await action(dispatch as unknown as Function);
+ await action(dispatch);
expect(maybeStartTrackingModule.maybeStartTracking).toHaveBeenCalled();
});
});
diff --git a/frontend/api/__tests__/crud_destroy_test.ts b/frontend/api/__tests__/crud_destroy_test.ts
index 0bc91f20c6..6245f3a0e8 100644
--- a/frontend/api/__tests__/crud_destroy_test.ts
+++ b/frontend/api/__tests__/crud_destroy_test.ts
@@ -69,7 +69,7 @@ describe("destroy", () => {
.mockImplementation(() => mockReadonlyState);
mockDelete = Promise.resolve({});
deleteSpy = jest.spyOn(axios, "delete")
- .mockImplementation(() => mockDelete as never);
+ .mockImplementation(() => mockDelete);
});
API.setBaseUrl("http://localhost:3000");
@@ -183,11 +183,11 @@ describe("destroyAll", () => {
.mockImplementation(() => mockReadonlyState);
mockDelete = Promise.resolve({});
deleteSpy = jest.spyOn(axios, "delete")
- .mockImplementation(() => mockDelete as never);
+ .mockImplementation(() => mockDelete);
});
it("confirmed", async () => {
- deleteSpy.mockResolvedValueOnce(undefined as never);
+ deleteSpy.mockResolvedValueOnce(undefined);
const result = fakeDestroyAll("FarmwareEnv", true);
if (!result) { return; }
await expect(result).resolves.toEqual(undefined);
@@ -230,7 +230,7 @@ describe("destroyAll", () => {
});
it("rejected", async () => {
- deleteSpy.mockRejectedValueOnce("error" as never);
+ deleteSpy.mockRejectedValueOnce("error");
const result = fakeDestroyAll("FarmwareEnv", true);
if (!result) { return; }
await expect(result).rejects.toEqual("error");
diff --git a/frontend/config/__tests__/actions_test.ts b/frontend/config/__tests__/actions_test.ts
index 80f8dd6cc5..59a051cb17 100644
--- a/frontend/config/__tests__/actions_test.ts
+++ b/frontend/config/__tests__/actions_test.ts
@@ -29,9 +29,9 @@ describe("ready()", () => {
didLoginSpy = jest.spyOn(authActions, "didLogin")
.mockImplementation(() => { });
maybeRefreshTokenSpy = jest.spyOn(refreshToken, "maybeRefreshToken")
- .mockImplementation(() => Promise.resolve(undefined) as never);
+ .mockImplementation(() => Promise.resolve(undefined));
timeoutSpy = jest.spyOn(promiseTimeoutModule, "timeout")
- .mockImplementation(() => mockTimeout as never);
+ .mockImplementation(() => mockTimeout);
fetchStoredTokenSpy = jest.spyOn(Session, "fetchStoredToken")
.mockReturnValue(undefined);
clearSpy = jest.spyOn(Session, "clear")
diff --git a/frontend/connectivity/__tests__/connect_device/index_test.ts b/frontend/connectivity/__tests__/connect_device/index_test.ts
index a5ea40cf76..74a50301cc 100644
--- a/frontend/connectivity/__tests__/connect_device/index_test.ts
+++ b/frontend/connectivity/__tests__/connect_device/index_test.ts
@@ -339,7 +339,7 @@ describe("onPublicBroadcast", () => {
log.message = "bot xyz is offline";
const taggedLog = fn(log);
const getStateSpy =
- jest.spyOn(store, "getState").mockReturnValue(fakeState() as never);
+ jest.spyOn(store, "getState").mockReturnValue(fakeState());
globalQueue.maybeWork();
getStateSpy.mockRestore();
expect(taggedLog?.kind).toEqual("Log");
diff --git a/frontend/controls/__tests__/pin_form_fields_test.tsx b/frontend/controls/__tests__/pin_form_fields_test.tsx
index 63496c1433..5cb1193cd6 100644
--- a/frontend/controls/__tests__/pin_form_fields_test.tsx
+++ b/frontend/controls/__tests__/pin_form_fields_test.tsx
@@ -1,4 +1,3 @@
-import React from "react";
import { fakeSensor } from "../../__test_support__/fake_state/resources";
import { Actions } from "../../constants";
import * as crud from "../../api/crud";
@@ -34,7 +33,7 @@ describe(" ", () => {
const input = NameInputBox(p);
input.props.onChange({
currentTarget: { value: "GPIO 3" },
- } as React.ChangeEvent);
+ });
expect(p.dispatch).toHaveBeenCalledWith(
expectedPayload({ label: "GPIO 3" }));
});
diff --git a/frontend/controls/move/__tests__/take_photo_button_test.tsx b/frontend/controls/move/__tests__/take_photo_button_test.tsx
index 6bf10e3fb5..4309c58807 100644
--- a/frontend/controls/move/__tests__/take_photo_button_test.tsx
+++ b/frontend/controls/move/__tests__/take_photo_button_test.tsx
@@ -18,7 +18,7 @@ describe(" ", () => {
mockPhotoOutcome = Promise.resolve();
takePhotoSpy =
jest.spyOn(deviceActions, "takePhoto")
- .mockImplementation(() => mockPhotoOutcome as never);
+ .mockImplementation(() => mockPhotoOutcome);
});
afterEach(() => {
diff --git a/frontend/demo/lua_runner/index.ts b/frontend/demo/lua_runner/index.ts
index 394742ed0d..a8a51036ae 100644
--- a/frontend/demo/lua_runner/index.ts
+++ b/frontend/demo/lua_runner/index.ts
@@ -26,14 +26,14 @@ export const collectDemoSequenceActions = (
}
const sequence = findSequenceById(resources, sequenceId);
const varData = resources.sequenceMetas[sequence.uuid];
- const sequenceVariables = Object.values(varData || {})
+ const sequenceVariables: ParameterApplication[] = Object.values(varData || {})
.map(v => v?.celeryNode)
.filter(v => v?.kind == "variable_declaration")
.filter(v => !bodyVariables?.map(v => v.args.label).includes(v.args.label))
.map(v => ({
kind: "parameter_application",
args: v.args,
- } as ParameterApplication));
+ }));
const variables = [...sequenceVariables, ...(bodyVariables || [])];
const actions: Action[] = [];
const firstVarArgs = variables[0]?.args;
diff --git a/frontend/demo/lua_runner/run.ts b/frontend/demo/lua_runner/run.ts
index de4687799d..d2d43eba1e 100644
--- a/frontend/demo/lua_runner/run.ts
+++ b/frontend/demo/lua_runner/run.ts
@@ -318,7 +318,7 @@ export const runLua =
lua.lua_pushjsfunction(L, () => {
const n = lua.lua_gettop(L);
- const args = [];
+ const args: string[] = [];
for (let i = 1; i <= n; i++) {
args.push(luaToJs(L, i) as string);
}
@@ -377,7 +377,7 @@ export const runLua =
lua.lua_setfield(L, envIndex, to_luastring("set_job"));
lua.lua_pushjsfunction(L, () => {
- const args = [];
+ const args: number[] = [];
const n = lua.lua_gettop(L);
if (n == 1) {
const params = luaToJs(L, 1) as XyzNumber;
@@ -394,7 +394,7 @@ export const runLua =
lua.lua_pushjsfunction(L, () => {
const n = lua.lua_gettop(L);
- const args = [];
+ const args: number[] = [];
for (let i = 1; i <= n; i++) {
args.push(luaToJs(L, i) as number);
}
diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts
index 2c1586ff62..b5822d578c 100644
--- a/frontend/devices/__tests__/actions_test.ts
+++ b/frontend/devices/__tests__/actions_test.ts
@@ -75,7 +75,7 @@ beforeEach(() => {
getDeviceSpy = jest.spyOn(deviceModule, "getDevice")
.mockImplementation(() => mockDevice.current as Farmbot);
axiosGetSpy = jest.spyOn(axios, "get")
- .mockImplementation(() => mockGet as never);
+ .mockImplementation(() => mockGet);
editSpy = jest.spyOn(crud, "edit").mockImplementation(jest.fn());
saveSpy = jest.spyOn(crud, "save").mockImplementation(jest.fn());
jest.spyOn(demoLuaRunner, "runDemoSequence").mockImplementation(jest.fn());
@@ -351,7 +351,7 @@ describe("takePhoto()", () => {
getDeviceSpy.mockImplementation(() => ({
...mockDeviceDefault,
takePhoto,
- }) as unknown as Farmbot);
+ }));
await deviceActions().takePhoto();
expect(takePhoto).toHaveBeenCalled();
expect(success).toHaveBeenCalledWith(Content.PROCESSING_PHOTO,
@@ -364,7 +364,7 @@ describe("takePhoto()", () => {
getDeviceSpy.mockImplementation(() => ({
...mockDeviceDefault,
takePhoto,
- }) as unknown as Farmbot);
+ }));
localStorage.setItem("myBotIs", "online");
await deviceActions().takePhoto();
expect(takePhoto).not.toHaveBeenCalled();
@@ -377,7 +377,7 @@ describe("takePhoto()", () => {
getDeviceSpy.mockImplementation(() => ({
...mockDeviceDefault,
takePhoto,
- }) as unknown as Farmbot);
+ }));
await deviceActions().takePhoto();
await expect(takePhoto).toHaveBeenCalled();
expect(success).not.toHaveBeenCalled();
diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts
index 85c1cb1f47..19e3398fce 100644
--- a/frontend/devices/actions.ts
+++ b/frontend/devices/actions.ts
@@ -27,7 +27,6 @@ import * as crud from "../api/crud";
import { CONFIG_DEFAULTS } from "farmbot/dist/config";
import { Log } from "farmbot/dist/resources/api_resources";
import { FbosConfig } from "farmbot/dist/resources/configs/fbos";
-import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware";
import { getFirmwareConfig, getFbosConfig } from "../resources/getters";
import { isObject, isString, get, noop } from "lodash";
import { t } from "../i18next_wrapper";
@@ -524,7 +523,7 @@ export function updateMCU(key: ConfigKey, val: string) {
function proceed() {
if (firmwareConfig) {
- dispatch(crud.edit(firmwareConfig, { [key]: val } as Partial));
+ dispatch(crud.edit(firmwareConfig, { [key]: val }));
dispatch(crud.save(firmwareConfig.uuid));
}
}
diff --git a/frontend/farm_designer/__tests__/index_test.tsx b/frontend/farm_designer/__tests__/index_test.tsx
index bcc2a3463d..bc15cfb2f7 100644
--- a/frontend/farm_designer/__tests__/index_test.tsx
+++ b/frontend/farm_designer/__tests__/index_test.tsx
@@ -51,7 +51,7 @@ describe(" ", () => {
.mockImplementation(((props: GardenMapLegendProps) => {
lastLegendProps = props;
return
;
- }) as never);
+ }));
gardenMapSpy = jest.spyOn(gardenMap, "GardenMap")
.mockImplementation(((props: GardenMapProps) => {
lastGardenMapProps = props;
diff --git a/frontend/farm_designer/__tests__/location_info_test.tsx b/frontend/farm_designer/__tests__/location_info_test.tsx
index cf476fa1ca..fcff27e34a 100644
--- a/frontend/farm_designer/__tests__/location_info_test.tsx
+++ b/frontend/farm_designer/__tests__/location_info_test.tsx
@@ -232,7 +232,7 @@ describe(" ", () => {
if (buttons.length === 0) {
return;
}
- fireEvent.click(buttons[0] as HTMLButtonElement);
+ fireEvent.click(buttons[0]);
expect(container.querySelectorAll(
"button.image-flipper-left, button.image-flipper-right").length)
.toBe(1);
diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
index 534e04d763..f4c7b39510 100644
--- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
+++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx
@@ -25,7 +25,7 @@ beforeEach(() => {
getPositionSpy = jest.spyOn(suncalc, "getPosition").mockReturnValue({
altitude: 0.5,
azimuth: 1.0,
- } as never);
+ });
});
afterEach(() => {
diff --git a/frontend/farm_designer/map/__tests__/garden_map_test.tsx b/frontend/farm_designer/map/__tests__/garden_map_test.tsx
index a9a4cab8ca..f7fb13efe7 100644
--- a/frontend/farm_designer/map/__tests__/garden_map_test.tsx
+++ b/frontend/farm_designer/map/__tests__/garden_map_test.tsx
@@ -68,9 +68,9 @@ interface RenderedGardenMap {
// eslint-disable-next-line complexity
const fire = (target: Element, event: EventName, payload?: unknown) => {
- const eventPayload = {
+ const eventPayload: Record = {
...((typeof payload === "object" && payload) ? payload : {}),
- } as Record;
+ };
if (!("clientX" in eventPayload) && "pageX" in eventPayload) {
eventPayload.clientX = eventPayload.pageX;
}
@@ -171,16 +171,16 @@ const fireWrapperEvent = (
dragStart?: (event: unknown) => void;
};
if ((selector == ".drop-area-svg" || selector == "svg") && event == "click") {
- instance.click?.(payload as never);
+ instance.click?.(payload);
return;
}
if (selector == ".drop-area-svg" || selector == "svg") {
if (event == "mouseDown") {
- instance.startDrag?.(payload as never);
+ instance.startDrag?.(payload);
return;
}
if (event == "mouseMove") {
- instance.drag?.(payload as never);
+ instance.drag?.(payload);
return;
}
if (event == "mouseUp") {
@@ -189,20 +189,20 @@ const fireWrapperEvent = (
}
}
if (selector == ".drop-area-background" && event == "mouseDown") {
- instance.startDragOnBackground?.(payload as never);
+ instance.startDragOnBackground?.(payload);
return;
}
if (selector == ".drop-area") {
if (event == "dragOver") {
- instance.handleDragOver?.(payload as never);
+ instance.handleDragOver?.(payload);
return;
}
if (event == "dragEnter") {
- instance.handleDragEnter?.(payload as never);
+ instance.handleDragEnter?.(payload);
return;
}
if (event == "dragStart") {
- instance.dragStart?.(payload as never);
+ instance.dragStart?.(payload);
return;
}
}
@@ -244,8 +244,10 @@ const makeWrapper = (
};
};
-const renderMap = (element: React.ReactElement) => makeWrapper(element);
-const renderMapWithContext = (element: React.ReactElement) =>
+// eslint-disable-next-line comma-spacing
+const renderMap = (element: React.ReactElement) => makeWrapper(element);
+// eslint-disable-next-line comma-spacing
+const renderMapWithContext = (element: React.ReactElement) =>
makeWrapper(element, true);
const DEFAULT_EVENT = { preventDefault: jest.fn(), pageX: NaN, pageY: NaN };
diff --git a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.ts b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.ts
index cbab8623e6..31725adf63 100644
--- a/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.ts
+++ b/frontend/farm_designer/map/background/__tests__/selection_box_actions_test.ts
@@ -89,8 +89,7 @@ describe("resizeBox", () => {
it("doesn't resize box: no location", () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.gardenCoords = undefined as any;
+ p.gardenCoords = undefined;
resizeBox(p);
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).not.toHaveBeenCalled();
@@ -98,8 +97,7 @@ describe("resizeBox", () => {
it("doesn't resize box: no box", () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.selectionBox = undefined as any;
+ p.selectionBox = undefined;
resizeBox(p);
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).not.toHaveBeenCalled();
@@ -156,8 +154,7 @@ describe("startNewSelectionBox", () => {
it("doesn't start box", () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.gardenCoords = undefined as any;
+ p.gardenCoords = undefined;
startNewSelectionBox(p);
expect(p.setMapState).not.toHaveBeenCalled();
expect(p.dispatch).toHaveBeenCalledWith({
diff --git a/frontend/farm_designer/map/layers/plants/__tests__/plant_actions_test.ts b/frontend/farm_designer/map/layers/plants/__tests__/plant_actions_test.ts
index a33eb138d4..08a199a8b7 100644
--- a/frontend/farm_designer/map/layers/plants/__tests__/plant_actions_test.ts
+++ b/frontend/farm_designer/map/layers/plants/__tests__/plant_actions_test.ts
@@ -209,8 +209,7 @@ describe("plantActions().dropPlant()", () => {
it("throws error", () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.gardenCoords = undefined as any;
+ p.gardenCoords = undefined;
expect(() => plantActions().dropPlant(p))
.toThrow(/while trying to add a plant/);
});
@@ -349,8 +348,7 @@ describe("plantActions().setActiveSpread()", () => {
it("sets crop spread value", async () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.selectedPlant = undefined as any;
+ p.selectedPlant = undefined;
await plantActions().setActiveSpread(p);
expect(p.setMapState).toHaveBeenCalledWith({ activeDragSpread: 75 });
});
@@ -369,8 +367,7 @@ describe("plantActions().beginPlantDrag()", () => {
it("starts drag: not plant", () => {
const p = fakeProps();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- p.plant = undefined as any;
+ p.plant = undefined;
plantActions().beginPlantDrag(p);
});
});
diff --git a/frontend/farm_designer/map/profile/__tests__/options_test.tsx b/frontend/farm_designer/map/profile/__tests__/options_test.tsx
index 10bc3f056e..7cf5810fa4 100644
--- a/frontend/farm_designer/map/profile/__tests__/options_test.tsx
+++ b/frontend/farm_designer/map/profile/__tests__/options_test.tsx
@@ -61,7 +61,7 @@ describe(" ", () => {
const p = fakeProps();
const { container } = render( );
const buttons = container.querySelectorAll("button");
- fireEvent.click(buttons[buttons.length - 1] as Element);
+ fireEvent.click(buttons[buttons.length - 1]);
expect(p.dispatch).toHaveBeenCalledWith({
type: Actions.SET_PROFILE_FOLLOW_BOT,
payload: true,
diff --git a/frontend/farm_designer/map/profile/__tests__/viewer_test.tsx b/frontend/farm_designer/map/profile/__tests__/viewer_test.tsx
index 2b66a53a29..00fbdee92b 100644
--- a/frontend/farm_designer/map/profile/__tests__/viewer_test.tsx
+++ b/frontend/farm_designer/map/profile/__tests__/viewer_test.tsx
@@ -131,7 +131,7 @@ describe(" ", () => {
p.designer.profilePosition = { x: 1, y: 2 };
const { container } = render( );
const icons = container.querySelectorAll("i");
- fireEvent.click(icons[icons.length - 1] as Element);
+ fireEvent.click(icons[icons.length - 1]);
expect(container.querySelector("svg")?.classList.contains("expand"))
.toBeFalsy();
fireEvent.click(container.querySelector(".profile-button") as Element);
diff --git a/frontend/farm_events/__tests__/add_farm_event_test.tsx b/frontend/farm_events/__tests__/add_farm_event_test.tsx
index 7535703f4c..56f78e4b13 100644
--- a/frontend/farm_events/__tests__/add_farm_event_test.tsx
+++ b/frontend/farm_events/__tests__/add_farm_event_test.tsx
@@ -198,10 +198,9 @@ describe(" ", () => {
p.findFarmEventByUuid = () => farmEvent;
const formRef = { current: undefined as unknown as EditFEForm };
const createRefSpy = jest.spyOn(React, "createRef")
- .mockReturnValue(formRef as React.RefObject);
+ .mockReturnValue(formRef);
const { container } = render( );
- formRef.current.commitViewModel =
- mockSave as unknown as EditFEForm["commitViewModel"];
+ formRef.current.commitViewModel = mockSave;
fireEvent.click(container.querySelector(".save-btn") as Element);
expect(mockSave).toHaveBeenCalled();
createRefSpy.mockRestore();
diff --git a/frontend/farm_events/__tests__/edit_farm_event_test.tsx b/frontend/farm_events/__tests__/edit_farm_event_test.tsx
index b249cfafbd..d181371f3c 100644
--- a/frontend/farm_events/__tests__/edit_farm_event_test.tsx
+++ b/frontend/farm_events/__tests__/edit_farm_event_test.tsx
@@ -74,10 +74,9 @@ describe(" ", () => {
it("calls farm event save", () => {
const formRef = { current: undefined as unknown as EditFEForm };
const createRefSpy = jest.spyOn(React, "createRef")
- .mockReturnValue(formRef as React.RefObject);
+ .mockReturnValue(formRef);
const { container } = render( );
- formRef.current.commitViewModel =
- mockSave as unknown as EditFEForm["commitViewModel"];
+ formRef.current.commitViewModel = mockSave;
fireEvent.click(container.querySelector(".save-btn") as Element);
expect(mockSave).toHaveBeenCalled();
createRefSpy.mockRestore();
@@ -85,7 +84,7 @@ describe(" ", () => {
it("doesn't call farm event save if event is missing", () => {
const p = fakeProps();
- p.getFarmEvent = () => undefined as never;
+ p.getFarmEvent = () => undefined;
location.pathname = Path.mock(Path.farmEvents("nope"));
const { container } = render( );
fireEvent.click(container.querySelector(".save-btn") as Element);
diff --git a/frontend/farm_events/__tests__/edit_fe_form_test.tsx b/frontend/farm_events/__tests__/edit_fe_form_test.tsx
index 9a819d329e..8448af3a1c 100644
--- a/frontend/farm_events/__tests__/edit_fe_form_test.tsx
+++ b/frontend/farm_events/__tests__/edit_fe_form_test.tsx
@@ -63,7 +63,7 @@ describe(" ", () => {
const update = isFunction(state) ? state(i.state, i.props) : state;
i.state = { ...i.state, ...update };
callback?.();
- }) as EditFEForm["setState"];
+ });
i.forceUpdate = jest.fn();
return i;
}
diff --git a/frontend/farm_events/__tests__/farm_event_repeat_form_test.tsx b/frontend/farm_events/__tests__/farm_event_repeat_form_test.tsx
index 9f8de7f9df..eb158599a1 100644
--- a/frontend/farm_events/__tests__/farm_event_repeat_form_test.tsx
+++ b/frontend/farm_events/__tests__/farm_event_repeat_form_test.tsx
@@ -72,7 +72,7 @@ describe(" ", () => {
defaultValue={props.value}
disabled={props.disabled}
onChange={() => { }}
- onBlur={e => props.onCommit(e)} />) as never);
+ onBlur={e => props.onCommit(e)} />));
});
afterEach(() => {
@@ -100,7 +100,7 @@ describe(" ", () => {
it("defaults to `daily` when a bad input it passed", () => {
const p = fakeProps();
- p.timeUnit = "never" as "daily";
+ p.timeUnit = "never";
const { container } = render( );
expect((container.querySelector(
`[data-testid="${Selectors.REPEAT}"]`) as HTMLInputElement).value)
diff --git a/frontend/farm_events/__tests__/farm_events_test.tsx b/frontend/farm_events/__tests__/farm_events_test.tsx
index 309848c7b5..1510681c14 100644
--- a/frontend/farm_events/__tests__/farm_events_test.tsx
+++ b/frontend/farm_events/__tests__/farm_events_test.tsx
@@ -124,7 +124,7 @@ describe(" ", () => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as FarmEvents["setState"];
+ });
instance.setState({ searchTerm: "farm events" });
instance.resetCalendar();
expect(mockScrollTo).toHaveBeenCalledWith(0, 0);
@@ -142,7 +142,7 @@ describe(" ", () => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as FarmEvents["setState"];
+ });
instance.setState({ searchTerm: "farm events" });
instance.resetCalendar();
expect(instance.state.searchTerm).toEqual("");
diff --git a/frontend/farmware/__tests__/actions_test.ts b/frontend/farmware/__tests__/actions_test.ts
index e909c8a86b..b6ea2a315e 100644
--- a/frontend/farmware/__tests__/actions_test.ts
+++ b/frontend/farmware/__tests__/actions_test.ts
@@ -14,9 +14,9 @@ beforeEach(() => {
{ package: "farmware0" },
{ package: "farmware1" },
]
- }) as never);
+ }));
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => Promise.resolve({}) as never);
+ .mockImplementation(() => Promise.resolve({}));
});
afterEach(() => {
diff --git a/frontend/farmware/panel/__tests__/info_test.tsx b/frontend/farmware/panel/__tests__/info_test.tsx
index a95a6e77b0..adaad97c1c 100644
--- a/frontend/farmware/panel/__tests__/info_test.tsx
+++ b/frontend/farmware/panel/__tests__/info_test.tsx
@@ -26,13 +26,13 @@ let activeFarmwareSpy: jest.SpyInstance;
beforeEach(() => {
designerPanelSpy = jest.spyOn(designerPanel, "DesignerPanel")
.mockImplementation(((props: React.PropsWithChildren) =>
- {props.children}
) as never);
+ {props.children}
));
designerPanelTopSpy = jest.spyOn(designerPanel, "DesignerPanelTop")
.mockImplementation(((props: React.PropsWithChildren) =>
- {props.children}
) as never);
+ {props.children}
));
designerPanelContentSpy = jest.spyOn(designerPanel, "DesignerPanelContent")
.mockImplementation(((props: React.PropsWithChildren) =>
- {props.children}
) as never);
+ {props.children}
));
activeFarmwareSpy = jest.spyOn(activeFarmware, "setActiveFarmwareByName")
.mockImplementation(jest.fn());
});
diff --git a/frontend/folders/__tests__/actions_test.ts b/frontend/folders/__tests__/actions_test.ts
index 0100643238..9db641704f 100644
--- a/frontend/folders/__tests__/actions_test.ts
+++ b/frontend/folders/__tests__/actions_test.ts
@@ -60,7 +60,7 @@ beforeEach(() => {
stepGetSpy = jest.spyOn(draggableActions, "stepGet")
.mockImplementation(() =>
(() => mockStepGetResult) as unknown as
- ReturnType);
+ ReturnType);
destroySpy = jest.spyOn(crudModule, "destroy").mockImplementation(jest.fn());
editSpy = jest.spyOn(crudModule, "edit").mockImplementation(jest.fn());
initSpy = jest.spyOn(crudModule, "init").mockImplementation(jest.fn());
@@ -340,7 +340,7 @@ describe("moveSequence", () => {
index: {
references: { [sequence.uuid]: sequence },
}
- } as Everything["resources"],
+ },
};
(store as unknown as { getState: () => DeepPartial }).getState =
() => localState;
diff --git a/frontend/folders/__tests__/component_test.tsx b/frontend/folders/__tests__/component_test.tsx
index 1d71a40cdc..558ba73c65 100644
--- a/frontend/folders/__tests__/component_test.tsx
+++ b/frontend/folders/__tests__/component_test.tsx
@@ -157,7 +157,7 @@ const setStateSync = ),
};
callback?.();
- }) as T["setState"];
+ });
return instance;
};
@@ -836,14 +836,14 @@ describe(" ", () => {
it("creates new folder", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(createFolder).toHaveBeenCalled();
});
it("creates new sequence", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.click(container.querySelectorAll("button")[2] as Element);
+ fireEvent.click(container.querySelectorAll("button")[2]);
expect(addNewSequenceToFolder).toHaveBeenCalled();
});
});
diff --git a/frontend/front_page/__tests__/demo_login_option_test.tsx b/frontend/front_page/__tests__/demo_login_option_test.tsx
index 256fb9eff6..05ce89c64e 100644
--- a/frontend/front_page/__tests__/demo_login_option_test.tsx
+++ b/frontend/front_page/__tests__/demo_login_option_test.tsx
@@ -24,7 +24,7 @@ describe(" ", () => {
.mockImplementation(() =>
typeof mockResponse === "string"
? Promise.resolve(mockResponse)
- : Promise.reject(mockResponse) as never);
+ : Promise.reject(mockResponse));
});
afterEach(() => {
@@ -65,7 +65,7 @@ describe(" ", () => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as DemoLoginOption["setState"];
+ });
expect(instance.state.productLine).toEqual("genesis_1.8");
const select = instance["seedDataSelect"]() as React.ReactElement<{
onChange: (ddi: { value: string }) => void;
diff --git a/frontend/front_page/__tests__/front_page_test.tsx b/frontend/front_page/__tests__/front_page_test.tsx
index 83a13ba7a6..9db581ad22 100644
--- a/frontend/front_page/__tests__/front_page_test.tsx
+++ b/frontend/front_page/__tests__/front_page_test.tsx
@@ -28,7 +28,7 @@ let originalPrivUrl: string;
const setStateSync = (instance: FrontPage) => {
instance.setState = ((state: Partial) => {
instance.state = { ...instance.state, ...state };
- }) as FrontPage["setState"];
+ });
return instance;
};
@@ -65,9 +65,9 @@ describe(" ", () => {
originalPrivUrl = globalConfig.PRIV_URL;
const mockState = fakeState();
getStateSpy = jest.spyOn(store, "getState")
- .mockReturnValue(mockState as never);
+ .mockReturnValue(mockState);
postSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => mockAxiosResponse as never);
+ .mockImplementation(() => mockAxiosResponse);
fetchStoredTokenSpy = jest.spyOn(Session, "fetchStoredToken")
.mockImplementation(() => mockAuth);
replaceTokenSpy = jest.spyOn(Session, "replaceToken")
@@ -140,8 +140,8 @@ describe(" ", () => {
const content = instance.defaultContent();
const createAccount = findElement(content,
element => element.type === CreateAccount) as React.ReactElement<{
- set: (key: keyof FrontPage["state"], val: string | boolean) => void;
- }>;
+ set: (key: keyof FrontPage["state"], val: string | boolean) => void;
+ }>;
if (!createAccount) {
throw new Error("Expected create account panel");
}
@@ -241,7 +241,8 @@ describe(" ", () => {
instance.submitRegistration(fakeFormEvent);
await flushPromises();
expect(axios.post).toHaveBeenCalledWith(
- "http://localhost:3000/api/users/", {
+ "http://localhost:3000/api/users/",
+ {
user: {
agree_to_terms: true, email: "foo@bar.io", name: "Foo Bar",
password: "password", password_confirmation: "password"
@@ -265,7 +266,8 @@ describe(" ", () => {
instance.submitRegistration(fakeFormEvent);
await flushPromises();
expect(axios.post).toHaveBeenCalledWith(
- "http://localhost:3000/api/users/", {
+ "http://localhost:3000/api/users/",
+ {
user: {
agree_to_terms: true, email: "foo@bar.io", name: "Foo Bar",
password: "password", password_confirmation: "password"
diff --git a/frontend/front_page/__tests__/login_test.tsx b/frontend/front_page/__tests__/login_test.tsx
index 184fe76529..28fe20d170 100644
--- a/frontend/front_page/__tests__/login_test.tsx
+++ b/frontend/front_page/__tests__/login_test.tsx
@@ -21,12 +21,12 @@ describe(" ", () => {
it("interacts with login options", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.change(container.querySelectorAll("input")[0] as Element, {
+ fireEvent.change(container.querySelectorAll("input")[0], {
target: { value: "email" }
});
expect(p.onEmailChange).toHaveBeenCalled();
expect((p.onEmailChange as jest.Mock).mock.calls.length).toEqual(1);
- fireEvent.change(container.querySelectorAll("input")[1] as Element, {
+ fireEvent.change(container.querySelectorAll("input")[1], {
target: { value: "password" }
});
expect(p.onLoginPasswordChange).toHaveBeenCalled();
diff --git a/frontend/front_page/__tests__/resend_verification_test.tsx b/frontend/front_page/__tests__/resend_verification_test.tsx
index c252030616..71b8e83567 100644
--- a/frontend/front_page/__tests__/resend_verification_test.tsx
+++ b/frontend/front_page/__tests__/resend_verification_test.tsx
@@ -17,7 +17,7 @@ describe(" ", () => {
beforeEach(() => {
mockPost = Promise.resolve({ data: "whatever" });
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => mockPost as never);
+ .mockImplementation(() => mockPost);
});
afterEach(() => {
diff --git a/frontend/help/documentation.tsx b/frontend/help/documentation.tsx
index e66753c1cd..0bb86af867 100644
--- a/frontend/help/documentation.tsx
+++ b/frontend/help/documentation.tsx
@@ -12,12 +12,8 @@ export interface DocumentationPanelProps {
export const DocumentationPanel = (props: DocumentationPanelProps) => {
const location = useLocation();
- const [src, setSrc] = React.useState("");
-
- React.useEffect(() => {
- const page = new URLSearchParams(location.search).get("page");
- setSrc(page ? `${props.url}/${page}` : props.url);
- }, [props, location]);
+ const page = new URLSearchParams(location.search).get("page");
+ const src = page ? `${props.url}/${page}` : props.url;
return
diff --git a/frontend/logs/__tests__/index_test.tsx b/frontend/logs/__tests__/index_test.tsx
index 96168dc4aa..0f073fb0a5 100644
--- a/frontend/logs/__tests__/index_test.tsx
+++ b/frontend/logs/__tests__/index_test.tsx
@@ -50,7 +50,7 @@ describe(" ", () => {
const setStateSync = (instance: Logs) => {
instance.setState = ((state: Partial) => {
instance.state = { ...instance.state, ...state };
- }) as Logs["setState"];
+ });
return instance;
};
diff --git a/frontend/logs/components/__tests__/settings_menu_test.tsx b/frontend/logs/components/__tests__/settings_menu_test.tsx
index d7b9424f8d..7f7fd52bac 100644
--- a/frontend/logs/components/__tests__/settings_menu_test.tsx
+++ b/frontend/logs/components/__tests__/settings_menu_test.tsx
@@ -95,7 +95,7 @@ describe(" ", () => {
p.dispatch = jest.fn();
const { container } = render( );
const buttons = container.querySelectorAll("button");
- fireEvent.click(buttons[buttons.length - 1] as Element);
+ fireEvent.click(buttons[buttons.length - 1]);
await Promise.resolve();
await Promise.resolve();
expect(crud.destroyAll).toHaveBeenCalledWith(
diff --git a/frontend/messages/__tests__/actions_test.ts b/frontend/messages/__tests__/actions_test.ts
index cb5a64dea6..1613825f31 100644
--- a/frontend/messages/__tests__/actions_test.ts
+++ b/frontend/messages/__tests__/actions_test.ts
@@ -18,9 +18,9 @@ beforeEach(() => {
toastErrorsSpy = jest.spyOn(toastErrorsModule, "toastErrors")
.mockImplementation(jest.fn());
axiosGetSpy = jest.spyOn(axios, "get")
- .mockImplementation(() => Promise.resolve({ data: { foo: "bar" } }) as never);
+ .mockImplementation(() => Promise.resolve({ data: { foo: "bar" } }));
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => mockPostResponse as never);
+ .mockImplementation(() => mockPostResponse);
});
afterEach(() => {
@@ -55,7 +55,7 @@ describe("seedAccount()", () => {
});
it("returns error while trying to seed account", async () => {
- axiosPostSpy.mockRejectedValueOnce({ response: { data: ["error"] } } as never);
+ axiosPostSpy.mockRejectedValueOnce({ response: { data: ["error"] } });
const dismiss = jest.fn();
await seedAccount(dismiss)({ label: "Genesis v1.2", value: "genesis_1.2" });
expect(axios.post).toHaveBeenCalledWith(API.current.accountSeedPath, {
diff --git a/frontend/messages/__tests__/cards_test.tsx b/frontend/messages/__tests__/cards_test.tsx
index 2bd0ade4b3..2dd1f2f65f 100644
--- a/frontend/messages/__tests__/cards_test.tsx
+++ b/frontend/messages/__tests__/cards_test.tsx
@@ -65,10 +65,10 @@ beforeEach(() => {
shouldDisplayFeatureSpy = jest.spyOn(shouldDisplay, "shouldDisplayFeature")
.mockImplementation(() => mockFeatureBoolean);
fetchBulletinContentSpy = jest.spyOn(messageActions, "fetchBulletinContent")
- .mockImplementation(() => Promise.resolve(mockData) as never);
+ .mockImplementation(() => Promise.resolve(mockData));
seedAccountSpy = jest.spyOn(messageActions, "seedAccount")
.mockImplementation(() => mockSeedAccount as never);
- axiosDeleteSpy = jest.spyOn(axios, "delete").mockResolvedValue({} as never);
+ axiosDeleteSpy = jest.spyOn(axios, "delete").mockResolvedValue({});
sessionClearSpy = jest.spyOn(Session, "clear")
.mockImplementation(() => undefined as never);
fbSelectSpy = jest.spyOn(ui, "FBSelect")
@@ -233,8 +233,8 @@ describe(" ", () => {
const { container } = render( );
expect(container.textContent).toContain("currently using");
const links = container.querySelectorAll("a");
- fireEvent.click(links[0] as Element);
- fireEvent.click(links[links.length - 1] as Element);
+ fireEvent.click(links[0]);
+ fireEvent.click(links[links.length - 1]);
expect(Session.clear).toHaveBeenCalledTimes(2);
});
@@ -341,7 +341,7 @@ describe(" ", () => {
const { container } = render( );
fireEvent.click(container.querySelector(".fb-select-mock") as Element);
const buttons = container.querySelectorAll("button");
- fireEvent.click(buttons[buttons.length - 1] as Element);
+ fireEvent.click(buttons[buttons.length - 1]);
expect(mockSeedAccount).toHaveBeenCalledWith({
label: "",
value: "selection",
diff --git a/frontend/messages/__tests__/reducer_test.ts b/frontend/messages/__tests__/reducer_test.ts
index cb65d79dc2..c57681df76 100644
--- a/frontend/messages/__tests__/reducer_test.ts
+++ b/frontend/messages/__tests__/reducer_test.ts
@@ -12,8 +12,7 @@ beforeEach(() => {
describe("Contextual `Alert` creation", () => {
it("toggles on", () => {
const c = fakeFbosConfig();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- c.body.firmware_hardware = undefined as any;
+ c.body.firmware_hardware = undefined;
const s: AlertReducerState = {
alerts: {}
};
diff --git a/frontend/password_reset/__tests__/password_reset_test.tsx b/frontend/password_reset/__tests__/password_reset_test.tsx
index ac98f989af..9469795af7 100644
--- a/frontend/password_reset/__tests__/password_reset_test.tsx
+++ b/frontend/password_reset/__tests__/password_reset_test.tsx
@@ -15,7 +15,7 @@ describe(" ", () => {
originalPathname = location.pathname;
location.pathname = "/password_resets/";
axiosPutSpy = jest.spyOn(axios, "put")
- .mockImplementation(() => mockPut as never);
+ .mockImplementation(() => mockPut);
});
afterEach(() => {
diff --git a/frontend/photos/images/__tests__/photos_test.tsx b/frontend/photos/images/__tests__/photos_test.tsx
index af3966b0e4..6dd14dcf48 100644
--- a/frontend/photos/images/__tests__/photos_test.tsx
+++ b/frontend/photos/images/__tests__/photos_test.tsx
@@ -96,7 +96,7 @@ describe(" ", () => {
...(update as Partial),
};
callback?.();
- }) as Photos["setState"];
+ });
return instance;
};
diff --git a/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx b/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx
index 63d6261c46..19e435b9c9 100644
--- a/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx
+++ b/frontend/photos/photo_filter_settings/__tests__/image_filter_menu_test.tsx
@@ -44,7 +44,7 @@ beforeEach(() => {
{ length: props.max - props.min + 1 },
(_, index) => props.min + index,
).map(day => {props.labelRenderer(day)} )}
- ) as never);
+
));
});
afterEach(() => {
@@ -64,7 +64,7 @@ describe(" ", () => {
const setStateSync = (instance: ImageFilterMenu) => {
instance.setState = (((update: Partial) => {
instance.state = { ...instance.state, ...update };
- }) as unknown) as typeof instance.setState;
+ }));
};
const setConfigDispatch = (
diff --git a/frontend/plants/__tests__/crop_info_test.tsx b/frontend/plants/__tests__/crop_info_test.tsx
index 70256cf0b5..e29e18584d 100644
--- a/frontend/plants/__tests__/crop_info_test.tsx
+++ b/frontend/plants/__tests__/crop_info_test.tsx
@@ -56,7 +56,7 @@ beforeEach(() => {
.mockImplementation(((props: BIProps) =>
- props.onCommit(e as React.SyntheticEvent)} />) as never);
+ props.onCommit(e)} />) as never);
});
afterEach(() => {
diff --git a/frontend/plants/crop_info.tsx b/frontend/plants/crop_info.tsx
index 51086226fe..25ecda4a00 100644
--- a/frontend/plants/crop_info.tsx
+++ b/frontend/plants/crop_info.tsx
@@ -273,10 +273,6 @@ export function mapStateToProps(props: Everything): CropInfoProps {
export const RawCropInfo = (props: CropInfoProps) => {
const [gridOpen, setGridOpen] = React.useState(false);
const toggleOpen = () => setGridOpen(!gridOpen);
- React.useEffect(() => {
- selectMostUsedCurves(Path.getCropSlug());
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
const selectMostUsedCurves = (slug: string) => {
const findCurve = findMostUsedCurveForCrop({
@@ -289,6 +285,10 @@ export const RawCropInfo = (props: CropInfoProps) => {
changeCurve(props.dispatch)(id, curveType);
});
};
+ React.useEffect(() => {
+ selectMostUsedCurves(Path.getCropSlug());
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
const { dispatch, designer } = props;
const slug = Path.getCropSlug();
diff --git a/frontend/point_groups/__tests__/actions_test.ts b/frontend/point_groups/__tests__/actions_test.ts
index 9a014bd582..2b318658e9 100644
--- a/frontend/point_groups/__tests__/actions_test.ts
+++ b/frontend/point_groups/__tests__/actions_test.ts
@@ -38,7 +38,7 @@ beforeEach(() => {
selectAllPlantPointersSpy = jest.spyOn(selectors, "selectAllPlantPointers")
.mockImplementation(jest.fn(() => []));
findUuidSpy = jest.spyOn(selectors, "findUuid")
- .mockImplementation((() => "PointGroup.0.0") as typeof selectors.findUuid);
+ .mockImplementation(() => "PointGroup.0.0");
});
afterEach(() => {
diff --git a/frontend/point_groups/criteria/__tests__/component_test.tsx b/frontend/point_groups/criteria/__tests__/component_test.tsx
index 3c80e4cf0b..d9962beec7 100644
--- a/frontend/point_groups/criteria/__tests__/component_test.tsx
+++ b/frontend/point_groups/criteria/__tests__/component_test.tsx
@@ -170,7 +170,7 @@ describe(" ", () => {
p.group.body.point_ids = [1, 2];
const { container } = render( );
window.confirm = () => true;
- fireEvent.click(container.querySelectorAll("button")[0] as Element);
+ fireEvent.click(container.querySelectorAll("button")[0]);
const expectedBody = cloneDeep(p.group.body);
expectedBody.point_ids = [];
expect(overwriteGroupSpy).toHaveBeenCalledWith(p.group, expectedBody);
@@ -181,7 +181,7 @@ describe(" ", () => {
p.group.body.point_ids = [1, 2];
const { container } = render( );
window.confirm = () => false;
- fireEvent.click(container.querySelectorAll("button")[0] as Element);
+ fireEvent.click(container.querySelectorAll("button")[0]);
expect(overwriteGroupSpy).not.toHaveBeenCalled();
});
@@ -189,7 +189,7 @@ describe(" ", () => {
const p = fakeProps();
const { container } = render( );
window.confirm = () => true;
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
const expectedBody = cloneDeep(p.group.body);
expectedBody.criteria = DEFAULT_CRITERIA;
expect(overwriteGroupSpy).toHaveBeenCalledWith(p.group, expectedBody);
@@ -199,7 +199,7 @@ describe(" ", () => {
const p = fakeProps();
const { container } = render( );
window.confirm = () => false;
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(overwriteGroupSpy).not.toHaveBeenCalled();
});
@@ -305,7 +305,7 @@ describe(" ", () => {
const p = fakeProps();
p.pointTypes = ["Plant", "Weed"];
const { container } = render( );
- fireEvent.click(container.querySelectorAll("input")[0] as Element);
+ fireEvent.click(container.querySelectorAll("input")[0]);
expect(togglePointTypeCriteriaSpy).toHaveBeenCalledWith(p.group, "Plant");
});
});
diff --git a/frontend/point_groups/criteria/__tests__/show_test.tsx b/frontend/point_groups/criteria/__tests__/show_test.tsx
index 91c4df7936..62523db63f 100644
--- a/frontend/point_groups/criteria/__tests__/show_test.tsx
+++ b/frontend/point_groups/criteria/__tests__/show_test.tsx
@@ -89,7 +89,7 @@ describe(" />", () => {
const p = fakeProps();
p.eqCriteria = { openfarm_slug: ["slug"] };
const { container } = render( {...p} />);
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(removeEqCriteriaValueSpy).toHaveBeenCalledWith(
p.group,
{ openfarm_slug: ["slug"] },
@@ -121,7 +121,7 @@ describe(" ", () => {
p.criteria.number_gt = { x: 1 };
const { container } = render( );
expect(container.textContent).toContain(">");
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(clearCriteriaFieldSpy).toHaveBeenCalledWith(
p.group,
["number_gt"],
@@ -192,7 +192,7 @@ describe(" ", () => {
it("changes number_gt", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.blur(container.querySelectorAll("input")[0] as Element, {
+ fireEvent.blur(container.querySelectorAll("input")[0], {
target: { value: "1" }
});
expect(editGtLtCriteriaFieldSpy).toHaveBeenCalledWith(
@@ -205,7 +205,7 @@ describe(" ", () => {
it("changes number_lt", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.blur(container.querySelectorAll("input")[1] as Element, {
+ fireEvent.blur(container.querySelectorAll("input")[1], {
target: { value: "1" }
});
expect(editGtLtCriteriaFieldSpy).toHaveBeenCalledWith(
diff --git a/frontend/points/__tests__/point_info_test.tsx b/frontend/points/__tests__/point_info_test.tsx
index bd3f550d5a..d4339d3e56 100644
--- a/frontend/points/__tests__/point_info_test.tsx
+++ b/frontend/points/__tests__/point_info_test.tsx
@@ -34,7 +34,7 @@ beforeEach(() => {
saveSpy = jest.spyOn(crud, "save").mockImplementation(jest.fn());
popoverSpy = jest.spyOn(popover, "Popover").mockImplementation(((
{ target, content }: PopoverProps,
- ) => {target}{content}
) as never);
+ ) => {target}{content}
));
});
afterEach(() => {
diff --git a/frontend/promo/__tests__/plants_test.ts b/frontend/promo/__tests__/plants_test.ts
index 035576743e..d3033d9c60 100644
--- a/frontend/promo/__tests__/plants_test.ts
+++ b/frontend/promo/__tests__/plants_test.ts
@@ -22,7 +22,7 @@ describe("calculatePlantPositions()", () => {
seed: expect.any(Number),
size: 150,
spread: 17.5,
- x: 350,
+ x: 200,
y: 680,
});
expect(positions.length).toEqual(65);
diff --git a/frontend/promo/plants.ts b/frontend/promo/plants.ts
index 21ef640187..a94fb9d187 100644
--- a/frontend/promo/plants.ts
+++ b/frontend/promo/plants.ts
@@ -7,11 +7,11 @@ import { Season, SEASON_DURATIONS } from "./constants";
export const calculatePlantPositions = (config: Config): ThreeDGardenPlant[] => {
const gardenPlants = GARDENS[config.plants] || [];
const positions: ThreeDGardenPlant[] = [];
- const startX = 350;
+ const startX = 200;
let nextX = startX;
let index = 0;
let nextId = 1;
- while (nextX <= config.bedLengthOuter - 100) {
+ while (nextX <= config.bedLengthOuter - 250) {
const plantKey = gardenPlants[index];
const plant = PLANTS[plantKey];
if (!plant) { return []; }
diff --git a/frontend/redux/__tests__/create_refresh_trigger_test.ts b/frontend/redux/__tests__/create_refresh_trigger_test.ts
index 10b4ce1384..5df00c7912 100644
--- a/frontend/redux/__tests__/create_refresh_trigger_test.ts
+++ b/frontend/redux/__tests__/create_refresh_trigger_test.ts
@@ -11,7 +11,7 @@ describe("createRefreshTrigger", () => {
.mockImplementation(jest.fn(() => jest.fn()));
maybeGetDeviceSpy = jest.spyOn(deviceModule, "maybeGetDevice")
.mockImplementation((() =>
- ({} as import("farmbot").Farmbot)) as typeof deviceModule.maybeGetDevice);
+ ({} as import("farmbot").Farmbot)));
});
afterEach(() => {
diff --git a/frontend/redux/__tests__/root_reducer_test.ts b/frontend/redux/__tests__/root_reducer_test.ts
index 5f8734a56e..23c0ff90fb 100644
--- a/frontend/redux/__tests__/root_reducer_test.ts
+++ b/frontend/redux/__tests__/root_reducer_test.ts
@@ -9,7 +9,7 @@ describe("rootReducer()", () => {
beforeEach(() => {
clearSpy = jest.spyOn(Session, "clear")
- .mockImplementation((() => undefined as never) as typeof Session.clear);
+ .mockImplementation((() => undefined) as typeof Session.clear);
});
afterEach(() => {
diff --git a/frontend/regimens/editor/__tests__/state_to_props_test.ts b/frontend/regimens/editor/__tests__/state_to_props_test.ts
index e7c977605e..4f8dccda79 100644
--- a/frontend/regimens/editor/__tests__/state_to_props_test.ts
+++ b/frontend/regimens/editor/__tests__/state_to_props_test.ts
@@ -63,7 +63,7 @@ describe("mapStateToProps()", () => {
const varData = fakeVariableNameSet();
state.resources.index.sequenceMetas[seq.uuid] = varData;
const findSequenceByIdSpy = jest.spyOn(selectors, "findSequenceById")
- .mockReturnValue(seq as never);
+ .mockReturnValue(seq);
try {
const props = mapStateToProps(state);
expect(props.variableData).toEqual(expect.objectContaining(varData));
diff --git a/frontend/regimens/list/__tests__/list_test.tsx b/frontend/regimens/list/__tests__/list_test.tsx
index 93ef214043..aff02c6639 100644
--- a/frontend/regimens/list/__tests__/list_test.tsx
+++ b/frontend/regimens/list/__tests__/list_test.tsx
@@ -28,15 +28,15 @@ describe(" ", () => {
designerPanelSpy = jest.spyOn(designerPanel, "DesignerPanel")
.mockImplementation(((
{ children }: React.ComponentProps) =>
- {children}
) as never);
+ {children}
));
designerPanelContentSpy = jest.spyOn(designerPanel, "DesignerPanelContent")
.mockImplementation(((
{ children }: React.ComponentProps) =>
- {children}
) as never);
+ {children}
));
designerPanelTopSpy = jest.spyOn(designerPanel, "DesignerPanelTop")
.mockImplementation(((
props: React.ComponentProps) =>
- mockDesignerPanelTop(props)) as never);
+ mockDesignerPanelTop(props)));
mockDesignerPanelTop.mockClear();
});
diff --git a/frontend/resources/reducer_support.ts b/frontend/resources/reducer_support.ts
index ed40a37878..9b5196e744 100644
--- a/frontend/resources/reducer_support.ts
+++ b/frontend/resources/reducer_support.ts
@@ -423,7 +423,7 @@ export function beforeEach(state: RestResources,
const { warning } = require("../toast/toast");
warning(`(${place}) Can't modify account data when in read-only mode.`);
};
- const { kind } = unpackUUID(get(action, "payload.uuid", "x.y.z") as string);
+ const { kind } = unpackUUID(get(action, "payload.uuid", "x.y.z"));
switch (action.type) {
case Actions.EDIT_RESOURCE:
diff --git a/frontend/resources/selectors_by_id.ts b/frontend/resources/selectors_by_id.ts
index 15e1f93f0d..843220e957 100644
--- a/frontend/resources/selectors_by_id.ts
+++ b/frontend/resources/selectors_by_id.ts
@@ -30,7 +30,7 @@ const byId =
const resources = findAll(index, kind);
const f = (x: TaggedResource) => (x.kind === kind) && (x.body.id === id);
// Maybe we should add a throw here?
- return resources.filter(f)[0] as T | undefined;
+ return resources.filter(f)[0];
};
export const findFarmEventById = (ri: ResourceIndex, fe_id: number) => {
diff --git a/frontend/resources/sequence_meta.ts b/frontend/resources/sequence_meta.ts
index e0e054beaa..9500d6bf0e 100644
--- a/frontend/resources/sequence_meta.ts
+++ b/frontend/resources/sequence_meta.ts
@@ -170,7 +170,7 @@ export const determineDropdown =
label: get({
"SavedGarden": t("Garden"),
"PointGroup": t("Group"),
- }, resourceType, resourceType) as string,
+ }, resourceType, resourceType),
value: data_value.args.resource_type,
headingId: "Resource",
};
diff --git a/frontend/saved_gardens/__tests__/garden_snapshot_test.tsx b/frontend/saved_gardens/__tests__/garden_snapshot_test.tsx
index 3bab78fe87..2f7a0509ae 100644
--- a/frontend/saved_gardens/__tests__/garden_snapshot_test.tsx
+++ b/frontend/saved_gardens/__tests__/garden_snapshot_test.tsx
@@ -14,7 +14,7 @@ let axiosPostSpy: jest.SpyInstance;
beforeEach(() => {
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => Promise.resolve({}) as never);
+ .mockImplementation(() => Promise.resolve({}));
snapshotGardenSpy = jest.spyOn(savedGardenActions, "snapshotGarden")
.mockImplementation(jest.fn());
newSavedGardenSpy = jest.spyOn(savedGardenActions, "newSavedGarden")
@@ -82,7 +82,7 @@ describe(" ", () => {
currentTarget: { value: "new saved garden" },
target: { value: "new saved garden" },
});
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(newSavedGarden).toHaveBeenCalledWith(expect.any(Function),
"new saved garden", "");
});
diff --git a/frontend/sensors/__tests__/sensor_list_test.tsx b/frontend/sensors/__tests__/sensor_list_test.tsx
index 32b528c583..fcccfd34ff 100644
--- a/frontend/sensors/__tests__/sensor_list_test.tsx
+++ b/frontend/sensors/__tests__/sensor_list_test.tsx
@@ -89,9 +89,9 @@ describe(" ", function () {
it("reads sensors", () => {
const { container } = render( );
const readSensorBtn = container.querySelectorAll("button");
- fireEvent.click(readSensorBtn[0] as Element);
+ fireEvent.click(readSensorBtn[0]);
expect(mockDevice.readPin).toHaveBeenCalledWith(expectedPayload(51, 0));
- fireEvent.click(readSensorBtn[1] as Element);
+ fireEvent.click(readSensorBtn[1]);
expect(mockDevice.readPin).toHaveBeenLastCalledWith(expectedPayload(50, 1));
expect(mockDevice.readPin).toHaveBeenCalledTimes(2);
});
@@ -101,8 +101,8 @@ describe(" ", function () {
p.disabled = true;
const { container } = render( );
const readSensorBtn = container.querySelectorAll("button");
- fireEvent.click(readSensorBtn[0] as Element);
- fireEvent.click(readSensorBtn[readSensorBtn.length - 1] as Element);
+ fireEvent.click(readSensorBtn[0]);
+ fireEvent.click(readSensorBtn[readSensorBtn.length - 1]);
expect(mockDevice.readPin).not.toHaveBeenCalled();
});
diff --git a/frontend/sequences/__tests__/actions_test.ts b/frontend/sequences/__tests__/actions_test.ts
index 4e9b1cc3aa..eeff629d14 100644
--- a/frontend/sequences/__tests__/actions_test.ts
+++ b/frontend/sequences/__tests__/actions_test.ts
@@ -46,7 +46,7 @@ beforeEach(() => {
overwriteSpy = jest.spyOn(crud, "overwrite")
.mockImplementation(jest.fn());
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => mockPost as never);
+ .mockImplementation(() => mockPost);
setActiveSequenceByNameSpy = jest.spyOn(
activeSequenceByName, "setActiveSequenceByName")
.mockImplementation(jest.fn());
diff --git a/frontend/sequences/__tests__/request_auto_generation_test.ts b/frontend/sequences/__tests__/request_auto_generation_test.ts
index d5d6961cd8..56de16188e 100644
--- a/frontend/sequences/__tests__/request_auto_generation_test.ts
+++ b/frontend/sequences/__tests__/request_auto_generation_test.ts
@@ -62,7 +62,7 @@ describe("requestAutoGeneration()", () => {
.mockResolvedValueOnce({ done: false, value: new Uint8Array([101]) })
.mockResolvedValueOnce({ done: false, value: new Uint8Array([100]) }),
)));
- global.fetch = fetchMock as unknown as typeof fetch;
+ global.fetch = fetchMock as never;
const p = fakeProps();
p.contextKey = "color";
actualRequestAutoGeneration(p);
@@ -90,7 +90,7 @@ describe("requestAutoGeneration()", () => {
jest.fn().mockResolvedValue({ done: true, value: "" }),
{ ok: false, body: undefined },
)));
- global.fetch = fetchMock as unknown as typeof fetch;
+ global.fetch = fetchMock as never;
const p = fakeProps();
p.contextKey = "lua";
actualRequestAutoGeneration(p);
diff --git a/frontend/sequences/__tests__/sequences_test.tsx b/frontend/sequences/__tests__/sequences_test.tsx
index 8df7b085a2..e3190c3e8a 100644
--- a/frontend/sequences/__tests__/sequences_test.tsx
+++ b/frontend/sequences/__tests__/sequences_test.tsx
@@ -33,7 +33,7 @@ beforeEach(() => {
data: [
{ id: 1, name: "name", description: "", path: "", color: "gray" },
]
- }) as never);
+ }));
});
afterEach(() => {
diff --git a/frontend/sequences/locals_list/__tests__/default_value_form_test.tsx b/frontend/sequences/locals_list/__tests__/default_value_form_test.tsx
index 0dac0d71ab..0252f786c6 100644
--- a/frontend/sequences/locals_list/__tests__/default_value_form_test.tsx
+++ b/frontend/sequences/locals_list/__tests__/default_value_form_test.tsx
@@ -31,7 +31,7 @@ describe(" ", () => {
mockVariableFormOnChangeArg.args.label,
)} />
;
- }) as never);
+ }));
});
afterEach(() => {
diff --git a/frontend/sequences/locals_list/__tests__/variable_form_test.tsx b/frontend/sequences/locals_list/__tests__/variable_form_test.tsx
index e3f588e5fa..f72023d545 100644
--- a/frontend/sequences/locals_list/__tests__/variable_form_test.tsx
+++ b/frontend/sequences/locals_list/__tests__/variable_form_test.tsx
@@ -54,7 +54,7 @@ beforeEach(() => {
...e, currentTarget: {
...e.currentTarget, value
}
- } as React.SyntheticEvent)} />
+ })} />
diff --git a/frontend/sequences/panel/__tests__/list_test.tsx b/frontend/sequences/panel/__tests__/list_test.tsx
index 7415a59bdc..51e4c4e695 100644
--- a/frontend/sequences/panel/__tests__/list_test.tsx
+++ b/frontend/sequences/panel/__tests__/list_test.tsx
@@ -52,7 +52,7 @@ beforeEach(() => {
color: "gray",
},
]
- }) as never);
+ }));
installSequenceSpy = jest.spyOn(sequenceActions, "installSequence")
.mockImplementation(() => jest.fn() as never);
addNewSequenceToFolderSpy = jest.spyOn(foldersActions, "addNewSequenceToFolder")
diff --git a/frontend/sequences/panel/__tests__/preview_test.tsx b/frontend/sequences/panel/__tests__/preview_test.tsx
index f35a97f148..fce4a30fe8 100644
--- a/frontend/sequences/panel/__tests__/preview_test.tsx
+++ b/frontend/sequences/panel/__tests__/preview_test.tsx
@@ -24,8 +24,8 @@ let postSpy: jest.SpyInstance;
let installSequenceSpy: jest.SpyInstance;
beforeEach(() => {
- getSpy = jest.spyOn(axios, "get").mockImplementation(() => mockGet as never);
- postSpy = jest.spyOn(axios, "post").mockResolvedValue({} as never);
+ getSpy = jest.spyOn(axios, "get").mockImplementation(() => mockGet);
+ postSpy = jest.spyOn(axios, "post").mockResolvedValue({});
installSequenceSpy = jest.spyOn(sequenceActions, "installSequence")
.mockImplementation(jest.fn(() => () => Promise.resolve()) as never);
});
diff --git a/frontend/sequences/step_tiles/__tests__/step_title_bar_test.tsx b/frontend/sequences/step_tiles/__tests__/step_title_bar_test.tsx
index db311c04e7..f99786a47d 100644
--- a/frontend/sequences/step_tiles/__tests__/step_title_bar_test.tsx
+++ b/frontend/sequences/step_tiles/__tests__/step_title_bar_test.tsx
@@ -2,13 +2,12 @@ import React from "react";
import { StepTitleBar } from "../step_title_bar";
import { render, fireEvent } from "@testing-library/react";
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
-import { Wait } from "farmbot";
import { StepTitleBarProps } from "../../interfaces";
import { FarmwareName } from "../tile_execute_script";
describe(" ", () => {
const fakeProps = (): StepTitleBarProps => ({
- step: { kind: "wait", args: { milliseconds: 100 } } as Wait,
+ step: { kind: "wait", args: { milliseconds: 100 } },
index: 0,
dispatch: jest.fn(),
readOnly: false,
diff --git a/frontend/sequences/step_tiles/__tests__/tile_execute_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_execute_test.tsx
index fe412b4569..54f2699421 100644
--- a/frontend/sequences/step_tiles/__tests__/tile_execute_test.tsx
+++ b/frontend/sequences/step_tiles/__tests__/tile_execute_test.tsx
@@ -51,7 +51,7 @@ beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(crud, "editStep").mockImplementation(mockEditStep);
jest.spyOn(selectorsById, "findSequenceById")
- .mockImplementation(() => mockSequence as never);
+ .mockImplementation(() => mockSequence);
mockEditStep.mockClear();
mockSequence = fakeSequence();
});
diff --git a/frontend/sequences/step_tiles/__tests__/tile_lua_support_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_lua_support_test.tsx
index d6f4365cc6..6111378bd8 100644
--- a/frontend/sequences/step_tiles/__tests__/tile_lua_support_test.tsx
+++ b/frontend/sequences/step_tiles/__tests__/tile_lua_support_test.tsx
@@ -21,7 +21,7 @@ describe(" ", () => {
: state;
Object.assign(component.state, update);
callback?.();
- }) as LuaTextArea["setState"];
+ });
return component;
};
@@ -45,7 +45,7 @@ describe(" ", () => {
const component = fakeComponent(p);
const updateStep = Object.assign(jest.fn(), { cancel: jest.fn(), flush: jest.fn() });
component.updateStep = updateStep;
- component.onChange(undefined as unknown as string);
+ component.onChange(undefined);
expect(updateStep).toHaveBeenCalledWith("");
expect(component.state.lua).toEqual("");
});
diff --git a/frontend/sequences/step_tiles/__tests__/tile_send_message_test.tsx b/frontend/sequences/step_tiles/__tests__/tile_send_message_test.tsx
index 9d691b32f1..ac97b5ae7c 100644
--- a/frontend/sequences/step_tiles/__tests__/tile_send_message_test.tsx
+++ b/frontend/sequences/step_tiles/__tests__/tile_send_message_test.tsx
@@ -127,7 +127,7 @@ describe(" ", () => {
const instance = new TileSendMessage(fakeProps());
instance.setState = jest.fn((update: { message: string }) => {
instance.state = { ...instance.state, ...update };
- }) as unknown as typeof instance.setState;
+ });
expect(instance.state.message).toEqual("send this message");
instance.updateMessage("k", "new");
expect(instance.state.message).toEqual("new");
diff --git a/frontend/sequences/step_tiles/pin_support/__tests__/mode_test.tsx b/frontend/sequences/step_tiles/pin_support/__tests__/mode_test.tsx
index a6f5df669c..9da202818f 100644
--- a/frontend/sequences/step_tiles/pin_support/__tests__/mode_test.tsx
+++ b/frontend/sequences/step_tiles/pin_support/__tests__/mode_test.tsx
@@ -1,6 +1,6 @@
const mockEditStep = jest.fn();
-import { NamedPin, WritePin, ALLOWED_PIN_MODES, ReadPin } from "farmbot";
+import { NamedPin, WritePin, ReadPin } from "farmbot";
import {
setPinMode, getPinModes, currentModeSelection, PinModeDropdown,
} from "../mode";
@@ -92,7 +92,7 @@ describe("setPinMode()", () => {
const p = fakeStepParams(step);
setPinMode({
label: "",
- value: "bad" as unknown as ALLOWED_PIN_MODES
+ value: "bad",
}, p);
const action = () => mockEditStep.mock.calls[0][0].executor(step);
expect(action).toThrow("pin_mode must be one of ALLOWED_PIN_MODES.");
diff --git a/frontend/sequences/step_tiles/tile_computed_move/__tests__/component_test.tsx b/frontend/sequences/step_tiles/tile_computed_move/__tests__/component_test.tsx
index 24fb1910a9..45aa8f67e2 100644
--- a/frontend/sequences/step_tiles/tile_computed_move/__tests__/component_test.tsx
+++ b/frontend/sequences/step_tiles/tile_computed_move/__tests__/component_test.tsx
@@ -39,7 +39,7 @@ const setStateSync = (instance: ComputedMove) => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as ComputedMove["setState"];
+ });
return instance;
};
diff --git a/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx b/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx
index dd64b3f53b..4b662a3574 100644
--- a/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx
+++ b/frontend/sequences/step_tiles/tile_mark_as/__tests__/component_test.tsx
@@ -28,7 +28,7 @@ const setStateSync = (instance: MarkAs) => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as MarkAs["setState"];
+ });
return instance;
};
diff --git a/frontend/sequences/step_ui/__tests__/step_header_test.tsx b/frontend/sequences/step_ui/__tests__/step_header_test.tsx
index 23aa941936..7b3160d0a6 100644
--- a/frontend/sequences/step_ui/__tests__/step_header_test.tsx
+++ b/frontend/sequences/step_ui/__tests__/step_header_test.tsx
@@ -12,7 +12,7 @@ let requestAutoGenerationSpy: jest.SpyInstance;
beforeEach(() => {
postSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => Promise.resolve({}) as never);
+ .mockImplementation(() => Promise.resolve({}));
requestAutoGenerationSpy = jest.spyOn(
requestAutoGenerationModule,
"requestAutoGeneration",
@@ -51,7 +51,7 @@ describe(" ", () => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as StepHeader["setState"];
+ });
return instance;
};
diff --git a/frontend/sequences/step_ui/__tests__/step_radio_test.tsx b/frontend/sequences/step_ui/__tests__/step_radio_test.tsx
index c3d00543e6..29d0afd93f 100644
--- a/frontend/sequences/step_ui/__tests__/step_radio_test.tsx
+++ b/frontend/sequences/step_ui/__tests__/step_radio_test.tsx
@@ -49,7 +49,7 @@ describe(" ", () => {
mockStep = p.currentStep;
const { container } = render( );
const inputs = container.querySelectorAll("input");
- fireEvent.click(inputs[inputs.length - 1] as Element);
+ fireEvent.click(inputs[inputs.length - 1]);
const expectedStep: FindHome = {
kind: "find_home",
args: { speed: 100, axis: "all" }
@@ -63,7 +63,7 @@ describe(" ", () => {
mockStep = p.currentStep;
const { container } = render( );
const inputs = container.querySelectorAll("input");
- fireEvent.click(inputs[inputs.length - 1] as Element);
+ fireEvent.click(inputs[inputs.length - 1]);
const expectedStep: Calibrate = {
kind: "calibrate",
args: { axis: "all" }
@@ -77,7 +77,7 @@ describe(" ", () => {
mockStep = p.currentStep;
const { container } = render( );
const inputs = container.querySelectorAll("input");
- fireEvent.click(inputs[inputs.length - 1] as Element);
+ fireEvent.click(inputs[inputs.length - 1]);
const expectedStep: Zero = {
kind: "zero",
args: { axis: "all" }
diff --git a/frontend/sequences/step_ui/__tests__/step_wrapper_test.tsx b/frontend/sequences/step_ui/__tests__/step_wrapper_test.tsx
index 10c0e61a07..e608754784 100644
--- a/frontend/sequences/step_ui/__tests__/step_wrapper_test.tsx
+++ b/frontend/sequences/step_ui/__tests__/step_wrapper_test.tsx
@@ -30,7 +30,7 @@ describe(" ", () => {
: state;
instance.state = { ...instance.state, ...update };
callback?.();
- }) as StepWrapper["setState"];
+ });
return instance;
};
diff --git a/frontend/settings/__tests__/farm_designer_settings_test.tsx b/frontend/settings/__tests__/farm_designer_settings_test.tsx
index 1c725153f1..5bd773a0f9 100644
--- a/frontend/settings/__tests__/farm_designer_settings_test.tsx
+++ b/frontend/settings/__tests__/farm_designer_settings_test.tsx
@@ -41,7 +41,7 @@ describe(" ", () => {
const labels = container.querySelectorAll("label");
const buttons = container.querySelectorAll("button");
expect(labels[0]?.textContent).toContain("animations");
- fireEvent.click(buttons[0] as Element);
+ fireEvent.click(buttons[0]);
expect(resetVirtualTrailSpy).not.toHaveBeenCalled();
});
@@ -53,7 +53,7 @@ describe(" ", () => {
const labels = container.querySelectorAll("label");
const buttons = container.querySelectorAll("button");
expect(labels[1]?.textContent).toContain("Trail");
- fireEvent.click(buttons[1] as Element);
+ fireEvent.click(buttons[1]);
expect(resetVirtualTrailSpy).toHaveBeenCalled();
});
});
diff --git a/frontend/settings/account/__tests__/account_settings_test.tsx b/frontend/settings/account/__tests__/account_settings_test.tsx
index c1e89ca4e3..8fa1a12a7f 100644
--- a/frontend/settings/account/__tests__/account_settings_test.tsx
+++ b/frontend/settings/account/__tests__/account_settings_test.tsx
@@ -41,7 +41,7 @@ beforeEach(() => {
.mockImplementation(((props: BIProps) =>
props.onCommit(e)}
onChange={() => { }} />) as never);
diff --git a/frontend/settings/account/__tests__/actions_test.ts b/frontend/settings/account/__tests__/actions_test.ts
index efc078a82e..1f76d4c02b 100644
--- a/frontend/settings/account/__tests__/actions_test.ts
+++ b/frontend/settings/account/__tests__/actions_test.ts
@@ -18,9 +18,9 @@ beforeEach(() => {
toastErrorsSpy = jest.spyOn(toastErrorsModule, "toastErrors")
.mockImplementation(jest.fn());
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => mockPost as never);
+ .mockImplementation(() => mockPost);
axiosDeleteSpy = jest.spyOn(axios, "delete")
- .mockImplementation(() => mockDelete as never);
+ .mockImplementation(() => mockDelete);
});
afterEach(() => {
diff --git a/frontend/settings/account/__tests__/change_password_test.tsx b/frontend/settings/account/__tests__/change_password_test.tsx
index c53eb380fa..7effd33a21 100644
--- a/frontend/settings/account/__tests__/change_password_test.tsx
+++ b/frontend/settings/account/__tests__/change_password_test.tsx
@@ -24,9 +24,9 @@ afterEach(() => {
beforeEach(() => {
axiosPatchSpy = jest.spyOn(axios, "patch")
- .mockImplementation(() => mockPatch() as never);
+ .mockImplementation(() => mockPatch());
reactUseRefSpy = jest.spyOn(React, "useRef")
- .mockImplementation(() => mockRef as never);
+ .mockImplementation(() => mockRef);
});
const setFields = (
diff --git a/frontend/settings/account/__tests__/dangerous_delete_widget_test.tsx b/frontend/settings/account/__tests__/dangerous_delete_widget_test.tsx
index 6f8cf0dc89..8596939897 100644
--- a/frontend/settings/account/__tests__/dangerous_delete_widget_test.tsx
+++ b/frontend/settings/account/__tests__/dangerous_delete_widget_test.tsx
@@ -14,7 +14,7 @@ beforeEach(() => {
jest.clearAllMocks();
mockRef = { current: { value: "" } };
reactUseRefSpy = jest.spyOn(React, "useRef")
- .mockImplementation(() => mockRef as never);
+ .mockImplementation(() => mockRef);
});
afterEach(() => {
diff --git a/frontend/settings/dev/dev_settings.tsx b/frontend/settings/dev/dev_settings.tsx
index 128927172a..7ae29118ea 100644
--- a/frontend/settings/dev/dev_settings.tsx
+++ b/frontend/settings/dev/dev_settings.tsx
@@ -37,16 +37,13 @@ export const DevWidgetFBOSRow = () => {
};
export const DevWidget3dCameraRow = () => {
- const [parseError, setParseError] = React.useState(false);
const value = DevSettings.get3dCamera();
- React.useEffect(() => {
- try {
- JSON.parse(value);
- setParseError(false);
- } catch {
- setParseError(true);
- }
- }, [value]);
+ let parseError = false;
+ try {
+ JSON.parse(value);
+ } catch {
+ parseError = true;
+ }
return
{"Change initial 3D camera position"}
diff --git a/frontend/settings/fbos_settings/farmbot_os_row.tsx b/frontend/settings/fbos_settings/farmbot_os_row.tsx
index 9cc20374dd..187baabd9e 100644
--- a/frontend/settings/fbos_settings/farmbot_os_row.tsx
+++ b/frontend/settings/fbos_settings/farmbot_os_row.tsx
@@ -36,9 +36,9 @@ export const getOsReleaseNotesForVersion = (
};
export const FarmbotOsRow = (props: FarmbotOsRowProps) => {
- const [version, setVersion] = React.useState(
+ const versionRef = React.useRef(
props.bot.hardware.informational_settings.controller_version);
- const [channel, setChannel] = React.useState(
+ const channelRef = React.useRef(
"" + props.sourceFbosConfig("update_channel").value);
const { dispatch, bot, sourceFbosConfig } = props;
@@ -53,21 +53,22 @@ export const FarmbotOsRow = (props: FarmbotOsRowProps) => {
}, []);
React.useEffect(() => {
- const versionChange = controller_version && version != controller_version;
- const channelChange = configChannel && channel != configChannel;
+ const versionChange =
+ controller_version && versionRef.current != controller_version;
+ const channelChange = configChannel && channelRef.current != configChannel;
if (versionChange || channelChange) {
- setVersion(controller_version);
- setChannel(configChannel);
+ versionRef.current = controller_version;
+ channelRef.current = configChannel;
dispatch(fetchOsUpdateVersion(target));
}
if (versionChange) {
removeToast("EOL");
}
- }, [dispatch, controller_version, target, configChannel, version, channel]);
+ }, [dispatch, controller_version, target, configChannel]);
const releaseNotes = getOsReleaseNotesForVersion(
props.bot.osReleaseNotes,
- version || props.device.body.fbos_version);
+ controller_version || props.device.body.fbos_version);
return {
{t("Version {{ version }}",
- { version: version || t("unknown (offline)") })}
+ { version: controller_version || t("unknown (offline)") })}
}
content={
", () => {
beforeEach(() => {
getDeviceSpy = jest.spyOn(deviceModule, "getDevice")
.mockImplementation((() =>
- mockDevice as unknown as import("farmbot").Farmbot) as
- typeof deviceModule.getDevice);
+ mockDevice as unknown as import("farmbot").Farmbot));
editSpy = jest.spyOn(crud, "edit").mockImplementation(jest.fn());
saveSpy = jest.spyOn(crud, "save").mockImplementation(jest.fn());
calibrationRowSpy = jest.spyOn(calibrationRowModule, "CalibrationRow")
diff --git a/frontend/settings/hardware_settings/__tests__/parameter_management_test.tsx b/frontend/settings/hardware_settings/__tests__/parameter_management_test.tsx
index ec853e9ff8..a3392a1ce9 100644
--- a/frontend/settings/hardware_settings/__tests__/parameter_management_test.tsx
+++ b/frontend/settings/hardware_settings/__tests__/parameter_management_test.tsx
@@ -37,7 +37,7 @@ beforeEach(() => {
.mockImplementation(((props: BIProps) =>
props.onCommit?.(e as never)} />) as never);
+ onChange={e => props.onCommit?.(e)} />) as never);
toggleButtonSpy = jest.spyOn(ui, "ToggleButton")
.mockImplementation(((props: ToggleButtonProps) =>
) as never);
diff --git a/frontend/settings/maybe_highlight.tsx b/frontend/settings/maybe_highlight.tsx
index 5cd9bbf4a9..7e322a557d 100644
--- a/frontend/settings/maybe_highlight.tsx
+++ b/frontend/settings/maybe_highlight.tsx
@@ -431,38 +431,55 @@ export interface HighlightProps {
pathPrefix?(path?: string): string;
}
-/** Wrap highlight-able settings. */
-export const Highlight = (props: HighlightProps) => {
- const { settingName } = props;
+interface HighlightBodyProps extends HighlightProps {
+ highlightMatch: boolean;
+ hidden: boolean;
+}
+const HighlightBody = (props: HighlightBodyProps) => {
const [hovered, setHovered] = React.useState(false);
- const [highlightClass, setHighlightClass] = React.useState("");
- const [highlightTimestamp, setHighlightTimestamp] = React.useState(0);
-
+ const [highlightClass, setHighlightClass] = React.useState(
+ props.highlightMatch ? "highlight" : "");
const navigate = useNavigate();
+
+ React.useEffect(() => {
+ if (!props.highlightMatch) { return; }
+ const timeout = setTimeout(() => setHighlightClass("unhighlight"), 200);
+ return () => clearTimeout(timeout);
+ }, [props.highlightMatch]);
+
+ return setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ hidden={props.hidden}>
+ {props.settingName &&
+ {
+ navigate(linkToSetting(props.settingName, props.pathPrefix));
+ }} />}
+ {props.children}
+
;
+};
+
+/** Wrap highlight-able settings. */
+export const Highlight = (props: HighlightProps) => {
+ const { settingName } = props;
const location = useLocation();
const highlightName = new URLSearchParams(location.search).get("highlight");
const highlightMatch = highlightName &&
compareValues(settingName).includes(highlightName.toLowerCase());
- React.useEffect(() => {
- if (highlightMatch) {
- setHighlightTimestamp(Date.now());
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [location]);
-
- React.useEffect(() => {
- if (!highlightMatch) {
- setHighlightClass("");
- return;
- }
- setHighlightClass("highlight");
- setTimeout(() => setHighlightClass("unhighlight"), 200);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [highlightTimestamp]);
-
const searchTerm = store.getState().app.settingsSearchTerm;
const isSectionHeader = props.className?.includes("section");
@@ -501,27 +518,16 @@ export const Highlight = (props: HighlightProps) => {
return props.hidden ? !highlightInSection : notHighlighted;
};
- return setHovered(true)}
- onMouseLeave={() => setHovered(false)}
- hidden={searchTerm ? !searchMatch() : hidden()}>
- {settingName &&
- {
- navigate(linkToSetting(settingName, props.pathPrefix));
- }} />}
- {props.children}
-
;
+ const highlightKey = highlightMatch
+ ? `${location.pathname}:${location.search}:${location.hash}`
+ : "no-highlight";
+
+ return ;
};
export const linkToSetting =
diff --git a/frontend/settings/pin_bindings/__tests__/model_test.tsx b/frontend/settings/pin_bindings/__tests__/model_test.tsx
index d2d7116902..201e62a815 100644
--- a/frontend/settings/pin_bindings/__tests__/model_test.tsx
+++ b/frontend/settings/pin_bindings/__tests__/model_test.tsx
@@ -63,15 +63,15 @@ describe(" ", () => {
material: { color: { set: mockSetColor } },
setOptions: jest.fn(),
}
- }) as never);
+ }));
useFrameSpy = jest.spyOn(threeFiber, "useFrame")
.mockImplementation(((callback, _renderPriority) => {
callback({
clock: { getElapsedTime: jest.fn(() => mockElapsedTime) }
- } as never, 0, undefined as never);
+ } as never, 0, undefined);
// eslint-disable-next-line no-null/no-null
return null;
- }) as typeof threeFiber.useFrame);
+ }));
execSequenceSpy = jest.spyOn(deviceActions, "execSequence")
.mockImplementation(jest.fn());
});
diff --git a/frontend/settings/transfer_ownership/__tests__/change_ownership_form_test.tsx b/frontend/settings/transfer_ownership/__tests__/change_ownership_form_test.tsx
index aa6c7f71c2..412bf655a1 100644
--- a/frontend/settings/transfer_ownership/__tests__/change_ownership_form_test.tsx
+++ b/frontend/settings/transfer_ownership/__tests__/change_ownership_form_test.tsx
@@ -63,7 +63,7 @@ describe(" ", () => {
// eslint-disable-next-line comma-spacing
.mockImplementation((initialValue: T) => {
const ref = originalUseRef(initialValue);
- refs.push(ref as React.RefObject);
+ refs.push(ref);
return ref;
});
try {
diff --git a/frontend/settings/transfer_ownership/__tests__/create_transfer_cert_test.ts b/frontend/settings/transfer_ownership/__tests__/create_transfer_cert_test.ts
index 3848545055..9449205460 100644
--- a/frontend/settings/transfer_ownership/__tests__/create_transfer_cert_test.ts
+++ b/frontend/settings/transfer_ownership/__tests__/create_transfer_cert_test.ts
@@ -19,7 +19,7 @@ beforeEach(() => {
getDeviceSpy = jest.spyOn(deviceModule, "getDevice")
.mockImplementation(() => mockDevice as never);
axiosPostSpy = jest.spyOn(axios, "post")
- .mockImplementation(() => Promise.resolve({ data: "FAKE CERT" }) as never);
+ .mockImplementation(() => Promise.resolve({ data: "FAKE CERT" }));
});
afterEach(() => {
diff --git a/frontend/settings/transfer_ownership/__tests__/transfer_ownership_test.ts b/frontend/settings/transfer_ownership/__tests__/transfer_ownership_test.ts
index 9ad5b181c9..bc1a76287d 100644
--- a/frontend/settings/transfer_ownership/__tests__/transfer_ownership_test.ts
+++ b/frontend/settings/transfer_ownership/__tests__/transfer_ownership_test.ts
@@ -20,7 +20,7 @@ beforeEach(() => {
jest.spyOn(deviceModule, "getDevice")
.mockImplementation(() => mockDevice as never);
jest.spyOn(axios, "post")
- .mockImplementation(() => mockPost as never);
+ .mockImplementation(() => mockPost);
mockPost = Promise.resolve(({ data: "FAKE CERT" }));
});
diff --git a/frontend/sync/__tests__/actions_test.ts b/frontend/sync/__tests__/actions_test.ts
index e2d6776ce2..9d79d9253a 100644
--- a/frontend/sync/__tests__/actions_test.ts
+++ b/frontend/sync/__tests__/actions_test.ts
@@ -7,7 +7,7 @@ describe("syncFail", () => {
const e = new Error("Whatever");
console.error = jest.fn();
jest.spyOn(Session, "clear")
- .mockImplementation((() => undefined as never) as typeof Session.clear);
+ .mockImplementation((() => undefined) as typeof Session.clear);
expect(() => syncFail(e)).toThrow(e);
expect(console.error).toHaveBeenCalledWith("DATA SYNC ERROR!");
expect(Session.clear).toHaveBeenCalled();
diff --git a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
index 8b1ad11bb3..9ed0ed5a89 100644
--- a/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
+++ b/frontend/three_d_garden/__tests__/camera_selection_ui_test.tsx
@@ -12,7 +12,7 @@ import {
unmountRenderer,
} from "../../__test_support__/test_renderer";
import * as threeFiber from "@react-three/fiber";
-import { Object3D, PerspectiveCamera } from "three";
+import { PerspectiveCamera } from "three";
describe(" ", () => {
let setWebAppConfigValueSpy: jest.SpyInstance;
@@ -87,7 +87,7 @@ describe(" ", () => {
node.props.ref({
userData: node.props.userData,
uuid: `mesh-${index}`,
- } as Object3D);
+ });
});
};
diff --git a/frontend/three_d_garden/__tests__/camera_test.ts b/frontend/three_d_garden/__tests__/camera_test.ts
index 399192f681..3e509a73b6 100644
--- a/frontend/three_d_garden/__tests__/camera_test.ts
+++ b/frontend/three_d_garden/__tests__/camera_test.ts
@@ -14,7 +14,7 @@ let isDesktopSpy: jest.SpyInstance;
beforeEach(() => {
get3dCameraSpy = jest.spyOn(devSupport.DevSettings, "get3dCamera")
.mockImplementation((() =>
- mockDev || "") as typeof devSupport.DevSettings.get3dCamera);
+ mockDev || ""));
isDesktopSpy = jest.spyOn(screenSize, "isDesktop")
.mockImplementation(() => mockIsDesktop);
});
diff --git a/frontend/three_d_garden/__tests__/components_test.tsx b/frontend/three_d_garden/__tests__/components_test.tsx
index c9672fdf25..d3d54c9098 100644
--- a/frontend/three_d_garden/__tests__/components_test.tsx
+++ b/frontend/three_d_garden/__tests__/components_test.tsx
@@ -114,7 +114,7 @@ describe(" ", () => {
describe(" ", () => {
const fakeProps = (): ThreeElements["instancedMesh"] => ({
- args: [undefined as never, undefined as never, 1],
+ args: [undefined, undefined, 1],
name: "instancedMesh",
});
diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx
index 366979870f..36974f2ccb 100644
--- a/frontend/three_d_garden/__tests__/garden_model_test.tsx
+++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx
@@ -39,12 +39,12 @@ describe(" ", () => {
.mockImplementation((initialState?: S | (() => S)) => {
useStateCalls += 1;
if (useStateCalls == 2) {
- return [{} as S, jest.fn()];
+ return [{}, jest.fn()];
}
const value = typeof initialState == "function"
? (initialState as () => S)()
: initialState;
- return [value as S, jest.fn()];
+ return [value, jest.fn()];
});
isDesktopSpy = jest.spyOn(screenSize, "isDesktop")
.mockImplementation(() => mockIsDesktop);
@@ -190,15 +190,15 @@ describe(" ", () => {
.mockImplementation((initialState?: S | (() => S)) => {
useStateCalls += 1;
if (useStateCalls == 1) {
- return [0 as S, jest.fn()];
+ return [0, jest.fn()];
}
if (useStateCalls == 2) {
- return [{} as S, jest.fn()];
+ return [{}, jest.fn()];
}
const value = typeof initialState == "function"
? (initialState as () => S)()
: initialState;
- return [value as S, jest.fn()];
+ return [value, jest.fn()];
});
const p = fakeProps();
const plant = fakePlant();
diff --git a/frontend/three_d_garden/__tests__/group_order_visual_test.tsx b/frontend/three_d_garden/__tests__/group_order_visual_test.tsx
index 0a077e43a1..2df0883386 100644
--- a/frontend/three_d_garden/__tests__/group_order_visual_test.tsx
+++ b/frontend/three_d_garden/__tests__/group_order_visual_test.tsx
@@ -28,7 +28,7 @@ beforeEach(() => {
jest.spyOn(criteriaApply, "pointsSelectedByGroup")
.mockImplementation(() => mockGroupPoints);
sortGroupBySpy = jest.spyOn(pointGroupSort, "sortGroupBy")
- .mockImplementation(((_method, pts) => pts) as typeof pointGroupSort.sortGroupBy);
+ .mockImplementation(((_method, pts) => pts));
});
diff --git a/frontend/three_d_garden/bed/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
index 98a709e9d3..119f5523c6 100644
--- a/frontend/three_d_garden/bed/__tests__/bed_test.tsx
+++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx
@@ -78,7 +78,6 @@ import { mockDispatch } from "../../../__test_support__/fake_dispatch";
import { fakePoint } from "../../../__test_support__/fake_state/resources";
import { SpecialStatus } from "farmbot";
import { BufferGeometry, Float32BufferAttribute } from "three";
-import { ActivePositionRef } from "../objects/pointer_objects";
import { Mode } from "../../../farm_designer/map/interfaces";
import * as mapUtil from "../../../farm_designer/map/util";
import * as plantActions from "../../../farm_designer/map/layers/plants/plant_actions";
@@ -146,7 +145,7 @@ describe(" ", () => {
sensors: [],
sensorReadings: [],
showMoistureReadings: true,
- activePositionRef: { current: { x: 0, y: 0 } } as ActivePositionRef,
+ activePositionRef: { current: { x: 0, y: 0 } },
});
it("renders bed", () => {
diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx
index 768e912201..5a6b47bd3b 100644
--- a/frontend/three_d_garden/bed/bed.tsx
+++ b/frontend/three_d_garden/bed/bed.tsx
@@ -272,18 +272,6 @@ export const Bed = (props: BedProps) => {
props.showMoistureMap,
]);
- const getSurfaceMaterial = () => {
- switch (props.config.surfaceDebug) {
- case SurfaceDebugOption.normals:
- return MeshNormalMaterial;
- case SurfaceDebugOption.height:
- return SurfaceHeightMaterial;
- default:
- return MeshPhongMaterial;
- }
- };
-
- const SurfaceMaterial = getSurfaceMaterial();
const surfaceTexture = soilTexture;
const mirroredAxesCount =
Number(props.config.mirrorX) + Number(props.config.mirrorY);
@@ -473,13 +461,27 @@ export const Bed = (props: BedProps) => {
-
- {surfaceTexture}
-
+ <>
+ {props.config.surfaceDebug == SurfaceDebugOption.normals &&
+
+ {surfaceTexture}
+ }
+ {props.config.surfaceDebug == SurfaceDebugOption.height &&
+
+ {surfaceTexture}
+ }
+ {![SurfaceDebugOption.normals, SurfaceDebugOption.height]
+ .includes(props.config.surfaceDebug) &&
+
+ {surfaceTexture}
+ }
+ >
diff --git a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
index 2ab254637d..d9364d49c9 100644
--- a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
+++ b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx
@@ -2,7 +2,6 @@ let mockIsMobile = false;
import React from "react";
import { useTexture } from "@react-three/drei";
import {
- ActivePositionRef,
BillboardRef,
ImageRef,
PointerObjects, PointerObjectsProps,
@@ -61,7 +60,7 @@ describe(" ", () => {
imageRef: { current: { scale: new Vector3(0, 0, 0) } } as ImageRef,
xCrosshairRef: { current: { position: new Vector3(0, 0, 0) } } as XCrosshairRef,
yCrosshairRef: { current: { position: new Vector3(0, 0, 0) } } as YCrosshairRef,
- activePositionRef: { current: { x: 0, y: 0 } } as ActivePositionRef,
+ activePositionRef: { current: { x: 0, y: 0 } },
});
it("renders", () => {
@@ -152,7 +151,7 @@ describe("soilPointerMove()", () => {
imageRef: { current: { scale: { set: jest.fn() } } } as unknown as ImageRef,
xCrosshairRef: { current: { position: { set: jest.fn() } } } as unknown as XCrosshairRef,
yCrosshairRef: { current: { position: { set: jest.fn() } } } as unknown as YCrosshairRef,
- activePositionRef: { current: { x: 0, y: 0 } } as ActivePositionRef,
+ activePositionRef: { current: { x: 0, y: 0 } },
});
it("updates plant position", () => {
diff --git a/frontend/three_d_garden/bed/objects/pointer_objects.tsx b/frontend/three_d_garden/bed/objects/pointer_objects.tsx
index ca8a53c77b..2993f9048f 100644
--- a/frontend/three_d_garden/bed/objects/pointer_objects.tsx
+++ b/frontend/three_d_garden/bed/objects/pointer_objects.tsx
@@ -89,9 +89,9 @@ export const PointerObjects = (props: PointerObjectsProps) => {
const gridPreview = mapPoints
.filter(p => p.specialStatus == SpecialStatus.DIRTY && p.body.meta.gridId)
.length > 0;
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
const boundsCenter = React.useMemo(getBoundsCenter(config), []);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
const halfSize = React.useMemo(getHalfSize(config), []);
return HOVER_OBJECT_MODES.includes(getMode()) &&
!isMobile() &&
diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx
index cc5e2fb3bd..527fee6841 100644
--- a/frontend/three_d_garden/bot/bot.tsx
+++ b/frontend/three_d_garden/bot/bot.tsx
@@ -16,9 +16,9 @@ import { ASSETS, LIB_DIR, PartName } from "../constants";
import { SVGLoader } from "three/examples/jsm/Addons.js";
import { range } from "lodash";
import {
- CrossSlide, CrossSlideFull,
+ CrossSlideFull, CrossSlideModel,
GantryWheelPlate, GantryWheelPlateFull,
- VacuumPumpCover, VacuumPumpCoverFull,
+ VacuumPumpCoverFull, VacuumPumpCoverModel,
} from "./parts";
import { PowerSupply } from "./power_supply";
import { Group, Mesh, MeshPhongMaterial } from "../components";
@@ -125,7 +125,6 @@ export const Bot = (props: FarmbotModelProps) => {
const leftBracket = useGLTF(ASSETS.models.leftBracket, LIB_DIR) as unknown as LeftBracket;
const rightBracket = useGLTF(ASSETS.models.rightBracket, LIB_DIR) as unknown as RightBracket;
const crossSlide = useGLTF(ASSETS.models.crossSlide, LIB_DIR) as unknown as CrossSlideFull;
- const CrossSlideComponent = CrossSlide(crossSlide);
const beltClip = useGLTF(ASSETS.models.beltClip, LIB_DIR) as unknown as BeltClip;
const zStop = useGLTF(ASSETS.models.zStop, LIB_DIR) as unknown as ZStop;
const utm = useGLTF(ASSETS.models.utm, LIB_DIR) as unknown as UTM;
@@ -137,7 +136,6 @@ export const Bot = (props: FarmbotModelProps) => {
ASSETS.models.zAxisMotorMount, LIB_DIR) as unknown as ZAxisMotorMount;
const vacuumPumpCover = useGLTF(
ASSETS.models.vacuumPumpCover, LIB_DIR) as unknown as VacuumPumpCoverFull;
- const VacuumPumpCoverComponent = VacuumPumpCover(vacuumPumpCover);
const cameraMountHalf = useGLTF(
ASSETS.models.cameraMountHalf, LIB_DIR) as unknown as CameraMountHalf;
const xAxisCCMount = useGLTF(ASSETS.models.xAxisCCMount, LIB_DIR) as unknown as XAxisCCMount;
@@ -184,10 +182,14 @@ export const Bot = (props: FarmbotModelProps) => {
});
}
});
- const aluminumTexture = useTexture(ASSETS.textures.aluminum + "?=bot");
- aluminumTexture.wrapS = RepeatWrapping;
- aluminumTexture.wrapT = RepeatWrapping;
- aluminumTexture.repeat.set(0.01, 0.0003);
+ const aluminumTextureBase = useTexture(ASSETS.textures.aluminum + "?=bot");
+ const aluminumTexture = React.useMemo(() => {
+ const texture = aluminumTextureBase.clone();
+ texture.wrapS = RepeatWrapping;
+ texture.wrapT = RepeatWrapping;
+ texture.repeat.set(0.01, 0.0003);
+ return texture;
+ }, [aluminumTextureBase]);
const yBeltPath = () => {
const radius = 12;
@@ -397,7 +399,9 @@ export const Bot = (props: FarmbotModelProps) => {
- {
opacity={0.75}
/>
-
diff --git a/frontend/three_d_garden/bot/components/__tests__/gantry_beam_test.tsx b/frontend/three_d_garden/bot/components/__tests__/gantry_beam_test.tsx
index 8cf29861c8..ff124d96be 100644
--- a/frontend/three_d_garden/bot/components/__tests__/gantry_beam_test.tsx
+++ b/frontend/three_d_garden/bot/components/__tests__/gantry_beam_test.tsx
@@ -31,7 +31,7 @@ let reactUseRefSpy: jest.SpyInstance;
describe(" ", () => {
beforeEach(() => {
reactUseRefSpy = jest.spyOn(React, "useRef")
- .mockImplementation(() => mockRef as never);
+ .mockImplementation(() => mockRef);
});
afterEach(() => {
diff --git a/frontend/three_d_garden/bot/components/__tests__/suction_animation_test.tsx b/frontend/three_d_garden/bot/components/__tests__/suction_animation_test.tsx
index 4801144ae6..f1a822c3e1 100644
--- a/frontend/three_d_garden/bot/components/__tests__/suction_animation_test.tsx
+++ b/frontend/three_d_garden/bot/components/__tests__/suction_animation_test.tsx
@@ -6,25 +6,20 @@ import * as threeFiber from "@react-three/fiber";
import { render } from "@testing-library/react";
import { SuctionAnimation, SuctionAnimationProps } from "../suction_animation";
-type MockRef = {
- position: typeof mockPosition;
- scale: { set: typeof mockScaleSet };
-};
-
describe(" ", () => {
beforeEach(() => {
jest.spyOn(threeFiber, "useFrame")
.mockImplementation(((callback, _renderPriority) => {
- callback({} as never, 0, undefined as never);
+ callback({} as never, 0, undefined);
// eslint-disable-next-line no-null/no-null
return null;
- }) as typeof threeFiber.useFrame);
+ }));
jest.spyOn(React, "useRef").mockReturnValue({
current: {
position: mockPosition,
scale: { set: mockScaleSet },
},
- } as React.RefObject);
+ });
mockPosition.x = 0;
mockPosition.y = 0;
mockPosition.z = 0;
diff --git a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx
index 16fa8fa658..5e8520f8b1 100644
--- a/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx
+++ b/frontend/three_d_garden/bot/components/__tests__/tools_test.tsx
@@ -26,7 +26,7 @@ const mockRotaryRef = () => {
traverse: (cb: (m: {}) => void) => cb(mesh),
rotation: mockRotation,
},
- } as never;
+ };
}
return originalUseRef(initialValue);
});
@@ -57,15 +57,15 @@ describe(" ", () => {
getModeSpy = jest.spyOn(mapUtil, "getMode").mockReturnValue(Mode.none);
jest.spyOn(threeFiber, "useFrame")
.mockImplementation(((callback, _renderPriority) => {
- callback({} as never, 0, undefined as never);
+ callback({} as never, 0, undefined);
// eslint-disable-next-line no-null/no-null
return null;
- }) as typeof threeFiber.useFrame);
+ }));
suctionAnimationSpy = jest.spyOn(suctionAnimationModule, "SuctionAnimation")
- .mockImplementation((() => <>>) as never);
+ .mockImplementation((() => <>>));
wateringAnimationsSpy = jest.spyOn(
wateringAnimationsModule, "WateringAnimations")
- .mockImplementation((() => <>>) as never);
+ .mockImplementation((() => <>>));
});
afterEach(() => {
diff --git a/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx
index 2f2699e243..17cadfb27c 100644
--- a/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx
+++ b/frontend/three_d_garden/bot/components/__tests__/water_stream_test.tsx
@@ -45,7 +45,7 @@ describe(" ", () => {
describe("useWaterFlowTexture", () => {
beforeEach(() => {
- frameCallback = jest.fn() as unknown as
+ frameCallback = jest.fn() as
(state: unknown, delta: number) => void;
loadTextureSpy = jest.spyOn(TextureLoader.prototype, "load")
.mockImplementation(() => new Texture());
diff --git a/frontend/three_d_garden/bot/components/tools.tsx b/frontend/three_d_garden/bot/components/tools.tsx
index 53e816bd82..e0e394a923 100644
--- a/frontend/three_d_garden/bot/components/tools.tsx
+++ b/frontend/three_d_garden/bot/components/tools.tsx
@@ -14,9 +14,9 @@ import {
ASSETS, HOVER_OBJECT_MODES, LIB_DIR, PartName, SeedTroughAssemblyMaterial,
} from "../../constants";
import {
- SoilSensor, SoilSensorFull,
- SeedTroughAssembly, SeedTroughAssemblyFull,
- SeedTroughHolder, SeedTroughHolderFull,
+ SoilSensorFull, SoilSensorModel,
+ SeedTroughAssemblyFull, SeedTroughAssemblyModel,
+ SeedTroughHolderFull, SeedTroughHolderModel,
} from "../parts";
import { Group, Mesh, MeshPhongMaterial } from "../../components";
import { distinguishableBlack, utmHeight } from "../bot";
@@ -118,289 +118,14 @@ export const Tools = (props: ToolsProps) => {
const {
bedLengthOuter, bedWidthOuter, bedWallThickness,
} = props.config;
- const get3DPosition = get3DPositionFunc(props.config);
- const get3DPositionNoMirror = get3DPositionNoMirrorFunc(props.config);
const mirroredBotX = props.config.mirrorX
? props.config.botSizeX - props.configPosition.x
: props.configPosition.x;
const mountedToolName = isUndefined(props.toolSlots)
? props.config.tool
: reduceToolName(props.mountedToolName);
- const zZero = zZeroFunc(props.config);
- const zDir = zDirFunc(props.config);
const toolbay3 = useGLTF(ASSETS.models.toolbay3, LIB_DIR) as unknown as Toolbay3;
- const toolbay1 = useGLTF(ASSETS.models.toolbay1, LIB_DIR) as unknown as Toolbay1;
- const rotaryToolBase =
- useGLTF(ASSETS.models.rotaryToolBase, LIB_DIR) as unknown as Model;
- const rotaryToolImplement =
- useGLTF(ASSETS.models.rotaryToolImplement, LIB_DIR) as unknown as Model;
- const seedBin = useGLTF(ASSETS.models.seedBin, LIB_DIR) as unknown as SeedBin;
- const seedTray = useGLTF(ASSETS.models.seedTray, LIB_DIR) as unknown as SeedTray;
- const seedTrough = useGLTF(ASSETS.models.seedTrough, LIB_DIR) as unknown as SeedTrough;
- const seedTroughHolder = useGLTF(
- ASSETS.models.seedTroughHolder, LIB_DIR) as unknown as SeedTroughHolderFull;
- const SeedTroughHolderComponent = SeedTroughHolder(seedTroughHolder);
- const seedTroughAssembly = useGLTF(
- ASSETS.models.seedTroughAssembly, LIB_DIR) as unknown as SeedTroughAssemblyFull;
- const SeedTroughAssemblyComponent = SeedTroughAssembly(seedTroughAssembly);
- const soilSensor = useGLTF(ASSETS.models.soilSensor, LIB_DIR) as unknown as SoilSensorFull;
- const seeder = useGLTF(ASSETS.models.seeder, LIB_DIR) as unknown as Seeder;
- const weeder = useGLTF(ASSETS.models.weeder, LIB_DIR) as unknown as Weeder;
- const SoilSensorComponent = SoilSensor(soilSensor);
- const wateringNozzle = useGLTF(
- ASSETS.models.wateringNozzle, LIB_DIR) as unknown as WateringNozzle;
-
- const displayedPulloutDirection = (
- toolPulloutDirection: ToolPulloutDirection,
- ): ToolPulloutDirection => {
- switch (toolPulloutDirection) {
- case ToolPulloutDirection.POSITIVE_X:
- return props.config.mirrorX
- ? ToolPulloutDirection.NEGATIVE_X
- : ToolPulloutDirection.POSITIVE_X;
- case ToolPulloutDirection.NEGATIVE_X:
- return props.config.mirrorX
- ? ToolPulloutDirection.POSITIVE_X
- : ToolPulloutDirection.NEGATIVE_X;
- case ToolPulloutDirection.POSITIVE_Y:
- return props.config.mirrorY
- ? ToolPulloutDirection.NEGATIVE_Y
- : ToolPulloutDirection.POSITIVE_Y;
- case ToolPulloutDirection.NEGATIVE_Y:
- return props.config.mirrorY
- ? ToolPulloutDirection.POSITIVE_Y
- : ToolPulloutDirection.NEGATIVE_Y;
- default:
- return toolPulloutDirection;
- }
- };
-
- const rotationFactor = (toolPulloutDirection: ToolPulloutDirection) => {
- switch (toolPulloutDirection) {
- case ToolPulloutDirection.POSITIVE_X: return 3;
- case ToolPulloutDirection.POSITIVE_Y: return 4;
- case ToolPulloutDirection.NEGATIVE_X: return 1;
- case ToolPulloutDirection.NEGATIVE_Y: return 2;
- }
- };
-
- interface ToolbaySlotProps {
- position: Record;
- children?: React.ReactNode;
- toolPulloutDirection: ToolPulloutDirection;
- mounted: boolean;
- id: number | undefined;
- inToolbay: boolean;
- }
-
- const ToolbaySlot = (slotProps: ToolbaySlotProps) => {
- const { position, children, toolPulloutDirection, mounted } = slotProps;
- const rotationMultiplier =
- rotationFactor(displayedPulloutDirection(toolPulloutDirection));
- const navigate = useNavigate();
- return {
- if (slotProps.id && !isUndefined(props.dispatch) &&
- !HOVER_OBJECT_MODES.includes(getMode())) {
- props.dispatch(setPanelOpen(true));
- navigate(Path.toolSlots(slotProps.id));
- }
- }}>
- {rotationMultiplier &&
-
-
-
-
-
-
-
- }
-
- {children}
-
- ;
- };
-
- interface ToolProps extends ThreeDTool {
- inToolbay: boolean;
- }
-
- // eslint-disable-next-line complexity
- const Tool = (toolProps: ToolProps) => {
- const { toolPulloutDirection, inToolbay, id } = toolProps;
- const mounted = inToolbay && toolProps.toolName == mountedToolName;
- const mirroredPosition = get3DPosition({ x: toolProps.x, y: toolProps.y });
- const noMirrorPosition = get3DPositionNoMirror({
- x: toolProps.x,
- y: toolProps.y,
- });
- const position = {
- x: inToolbay ? mirroredPosition.x : noMirrorPosition.x,
- y: inToolbay && !toolProps.gantryMounted
- ? mirroredPosition.y
- : noMirrorPosition.y,
- z: zZero - zDir * toolProps.z + (inToolbay ? 0 : (utmHeight / 2 - 15)),
- };
- const common: ToolbaySlotProps = {
- mounted, position, toolPulloutDirection, id, inToolbay,
- };
-
- // eslint-disable-next-line no-null/no-null
- const rotaryToolImplementRef = React.useRef(null);
-
- useFrame(() => {
- if (rotaryToolImplementRef.current && !inToolbay && props.config.rotary) {
- const time = Date.now();
- const speed = props.config.rotary > 0 ? 0.01 : -0.01;
- rotaryToolImplementRef.current.rotation.z = time * speed;
- }
- });
- const X = 5.5;
- switch (toolProps.toolName) {
- case ToolName.rotaryTool:
- return
-
-
-
-
-
-
- ;
- case ToolName.wateringNozzle:
- return
-
- ;
- case ToolName.seedBin:
- return
-
-
-
- ;
- case ToolName.seedTray:
- return
-
-
-
- ;
- case ToolName.soilSensor:
- return
-
- ;
- case ToolName.seeder:
- return
-
- {!inToolbay && props.config.vacuum &&
-
- {[-50, -80, -95, -100].map(z =>
- )}
- }
- ;
- case ToolName.weeder:
- return
-
- ;
- case ToolName.seedTrough:
- return
- {toolProps.firstTrough
- ?
-
-
-
- : }
- ;
- default:
- return ;
- }
- };
const tools = isUndefined(props.toolSlots)
? PROMO_TOOLS(props.config, props.configPosition)
@@ -408,6 +133,8 @@ export const Tools = (props: ToolsProps) => {
return
{
}
{tools.map((tool, i) =>
{
}, [props.opacity]);
return {props.children} ;
};
+
+const displayedPulloutDirection = (
+ toolPulloutDirection: ToolPulloutDirection,
+ mirrorX: boolean,
+ mirrorY: boolean,
+): ToolPulloutDirection => {
+ switch (toolPulloutDirection) {
+ case ToolPulloutDirection.POSITIVE_X:
+ return mirrorX
+ ? ToolPulloutDirection.NEGATIVE_X
+ : ToolPulloutDirection.POSITIVE_X;
+ case ToolPulloutDirection.NEGATIVE_X:
+ return mirrorX
+ ? ToolPulloutDirection.POSITIVE_X
+ : ToolPulloutDirection.NEGATIVE_X;
+ case ToolPulloutDirection.POSITIVE_Y:
+ return mirrorY
+ ? ToolPulloutDirection.NEGATIVE_Y
+ : ToolPulloutDirection.POSITIVE_Y;
+ case ToolPulloutDirection.NEGATIVE_Y:
+ return mirrorY
+ ? ToolPulloutDirection.POSITIVE_Y
+ : ToolPulloutDirection.NEGATIVE_Y;
+ default:
+ return toolPulloutDirection;
+ }
+};
+
+const rotationFactor = (toolPulloutDirection: ToolPulloutDirection) => {
+ switch (toolPulloutDirection) {
+ case ToolPulloutDirection.POSITIVE_X: return 3;
+ case ToolPulloutDirection.POSITIVE_Y: return 4;
+ case ToolPulloutDirection.NEGATIVE_X: return 1;
+ case ToolPulloutDirection.NEGATIVE_Y: return 2;
+ }
+};
+
+interface ToolbaySlotProps {
+ position: Record;
+ children?: React.ReactNode;
+ toolPulloutDirection: ToolPulloutDirection;
+ mounted: boolean;
+ id: number | undefined;
+ inToolbay: boolean;
+ dispatch?: Function;
+ config: Config;
+}
+
+const ToolbaySlot = (props: ToolbaySlotProps) => {
+ const { position, children, toolPulloutDirection, mounted } = props;
+ const rotationMultiplier =
+ rotationFactor(displayedPulloutDirection(
+ toolPulloutDirection,
+ props.config.mirrorX,
+ props.config.mirrorY));
+ const navigate = useNavigate();
+ const toolbay1 = useGLTF(ASSETS.models.toolbay1, LIB_DIR) as unknown as Toolbay1;
+ return {
+ if (props.id && !isUndefined(props.dispatch) &&
+ !HOVER_OBJECT_MODES.includes(getMode())) {
+ props.dispatch(setPanelOpen(true));
+ navigate(Path.toolSlots(props.id));
+ }
+ }}>
+ {rotationMultiplier &&
+
+
+
+
+
+
+
+ }
+
+ {children}
+
+ ;
+};
+
+interface ToolProps extends ThreeDTool {
+ inToolbay: boolean;
+ mountedToolName: string | undefined;
+ config: Config;
+ dispatch?: Function;
+}
+
+// eslint-disable-next-line complexity
+const Tool = (props: ToolProps) => {
+ const {
+ toolPulloutDirection, inToolbay, id, mountedToolName, config, dispatch,
+ } = props;
+ const mounted = inToolbay && props.toolName == mountedToolName;
+ const get3DPosition = get3DPositionFunc(config);
+ const get3DPositionNoMirror = get3DPositionNoMirrorFunc(config);
+ const mirroredPosition = get3DPosition({ x: props.x, y: props.y });
+ const noMirrorPosition = get3DPositionNoMirror({
+ x: props.x,
+ y: props.y,
+ });
+ const zZero = zZeroFunc(props.config);
+ const zDir = zDirFunc(props.config);
+ const position = {
+ x: inToolbay ? mirroredPosition.x : noMirrorPosition.x,
+ y: inToolbay && !props.gantryMounted
+ ? mirroredPosition.y
+ : noMirrorPosition.y,
+ z: zZero - zDir * props.z + (inToolbay ? 0 : (utmHeight / 2 - 15)),
+ };
+ const common: ToolbaySlotProps = {
+ mounted, position, toolPulloutDirection, id, inToolbay, config, dispatch,
+ };
+
+ const rotaryToolBase =
+ useGLTF(ASSETS.models.rotaryToolBase, LIB_DIR) as unknown as Model;
+ const rotaryToolImplement =
+ useGLTF(ASSETS.models.rotaryToolImplement, LIB_DIR) as unknown as Model;
+ const seedBin = useGLTF(ASSETS.models.seedBin, LIB_DIR) as unknown as SeedBin;
+ const seedTray = useGLTF(ASSETS.models.seedTray, LIB_DIR) as unknown as SeedTray;
+ const seedTrough = useGLTF(ASSETS.models.seedTrough, LIB_DIR) as unknown as SeedTrough;
+ const seedTroughHolder = useGLTF(
+ ASSETS.models.seedTroughHolder, LIB_DIR) as unknown as SeedTroughHolderFull;
+ const seedTroughAssembly = useGLTF(
+ ASSETS.models.seedTroughAssembly, LIB_DIR) as unknown as SeedTroughAssemblyFull;
+ const soilSensor = useGLTF(ASSETS.models.soilSensor, LIB_DIR) as unknown as SoilSensorFull;
+ const seeder = useGLTF(ASSETS.models.seeder, LIB_DIR) as unknown as Seeder;
+ const weeder = useGLTF(ASSETS.models.weeder, LIB_DIR) as unknown as Weeder;
+ const wateringNozzle = useGLTF(
+ ASSETS.models.wateringNozzle, LIB_DIR) as unknown as WateringNozzle;
+
+ // eslint-disable-next-line no-null/no-null
+ const rotaryToolImplementRef = React.useRef(null);
+
+ useFrame(() => {
+ if (rotaryToolImplementRef.current && !inToolbay && props.config.rotary) {
+ const time = Date.now();
+ const speed = props.config.rotary > 0 ? 0.01 : -0.01;
+ rotaryToolImplementRef.current.rotation.z = time * speed;
+ }
+ });
+ const X = 5.5;
+ switch (props.toolName) {
+ case ToolName.rotaryTool:
+ return
+
+
+
+
+
+
+ ;
+ case ToolName.wateringNozzle:
+ return
+
+ ;
+ case ToolName.seedBin:
+ return
+
+
+
+ ;
+ case ToolName.seedTray:
+ return
+
+
+
+ ;
+ case ToolName.soilSensor:
+ return
+
+ ;
+ case ToolName.seeder:
+ return
+
+ {!inToolbay && props.config.vacuum &&
+
+ {[-50, -80, -95, -100].map(z =>
+ )}
+ }
+ ;
+ case ToolName.weeder:
+ return
+
+ ;
+ case ToolName.seedTrough:
+ return
+ {props.firstTrough
+ ?
+
+
+
+ : }
+ ;
+ default:
+ return ;
+ }
+};
diff --git a/frontend/three_d_garden/bot/parts/cross_slide.tsx b/frontend/three_d_garden/bot/parts/cross_slide.tsx
index 312ffa9817..c648a3a657 100644
--- a/frontend/three_d_garden/bot/parts/cross_slide.tsx
+++ b/frontend/three_d_garden/bot/parts/cross_slide.tsx
@@ -146,401 +146,409 @@ export type CrossSlideFull = GLTF & {
};
}
+interface CrossSlideProps extends Omit {
+ model: CrossSlideFull;
+}
+
+export const CrossSlideModel = (props: CrossSlideProps) => {
+ const { model, ...groupProps } = props;
+ const { nodes, materials } = model;
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+};
+
export const CrossSlide = (model: CrossSlideFull) =>
- (props: Omit) => {
- const { nodes, materials } = model;
- return
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ;
- };
+ (props: Omit) =>
+ ;
diff --git a/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx b/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx
index 885373ef02..41ae6d69f2 100644
--- a/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx
+++ b/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx
@@ -19,22 +19,30 @@ export type SeedTroughAssemblyFull = GLTF & {
};
}
-export const SeedTroughAssembly = (model: SeedTroughAssemblyFull) =>
- (props: Omit) => {
- const { nodes, materials } = model;
- // eslint-disable-next-line no-null/no-null
- return
-
-
-
-
+interface SeedTroughAssemblyProps extends Omit {
+ model: SeedTroughAssemblyFull;
+}
+
+export const SeedTroughAssemblyModel = (props: SeedTroughAssemblyProps) => {
+ const { model, ...groupProps } = props;
+ const { nodes, materials } = model;
+ // eslint-disable-next-line no-null/no-null
+ return
+
- ;
- };
+ geometry={nodes.mesh0_mesh.geometry}
+ material={materials[SeedTroughAssemblyMaterial.one]} />
+
+
+
+ ;
+};
+
+export const SeedTroughAssembly = (model: SeedTroughAssemblyFull) =>
+ (props: Omit) =>
+ ;
diff --git a/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx b/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx
index bd6b6e74d3..4706b9c501 100644
--- a/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx
+++ b/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx
@@ -17,19 +17,27 @@ export type SeedTroughHolderFull = GLTF & {
};
}
+interface SeedTroughHolderProps extends Omit {
+ model: SeedTroughHolderFull;
+}
+
+export const SeedTroughHolderModel = (props: SeedTroughHolderProps) => {
+ const { model, ...groupProps } = props;
+ const { nodes, materials } = model;
+ // eslint-disable-next-line no-null/no-null
+ return
+
+
+ ;
+};
+
export const SeedTroughHolder = (model: SeedTroughHolderFull) =>
- (props: Omit) => {
- const { nodes, materials } = model;
- // eslint-disable-next-line no-null/no-null
- return
-
-
- ;
- };
+ (props: Omit) =>
+ ;
diff --git a/frontend/three_d_garden/bot/parts/soil_sensor.tsx b/frontend/three_d_garden/bot/parts/soil_sensor.tsx
index a3bb170c63..a44836cedd 100644
--- a/frontend/three_d_garden/bot/parts/soil_sensor.tsx
+++ b/frontend/three_d_garden/bot/parts/soil_sensor.tsx
@@ -61,146 +61,154 @@ export type SoilSensorFull = GLTF & {
};
}
+interface SoilSensorProps extends Omit {
+ model: SoilSensorFull;
+}
+
+export const SoilSensorModel = (props: SoilSensorProps) => {
+ const { model, ...groupProps } = props;
+ const { nodes, materials } = model;
+ // eslint-disable-next-line no-null/no-null
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+};
+
export const SoilSensor = (model: SoilSensorFull) =>
- (props: Omit) => {
- const { nodes, materials } = model;
- // eslint-disable-next-line no-null/no-null
- return
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ;
- };
+ (props: Omit) =>
+ ;
diff --git a/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx b/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx
index 683321fc9e..50b224a6ee 100644
--- a/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx
+++ b/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx
@@ -16,18 +16,26 @@ export type VacuumPumpCoverFull = GLTF & {
};
}
+interface VacuumPumpCoverProps extends Omit {
+ model: VacuumPumpCoverFull;
+}
+
+export const VacuumPumpCoverModel = (props: VacuumPumpCoverProps) => {
+ const { model, ...groupProps } = props;
+ const { nodes, materials } = model;
+ // eslint-disable-next-line no-null/no-null
+ return
+
+
+ ;
+};
+
export const VacuumPumpCover = (model: VacuumPumpCoverFull) =>
- (props: Omit) => {
- const { nodes, materials } = model;
- // eslint-disable-next-line no-null/no-null
- return
-
-
- ;
- };
+ (props: Omit) =>
+ ;
diff --git a/frontend/three_d_garden/fps_probe.tsx b/frontend/three_d_garden/fps_probe.tsx
index 3a9960b586..5c1c0de2f7 100644
--- a/frontend/three_d_garden/fps_probe.tsx
+++ b/frontend/three_d_garden/fps_probe.tsx
@@ -84,7 +84,7 @@ export const FPSProbe = () => {
samples.current.push(fps);
const { calls, triangles, points, lines } = gl.info.render;
const { geometries, textures } = gl.info.memory;
- const sceneCounts = countSceneObjects(scene as Scene);
+ const sceneCounts = countSceneObjects(scene);
window.__fps = fps;
const linesToLogObj: Record = {
epoch: Date.now(),
diff --git a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
index f3dcde67e1..75a4b4a518 100644
--- a/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plant_instances_test.tsx
@@ -57,7 +57,7 @@ describe(" ", () => {
.mockImplementation(() => {
const ref = mockRefImpl();
allRefs.push(ref);
- return ref as never;
+ return ref;
});
location.pathname = Path.mock(Path.designer());
(useFrame as jest.Mock).mockClear();
@@ -238,15 +238,15 @@ describe(" ", () => {
const actualUseRef = reactUseRefSpy.getMockImplementation();
reactUseRefSpy
.mockImplementationOnce(() =>
- instancedRef as unknown as ReturnType)
+ instancedRef)
.mockImplementationOnce(() =>
- materialRef as unknown as ReturnType)
+ materialRef)
.mockImplementationOnce(() =>
- lastBrightnessRef as unknown as ReturnType)
+ lastBrightnessRef)
.mockImplementation(actualUseRef as never);
const p = fakeProps();
+ p.config.sunInclination = 0;
p.plants = [p.plants[0]];
- p.sunFactorRef = { current: 0.5 };
render( );
materialRef.current = { color: { setScalar } };
(useFrame as jest.Mock).mock.calls.forEach(([frameFn]) =>
diff --git a/frontend/three_d_garden/garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
index 202ff91d9e..3b1c48e7db 100644
--- a/frontend/three_d_garden/garden/__tests__/plants_test.tsx
+++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx
@@ -69,7 +69,7 @@ describe(" ", () => {
.mockImplementation(() => {
const nextRef = refQueue.shift() || mockRefImpl();
allRefs.push(nextRef);
- return nextRef as never;
+ return nextRef;
});
const actualUseImperativeHandle = jest.requireActual("react")
.useImperativeHandle as typeof React.useImperativeHandle;
@@ -135,7 +135,7 @@ describe(" ", () => {
.mockImplementation(() => {
const nextRef = refQueue.shift() || mockRefImpl();
allRefs.push(nextRef);
- return nextRef as never;
+ return nextRef;
});
const actualUseImperativeHandle = jest.requireActual("react")
.useImperativeHandle as typeof React.useImperativeHandle;
@@ -317,7 +317,7 @@ describe(" ", () => {
setMockInstanceId(0);
queueMeshRef();
const p = fakeProps();
- p.plants[0].id = undefined as unknown as number;
+ p.plants[0].id = undefined;
const dispatch = jest.fn();
p.dispatch = mockDispatch(dispatch);
const { container } = render( );
diff --git a/frontend/three_d_garden/garden/plant_instances.tsx b/frontend/three_d_garden/garden/plant_instances.tsx
index 0bb4dfedbc..96d85fd9bd 100644
--- a/frontend/three_d_garden/garden/plant_instances.tsx
+++ b/frontend/three_d_garden/garden/plant_instances.tsx
@@ -24,6 +24,8 @@ import {
getPlantIconTextureUrl,
} from "./plant_icon_atlas";
import { Mode } from "../../farm_designer/map/interfaces";
+import moment from "moment";
+import { calcSunCoordinate, calcSunI, getCycleLength } from "./sun";
export interface PlantInstancesProps {
plants: ThreeDGardenPlant[];
@@ -32,7 +34,6 @@ export interface PlantInstancesProps {
visible?: boolean;
startTimeRef?: React.RefObject;
dispatch?: Function;
- sunFactorRef?: React.MutableRefObject;
}
interface PlantIconInstancesProps extends PlantInstancesProps {
@@ -72,7 +73,16 @@ const PlantIconInstances = (props: PlantIconInstancesProps) => {
useFrame(state => {
const mesh = instancedRef.current;
if (!mesh) { return; }
- const brightness = plantIconBrightness(props.sunFactorRef?.current);
+ let sunFactor = calcSunI(config.sunInclination);
+ if (config.animateSeasons && startTimeRef) {
+ const totalCycle = getCycleLength(config.plants);
+ const currentTime = performance.now() / 1000;
+ const t = currentTime - (startTimeRef.current || 0);
+ const timeOffset = Math.min(t / totalCycle, 1) * 24 * 60 * 60;
+ const date = moment().utc().startOf("day").add(timeOffset, "seconds").toDate();
+ sunFactor = calcSunI(calcSunCoordinate(date, 0, 52, 0).inclination);
+ }
+ const brightness = plantIconBrightness(sunFactor);
if (materialRef.current &&
materialRef.current.color &&
brightness != lastBrightness.current) {
diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx
index 0b95b0639b..e6b7a2b71f 100644
--- a/frontend/three_d_garden/garden/plants.tsx
+++ b/frontend/three_d_garden/garden/plants.tsx
@@ -116,9 +116,9 @@ export const PlantSpreadInstances = (props: PlantSpreadInstancesProps) => {
const tempQuaternion = React.useMemo(() => new Quaternion(), []);
const tempColor = React.useMemo(() => new Color(), []);
const get3DPosition = React.useMemo(() => get3DPositionFunc(config), [config]);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
const boundsCenter = React.useMemo(getBoundsCenter(config), []);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps, react-hooks/use-memo
const halfSize = React.useMemo(getHalfSize(config), []);
const plantIndexes = React.useMemo(() =>
plants.map((_, index) => index), [plants]);
diff --git a/frontend/three_d_garden/garden/sun.tsx b/frontend/three_d_garden/garden/sun.tsx
index 91e093a4af..7ea48ccfc9 100644
--- a/frontend/three_d_garden/garden/sun.tsx
+++ b/frontend/three_d_garden/garden/sun.tsx
@@ -32,7 +32,6 @@ export interface SunProps {
config: Config;
startTimeRef?: React.RefObject;
skyRef: React.RefObject;
- sunFactorRef?: React.MutableRefObject;
}
const SUN_COUNT = 1;
@@ -152,11 +151,10 @@ export const Sun = (props: SunProps) => {
const [points, setPoints] = React.useState(
range(SUN_COUNT).map(index => new Vector3(...offsetSunPos(sunPos, index))),
);
- const localSunFactorRef = React.useRef(1);
- const sunFactorRef = props.sunFactorRef || localSunFactorRef;
// eslint-disable-next-line no-null/no-null
const starsRef = React.useRef(null);
const origin = new Vector3(0, 0, 0);
+ const renderedSunFactor = calcSunI(config.sunInclination);
const shadowBounds = React.useMemo(() => {
const bedXBounds = Math.max(
Math.abs(config.bedXOffset),
@@ -177,17 +175,16 @@ export const Sun = (props: SunProps) => {
config.botSizeY,
]);
- const setSunSky = (inclination: number, sunValue: number) => {
- sunFactorRef.current = calcSunI(inclination);
+ const setSunSky = (sunFactor: number, sunValue: number) => {
props.skyRef.current?.color?.setRGB(
- ...skyColor(sunFactorRef.current * sunValue),
+ ...skyColor(sunFactor * sunValue),
);
starsRef.current &&
- (starsRef.current.opacity = (1 - sunFactorRef.current));
+ (starsRef.current.opacity = (1 - sunFactor));
};
React.useEffect(() => {
- setSunSky(config.sunInclination, config.sun);
+ setSunSky(renderedSunFactor, config.sun);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.sunInclination, config.sun]);
@@ -201,18 +198,19 @@ export const Sun = (props: SunProps) => {
const timeOffset = Math.min(t / totalCycle, 1) * 24 * 60 * 60;
const date = moment().utc().startOf("day").add(timeOffset, "seconds").toDate();
const { azimuth, inclination } = calcSunCoordinate(date, 0, 52, 0);
+ const sunFactor = calcSunI(inclination);
const position = (index: number) => {
const sunPos = sunPosition(inclination, azimuth, BigDistance.sunActual);
return offsetSunPos(sunPos, index);
};
- setSunSky(inclination, config.sun);
+ setSunSky(sunFactor, config.sun);
lightRefs.current.forEach((light, index) => {
if (light) {
light.position?.set(...position(index));
light.intensity =
- sunIntensity * config.sun / 100 * sunFactorRef.current;
+ sunIntensity * config.sun / 100 * sunFactor;
}
});
@@ -239,7 +237,7 @@ export const Sun = (props: SunProps) => {
{range(SUN_COUNT).map(index => {
const position = offsetSunPos(sunPos, index);
const color = SUN_COLOR[index];
- const intensity = sunIntensity * config.sun / 100 * sunFactorRef.current;
+ const intensity = sunIntensity * config.sun / 100 * renderedSunFactor;
return
{
diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx
index 93a673fa35..c508c6a54b 100644
--- a/frontend/three_d_garden/garden_model.tsx
+++ b/frontend/three_d_garden/garden_model.tsx
@@ -80,7 +80,6 @@ export const GardenModel = (props: GardenModelProps) => {
const [hoveredPlant, setHoveredPlant] =
React.useState(undefined);
- const hoveredPlantRef = React.useRef(undefined);
const getI = (e: ThreeEvent) => {
if (e.buttons) { return -1; }
@@ -101,8 +100,6 @@ export const GardenModel = (props: GardenModelProps) => {
? (e: ThreeEvent) => {
e.stopPropagation();
const nextHover = active ? getI(e) : undefined;
- if (hoveredPlantRef.current === nextHover) { return; }
- hoveredPlantRef.current = nextHover;
setHoveredPlant(nextHover);
}
: undefined;
@@ -169,7 +166,6 @@ export const GardenModel = (props: GardenModelProps) => {
// eslint-disable-next-line no-null/no-null
const skyRef = React.useRef(null);
- const sunFactorRef = React.useRef(1);
// eslint-disable-next-line no-null/no-null
const activePositionRef = React.useRef<{ x: number, y: number }>(null);
@@ -266,8 +262,7 @@ export const GardenModel = (props: GardenModelProps) => {
+ startTimeRef={props.startTimeRef} />
@@ -319,8 +314,7 @@ export const GardenModel = (props: GardenModelProps) => {
getZ={getZ}
visible={plantsVisible}
startTimeRef={props.startTimeRef}
- dispatch={dispatch}
- sunFactorRef={sunFactorRef} />
+ dispatch={dispatch} />
", () => {
p.slotLocation.z = 3;
p.gantryMounted = false;
const { container } = render( );
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(deviceActions.move).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
});
@@ -331,7 +331,7 @@ describe(" ", () => {
p.slotLocation.z = 3;
p.gantryMounted = true;
const { container } = render( );
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(deviceActions.move).toHaveBeenCalledWith({ x: 10, y: 2, z: 3 });
});
@@ -343,7 +343,7 @@ describe(" ", () => {
p.slotLocation.z = 3;
p.gantryMounted = true;
const { container } = render( );
- fireEvent.click(container.querySelectorAll("button")[1] as Element);
+ fireEvent.click(container.querySelectorAll("button")[1]);
expect(deviceActions.move).toHaveBeenCalledWith({ x: 1, y: 2, z: 3 });
});
});
diff --git a/frontend/tos_update/__tests__/component_test.tsx b/frontend/tos_update/__tests__/component_test.tsx
index 42679b14c4..8f4670420b 100644
--- a/frontend/tos_update/__tests__/component_test.tsx
+++ b/frontend/tos_update/__tests__/component_test.tsx
@@ -48,7 +48,7 @@ beforeEach(() => {
jest.spyOn(i18n, "detectLanguage")
.mockImplementation(() => Promise.resolve({}));
jest.spyOn(axios, "post")
- .mockImplementation(() => mockPostResponse as never);
+ .mockImplementation(() => mockPostResponse);
jest.spyOn(Session, "replaceToken")
.mockImplementation(() => { });
});
diff --git a/frontend/ui/__tests__/color_picker_test.tsx b/frontend/ui/__tests__/color_picker_test.tsx
index 916936806c..a06ceca875 100644
--- a/frontend/ui/__tests__/color_picker_test.tsx
+++ b/frontend/ui/__tests__/color_picker_test.tsx
@@ -49,7 +49,7 @@ describe(" ", () => {
it("changes color", () => {
const p = fakeProps();
const { container } = render( );
- fireEvent.click(container.querySelectorAll("div")[1] as Element);
+ fireEvent.click(container.querySelectorAll("div")[1]);
expect(p.onChange).toHaveBeenCalledWith("blue");
});
});
diff --git a/frontend/ui/__tests__/delete_button_test.tsx b/frontend/ui/__tests__/delete_button_test.tsx
index 2282cda71a..e261d4516a 100644
--- a/frontend/ui/__tests__/delete_button_test.tsx
+++ b/frontend/ui/__tests__/delete_button_test.tsx
@@ -20,7 +20,7 @@ describe(" ", () => {
const wrapper = createRenderer( );
await wrapper.root.findByType("button").props.onClick?.({
preventDefault: jest.fn(),
- } as unknown as React.MouseEvent);
+ });
expect(p.dispatch).toHaveBeenCalledWith(destroyThunk);
unmountRenderer(wrapper);
destroySpy.mockRestore();
diff --git a/frontend/ui/__tests__/filter_search_test.tsx b/frontend/ui/__tests__/filter_search_test.tsx
index 98e9babac1..d029d4d5be 100644
--- a/frontend/ui/__tests__/filter_search_test.tsx
+++ b/frontend/ui/__tests__/filter_search_test.tsx
@@ -44,7 +44,7 @@ describe(" ", () => {
const wrapper = createWrapper(p);
const item = fakeItem();
actRenderer(() => {
- getInstance(wrapper)["handleValueChange"](item as never);
+ getInstance(wrapper)["handleValueChange"](item);
});
expect(p.onChange).toHaveBeenCalledWith(item);
});
@@ -53,7 +53,7 @@ describe(" ", () => {
const p = fakeProps();
const wrapper = createWrapper(p);
actRenderer(() => {
- getInstance(wrapper)["handleValueChange"](undefined as never);
+ getInstance(wrapper)["handleValueChange"](undefined);
});
expect(p.onChange).not.toHaveBeenCalled();
});
@@ -63,7 +63,7 @@ describe(" ", () => {
const wrapper = createWrapper(p);
const item = fakeItem({ heading: true });
actRenderer(() => {
- getInstance(wrapper)["handleValueChange"](item as never);
+ getInstance(wrapper)["handleValueChange"](item);
});
expect(p.onChange).not.toHaveBeenCalled();
});
diff --git a/frontend/util/__tests__/errors_test.ts b/frontend/util/__tests__/errors_test.ts
index 050bcb04c0..b33038acba 100644
--- a/frontend/util/__tests__/errors_test.ts
+++ b/frontend/util/__tests__/errors_test.ts
@@ -27,7 +27,7 @@ describe("prettyPrintApiErrors", () => {
describe("catchErrors", () => {
const e = new Error("TEST");
- const windowWithRollbar = window as typeof window & { Rollbar?: unknown };
+ const windowWithRollbar = window;
beforeEach(() => { delete windowWithRollbar.Rollbar; });
afterEach(() => { delete windowWithRollbar.Rollbar; });
diff --git a/frontend/util/__tests__/page_test.tsx b/frontend/util/__tests__/page_test.tsx
index 6fa6d73a42..b100e822de 100644
--- a/frontend/util/__tests__/page_test.tsx
+++ b/frontend/util/__tests__/page_test.tsx
@@ -41,7 +41,7 @@ describe("attachToRoot()", () => {
clear();
const render = jest.fn();
jest.spyOn(reactDomClient, "createRoot").mockImplementation(() =>
- ({ render, unmount: jest.fn() }) as unknown as ReturnType);
+ ({ render, unmount: jest.fn() }));
expect(() => attachToRoot(Foo, { text: "Bar" })).not.toThrow();
expect(reactDomClient.createRoot).toHaveBeenCalledWith(
document.getElementById("root"));
@@ -56,7 +56,7 @@ describe("entryPoint()", () => {
const { entryPoint } = jest.requireActual("../page");
const render = jest.fn();
jest.spyOn(reactDomClient, "createRoot").mockImplementation(() =>
- ({ render, unmount: jest.fn() }) as unknown as ReturnType);
+ ({ render, unmount: jest.fn() }));
jest.spyOn(i18n, "detectLanguage").mockResolvedValue({
lng: "en",
fallbackLng: "en",
@@ -68,7 +68,7 @@ describe("entryPoint()", () => {
const initSpy = jest.spyOn(i18next, "init")
.mockImplementation((_conf, cb) => {
// eslint-disable-next-line no-null/no-null
- cb?.(null as never, (() => "") as never);
+ cb?.(null, (() => "") as never);
return {} as unknown as ReturnType;
});
const result = entryPoint(Foo) as Promise | undefined;
diff --git a/frontend/weeds/__tests__/weeds_inventory_test.tsx b/frontend/weeds/__tests__/weeds_inventory_test.tsx
index 23d7c5c292..6a36cf2de2 100644
--- a/frontend/weeds/__tests__/weeds_inventory_test.tsx
+++ b/frontend/weeds/__tests__/weeds_inventory_test.tsx
@@ -277,7 +277,7 @@ describe(" ", () => {
const wrapper = createWrapper( );
wrapper.root.findByType(ToggleButton).props.toggleAction({
stopPropagation: jest.fn(),
- } as unknown as React.MouseEvent);
+ });
expect(p.dispatch).toHaveBeenCalled();
});
diff --git a/frontend/wizard/__tests__/checks_test.tsx b/frontend/wizard/__tests__/checks_test.tsx
index c1ed3c0353..f1a851b6a8 100644
--- a/frontend/wizard/__tests__/checks_test.tsx
+++ b/frontend/wizard/__tests__/checks_test.tsx
@@ -209,7 +209,7 @@ describe(" ", () => {
currentTarget: {
value: "New Language",
},
- } as never);
+ });
expect(edit).toHaveBeenCalledWith(expect.any(Object),
{ language: "New Language" });
expect(save).toHaveBeenCalledWith(user.uuid);
@@ -936,7 +936,7 @@ describe(" ", () => {
it("changes offset", () => {
const { container } = render( );
const inputs = container.querySelectorAll("input");
- changeBlurableInputRTL(inputs[0] as HTMLElement, "100");
+ changeBlurableInputRTL(inputs[0], "100");
expect(initSave).toHaveBeenCalledWith("FarmwareEnv", {
key: "CAMERA_CALIBRATION_camera_offset_x", value: "100",
});
@@ -1027,7 +1027,7 @@ describe(" ", () => {
const { container } = render( );
const inputs = container.querySelectorAll("input");
expect(inputs.length).toEqual(3);
- changeBlurableInputRTL(inputs[0] as HTMLElement, "100");
+ changeBlurableInputRTL(inputs[0], "100");
expect(edit).toHaveBeenCalledWith(expect.any(Object), { x: 100 });
expect(save).toHaveBeenCalledWith(expect.any(String));
expect(container.textContent).toContain("Slot 1");
diff --git a/frontend/wizard/__tests__/prerequisites_test.tsx b/frontend/wizard/__tests__/prerequisites_test.tsx
index 1ed7670196..96da68ce0f 100644
--- a/frontend/wizard/__tests__/prerequisites_test.tsx
+++ b/frontend/wizard/__tests__/prerequisites_test.tsx
@@ -44,7 +44,7 @@ describe(" ", () => {
}
input.props.onCommit({
currentTarget: { value: "123" },
- } as React.FocusEvent);
+ });
expect(setOrderNumberSpy).toHaveBeenCalledWith(expect.any(Object), "123");
});
});
diff --git a/package.json b/package.json
index 928e547381..88ed025076 100644
--- a/package.json
+++ b/package.json
@@ -38,27 +38,27 @@
"mqtt": "mqtt/dist/mqtt.esm.js"
},
"dependencies": {
- "@blueprintjs/core": "6.11.3",
- "@blueprintjs/select": "6.1.8",
+ "@blueprintjs/core": "6.12.0",
+ "@blueprintjs/select": "6.1.9",
"@monaco-editor/react": "4.7.0",
"@react-spring/three": "10.0.3",
"@react-three/drei": "10.7.7",
- "@react-three/fiber": "9.5.0",
+ "@react-three/fiber": "9.6.0",
"@rollbar/react": "1.0.0",
- "@types/bun": "^1.3.11",
+ "@types/bun": "1.3.13",
"@types/lodash": "4.17.24",
"@types/markdown-it": "14.1.2",
- "@types/markdown-it-emoji": "^3.0.1",
+ "@types/markdown-it-emoji": "3.0.1",
"@types/promise-timeout": "1.3.3",
"@types/react": "19.2.14",
"@types/react-color": "3.0.13",
"@types/react-dom": "19.2.3",
- "@types/react-test-renderer": "^19.1.0",
- "@types/redux-immutable-state-invariant": "^2.1.4",
- "@types/three": "0.183.1",
+ "@types/react-test-renderer": "19.1.0",
+ "@types/redux-immutable-state-invariant": "2.1.4",
+ "@types/three": "0.184.0",
"@types/ws": "8.18.1",
"@xterm/xterm": "6.0.0",
- "axios": "1.14.0",
+ "axios": "1.15.2",
"bowser": "2.14.1",
"browser-speech": "1.1.1",
"delaunator": "5.1.0",
@@ -66,7 +66,7 @@
"farmbot": "15.9.3",
"fengari": "0.1.5",
"fengari-web": "0.1.4",
- "i18next": "26.0.3",
+ "i18next": "26.0.6",
"lodash": "4.18.1",
"markdown-it": "14.1.1",
"markdown-it-emoji": "3.0.0",
@@ -77,24 +77,24 @@
"promise-timeout": "1.3.0",
"punycode": "2.3.1",
"querystring-es3": "0.2.1",
- "react": "19.2.4",
+ "react": "19.2.5",
"react-color": "2.19.3",
- "react-dom": "19.2.4",
+ "react-dom": "19.2.5",
"react-redux": "9.2.0",
- "react-router": "7.14.0",
+ "react-router": "7.14.2",
"redux": "5.0.1",
"redux-immutable-state-invariant": "2.1.0",
"redux-thunk": "3.1.0",
"rollbar": "3.1.0",
"suncalc": "1.9.0",
"takeme": "0.12.0",
- "three": "0.183.2",
- "typescript": "6.0.2",
+ "three": "0.184.0",
+ "typescript": "6.0.3",
"url": "0.11.4"
},
"devDependencies": {
"@eslint/js": "10.0.1",
- "@happy-dom/global-registrator": "20.8.9",
+ "@happy-dom/global-registrator": "20.9.0",
"@react-three/eslint-plugin": "0.1.2",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.9.1",
@@ -104,17 +104,17 @@
"@types/jest": "30.0.0",
"@types/readable-stream": "4.0.23",
"@types/suncalc": "1.9.2",
- "@typescript-eslint/eslint-plugin": "8.58.0",
- "@typescript-eslint/parser": "8.58.0",
- "eslint": "10.2.0",
+ "@typescript-eslint/eslint-plugin": "8.59.0",
+ "@typescript-eslint/parser": "8.59.0",
+ "eslint": "10.2.1",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-jest": "29.15.1",
+ "eslint-plugin-jest": "29.15.2",
"eslint-plugin-no-null": "1.0.2",
"eslint-plugin-promise": "7.2.1",
"eslint-plugin-react": "7.37.5",
- "eslint-plugin-react-hooks": "7.0.1",
- "happy-dom": "20.8.9",
+ "eslint-plugin-react-hooks": "7.1.1",
+ "happy-dom": "20.9.0",
"jest": "30.3.0",
"jest-canvas-mock": "2.5.2",
"jest-cli": "30.3.0",
@@ -125,14 +125,14 @@
"madge": "8.0.0",
"path-browserify": "1.0.1",
"playwright": "1.59.1",
- "postcss": "^8.5.9",
- "postcss-scss": "^4.0.9",
+ "postcss": "8.5.10",
+ "postcss-scss": "4.0.9",
"raf": "3.4.1",
- "react-test-renderer": "19.2.4",
+ "react-test-renderer": "19.2.5",
"sass": "1.99.0",
"sass-lint": "1.13.1",
- "stylelint": "^17.6.0",
- "stylelint-config-standard-scss": "^17.0.0",
+ "stylelint": "17.8.0",
+ "stylelint-config-standard-scss": "17.0.0",
"ts-jest": "29.4.9",
"tslint": "6.1.3"
}
From 2d7b96a2916e0be93fbef91f460ee658968f21a1 Mon Sep 17 00:00:00 2001
From: gabrielburnworth
Date: Wed, 22 Apr 2026 15:11:28 -0700
Subject: [PATCH 11/11] fix 3D plant interactions and other bugs
---
app/views/layouts/dashboard.html.erb | 2 +-
frontend/plants/__tests__/crop_info_test.tsx | 1 +
frontend/plants/crop_info.tsx | 2 +-
frontend/settings/maybe_highlight.tsx | 4 +++
.../__tests__/instanced_mesh_key_test.ts | 32 +++++++++++++++++++
.../garden/instanced_mesh_key.ts | 13 ++++++++
.../three_d_garden/garden/plant_instances.tsx | 2 ++
frontend/three_d_garden/garden/plants.tsx | 2 ++
8 files changed, 56 insertions(+), 2 deletions(-)
create mode 100644 frontend/three_d_garden/garden/__tests__/instanced_mesh_key_test.ts
create mode 100644 frontend/three_d_garden/garden/instanced_mesh_key.ts
diff --git a/app/views/layouts/dashboard.html.erb b/app/views/layouts/dashboard.html.erb
index 03eb8934d4..05fe4d426c 100644
--- a/app/views/layouts/dashboard.html.erb
+++ b/app/views/layouts/dashboard.html.erb
@@ -17,7 +17,7 @@
.initial-loading-text { position: absolute; top: 385px;
text-align: center; width: 100%; padding-top: 10%; color: #434343; }
- <%= stylesheet_link_tag *@css_assets %>
+ <%= stylesheet_link_tag(*@css_assets, preload_links_header: false) %>
<% manifest_file =
diff --git a/frontend/plants/__tests__/crop_info_test.tsx b/frontend/plants/__tests__/crop_info_test.tsx
index e29e18584d..c680fddc57 100644
--- a/frontend/plants/__tests__/crop_info_test.tsx
+++ b/frontend/plants/__tests__/crop_info_test.tsx
@@ -246,6 +246,7 @@ describe(" ", () => {
const { container } = render( );
expect(normalizedText(container)).toContain("sowingnotavailable");
expect(normalizedText(container)).toContain("commonnamesnotavailable");
+ expect(container.querySelector("img[src='']")).toBeNull();
});
it("handles string of names", () => {
diff --git a/frontend/plants/crop_info.tsx b/frontend/plants/crop_info.tsx
index 25ecda4a00..c37840f4f7 100644
--- a/frontend/plants/crop_info.tsx
+++ b/frontend/plants/crop_info.tsx
@@ -387,7 +387,7 @@ export const RawCropInfo = (props: CropInfoProps) => {
findCurve={findCurve(props.curves, designer)}
plants={props.plants}
onChange={changeCurve(dispatch)} />
-
+ {image && }
;
};
diff --git a/frontend/settings/maybe_highlight.tsx b/frontend/settings/maybe_highlight.tsx
index 7e322a557d..4e09dbdd84 100644
--- a/frontend/settings/maybe_highlight.tsx
+++ b/frontend/settings/maybe_highlight.tsx
@@ -48,6 +48,7 @@ const AXES_PANEL = [
DeviceSetting.findAxisLength,
DeviceSetting.setAxisLength,
DeviceSetting.axisLength,
+ DeviceSetting.gantryHeight,
DeviceSetting.safeHeight,
DeviceSetting.fallbackSoilHeight,
DeviceSetting.defaultAxisOrder,
@@ -143,9 +144,12 @@ const FARM_DESIGNER_PANEL = [
DeviceSetting.mapSize,
DeviceSetting.rotateMap,
DeviceSetting.mapOrigin,
+ DeviceSetting.topDownView,
+ DeviceSetting.setCameraStartingLocation,
DeviceSetting.cropMapImages,
DeviceSetting.clipPhotosOutOfBounds,
DeviceSetting.cameraView,
+ DeviceSetting.uncroppedCameraView,
DeviceSetting.confirmPlantDeletion,
DeviceSetting.defaultPlantDepth,
];
diff --git a/frontend/three_d_garden/garden/__tests__/instanced_mesh_key_test.ts b/frontend/three_d_garden/garden/__tests__/instanced_mesh_key_test.ts
new file mode 100644
index 0000000000..010a8b4f84
--- /dev/null
+++ b/frontend/three_d_garden/garden/__tests__/instanced_mesh_key_test.ts
@@ -0,0 +1,32 @@
+import { instancedMeshKey } from "../instanced_mesh_key";
+import { ThreeDGardenPlant } from "../plants";
+
+describe("instancedMeshKey()", () => {
+ const plant = (overrides: Partial = {}): ThreeDGardenPlant => ({
+ id: 1,
+ label: "Spinach",
+ icon: "/crops/icons/spinach.avif",
+ size: 50,
+ spread: 300,
+ x: 100,
+ y: 200,
+ key: "",
+ seed: 0,
+ ...overrides,
+ });
+
+ it("changes when instance membership changes", () => {
+ const before = instancedMeshKey([plant()]);
+ const after = instancedMeshKey([
+ plant({ id: undefined, x: 300, y: 400 }),
+ plant(),
+ ]);
+ expect(after).not.toEqual(before);
+ });
+
+ it("changes when a plant moves", () => {
+ const before = instancedMeshKey([plant()]);
+ const after = instancedMeshKey([plant({ x: 101 })]);
+ expect(after).not.toEqual(before);
+ });
+});
diff --git a/frontend/three_d_garden/garden/instanced_mesh_key.ts b/frontend/three_d_garden/garden/instanced_mesh_key.ts
new file mode 100644
index 0000000000..f7beaf0d66
--- /dev/null
+++ b/frontend/three_d_garden/garden/instanced_mesh_key.ts
@@ -0,0 +1,13 @@
+import { ThreeDGardenPlant } from "./plants";
+
+export const instancedMeshKey = (plants: ThreeDGardenPlant[]) =>
+ plants.map(plant =>
+ [
+ plant.id ?? "new",
+ plant.icon,
+ plant.x,
+ plant.y,
+ plant.size,
+ plant.spread,
+ ].join(":"))
+ .join("|");
diff --git a/frontend/three_d_garden/garden/plant_instances.tsx b/frontend/three_d_garden/garden/plant_instances.tsx
index 96d85fd9bd..74762020cb 100644
--- a/frontend/three_d_garden/garden/plant_instances.tsx
+++ b/frontend/three_d_garden/garden/plant_instances.tsx
@@ -26,6 +26,7 @@ import {
import { Mode } from "../../farm_designer/map/interfaces";
import moment from "moment";
import { calcSunCoordinate, calcSunI, getCycleLength } from "./sun";
+import { instancedMeshKey } from "./instanced_mesh_key";
export interface PlantInstancesProps {
plants: ThreeDGardenPlant[];
@@ -121,6 +122,7 @@ const PlantIconInstances = (props: PlantIconInstancesProps) => {
};
return {
};
return