From ffefa8034903b21d850460439baf162b8baa2919 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 31 Jan 2025 16:45:54 -0800 Subject: [PATCH 01/27] add points and weeds to 3D garden --- frontend/__test_support__/three_d_mocks.tsx | 4 +- .../__tests__/three_d_garden_map_test.tsx | 4 + frontend/farm_designer/index.tsx | 2 + frontend/farm_designer/three_d_garden_map.tsx | 8 +- .../three_d_garden/__tests__/garden_test.tsx | 14 +++- .../three_d_garden/__tests__/index_test.tsx | 2 + frontend/three_d_garden/constants.ts | 1 + frontend/three_d_garden/garden.tsx | 74 ++++++++++++++++-- frontend/three_d_garden/index.tsx | 5 ++ public/3D/icons/generic-weed.avif | Bin 0 -> 12850 bytes 10 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 public/3D/icons/generic-weed.avif diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index ad1a153a8d..d2bce63499 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -612,8 +612,8 @@ jest.mock("@react-three/drei", () => {
{name}
, Billboard: ({ name, children }: { name: string, children: ReactNode }) =>
{children}
, - Image: ({ name }: { name: string }) => -
{name}
, + Image: ({ name, url }: { name: string, url: string }) => +
{name} {url}
, Clouds: ({ name }: { name: string }) =>
{name}
, Cloud: ({ name }: { name: string }) => 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 ab47204a77..cff767bba9 100644 --- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx +++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx @@ -25,6 +25,8 @@ describe("", () => { dispatch: jest.fn(), getWebAppConfigValue: jest.fn(), curves: [], + mapPoints: [], + weeds: [], }); it("converts props", () => { @@ -55,6 +57,8 @@ describe("", () => { expect(ThreeDGarden).toHaveBeenCalledWith({ config: expectedConfig, addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], }, {}); }); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index cb2194b5d0..f619a45ad9 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -216,6 +216,8 @@ export class RawFarmDesigner botSize={this.props.botSize} dispatch={this.props.dispatch} curves={this.props.curves} + mapPoints={this.props.genericPoints} + weeds={this.props.weeds} getWebAppConfigValue={this.props.getConfigValue} /> :
{ @@ -59,6 +63,8 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { return ", () => { const fakeProps = (): GardenModelProps => ({ @@ -19,6 +20,8 @@ describe("", () => { activeFocus: "", setActiveFocus: jest.fn(), addPlantProps: fakeAddPlantProps([]), + mapPoints: [], + weeds: [], }); it("renders", () => { @@ -46,6 +49,15 @@ describe("", () => { expect(plantLabels.length).toEqual(1); }); + it("renders points and weeds", () => { + const p = fakeProps(); + p.mapPoints = [fakePoint()]; + p.weeds = [fakeWeed()]; + const { container } = render(); + expect(container).toContainHTML("cylinder"); + expect(container).toContainHTML(ASSETS.other.weed); + }); + it("renders promo plants", () => { const p = fakeProps(); p.addPlantProps = undefined; diff --git a/frontend/three_d_garden/__tests__/index_test.tsx b/frontend/three_d_garden/__tests__/index_test.tsx index ee85f796cf..9b47400b2a 100644 --- a/frontend/three_d_garden/__tests__/index_test.tsx +++ b/frontend/three_d_garden/__tests__/index_test.tsx @@ -11,6 +11,8 @@ describe("", () => { const fakeProps = (): ThreeDGardenProps => ({ config: clone(INITIAL), addPlantProps: fakeAddPlantProps([]), + mapPoints: [], + weeds: [], }); it("renders", () => { diff --git a/frontend/three_d_garden/constants.ts b/frontend/three_d_garden/constants.ts index 96889e1921..5cdd1a7789 100644 --- a/frontend/three_d_garden/constants.ts +++ b/frontend/three_d_garden/constants.ts @@ -56,6 +56,7 @@ export const ASSETS: Record> = { }, other: { gear: "/app-resources/img/icons/settings.svg", + weed: "/3D/icons/generic-weed.avif", }, people: { person1: "/3D/people/person_1.avif", diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index aaccede1b6..ff5b4c7bf1 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -7,11 +7,13 @@ import { Detailed, Sphere, useTexture, Line, + Cylinder, + Billboard, } from "@react-three/drei"; -import { RepeatWrapping, BackSide } from "three"; +import { RepeatWrapping, BackSide, DoubleSide } from "three"; import { Bot } from "./bot"; import { AddPlantProps, Bed } from "./bed"; -import { zero as zeroFunc, extents as extentsFunc } from "./helpers"; +import { zero as zeroFunc, extents as extentsFunc, threeSpace } from "./helpers"; import { Sky } from "./sky"; import { Config, detailLevels, seasonProperties } from "./config"; import { ASSETS } from "./constants"; @@ -26,10 +28,10 @@ import { } from "./components"; import { isDesktop } from "../screen_size"; import { isUndefined, range } from "lodash"; -import { - calculatePlantPositions, convertPlants, ThreeDPlant, -} from "./plants"; import { ICON_URLS } from "../crops/constants"; +import { calculatePlantPositions, convertPlants, ThreeDPlant } from "./plants"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; +import { BooleanSetting } from "../session_keys"; const AnimatedGroup = animated(Group); @@ -38,6 +40,8 @@ export interface GardenModelProps { activeFocus: string; setActiveFocus(focus: string): void; addPlantProps?: AddPlantProps; + mapPoints?: TaggedGenericPointer[]; + weeds?: TaggedWeedPointer[]; } // eslint-disable-next-line complexity @@ -201,7 +205,9 @@ export const GardenModel = (props: GardenModelProps) => { ]} />)} @@ -211,6 +217,62 @@ export const GardenModel = (props: GardenModelProps) => { config={config} hoveredPlant={hoveredPlant} />)} + + {props.mapPoints?.map(point => + + + + + + + + )} + + + {props.weeds?.map(weed => + + + + + + + + )} + ; diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx index 8c5e90b503..82c19b0042 100644 --- a/frontend/three_d_garden/index.tsx +++ b/frontend/three_d_garden/index.tsx @@ -4,10 +4,13 @@ import { Config } from "./config"; import { GardenModel } from "./garden"; import { noop } from "lodash"; import { AddPlantProps } from "./bed"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; export interface ThreeDGardenProps { config: Config; addPlantProps: AddPlantProps; + mapPoints: TaggedGenericPointer[]; + weeds: TaggedWeedPointer[]; } export const ThreeDGarden = (props: ThreeDGardenProps) => { @@ -18,6 +21,8 @@ export const ThreeDGarden = (props: ThreeDGardenProps) => { config={props.config} activeFocus={""} setActiveFocus={noop} + mapPoints={props.mapPoints} + weeds={props.weeds} addPlantProps={props.addPlantProps} />
diff --git a/public/3D/icons/generic-weed.avif b/public/3D/icons/generic-weed.avif new file mode 100644 index 0000000000000000000000000000000000000000..c14ea7971918d64f4e7b4431bb867bf4be6b5400 GIT binary patch literal 12850 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�Lx!=?om*KdtHy1! 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 Date: Fri, 31 Jan 2025 16:57:40 -0800 Subject: [PATCH 02/27] remove unreachable plant info code --- frontend/plants/plant_panel.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/plants/plant_panel.tsx b/frontend/plants/plant_panel.tsx index 670ee038e2..cb7b89092b 100644 --- a/frontend/plants/plant_panel.tsx +++ b/frontend/plants/plant_panel.tsx @@ -179,7 +179,7 @@ export function PlantPanel(props: PlantPanelProps) { navigate(Path.cropSearch()); }} /> - {(timeSettings && !inSavedGarden) && + {timeSettings && !inSavedGarden &&
@@ -187,9 +187,7 @@ export function PlantPanel(props: PlantPanelProps) { datePlanted={plantedAt} timeSettings={timeSettings} />
- {(!inSavedGarden) - ? - : t(startCase(plantStatus))} +
{daysOldText({ age: daysOld, stage: plantStatus })} From 408f4038061c5289765b9272097c6be90c738bba Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Mon, 3 Feb 2025 11:29:41 -0800 Subject: [PATCH 03/27] hide saved garden HUD on mobile --- .../farm_designer/__tests__/index_test.tsx | 32 +++++++++++++++++++ frontend/farm_designer/index.tsx | 5 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/frontend/farm_designer/__tests__/index_test.tsx b/frontend/farm_designer/__tests__/index_test.tsx index d2f524ff40..5a6f8d6cd9 100644 --- a/frontend/farm_designer/__tests__/index_test.tsx +++ b/frontend/farm_designer/__tests__/index_test.tsx @@ -5,6 +5,13 @@ jest.mock("../../api/crud", () => ({ jest.mock("../../plants/plant_inventory", () => ({ Plants: () =>
})); +let mockIsMobile = false; +let mockIsDesktop = false; +jest.mock("../../screen_size", () => ({ + isMobile: () => mockIsMobile, + isDesktop: () => mockIsDesktop, +})); + import React from "react"; import { getDefaultAxisLength, getGridSize, RawFarmDesigner as FarmDesigner, @@ -122,13 +129,38 @@ describe("", () => { }); it("renders saved garden indicator", () => { + mockIsMobile = false; + mockIsDesktop = true; + const p = fakeProps(); + p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).toContain("viewing saved garden"); + expect(wrapper.html()).not.toContain("three-d-garden"); + }); + + it("renders saved garden indicator on medium screens", () => { + mockIsMobile = false; + mockIsDesktop = false; const p = fakeProps(); p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("viewing saved garden"); expect(wrapper.html()).not.toContain("three-d-garden"); }); + it("doesn't render saved garden indicator", () => { + mockIsMobile = true; + mockIsDesktop = false; + const p = fakeProps(); + p.designer.openedSavedGarden = 1; + p.designer.panelOpen = false; + const wrapper = mount(); + expect(wrapper.text().toLowerCase()).not.toContain("viewing saved garden"); + expect(wrapper.html()).not.toContain("three-d-garden"); + }); + it("toggles setting", () => { const p = fakeProps(); const state = fakeState(); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index f619a45ad9..8958f1b835 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -27,6 +27,7 @@ import { ThreeDGardenMap } from "./three_d_garden_map"; import { Outlet } from "react-router"; import { ErrorBoundary } from "../error_boundary"; import { get3DConfigValueFunction } from "../settings/three_d_settings"; +import { isDesktop, isMobile } from "../screen_size"; export const getDefaultAxisLength = (getConfigValue: GetWebAppConfigValue): Record => { @@ -268,7 +269,9 @@ export class RawFarmDesigner dispatch={this.props.dispatch} />
} - {this.props.designer.openedSavedGarden && + {this.props.designer.openedSavedGarden + && !isMobile() + && (isDesktop() || !this.props.designer.panelOpen) && } {!this.props.getConfigValue(BooleanSetting.three_d_garden) && From 06e8c55377f6b1a1b64d49c8b694fcc7083e385b Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Tue, 4 Feb 2025 21:18:29 -0800 Subject: [PATCH 04/27] use kitVersion param in promo --- frontend/external_urls.ts | 4 ++-- frontend/three_d_garden/config_overlays.tsx | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts index 49d9d88a9a..a153634f06 100644 --- a/frontend/external_urls.ts +++ b/frontend/external_urls.ts @@ -91,7 +91,7 @@ export namespace ExternalUrl { export const cameraCalibrationCard = `${PRODUCTS}/camera-calibration-card`; export const cameraReplacement = `${PRODUCTS}/genesis-v1-5-express-v1-0-camera-free-replacement`; - export const genesisKit = `${KITS}/farmbot-genesis-v1-7`; - export const genesisXlKit = `${KITS}/farmbot-genesis-xl-v1-7`; + export const genesisKitBase = `${KITS}/farmbot-genesis`; + export const genesisXlKitBase = `${KITS}/farmbot-genesis-xl`; } } diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx index 669748c5db..d48e6ed85e 100644 --- a/frontend/three_d_garden/config_overlays.tsx +++ b/frontend/three_d_garden/config_overlays.tsx @@ -98,16 +98,20 @@ export const PublicOverlay = (props: OverlayProps) => { }} />
} {config.promoInfo && !props.activeFocus && - } + }
; }; interface PromoInfoProps { isGenesis: boolean; + kitVersion: string; } const PromoInfo = (props: PromoInfoProps) => { - const { isGenesis } = props; + const { isGenesis, kitVersion } = props; + const kitVersionSlug = kitVersion.replace(".", "-"); return

Explore our models

{isGenesis @@ -118,7 +122,7 @@ const PromoInfo = (props: PromoInfoProps) => {

FarmBot Genesis is our flagship kit for prosumers and enthusiasts featuring our most advanced technology, features, and options. - Coming 90% pre-assembled in the box, Genesis can be installed on + Coming 95% pre-assembled in the box, Genesis can be installed on an existing raised bed in an afternoon. It is suitable for fixed or mobile raised beds in classrooms, research labs, and backyards.

@@ -139,13 +143,14 @@ const PromoInfo = (props: PromoInfoProps) => { + ? ExternalUrl.Store.genesisKitBase + "-" + kitVersionSlug + : ExternalUrl.Store.genesisXlKitBase + "-" + kitVersionSlug}>

Order Genesis

XL

+

{kitVersion}

; }; From 7a0196cab3aac62735c1ae6797e836b465d16624 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Wed, 5 Feb 2025 02:15:12 -0800 Subject: [PATCH 05/27] promo greenhouse scene --- .../css/farm_designer/three_d_garden.scss | 6 +- frontend/three_d_garden/config.ts | 6 +- frontend/three_d_garden/config_overlays.tsx | 3 +- frontend/three_d_garden/constants.ts | 6 + frontend/three_d_garden/garden.tsx | 24 +++- frontend/three_d_garden/greenhouse.tsx | 125 ++++++++++++++++++ frontend/three_d_garden/greenhouse_wall.tsx | 83 ++++++++++++ frontend/three_d_garden/potted_plant.tsx | 49 +++++++ frontend/three_d_garden/starter_tray.tsx | 42 ++++++ public/3D/icons/generic-plant.avif | Bin 0 -> 6983 bytes public/3D/people/person_3.avif | Bin 0 -> 2573 bytes public/3D/people/person_3_flipped.avif | Bin 0 -> 2656 bytes public/3D/people/person_4.avif | Bin 0 -> 2672 bytes public/3D/people/person_4_flipped.avif | Bin 0 -> 2443 bytes public/3D/textures/bricks.avif | Bin 0 -> 82528 bytes 15 files changed, 332 insertions(+), 12 deletions(-) create mode 100644 frontend/three_d_garden/greenhouse.tsx create mode 100644 frontend/three_d_garden/greenhouse_wall.tsx create mode 100644 frontend/three_d_garden/potted_plant.tsx create mode 100644 frontend/three_d_garden/starter_tray.tsx create mode 100644 public/3D/icons/generic-plant.avif create mode 100644 public/3D/people/person_3.avif create mode 100644 public/3D/people/person_3_flipped.avif create mode 100644 public/3D/people/person_4.avif create mode 100644 public/3D/people/person_4_flipped.avif create mode 100644 public/3D/textures/bricks.avif diff --git a/frontend/css/farm_designer/three_d_garden.scss b/frontend/css/farm_designer/three_d_garden.scss index 4c31e5f76b..eff5d11360 100644 --- a/frontend/css/farm_designer/three_d_garden.scss +++ b/frontend/css/farm_designer/three_d_garden.scss @@ -158,11 +158,7 @@ white-space: nowrap; color: $off_black; - &.outdoor.active, - &.lab.active, - &.genesis.active, - &.standard.active, - &.mobile.active { + &.active { background: rgba(255, 255, 255, 0.6); } diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts index 2e16390f67..e9cffd8ab4 100644 --- a/frontend/three_d_garden/config.ts +++ b/frontend/three_d_garden/config.ts @@ -369,10 +369,10 @@ export const modifyConfig = (config: Config, update: Partial) => { } if (update.scene) { newConfig.lab = update.scene == "Lab"; - newConfig.clouds = update.scene != "Lab"; - newConfig.people = update.scene == "Lab"; + newConfig.clouds = update.scene == "Outdoor"; + newConfig.people = update.scene != "Outdoor"; newConfig.bedType = - (update.scene == "Lab" && newConfig.sizePreset != "Genesis XL") + (update.scene != "Outdoor" && newConfig.sizePreset != "Genesis XL") ? "Mobile" : "Standard"; } diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx index d48e6ed85e..9f9cf2f0e2 100644 --- a/frontend/three_d_garden/config_overlays.tsx +++ b/frontend/three_d_garden/config_overlays.tsx @@ -95,6 +95,7 @@ export const PublicOverlay = (props: OverlayProps) => { options={{ "outdoor": "Outdoor", "lab": "Lab", + "greenhouse": "Greenhouse", }} /> } {config.promoInfo && !props.activeFocus && @@ -334,7 +335,7 @@ export const PrivateOverlay = (props: OverlayProps) => { + options={["Outdoor", "Lab", "Greenhouse"]} /> diff --git a/frontend/three_d_garden/constants.ts b/frontend/three_d_garden/constants.ts index 5cdd1a7789..e67d5af777 100644 --- a/frontend/three_d_garden/constants.ts +++ b/frontend/three_d_garden/constants.ts @@ -15,6 +15,7 @@ export const ASSETS: Record> = { aluminum: "/3D/textures/aluminum.avif", concrete: "/3D/textures/concrete.avif", screen: "/3D/textures/screen.avif", + bricks: "/3D/textures/bricks.avif", }, shapes: { track: "/3D/shapes/track.svg", @@ -57,12 +58,17 @@ export const ASSETS: Record> = { other: { gear: "/app-resources/img/icons/settings.svg", weed: "/3D/icons/generic-weed.avif", + plant: "/3D/icons/generic-plant.avif", }, people: { person1: "/3D/people/person_1.avif", person1Flipped: "/3D/people/person_1_flipped.avif", person2: "/3D/people/person_2.avif", person2Flipped: "/3D/people/person_2_flipped.avif", + person3: "/3D/people/person_3.avif", + person3Flipped: "/3D/people/person_3_flipped.avif", + person4: "/3D/people/person_4.avif", + person4Flipped: "/3D/people/person_4_flipped.avif", }, }; diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index ff5b4c7bf1..4535bdaf96 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -32,6 +32,7 @@ import { ICON_URLS } from "../crops/constants"; import { calculatePlantPositions, convertPlants, ThreeDPlant } from "./plants"; import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; import { BooleanSetting } from "../session_keys"; +import { Greenhouse } from "./greenhouse"; const AnimatedGroup = animated(Group); @@ -86,6 +87,10 @@ export const GardenModel = (props: GardenModelProps) => { labFloorTexture.wrapS = RepeatWrapping; labFloorTexture.wrapT = RepeatWrapping; labFloorTexture.repeat.set(16, 24); + const brickTexture = useTexture(ASSETS.textures.bricks + "?=bricks"); + brickTexture.wrapS = RepeatWrapping; + brickTexture.wrapT = RepeatWrapping; + brickTexture.repeat.set(30, 30); const Ground = ({ children }: { children: React.ReactElement }) => { @@ -275,5 +292,6 @@ export const GardenModel = (props: GardenModelProps) => { + ; }; diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/greenhouse.tsx new file mode 100644 index 0000000000..b01176f58a --- /dev/null +++ b/frontend/three_d_garden/greenhouse.tsx @@ -0,0 +1,125 @@ +import React from "react"; +import { Box, Billboard, Image, useTexture } from "@react-three/drei"; +import { DoubleSide, RepeatWrapping } from "three"; +import { ASSETS } from "./constants"; +import { threeSpace } from "./helpers"; +import { Config } from "./config"; +import { Group, MeshPhongMaterial } from "./components"; +import { StarterTray } from "./starter_tray"; +import PottedPlant from "./potted_plant"; +import { GreenhouseWall } from "./greenhouse_wall"; + +export interface GreenhouseProps { + config: Config; + activeFocus: string; +} + +const wallLength = 10000; +const wallOffset = 2000; +const shelfThickness = 50; +const shelfHeight = 800; +const shelfDepth = 600; + +export const Greenhouse = (props: GreenhouseProps) => { + const { config } = props; + const groundZ = -config.bedZOffset - config.bedHeight; + + const shelfWoodTexture = useTexture(ASSETS.textures.wood + "?=shelf"); + shelfWoodTexture.wrapS = RepeatWrapping; + shelfWoodTexture.wrapT = RepeatWrapping; + shelfWoodTexture.repeat.set(0.3, 0.3); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/frontend/three_d_garden/greenhouse_wall.tsx b/frontend/three_d_garden/greenhouse_wall.tsx new file mode 100644 index 0000000000..44eda08a21 --- /dev/null +++ b/frontend/three_d_garden/greenhouse_wall.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Box } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { Group, MeshPhongMaterial } from "./components"; + +const wallLength = 10000; +const wallHeight = 2500; +const glassThickness = 10; + +const numWallCols = 8; +const numWallRows = 4; +const wallGap = 20; +const paneWidth = (wallLength - (numWallCols + 1) * wallGap) / numWallCols; +const paneHeight = (wallHeight - (numWallRows + 1) * wallGap) / numWallRows; + +const openPanels = [ + { row: 2, col: 1 }, + { row: 2, col: 2 }, + { row: 2, col: 3 }, +]; + +export const GreenhouseWall = () => { + + return ( + + {Array.from({ length: numWallRows }).map((_, row) => + Array.from({ length: numWallCols }).map((_, col) => { + const isOpen = openPanels.some( + (panel) => panel.row === row && panel.col === col + ); + return ( + + + + ); + }) + )} + {Array.from({ length: numWallCols + 1 }).map((_, col) => ( + + + + ))} + {Array.from({ length: numWallRows + 1 }).map((_, row) => ( + + + + ))} + + ); +}; diff --git a/frontend/three_d_garden/potted_plant.tsx b/frontend/three_d_garden/potted_plant.tsx new file mode 100644 index 0000000000..bc6f861315 --- /dev/null +++ b/frontend/three_d_garden/potted_plant.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react' +import { Billboard, Circle, Image } from '@react-three/drei' +import * as THREE from 'three' +import { Group, MeshPhongMaterial, Mesh } from './components' + +const potHeight = 400 +const plantHeight = 500 + +const PottedPlant = () => { + const points = useMemo(() => [ + new THREE.Vector2(0, 0), // Bottom center + new THREE.Vector2(0.3, 0), // Base width + new THREE.Vector2(0.35, 0.1), // Slight flare at the bottom + new THREE.Vector2(0.25, 0.6), // Narrowing midsection + new THREE.Vector2(0.3, 0.8), // Widening towards the top + new THREE.Vector2(0.4, 1), // Outer lip + new THREE.Vector2(0.35, 1), // Inner lip + new THREE.Vector2(0.2, 0.6), // Depth + new THREE.Vector2(0, 0.6) // Close the profile + ], []) + + const geometry = useMemo(() => new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]) + + return ( + + + + + + + + + + + + ) +} + +export default PottedPlant \ No newline at end of file diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/starter_tray.tsx new file mode 100644 index 0000000000..6b2008ac53 --- /dev/null +++ b/frontend/three_d_garden/starter_tray.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Box, Billboard, Image } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { ASSETS } from "./constants"; +import { Group, MeshPhongMaterial } from "./components"; + +const length = 250; +const width = 700; +const height = 50; +const cellSize = 50; +const seedlingSize = 40; + +export const StarterTray = () => { + + return ( + + + + + {Array.from({ length: 5 }, (_, row) => + Array.from({ length: 14 }, (_, col) => { + const x = -width / 2 + cellSize / 2 + col * cellSize; + const y = -length / 2 + cellSize / 2 + row * cellSize; + return ( + + + + ); + }) + )} + + ); +}; diff --git a/public/3D/icons/generic-plant.avif b/public/3D/icons/generic-plant.avif new file mode 100644 index 0000000000000000000000000000000000000000..8074146d51864a0a11c74d5a439c688ba70ec8c3 GIT binary patch literal 6983 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 PGfn5176(xjdX@8k>q;4q literal 0 HcmV?d00001 diff --git a/public/3D/people/person_3.avif b/public/3D/people/person_3.avif new file mode 100644 index 0000000000000000000000000000000000000000..3081d8613ec0fd44a205ae33cec6e8afaac7eea9 GIT binary patch literal 2573 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 Prhc69+au!Vi^%^12^V3B literal 0 HcmV?d00001 diff --git a/public/3D/people/person_3_flipped.avif b/public/3D/people/person_3_flipped.avif new file mode 100644 index 0000000000000000000000000000000000000000..c4ccacb146bc2a74736a4a4001d799ed2daef5e5 GIT binary patch literal 2656 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 literal 0 HcmV?d00001 diff --git a/public/3D/people/person_4.avif b/public/3D/people/person_4.avif new file mode 100644 index 0000000000000000000000000000000000000000..5a56f991f29a243db82690dbb1b87b3fba12cf37 GIT binary patch literal 2672 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 LzbMqds&nHXaJryN literal 0 HcmV?d00001 diff --git a/public/3D/people/person_4_flipped.avif b/public/3D/people/person_4_flipped.avif new file mode 100644 index 0000000000000000000000000000000000000000..0a673b4b285df431cd432ff5eb6d3e2aac8d44db GIT binary patch literal 2443 zcmYjSc{tQ<7yb=nFOA98SQ=EkGBTDh*2cb!Y*`8!!|-Ev`^T&V4`Uoag!H0RR9=q{mS3(I64v8V|`J z5lsg1MBFhn762frKj~(R_2<4MqXa zg1ijBu&^)}cHyJZ7737DywqMo1c@3>Af4q_wdI$FCi}8>#Xn924a@WAqQ#C ziQv14wdQjM?7VW{cc$&>?lMw;?ViRiS7wV(=?3erX019U`bj3gtvoa#)@nL;H@|Fp zbnbeXNBVaePI);0K=s@@gE(|9PDrD?DN9?qV@tf3?J6tQVn02);Fj(m7NDQIcCbtk zlb8SzbVFsKvZEo@Q_UKYb%gHyed+v`m^)jAYy;G)6Z$ zx>J$?mljG5vR&tEpKBICGX5yBagOqR!e4H{u=m4R|HSVb+G814XP3hL^@Q4y!XL1jSEx&lwOsw>Ze$%=1^ol1IQt~$bA}V4) zS#3i(>CIZmqW*)lhwQN*Ra(+t;#?e((?==h#ULu7H!TcS)LSsZoOXWo!umo@D93DV z0DBLI{!KZ&L`>9u3K5wfYG`{cTEc95f?-r?i!_S>qBRTmJYIbnS3@1ThGrdRc}%GQ?TTd+B$&#C(;MspQcvi2e0sUY>1$1KjfHh+Kj*cbN2^3}5pM)#%s1}IB_HBfU3NPBzP)9h#+`^}H*`P8R1QL816e{U|7 zq=gA87C_|#Y8tzeHBGYA8<%4-!A_cW7Ydzv?6;9-kH(8Sz9=xE+cRtaP{adQ+sz*3 zBa&wIiA$aN5AJ>i92xa{;)iF z;p)x#6!~^x*~<9XnfwM?=g4h;1JFugAQo$Ifb>l1()5I0<qM-mrOXOjk8qp^vXD1cv0>y^BJ%q4#5n4s%1E)j(g^&MlS1IKY_5E7x+*` z)t^@B?-H>KJzmsuWc}RZO;<={0@Ac>AswCPQB0F*irw4sU`;0qb65Yh@K%~hE(mQO ze8Paj!-Uc&l078~x(LBOa}OnI&%%% zw%B$yl0y&UzSX%w2b-a$sBdvrxk@e*j`luAq8jj5;T_}V2lq6e{kmrwP_b<=ZFPB8b3EYj?cHsq*mkf;-n@tTcuR^*N@z)yl7 zwN1PAUXr@G5>q|E6rL73$_hhUe;&bE7{SI7d)}+f2L|MqmSIAnmKB2Vcb2}*wcF;+ zgQk0wQ#GPxMO0%ziDZH)T=PECCaAUi^QzKz<@!%qTSn7Z`+jWf#V}KIlN+{!*LCg> z=+CrDqB^gab9yw6t56*T>?VBfuWX6o#S8%ty(RBzvBJC`rs~_Y@#xHBJ_e~TrHgm# z@A7lMbgc4I$r6&kdP~))AvY;P+Y~j)@Aco&-6(-p$Gje?2)(R9t1Ws^F~E} zFs_2WVv91HN#>A9$XO!d$E8h?Yc0vsAYc6w&aHs1 z@DO806Zh{G-T8ZkVK7~!w!LLG1PU`ZZ_|@NPH>idKV<%`Ev+jexF@{s&lKyIOR=*j s%54|=Ns&sELX)h35y_1_LWZ+!i}d{&9JUa?jXvUUhYt)9!=lXp3-9zKp8x;= literal 0 HcmV?d00001 diff --git a/public/3D/textures/bricks.avif b/public/3D/textures/bricks.avif new file mode 100644 index 0000000000000000000000000000000000000000..12b439b42277542978b0624ebc21a19374722bba GIT binary patch literal 82528 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 literal 0 HcmV?d00001 From 1a97591494477e0adb86b23e8069bcc4d892a839 Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Wed, 5 Feb 2025 02:41:45 -0800 Subject: [PATCH 06/27] lint --- frontend/three_d_garden/garden.tsx | 35 +++++++++-------- frontend/three_d_garden/greenhouse.tsx | 4 +- frontend/three_d_garden/greenhouse_wall.tsx | 15 +++++--- frontend/three_d_garden/potted_plant.tsx | 42 ++++++++++----------- frontend/three_d_garden/starter_tray.tsx | 2 +- 5 files changed, 53 insertions(+), 45 deletions(-) diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index 4535bdaf96..76155cf007 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -111,6 +111,23 @@ export const GardenModel = (props: GardenModelProps) => { const gridZ = zero.z - config.soilHeight + 5; const extents = extentsFunc(config); + let groundTexture; + let groundColor; + let lowDetailGroundColor; + if (config.scene === "Greenhouse") { + groundTexture = brickTexture; + groundColor = "#999"; + lowDetailGroundColor = "firebrick"; + } else if (config.scene === "Lab") { + groundTexture = labFloorTexture; + groundColor = "#aaa"; + lowDetailGroundColor = "gray"; + } else { + groundTexture = grassTexture; + groundColor = "#ddd"; + lowDetailGroundColor = "darkgreen"; + } + // eslint-disable-next-line no-null/no-null return { diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/greenhouse.tsx index b01176f58a..7ea6fa48c3 100644 --- a/frontend/three_d_garden/greenhouse.tsx +++ b/frontend/three_d_garden/greenhouse.tsx @@ -6,7 +6,7 @@ import { threeSpace } from "./helpers"; import { Config } from "./config"; import { Group, MeshPhongMaterial } from "./components"; import { StarterTray } from "./starter_tray"; -import PottedPlant from "./potted_plant"; +import { PottedPlant } from "./potted_plant"; import { GreenhouseWall } from "./greenhouse_wall"; export interface GreenhouseProps { @@ -116,7 +116,7 @@ export const Greenhouse = (props: GreenhouseProps) => { position={[ threeSpace(-1750, config.bedLengthOuter), threeSpace(850, -config.bedWidthOuter), - groundZ + groundZ, ]}> diff --git a/frontend/three_d_garden/greenhouse_wall.tsx b/frontend/three_d_garden/greenhouse_wall.tsx index 44eda08a21..8180a9a767 100644 --- a/frontend/three_d_garden/greenhouse_wall.tsx +++ b/frontend/three_d_garden/greenhouse_wall.tsx @@ -27,7 +27,7 @@ export const GreenhouseWall = () => { {Array.from({ length: numWallRows }).map((_, row) => Array.from({ length: numWallCols }).map((_, col) => { const isOpen = openPanels.some( - (panel) => panel.row === row && panel.col === col + (panel) => panel.row === row && panel.col === col, ); return ( { position={[ wallGap + paneWidth / 2 + col * (paneWidth + wallGap), 0, - wallGap + paneHeight / 2 + row * (paneHeight + wallGap) + wallGap + paneHeight / 2 + row * (paneHeight + wallGap), ]} rotation={isOpen ? [-Math.PI / 3, 0, 0] : [0, 0, 0]}> - + ); - }) + }), )} {Array.from({ length: numWallCols + 1 }).map((_, col) => ( { position={[ col * (paneWidth + wallGap) + wallGap / 2, 0, - wallHeight / 2 + wallHeight / 2, ]}> { +export const PottedPlant = () => { const points = useMemo(() => [ - new THREE.Vector2(0, 0), // Bottom center - new THREE.Vector2(0.3, 0), // Base width - new THREE.Vector2(0.35, 0.1), // Slight flare at the bottom - new THREE.Vector2(0.25, 0.6), // Narrowing midsection - new THREE.Vector2(0.3, 0.8), // Widening towards the top - new THREE.Vector2(0.4, 1), // Outer lip - new THREE.Vector2(0.35, 1), // Inner lip - new THREE.Vector2(0.2, 0.6), // Depth - new THREE.Vector2(0, 0.6) // Close the profile - ], []) + new THREE.Vector2(0, 0), + new THREE.Vector2(0.3, 0), + new THREE.Vector2(0.35, 0.1), + new THREE.Vector2(0.25, 0.6), + new THREE.Vector2(0.3, 0.8), + new THREE.Vector2(0.4, 1), + new THREE.Vector2(0.35, 1), + new THREE.Vector2(0.2, 0.6), + new THREE.Vector2(0, 0.6), + ], []); - const geometry = useMemo(() => new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]) + const geometry = useMemo(() => new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]); return ( @@ -43,7 +43,5 @@ const PottedPlant = () => { /> - ) -} - -export default PottedPlant \ No newline at end of file + ); +}; diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/starter_tray.tsx index 6b2008ac53..8ad9997ea9 100644 --- a/frontend/three_d_garden/starter_tray.tsx +++ b/frontend/three_d_garden/starter_tray.tsx @@ -35,7 +35,7 @@ export const StarterTray = () => { transparent={true} /> ); - }) + }), )} ); From 07056c04c3b5e03697105ea38dd2b887bf8fdbff Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Wed, 5 Feb 2025 03:29:49 -0800 Subject: [PATCH 07/27] better low detail color --- frontend/three_d_garden/garden.tsx | 2 +- frontend/three_d_garden/starter_tray.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index 76155cf007..b603a7df58 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -117,7 +117,7 @@ export const GardenModel = (props: GardenModelProps) => { if (config.scene === "Greenhouse") { groundTexture = brickTexture; groundColor = "#999"; - lowDetailGroundColor = "firebrick"; + lowDetailGroundColor = "#8c6f64"; } else if (config.scene === "Lab") { groundTexture = labFloorTexture; groundColor = "#aaa"; diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/starter_tray.tsx index 8ad9997ea9..d52249ea05 100644 --- a/frontend/three_d_garden/starter_tray.tsx +++ b/frontend/three_d_garden/starter_tray.tsx @@ -32,6 +32,7 @@ export const StarterTray = () => { ); From 2c7a5ccc30d61fd8627a79e2b3c31126f8f08c7c Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Wed, 5 Feb 2025 04:25:43 -0800 Subject: [PATCH 08/27] tests --- .../__tests__/config_overlays_test.tsx | 10 +++++++ .../three_d_garden/__tests__/garden_test.tsx | 9 ++++++ .../__tests__/greenhouse_test.tsx | 29 +++++++++++++++++++ frontend/three_d_garden/garden.tsx | 2 +- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 frontend/three_d_garden/__tests__/greenhouse_test.tsx diff --git a/frontend/three_d_garden/__tests__/config_overlays_test.tsx b/frontend/three_d_garden/__tests__/config_overlays_test.tsx index 9d4b8ff925..f3a4a47091 100644 --- a/frontend/three_d_garden/__tests__/config_overlays_test.tsx +++ b/frontend/three_d_garden/__tests__/config_overlays_test.tsx @@ -53,6 +53,16 @@ describe("", () => { text: "", }); }); + + it("sets buy button url and text", () => { + const p = fakeProps(); + p.config.sizePreset = "Genesis XL"; + p.config.kitVersion = "v1.8"; + const wrapper = mount(); + const buyButton = wrapper.find(".buy-button").first(); + expect(buyButton.props().href).toContain("genesis-xl-v1-8"); + expect(buyButton.text()).toContain("GenesisXLv1.8"); + }); }); describe("", () => { diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx index 2b405d183c..b7fd36281d 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_test.tsx @@ -143,4 +143,13 @@ describe("", () => { }); expect(console.log).toHaveBeenCalledWith(["1", "2"]); }); + + it("renders different ground", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + const { container } = render(); + expect(container.innerHTML).toContain("ground Greenhouse"); + expect(container.innerHTML).not.toContain("ground Lab"); + expect(container.innerHTML).not.toContain("ground Outdoor"); + }); }); diff --git a/frontend/three_d_garden/__tests__/greenhouse_test.tsx b/frontend/three_d_garden/__tests__/greenhouse_test.tsx new file mode 100644 index 0000000000..c5c971b247 --- /dev/null +++ b/frontend/three_d_garden/__tests__/greenhouse_test.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { mount } from "enzyme"; +import { Greenhouse, GreenhouseProps } from "../greenhouse"; +import { INITIAL } from "../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GreenhouseProps => ({ + config: clone(INITIAL), + activeFocus: "", + }); + + it("renders", () => { + const p = fakeProps(); + p.config.people = false; + p.activeFocus = ""; + const wrapper = mount(); + expect(wrapper.html()).toContain("greenhouse-environment"); + expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); + }); + + it("renders with people", () => { + const p = fakeProps(); + p.config.people = true; + p.activeFocus = ""; + const wrapper = mount(); + expect(wrapper.find({ name: "people" }).first().props().visible).toBeTruthy(); + }); +}); diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index b603a7df58..3c6f593c83 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -93,7 +93,7 @@ export const GardenModel = (props: GardenModelProps) => { brickTexture.repeat.set(30, 30); const Ground = ({ children }: { children: React.ReactElement }) => - Date: Wed, 5 Feb 2025 05:06:48 -0800 Subject: [PATCH 09/27] more tests --- .../__tests__/greenhouse_test.tsx | 21 +++++++++++++++++++ .../three_d_garden/__tests__/lab_test.tsx | 7 +++++++ frontend/three_d_garden/lab.tsx | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/three_d_garden/__tests__/greenhouse_test.tsx b/frontend/three_d_garden/__tests__/greenhouse_test.tsx index c5c971b247..fd608602bd 100644 --- a/frontend/three_d_garden/__tests__/greenhouse_test.tsx +++ b/frontend/three_d_garden/__tests__/greenhouse_test.tsx @@ -17,6 +17,18 @@ describe("", () => { const wrapper = mount(); expect(wrapper.html()).toContain("greenhouse-environment"); expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); + expect(wrapper.html()).toContain("starter-tray-1"); + expect(wrapper.html()).toContain("starter-tray-2"); + expect(wrapper.html()).toContain("left-greenhouse-wall"); + expect(wrapper.html()).toContain("right-greenhouse-wall"); + expect(wrapper.html()).toContain("potted-plant"); + }); + + it("not visible when scene is not greenhouse", () => { + const p = fakeProps(); + p.config.scene = "Lab"; + const wrapper = mount(); + expect(wrapper.find({ name: "greenhouse-environment" }).first().props().visible).toBeFalsy(); }); it("renders with people", () => { @@ -26,4 +38,13 @@ describe("", () => { const wrapper = mount(); expect(wrapper.find({ name: "people" }).first().props().visible).toBeTruthy(); }); + + it("doesn't render people or potted plant when active focus is set", () => { + const p = fakeProps(); + p.config.people = true; + p.activeFocus = "foo"; + const wrapper = mount(); + expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); + expect(wrapper.find({ name: "potted-plant" }).first().props().visible).toBeFalsy(); + }); }); diff --git a/frontend/three_d_garden/__tests__/lab_test.tsx b/frontend/three_d_garden/__tests__/lab_test.tsx index 9fc46cfd28..1dc1a5f292 100644 --- a/frontend/three_d_garden/__tests__/lab_test.tsx +++ b/frontend/three_d_garden/__tests__/lab_test.tsx @@ -19,6 +19,13 @@ describe("", () => { expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); }); + it("not visible when scene is not lab", () => { + const p = fakeProps(); + p.config.scene = "Greenhouse"; + const wrapper = mount(); + expect(wrapper.find({ name: "lab-environment" }).first().props().visible).toBeFalsy(); + }); + it("renders with people", () => { const p = fakeProps(); p.config.people = true; diff --git a/frontend/three_d_garden/lab.tsx b/frontend/three_d_garden/lab.tsx index a41674470f..fa46593b2a 100644 --- a/frontend/three_d_garden/lab.tsx +++ b/frontend/three_d_garden/lab.tsx @@ -42,7 +42,7 @@ export const Lab = (props: LabProps) => { shelfWoodTexture.wrapT = RepeatWrapping; shelfWoodTexture.repeat.set(0.3, 0.3); - return + return Date: Wed, 5 Feb 2025 05:26:38 -0800 Subject: [PATCH 10/27] 3rd times the charm? --- .../three_d_garden/__tests__/garden_test.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx index b7fd36281d..20c363580c 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_test.tsx @@ -144,12 +144,20 @@ describe("", () => { expect(console.log).toHaveBeenCalledWith(["1", "2"]); }); - it("renders different ground", () => { - const p = fakeProps(); - p.config.scene = "Greenhouse"; - const { container } = render(); - expect(container.innerHTML).toContain("ground Greenhouse"); - expect(container.innerHTML).not.toContain("ground Lab"); - expect(container.innerHTML).not.toContain("ground Outdoor"); + it("renders different ground based on scene", () => { + const scenes = [ + { name: "Greenhouse", expectedClass: "ground Greenhouse", unexpectedClasses: ["ground Lab", "ground Outdoor"] }, + { name: "Lab", expectedClass: "ground Lab", unexpectedClasses: ["ground Greenhouse", "ground Outdoor"] }, + { name: "Outdoor", expectedClass: "ground Outdoor", unexpectedClasses: ["ground Greenhouse", "ground Lab"] }, + ]; + scenes.forEach(({ name, expectedClass, unexpectedClasses }) => { + const p = fakeProps(); + p.config.scene = name; + const { container } = render(); + expect(container.innerHTML).toContain(expectedClass); + unexpectedClasses.forEach(unexpectedClass => { + expect(container.innerHTML).not.toContain(unexpectedClass); + }); + }); }); }); From 31207455da7c3cb401558af3be70e52291b2c5aa Mon Sep 17 00:00:00 2001 From: Rory Aronson Date: Wed, 5 Feb 2025 05:35:27 -0800 Subject: [PATCH 11/27] lint --- frontend/three_d_garden/__tests__/garden_test.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx index 20c363580c..60a74b0042 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_test.tsx @@ -146,18 +146,15 @@ describe("", () => { it("renders different ground based on scene", () => { const scenes = [ - { name: "Greenhouse", expectedClass: "ground Greenhouse", unexpectedClasses: ["ground Lab", "ground Outdoor"] }, - { name: "Lab", expectedClass: "ground Lab", unexpectedClasses: ["ground Greenhouse", "ground Outdoor"] }, - { name: "Outdoor", expectedClass: "ground Outdoor", unexpectedClasses: ["ground Greenhouse", "ground Lab"] }, + { name: "Greenhouse", expectedClass: "ground Greenhouse" }, + { name: "Lab", expectedClass: "ground Lab" }, + { name: "Outdoor", expectedClass: "ground Outdoor" }, ]; - scenes.forEach(({ name, expectedClass, unexpectedClasses }) => { + scenes.forEach(({ name, expectedClass }) => { const p = fakeProps(); p.config.scene = name; const { container } = render(); expect(container.innerHTML).toContain(expectedClass); - unexpectedClasses.forEach(unexpectedClass => { - expect(container.innerHTML).not.toContain(unexpectedClass); - }); }); }); }); From e20cbff852184a93b1f08cdc3c8b6b83dcb9cda4 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 5 Feb 2025 09:43:42 -0800 Subject: [PATCH 12/27] refactor and mock group to improve tests --- frontend/__test_support__/fake_html_events.ts | 11 -- frontend/__test_support__/fake_props.ts | 2 +- frontend/__test_support__/three_d_mocks.tsx | 13 ++ frontend/external_urls.ts | 6 +- .../three_d_garden/__tests__/bot_test.tsx | 18 +- .../__tests__/components_test.tsx | 12 ++ .../three_d_garden/__tests__/garden_test.tsx | 20 +-- .../__tests__/greenhouse_test.tsx | 36 ++-- .../three_d_garden/__tests__/lab_test.tsx | 23 ++- .../__tests__/packaging_test.tsx | 16 +- .../three_d_garden/__tests__/solar_test.tsx | 8 +- frontend/three_d_garden/config_overlays.tsx | 5 +- frontend/three_d_garden/garden.tsx | 34 ++-- frontend/three_d_garden/greenhouse.tsx | 167 +++++++++--------- frontend/three_d_garden/greenhouse_wall.tsx | 112 ++++++------ frontend/three_d_garden/potted_plant.tsx | 47 +++-- frontend/three_d_garden/starter_tray.tsx | 53 +++--- 17 files changed, 309 insertions(+), 274 deletions(-) diff --git a/frontend/__test_support__/fake_html_events.ts b/frontend/__test_support__/fake_html_events.ts index dd3fa91a56..0584a9230b 100644 --- a/frontend/__test_support__/fake_html_events.ts +++ b/frontend/__test_support__/fake_html_events.ts @@ -12,17 +12,6 @@ export const changeEvent = (value: string): ChangeEvent => { return event as ChangeEvent; }; -type IMGEvent = React.SyntheticEvent; -export const imgEvent = (): IMGEvent => { - const event: DeepPartial = { - currentTarget: { - getAttribute: jest.fn(), - setAttribute: jest.fn(), - } - }; - return event as IMGEvent; -}; - type FormEvent = React.FormEvent; export const formEvent = (): FormEvent => { const event: Partial = { preventDefault: jest.fn() }; diff --git a/frontend/__test_support__/fake_props.ts b/frontend/__test_support__/fake_props.ts index 6020edd1ab..68a4c9dfa2 100644 --- a/frontend/__test_support__/fake_props.ts +++ b/frontend/__test_support__/fake_props.ts @@ -6,7 +6,7 @@ export const fakeAddPlantProps = (plants: TaggedPlant[]): AddPlantProps => ({ gridSize: { x: 1000, y: 2000 }, dispatch: jest.fn(), - getConfigValue: jest.fn(), + getConfigValue: jest.fn(() => true), plants, curves: [], designer: fakeDesignerState(), diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index d2bce63499..e123b2b23f 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -9,6 +9,19 @@ import { import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; +import { ThreeElements } from "@react-three/fiber"; + +const GroupForTests = (props: ThreeElements["group"]) => + // @ts-expect-error Property does not exist on type JSX.IntrinsicElements + ; + +jest.mock("../three_d_garden/components", () => ({ + ...jest.requireActual("../three_d_garden/components"), + Group: (props: ThreeElements["group"]) => + props.visible === false + ? <> + : , +})); jest.mock("three/examples/jsm/Addons.js", () => ({ SVGLoader: class { diff --git a/frontend/external_urls.ts b/frontend/external_urls.ts index a153634f06..72acfc3b4c 100644 --- a/frontend/external_urls.ts +++ b/frontend/external_urls.ts @@ -91,7 +91,9 @@ export namespace ExternalUrl { export const cameraCalibrationCard = `${PRODUCTS}/camera-calibration-card`; export const cameraReplacement = `${PRODUCTS}/genesis-v1-5-express-v1-0-camera-free-replacement`; - export const genesisKitBase = `${KITS}/farmbot-genesis`; - export const genesisXlKitBase = `${KITS}/farmbot-genesis-xl`; + export const genesisKit = (version: string) => + `${KITS}/farmbot-genesis-${version.replace(".", "-")}`; + export const genesisXlKit = (version: string) => + `${KITS}/farmbot-genesis-xl-${version.replace(".", "-")}`; } } diff --git a/frontend/three_d_garden/__tests__/bot_test.tsx b/frontend/three_d_garden/__tests__/bot_test.tsx index 9bdcc195d2..03b0db42a4 100644 --- a/frontend/three_d_garden/__tests__/bot_test.tsx +++ b/frontend/three_d_garden/__tests__/bot_test.tsx @@ -6,10 +6,16 @@ import { clone } from "lodash"; import { SVGLoader } from "three/examples/jsm/Addons"; describe("", () => { - const fakeProps = (): FarmbotModelProps => ({ - config: clone(INITIAL), - activeFocus: "", - }); + const fakeProps = (): FarmbotModelProps => { + const config = clone(INITIAL); + config.bot = true; + config.tracks = true; + config.cableCarriers = true; + return { + config, + activeFocus: "", + }; + }; it("renders", () => { const p = fakeProps(); @@ -38,14 +44,14 @@ describe("", () => { const p = fakeProps(); p.config.kitVersion = "v1.7"; const wrapper = mount(); - expect(wrapper.find({ name: "button-group" }).length).toEqual(10); + expect(wrapper.find({ name: "button-group" }).length).toEqual(15); // 5 * 3 }); it("renders: v1.8", () => { const p = fakeProps(); p.config.kitVersion = "v1.8"; const wrapper = mount(); - expect(wrapper.find({ name: "button-group" }).length).toEqual(6); + expect(wrapper.find({ name: "button-group" }).length).toEqual(9); // 3 * 3 }); it("loads shapes", () => { diff --git a/frontend/three_d_garden/__tests__/components_test.tsx b/frontend/three_d_garden/__tests__/components_test.tsx index 3681362ed3..dbff612534 100644 --- a/frontend/three_d_garden/__tests__/components_test.tsx +++ b/frontend/three_d_garden/__tests__/components_test.tsx @@ -3,12 +3,24 @@ import { mount } from "enzyme"; import { AmbientLight, DirectionalLight, + Group, Mesh, MeshBasicMaterial, PointLight, } from "../components"; import { ThreeElements } from "@react-three/fiber"; +describe("", () => { + const fakeProps = (): ThreeElements["group"] => ({ + visible: true, + }); + + it("adds props", () => { + const wrapper = mount(); + expect(wrapper.props().visible).toEqual(true); + }); +}); + describe("", () => { const fakeProps = (): ThreeElements["ambientLight"] => ({ intensity: 0.5, diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx index 60a74b0042..804557cbe5 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_test.tsx @@ -10,7 +10,9 @@ import { GardenModelProps, GardenModel } from "../garden"; import { clone } from "lodash"; import { INITIAL } from "../config"; import { render, screen } from "@testing-library/react"; -import { fakePlant, fakePoint, fakeWeed } from "../../__test_support__/fake_state/resources"; +import { + fakePlant, fakePoint, fakeWeed, +} from "../../__test_support__/fake_state/resources"; import { fakeAddPlantProps } from "../../__test_support__/fake_props"; import { ASSETS } from "../constants"; @@ -144,17 +146,15 @@ describe("", () => { expect(console.log).toHaveBeenCalledWith(["1", "2"]); }); - it("renders different ground based on scene", () => { - const scenes = [ - { name: "Greenhouse", expectedClass: "ground Greenhouse" }, - { name: "Lab", expectedClass: "ground Lab" }, - { name: "Outdoor", expectedClass: "ground Outdoor" }, - ]; - scenes.forEach(({ name, expectedClass }) => { + it.each<[string, string]>([ + ["Greenhouse", "ground Greenhouse"], + ["Lab", "ground Lab"], + ["Outdoor", "ground Outdoor"], + ])("renders different ground based on scene: %s %s", + (sceneName, expectedClass) => { const p = fakeProps(); - p.config.scene = name; + p.config.scene = sceneName; const { container } = render(); expect(container.innerHTML).toContain(expectedClass); }); - }); }); diff --git a/frontend/three_d_garden/__tests__/greenhouse_test.tsx b/frontend/three_d_garden/__tests__/greenhouse_test.tsx index fd608602bd..52f9debe39 100644 --- a/frontend/three_d_garden/__tests__/greenhouse_test.tsx +++ b/frontend/three_d_garden/__tests__/greenhouse_test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { mount } from "enzyme"; +import { render } from "@testing-library/react"; import { Greenhouse, GreenhouseProps } from "../greenhouse"; import { INITIAL } from "../config"; import { clone } from "lodash"; @@ -12,39 +12,43 @@ describe("", () => { it("renders", () => { const p = fakeProps(); + p.config.scene = "Greenhouse"; p.config.people = false; p.activeFocus = ""; - const wrapper = mount(); - expect(wrapper.html()).toContain("greenhouse-environment"); - expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); - expect(wrapper.html()).toContain("starter-tray-1"); - expect(wrapper.html()).toContain("starter-tray-2"); - expect(wrapper.html()).toContain("left-greenhouse-wall"); - expect(wrapper.html()).toContain("right-greenhouse-wall"); - expect(wrapper.html()).toContain("potted-plant"); + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).not.toContainHTML("people"); + expect(container).toContainHTML("starter-tray-1"); + expect(container).toContainHTML("starter-tray-2"); + expect(container).toContainHTML("left-greenhouse-wall"); + expect(container).toContainHTML("right-greenhouse-wall"); + expect(container).toContainHTML("potted-plant"); }); it("not visible when scene is not greenhouse", () => { const p = fakeProps(); p.config.scene = "Lab"; - const wrapper = mount(); - expect(wrapper.find({ name: "greenhouse-environment" }).first().props().visible).toBeFalsy(); + const { container } = render(); + expect(container).not.toContainHTML("greenhouse-environment"); }); it("renders with people", () => { const p = fakeProps(); + p.config.scene = "Greenhouse"; p.config.people = true; p.activeFocus = ""; - const wrapper = mount(); - expect(wrapper.find({ name: "people" }).first().props().visible).toBeTruthy(); + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).toContainHTML("people"); }); it("doesn't render people or potted plant when active focus is set", () => { const p = fakeProps(); + p.config.scene = "Greenhouse"; p.config.people = true; p.activeFocus = "foo"; - const wrapper = mount(); - expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); - expect(wrapper.find({ name: "potted-plant" }).first().props().visible).toBeFalsy(); + const { container } = render(); + expect(container).toContainHTML("greenhouse-environment"); + expect(container).not.toContainHTML("people"); }); }); diff --git a/frontend/three_d_garden/__tests__/lab_test.tsx b/frontend/three_d_garden/__tests__/lab_test.tsx index 1dc1a5f292..5ad6c55022 100644 --- a/frontend/three_d_garden/__tests__/lab_test.tsx +++ b/frontend/three_d_garden/__tests__/lab_test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { mount } from "enzyme"; +import { render } from "@testing-library/react"; import { Lab, LabProps } from "../lab"; import { INITIAL } from "../config"; import { clone } from "lodash"; @@ -12,25 +12,32 @@ describe("", () => { it("renders", () => { const p = fakeProps(); + p.config.scene = "Lab"; p.config.people = false; p.activeFocus = ""; - const wrapper = mount(); - expect(wrapper.html()).toContain("lab"); - expect(wrapper.find({ name: "people" }).first().props().visible).toBeFalsy(); + render(); + const { container } = render(); + expect(container).toContainHTML("shelf"); + expect(container).not.toContainHTML("people"); }); it("not visible when scene is not lab", () => { const p = fakeProps(); p.config.scene = "Greenhouse"; - const wrapper = mount(); - expect(wrapper.find({ name: "lab-environment" }).first().props().visible).toBeFalsy(); + render(); + const { container } = render(); + expect(container).not.toContainHTML("shelf"); + expect(container).not.toContainHTML("people"); }); it("renders with people", () => { const p = fakeProps(); + p.config.scene = "Lab"; p.config.people = true; p.activeFocus = ""; - const wrapper = mount(); - expect(wrapper.find({ name: "people" }).first().props().visible).toBeTruthy(); + render(); + const { container } = render(); + expect(container).toContainHTML("shelf"); + expect(container).toContainHTML("people"); }); }); diff --git a/frontend/three_d_garden/__tests__/packaging_test.tsx b/frontend/three_d_garden/__tests__/packaging_test.tsx index 523440ace8..9afd4ef9c3 100644 --- a/frontend/three_d_garden/__tests__/packaging_test.tsx +++ b/frontend/three_d_garden/__tests__/packaging_test.tsx @@ -11,6 +11,7 @@ describe("", () => { it("renders", () => { const p = fakeProps(); + p.config.packaging = true; p.config.kitVersion = "v1.7"; const wrapper = mount(); expect(wrapper.html()).toContain("packaging"); @@ -18,12 +19,23 @@ describe("", () => { expect(wrapper.html()).not.toContain("170"); }); - it("renders: XL", () => { + it("renders: v1.7 XL", () => { const p = fakeProps(); + p.config.packaging = true; p.config.sizePreset = "Genesis XL"; - p.config.kitVersion = "v1.8"; + p.config.kitVersion = "v1.7"; const wrapper = mount(); expect(wrapper.html()).toContain("170"); expect(wrapper.html()).not.toContain("100"); }); + + it("renders: v1.8 XL", () => { + const p = fakeProps(); + p.config.packaging = true; + p.config.sizePreset = "Genesis XL"; + p.config.kitVersion = "v1.8"; + const wrapper = mount(); + expect(wrapper.html()).not.toContain("170"); + expect(wrapper.html()).not.toContain("100"); + }); }); diff --git a/frontend/three_d_garden/__tests__/solar_test.tsx b/frontend/three_d_garden/__tests__/solar_test.tsx index 901b87e49c..99192e2e2b 100644 --- a/frontend/three_d_garden/__tests__/solar_test.tsx +++ b/frontend/three_d_garden/__tests__/solar_test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { mount } from "enzyme"; +import { render } from "@testing-library/react"; import { Solar, SolarProps } from "../solar"; import { INITIAL } from "../config"; import { clone } from "lodash"; @@ -11,7 +11,9 @@ describe("", () => { }); it("renders", () => { - const wrapper = mount(); - expect(wrapper.html()).toContain("solar"); + const p = fakeProps(); + p.config.solar = true; + const { container } = render(); + expect(container).toContainHTML("solar"); }); }); diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx index 9f9cf2f0e2..034ad9a37f 100644 --- a/frontend/three_d_garden/config_overlays.tsx +++ b/frontend/three_d_garden/config_overlays.tsx @@ -112,7 +112,6 @@ interface PromoInfoProps { const PromoInfo = (props: PromoInfoProps) => { const { isGenesis, kitVersion } = props; - const kitVersionSlug = kitVersion.replace(".", "-"); return

Explore our models

{isGenesis @@ -144,8 +143,8 @@ const PromoInfo = (props: PromoInfoProps) => { + ? ExternalUrl.Store.genesisKit(kitVersion) + : ExternalUrl.Store.genesisXlKit(kitVersion)}>

Order Genesis

diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx index 3c6f593c83..e5aaf7e583 100644 --- a/frontend/three_d_garden/garden.tsx +++ b/frontend/three_d_garden/garden.tsx @@ -111,22 +111,18 @@ export const GardenModel = (props: GardenModelProps) => { const gridZ = zero.z - config.soilHeight + 5; const extents = extentsFunc(config); - let groundTexture; - let groundColor; - let lowDetailGroundColor; - if (config.scene === "Greenhouse") { - groundTexture = brickTexture; - groundColor = "#999"; - lowDetailGroundColor = "#8c6f64"; - } else if (config.scene === "Lab") { - groundTexture = labFloorTexture; - groundColor = "#aaa"; - lowDetailGroundColor = "gray"; - } else { - groundTexture = grassTexture; - groundColor = "#ddd"; - lowDetailGroundColor = "darkgreen"; - } + const getGroundProperties = (sceneName: string) => { + switch (sceneName) { + case "Greenhouse": + return { texture: brickTexture, color: "#999", lowDetailColor: "#8c6f64" }; + case "Lab": + return { texture: labFloorTexture, color: "#aaa", lowDetailColor: "gray" }; + default: + return { texture: grassTexture, color: "#ddd", lowDetailColor: "darkgreen" }; + } + }; + + const groundProperties = getGroundProperties(config.scene); // eslint-disable-next-line no-null/no-null return { diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/greenhouse.tsx index 7ea6fa48c3..dae9b95e55 100644 --- a/frontend/three_d_garden/greenhouse.tsx +++ b/frontend/three_d_garden/greenhouse.tsx @@ -29,97 +29,96 @@ export const Greenhouse = (props: GreenhouseProps) => { shelfWoodTexture.wrapT = RepeatWrapping; shelfWoodTexture.repeat.set(0.3, 0.3); - return ( - + return - - - - - - - - - - - + + + + + + + - - - + + + - - - - - - - - + + + - + - - + + + + + + + + + - ); + ; }; diff --git a/frontend/three_d_garden/greenhouse_wall.tsx b/frontend/three_d_garden/greenhouse_wall.tsx index 8180a9a767..aeea9e67ee 100644 --- a/frontend/three_d_garden/greenhouse_wall.tsx +++ b/frontend/three_d_garden/greenhouse_wall.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Box } from "@react-three/drei"; import { DoubleSide } from "three"; import { Group, MeshPhongMaterial } from "./components"; +import { range } from "lodash"; const wallLength = 10000; const wallHeight = 2500; @@ -21,68 +22,65 @@ const openPanels = [ export const GreenhouseWall = () => { - return ( - - {Array.from({ length: numWallRows }).map((_, row) => - Array.from({ length: numWallCols }).map((_, col) => { - const isOpen = openPanels.some( - (panel) => panel.row === row && panel.col === col, - ); - return ( - - - - ); - }), - )} - {Array.from({ length: numWallCols + 1 }).map((_, col) => ( - + {range(numWallRows).map(row => + range(numWallCols).map(col => { + const isOpen = openPanels.some(panel => + panel.row === row && + panel.col === col, + ); + return + wallGap + paneHeight / 2 + row * (paneHeight + wallGap), + ]} + rotation={isOpen ? [-Math.PI / 3, 0, 0] : [0, 0, 0]}> - - ))} - {Array.from({ length: numWallRows + 1 }).map((_, row) => ( - - - - ))} - - ); + ; + }), + )} + {range(numWallCols + 1).map(col => ( + + + + ))} + {range(numWallRows + 1).map(row => ( + + + + ))} + ; }; diff --git a/frontend/three_d_garden/potted_plant.tsx b/frontend/three_d_garden/potted_plant.tsx index 38493ad49e..06eb00f3f0 100644 --- a/frontend/three_d_garden/potted_plant.tsx +++ b/frontend/three_d_garden/potted_plant.tsx @@ -19,29 +19,28 @@ export const PottedPlant = () => { new THREE.Vector2(0, 0.6), ], []); - const geometry = useMemo(() => new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]); + const geometry = useMemo(() => + new THREE.LatheGeometry(points, 32, 0, Math.PI * 2), [points]); - return ( - - - - - - - - - - - - ); + return + + + + + + + + + + ; }; diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/starter_tray.tsx index d52249ea05..1b421a1d26 100644 --- a/frontend/three_d_garden/starter_tray.tsx +++ b/frontend/three_d_garden/starter_tray.tsx @@ -3,6 +3,7 @@ import { Box, Billboard, Image } from "@react-three/drei"; import { DoubleSide } from "three"; import { ASSETS } from "./constants"; import { Group, MeshPhongMaterial } from "./components"; +import { range } from "lodash"; const length = 250; const width = 700; @@ -12,32 +13,28 @@ const seedlingSize = 40; export const StarterTray = () => { - return ( - - - - - {Array.from({ length: 5 }, (_, row) => - Array.from({ length: 14 }, (_, col) => { - const x = -width / 2 + cellSize / 2 + col * cellSize; - const y = -length / 2 + cellSize / 2 + row * cellSize; - return ( - - - - ); - }), - )} - - ); + return + + + + {range(5).map(row => + range(14).map(col => { + const x = -width / 2 + cellSize / 2 + col * cellSize; + const y = -length / 2 + cellSize / 2 + row * cellSize; + return + + ; + }), + )} + ; }; From 26f8f145190a3f82710b940e9eaeab9f08071ca9 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 5 Feb 2025 11:26:34 -0800 Subject: [PATCH 13/27] refactor people --- .../__tests__/components_test.tsx | 4 ++ .../three_d_garden/__tests__/garden_test.tsx | 2 - .../three_d_garden/__tests__/people_test.tsx | 20 +++++++ frontend/three_d_garden/greenhouse.tsx | 60 ++++++++----------- frontend/three_d_garden/lab.tsx | 57 ++++++++---------- frontend/three_d_garden/people.tsx | 51 ++++++++++++++++ 6 files changed, 123 insertions(+), 71 deletions(-) create mode 100644 frontend/three_d_garden/__tests__/people_test.tsx create mode 100644 frontend/three_d_garden/people.tsx diff --git a/frontend/three_d_garden/__tests__/components_test.tsx b/frontend/three_d_garden/__tests__/components_test.tsx index dbff612534..4383a7560c 100644 --- a/frontend/three_d_garden/__tests__/components_test.tsx +++ b/frontend/three_d_garden/__tests__/components_test.tsx @@ -1,3 +1,7 @@ +jest.mock("../components", () => ({ + ...jest.requireActual("../components"), +})); + import React from "react"; import { mount } from "enzyme"; import { diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_test.tsx index 804557cbe5..e36daf84a8 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_test.tsx @@ -22,8 +22,6 @@ describe("", () => { activeFocus: "", setActiveFocus: jest.fn(), addPlantProps: fakeAddPlantProps([]), - mapPoints: [], - weeds: [], }); it("renders", () => { diff --git a/frontend/three_d_garden/__tests__/people_test.tsx b/frontend/three_d_garden/__tests__/people_test.tsx new file mode 100644 index 0000000000..db0e88ccd5 --- /dev/null +++ b/frontend/three_d_garden/__tests__/people_test.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { People, PeopleProps } from "../people"; +import { INITIAL } from "../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): PeopleProps => ({ + activeFocus: "", + config: clone(INITIAL), + people: [], + }); + + it("renders", () => { + const p = fakeProps(); + p.config.people = true; + const { container } = render(); + expect(container).toContainHTML("people"); + }); +}); diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/greenhouse.tsx index dae9b95e55..0e83c63aeb 100644 --- a/frontend/three_d_garden/greenhouse.tsx +++ b/frontend/three_d_garden/greenhouse.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { Box, Billboard, Image, useTexture } from "@react-three/drei"; -import { DoubleSide, RepeatWrapping } from "three"; +import { Box, useTexture } from "@react-three/drei"; +import { DoubleSide, RepeatWrapping, Vector3 } from "three"; import { ASSETS } from "./constants"; import { threeSpace } from "./helpers"; import { Config } from "./config"; @@ -8,6 +8,7 @@ import { Group, MeshPhongMaterial } from "./components"; import { StarterTray } from "./starter_tray"; import { PottedPlant } from "./potted_plant"; import { GreenhouseWall } from "./greenhouse_wall"; +import { People } from "./people"; export interface GreenhouseProps { config: Config; @@ -75,40 +76,27 @@ export const Greenhouse = (props: GreenhouseProps) => { - - - - - - - - + { ))} - - - - - - - - + ; }; diff --git a/frontend/three_d_garden/people.tsx b/frontend/three_d_garden/people.tsx new file mode 100644 index 0000000000..5cd80cd353 --- /dev/null +++ b/frontend/three_d_garden/people.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Billboard, Image } from "@react-three/drei"; +import { Group } from "./components"; +import { Config } from "./config"; +import { threeSpace } from "./helpers"; +import { Vector3 } from "three"; + + +export interface PeopleProps { + config: Config; + activeFocus: string; + people: { url: string, offset: Vector3 }[]; +} + +export const People = (props: PeopleProps) => { + const { people, config } = props; + const groundZ = -config.bedZOffset - config.bedHeight; + return + {people[0] && + + + } + {people[1] && + + + } + ; +}; From 990c228dab4ca44c2d3517b64a277e5162837cf8 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 5 Feb 2025 12:29:36 -0800 Subject: [PATCH 14/27] fix typo --- frontend/three_d_garden/people.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/three_d_garden/people.tsx b/frontend/three_d_garden/people.tsx index 5cd80cd353..247aec3902 100644 --- a/frontend/three_d_garden/people.tsx +++ b/frontend/three_d_garden/people.tsx @@ -35,8 +35,8 @@ export const People = (props: PeopleProps) => { {people[1] && Date: Wed, 5 Feb 2025 13:18:57 -0800 Subject: [PATCH 15/27] people fixes and cleanup --- frontend/three_d_garden/greenhouse.tsx | 14 ++---- frontend/three_d_garden/greenhouse_wall.tsx | 6 +-- frontend/three_d_garden/lab.tsx | 14 ++---- frontend/three_d_garden/people.tsx | 54 +++++++++++---------- frontend/three_d_garden/starter_tray.tsx | 2 +- 5 files changed, 39 insertions(+), 51 deletions(-) diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/greenhouse.tsx index 0e83c63aeb..d7fdf7c331 100644 --- a/frontend/three_d_garden/greenhouse.tsx +++ b/frontend/three_d_garden/greenhouse.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Box, useTexture } from "@react-three/drei"; -import { DoubleSide, RepeatWrapping, Vector3 } from "three"; +import { DoubleSide, RepeatWrapping } from "three"; import { ASSETS } from "./constants"; import { threeSpace } from "./helpers"; import { Config } from "./config"; @@ -82,19 +82,11 @@ export const Greenhouse = (props: GreenhouseProps) => { people={[ { url: ASSETS.people.person3, - offset: new Vector3( - -400, - -400, - 0, - ) + offset: [-400, -400], }, { url: ASSETS.people.person4Flipped, - offset: new Vector3( - 0, - config.bedWidthOuter + 900, - 0, - ) + offset: [0, config.bedWidthOuter + 900], }, ]} /> diff --git a/frontend/three_d_garden/greenhouse_wall.tsx b/frontend/three_d_garden/greenhouse_wall.tsx index aeea9e67ee..6c55e31c52 100644 --- a/frontend/three_d_garden/greenhouse_wall.tsx +++ b/frontend/three_d_garden/greenhouse_wall.tsx @@ -30,7 +30,7 @@ export const GreenhouseWall = () => { panel.row === row && panel.col === col, ); - return { }), )} {range(numWallCols + 1).map(col => ( - { ))} {range(numWallRows + 1).map(row => ( - { people={[ { url: ASSETS.people.person1Flipped, - offset: new Vector3( - -300, - -300, - 0, - ) + offset: [-300, -300], }, { url: ASSETS.people.person2Flipped, - offset: new Vector3( - config.bedLengthOuter / 2, - config.bedWidthOuter + 500, - 0, - ) + offset: [config.bedLengthOuter / 2, config.bedWidthOuter + 500], }, ]} /> ; diff --git a/frontend/three_d_garden/people.tsx b/frontend/three_d_garden/people.tsx index 247aec3902..89e8178dab 100644 --- a/frontend/three_d_garden/people.tsx +++ b/frontend/three_d_garden/people.tsx @@ -4,12 +4,12 @@ import { Group } from "./components"; import { Config } from "./config"; import { threeSpace } from "./helpers"; import { Vector3 } from "three"; - +import { ASSETS } from "./constants"; export interface PeopleProps { config: Config; activeFocus: string; - people: { url: string, offset: Vector3 }[]; + people: { url: string, offset: number[] }[]; } export const People = (props: PeopleProps) => { @@ -17,35 +17,39 @@ export const People = (props: PeopleProps) => { const groundZ = -config.bedZOffset - config.bedHeight; return - {people[0] && - - - } - {people[1] && - { + const scalingData = SCALING_DATA[person.url]; + const offset = new Vector3(...person.offset); + return - } + ; + })} ; }; + +interface DataRecord { + scale: [number, number]; + position: number[]; +} + +const SCALING_DATA: Record = { + [ASSETS.people.person1]: { scale: [900, 1800], position: [0, 900, 0] }, + [ASSETS.people.person1Flipped]: { scale: [900, 1800], position: [0, 900, 0] }, + [ASSETS.people.person2]: { scale: [700, 1700], position: [0, 850, 0] }, + [ASSETS.people.person2Flipped]: { scale: [700, 1700], position: [0, 850, 0] }, + [ASSETS.people.person3]: { scale: [875, 1800], position: [0, 900, 0] }, + [ASSETS.people.person3Flipped]: { scale: [875, 1800], position: [0, 900, 0] }, + [ASSETS.people.person4]: { scale: [580, 1700], position: [0, 850, 0] }, + [ASSETS.people.person4Flipped]: { scale: [580, 1700], position: [0, 850, 0] }, +}; diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/starter_tray.tsx index 1b421a1d26..5a12cd5091 100644 --- a/frontend/three_d_garden/starter_tray.tsx +++ b/frontend/three_d_garden/starter_tray.tsx @@ -25,7 +25,7 @@ export const StarterTray = () => { range(14).map(col => { const x = -width / 2 + cellSize / 2 + col * cellSize; const y = -length / 2 + cellSize / 2 + row * cellSize; - return Date: Thu, 6 Feb 2025 23:05:32 -0800 Subject: [PATCH 16/27] organize 3D code --- .gitignore | 1 + .madgerc | 14 + frontend/__test_support__/fake_props.ts | 2 +- frontend/promo/promo.tsx | 2 +- ...{garden_test.tsx => garden_model_test.tsx} | 2 +- .../{ => bed}/__tests__/bed_test.tsx | 12 +- frontend/three_d_garden/{ => bed}/bed.tsx | 33 +- .../objects}/__tests__/caster_test.tsx | 2 +- .../objects}/__tests__/farmbot_axes_test.tsx | 2 +- .../objects}/__tests__/packaging_test.tsx | 2 +- .../__tests__/utilities_post_test.tsx | 2 +- .../{ => bed/objects}/caster.tsx | 4 +- .../{ => bed/objects}/farmbot_axes.tsx | 8 +- frontend/three_d_garden/bed/objects/index.ts | 4 + .../{ => bed/objects}/packaging.tsx | 8 +- .../{ => bed/objects}/utilities_post.tsx | 10 +- .../{ => bot}/__tests__/bot_test.tsx | 2 +- .../{ => bot}/__tests__/power_supply_test.tsx | 2 +- .../__tests__/x_axis_water_tube_test.tsx | 2 +- frontend/three_d_garden/{ => bot}/bot.tsx | 26 +- .../parts/__tests__/cross_slide_test.tsx | 2 +- .../__tests__/gantry_wheel_plate_test.tsx | 2 +- .../parts/__tests__/rotary_tool_test.tsx | 2 +- .../__tests__/seed_trough_assembly_test.tsx | 2 +- .../__tests__/seed_trough_holder_test.tsx | 2 +- .../parts/__tests__/soil_sensor_test.tsx | 2 +- .../__tests__/vacuum_pump_cover_test.tsx | 2 +- .../{ => bot}/parts/cross_slide.tsx | 2 +- .../{ => bot}/parts/gantry_wheel_plate.tsx | 2 +- frontend/three_d_garden/bot/parts/index.ts | 7 + .../{ => bot}/parts/rotary_tool.tsx | 2 +- .../{ => bot}/parts/seed_trough_assembly.tsx | 4 +- .../{ => bot}/parts/seed_trough_holder.tsx | 4 +- .../{ => bot}/parts/soil_sensor.tsx | 2 +- .../{ => bot}/parts/vacuum_pump_cover.tsx | 4 +- .../three_d_garden/{ => bot}/power_supply.tsx | 8 +- .../{ => bot}/x_axis_water_tube.tsx | 6 +- .../{ => elements}/__tests__/arrow_test.tsx | 0 .../{ => elements}/__tests__/button_test.tsx | 0 .../__tests__/distance_indicator_test.tsx | 0 .../elements/__tests__/text_test.tsx | 18 ++ .../three_d_garden/{ => elements}/arrow.tsx | 2 +- .../three_d_garden/{ => elements}/button.tsx | 2 +- .../{ => elements}/distance_indicator.tsx | 2 +- .../three_d_garden/{ => elements}/text.tsx | 6 +- frontend/three_d_garden/garden.tsx | 298 ------------------ .../garden/__tests__/clouds_test.tsx | 16 + .../garden/__tests__/grid_test.tsx | 18 ++ .../garden/__tests__/ground_test.tsx | 16 + .../{ => garden}/__tests__/plants_test.tsx | 6 +- .../garden/__tests__/point_test.tsx | 18 ++ .../{ => garden}/__tests__/sky_test.tsx | 0 .../{ => garden}/__tests__/solar_test.tsx | 2 +- .../{ => garden}/__tests__/sun_test.tsx | 2 +- .../garden/__tests__/weed_test.tsx | 18 ++ .../__tests__/zoom_beacons_test.tsx | 4 +- frontend/three_d_garden/garden/clouds.tsx | 29 ++ frontend/three_d_garden/garden/grid.tsx | 33 ++ frontend/three_d_garden/garden/ground.tsx | 64 ++++ frontend/three_d_garden/garden/index.ts | 10 + .../three_d_garden/{ => garden}/plants.tsx | 12 +- frontend/three_d_garden/garden/point.tsx | 53 ++++ frontend/three_d_garden/{ => garden}/sky.tsx | 2 +- .../three_d_garden/{ => garden}/solar.tsx | 6 +- frontend/three_d_garden/{ => garden}/sun.tsx | 4 +- frontend/three_d_garden/garden/weed.tsx | 41 +++ .../{ => garden}/zoom_beacons.tsx | 8 +- frontend/three_d_garden/garden_model.tsx | 161 ++++++++++ frontend/three_d_garden/index.tsx | 4 +- .../__tests__/greenhouse_test.tsx | 2 +- .../{ => scenes}/__tests__/lab_test.tsx | 2 +- .../{ => scenes}/greenhouse.tsx | 13 +- frontend/three_d_garden/scenes/index.ts | 2 + frontend/three_d_garden/{ => scenes}/lab.tsx | 11 +- .../props}/__tests__/desk_test.tsx | 2 +- .../props/__tests__/greenhouse_wall_test.tsx | 10 + .../props}/__tests__/people_test.tsx | 2 +- .../props/__tests__/potted_plant_test.tsx | 10 + .../props/__tests__/starter_tray_test.tsx | 17 + .../{ => scenes/props}/desk.tsx | 8 +- .../{ => scenes/props}/greenhouse_wall.tsx | 2 +- frontend/three_d_garden/scenes/props/index.ts | 5 + .../{ => scenes/props}/people.tsx | 8 +- .../{ => scenes/props}/potted_plant.tsx | 2 +- .../{ => scenes/props}/starter_tray.tsx | 4 +- package.json | 2 + 86 files changed, 707 insertions(+), 445 deletions(-) create mode 100644 .madgerc rename frontend/three_d_garden/__tests__/{garden_test.tsx => garden_model_test.tsx} (98%) rename frontend/three_d_garden/{ => bed}/__tests__/bed_test.tsx (88%) rename frontend/three_d_garden/{ => bed}/bed.tsx (91%) rename frontend/three_d_garden/{ => bed/objects}/__tests__/caster_test.tsx (91%) rename frontend/three_d_garden/{ => bed/objects}/__tests__/farmbot_axes_test.tsx (90%) rename frontend/three_d_garden/{ => bed/objects}/__tests__/packaging_test.tsx (96%) rename frontend/three_d_garden/{ => bed/objects}/__tests__/utilities_post_test.tsx (91%) rename frontend/three_d_garden/{ => bed/objects}/caster.tsx (94%) rename frontend/three_d_garden/{ => bed/objects}/farmbot_axes.tsx (78%) create mode 100644 frontend/three_d_garden/bed/objects/index.ts rename frontend/three_d_garden/{ => bed/objects}/packaging.tsx (95%) rename frontend/three_d_garden/{ => bed/objects}/utilities_post.tsx (95%) rename frontend/three_d_garden/{ => bot}/__tests__/bot_test.tsx (97%) rename frontend/three_d_garden/{ => bot}/__tests__/power_supply_test.tsx (95%) rename frontend/three_d_garden/{ => bot}/__tests__/x_axis_water_tube_test.tsx (91%) rename frontend/three_d_garden/{ => bot}/bot.tsx (98%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/cross_slide_test.tsx (91%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/gantry_wheel_plate_test.tsx (92%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/rotary_tool_test.tsx (91%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/seed_trough_assembly_test.tsx (92%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/seed_trough_holder_test.tsx (92%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/soil_sensor_test.tsx (91%) rename frontend/three_d_garden/{ => bot}/parts/__tests__/vacuum_pump_cover_test.tsx (92%) rename frontend/three_d_garden/{ => bot}/parts/cross_slide.tsx (99%) rename frontend/three_d_garden/{ => bot}/parts/gantry_wheel_plate.tsx (99%) create mode 100644 frontend/three_d_garden/bot/parts/index.ts rename frontend/three_d_garden/{ => bot}/parts/rotary_tool.tsx (99%) rename frontend/three_d_garden/{ => bot}/parts/seed_trough_assembly.tsx (91%) rename frontend/three_d_garden/{ => bot}/parts/seed_trough_holder.tsx (90%) rename frontend/three_d_garden/{ => bot}/parts/soil_sensor.tsx (99%) rename frontend/three_d_garden/{ => bot}/parts/vacuum_pump_cover.tsx (89%) rename frontend/three_d_garden/{ => bot}/power_supply.tsx (96%) rename frontend/three_d_garden/{ => bot}/x_axis_water_tube.tsx (90%) rename frontend/three_d_garden/{ => elements}/__tests__/arrow_test.tsx (100%) rename frontend/three_d_garden/{ => elements}/__tests__/button_test.tsx (100%) rename frontend/three_d_garden/{ => elements}/__tests__/distance_indicator_test.tsx (100%) create mode 100644 frontend/three_d_garden/elements/__tests__/text_test.tsx rename frontend/three_d_garden/{ => elements}/arrow.tsx (94%) rename frontend/three_d_garden/{ => elements}/button.tsx (97%) rename frontend/three_d_garden/{ => elements}/distance_indicator.tsx (97%) rename frontend/three_d_garden/{ => elements}/text.tsx (85%) delete mode 100644 frontend/three_d_garden/garden.tsx create mode 100644 frontend/three_d_garden/garden/__tests__/clouds_test.tsx create mode 100644 frontend/three_d_garden/garden/__tests__/grid_test.tsx create mode 100644 frontend/three_d_garden/garden/__tests__/ground_test.tsx rename frontend/three_d_garden/{ => garden}/__tests__/plants_test.tsx (94%) create mode 100644 frontend/three_d_garden/garden/__tests__/point_test.tsx rename frontend/three_d_garden/{ => garden}/__tests__/sky_test.tsx (100%) rename frontend/three_d_garden/{ => garden}/__tests__/solar_test.tsx (92%) rename frontend/three_d_garden/{ => garden}/__tests__/sun_test.tsx (90%) create mode 100644 frontend/three_d_garden/garden/__tests__/weed_test.tsx rename frontend/three_d_garden/{ => garden}/__tests__/zoom_beacons_test.tsx (97%) create mode 100644 frontend/three_d_garden/garden/clouds.tsx create mode 100644 frontend/three_d_garden/garden/grid.tsx create mode 100644 frontend/three_d_garden/garden/ground.tsx create mode 100644 frontend/three_d_garden/garden/index.ts rename frontend/three_d_garden/{ => garden}/plants.tsx (90%) create mode 100644 frontend/three_d_garden/garden/point.tsx rename frontend/three_d_garden/{ => garden}/sky.tsx (97%) rename frontend/three_d_garden/{ => garden}/solar.tsx (94%) rename frontend/three_d_garden/{ => garden}/sun.tsx (93%) create mode 100644 frontend/three_d_garden/garden/weed.tsx rename frontend/three_d_garden/{ => garden}/zoom_beacons.tsx (94%) create mode 100644 frontend/three_d_garden/garden_model.tsx rename frontend/three_d_garden/{ => scenes}/__tests__/greenhouse_test.tsx (97%) rename frontend/three_d_garden/{ => scenes}/__tests__/lab_test.tsx (96%) rename frontend/three_d_garden/{ => scenes}/greenhouse.tsx (88%) create mode 100644 frontend/three_d_garden/scenes/index.ts rename frontend/three_d_garden/{ => scenes}/lab.tsx (91%) rename frontend/three_d_garden/{ => scenes/props}/__tests__/desk_test.tsx (90%) create mode 100644 frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx rename frontend/three_d_garden/{ => scenes/props}/__tests__/people_test.tsx (91%) create mode 100644 frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx create mode 100644 frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx rename frontend/three_d_garden/{ => scenes/props}/desk.tsx (94%) rename frontend/three_d_garden/{ => scenes/props}/greenhouse_wall.tsx (97%) create mode 100644 frontend/three_d_garden/scenes/props/index.ts rename frontend/three_d_garden/{ => scenes/props}/people.tsx (91%) rename frontend/three_d_garden/{ => scenes/props}/potted_plant.tsx (95%) rename frontend/three_d_garden/{ => scenes/props}/starter_tray.tsx (91%) diff --git a/.gitignore b/.gitignore index 2231fe0333..43a33afd53 100755 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ upgrade_deps.sh # ActiveStorage blobs: storage/* tmp +module_graph.* diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000000..588d2038a7 --- /dev/null +++ b/.madgerc @@ -0,0 +1,14 @@ +{ + "excludeRegExp": [ + "^(?!.*three_d_garden)", + "[/\\\\]components\\.tsx$", + "[/\\\\]config\\.ts$", + "[/\\\\]helpers\\.ts$", + "[/\\\\]constants\\.ts$", + "[/\\\\]__tests__[/\\\\]" + ], + "fileExtensions": [ + "ts", + "tsx" + ] +} diff --git a/frontend/__test_support__/fake_props.ts b/frontend/__test_support__/fake_props.ts index 68a4c9dfa2..f96f77d378 100644 --- a/frontend/__test_support__/fake_props.ts +++ b/frontend/__test_support__/fake_props.ts @@ -1,5 +1,5 @@ import { TaggedPlant } from "../farm_designer/map/interfaces"; -import { AddPlantProps } from "../three_d_garden/bed"; +import { AddPlantProps } from "../three_d_garden/bed/bed"; import { fakeDesignerState } from "./fake_designer_state"; export const fakeAddPlantProps = diff --git a/frontend/promo/promo.tsx b/frontend/promo/promo.tsx index bac40a5589..b001f4747c 100644 --- a/frontend/promo/promo.tsx +++ b/frontend/promo/promo.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Config, INITIAL, modifyConfigsFromUrlParams, } from "../three_d_garden/config"; -import { GardenModel } from "../three_d_garden/garden"; +import { GardenModel } from "../three_d_garden/garden_model"; import { Canvas } from "@react-three/fiber"; import { PrivateOverlay, PublicOverlay, ToolTip, diff --git a/frontend/three_d_garden/__tests__/garden_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx similarity index 98% rename from frontend/three_d_garden/__tests__/garden_test.tsx rename to frontend/three_d_garden/__tests__/garden_model_test.tsx index e36daf84a8..77ed1f2fed 100644 --- a/frontend/three_d_garden/__tests__/garden_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx @@ -6,7 +6,7 @@ jest.mock("../../screen_size", () => ({ import React from "react"; import { mount } from "enzyme"; -import { GardenModelProps, GardenModel } from "../garden"; +import { GardenModelProps, GardenModel } from "../garden_model"; import { clone } from "lodash"; import { INITIAL } from "../config"; import { render, screen } from "@testing-library/react"; diff --git a/frontend/three_d_garden/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx similarity index 88% rename from frontend/three_d_garden/__tests__/bed_test.tsx rename to frontend/three_d_garden/bed/__tests__/bed_test.tsx index a4dbc2278b..e783eab921 100644 --- a/frontend/three_d_garden/__tests__/bed_test.tsx +++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx @@ -1,9 +1,9 @@ -jest.mock("../../farm_designer/map/layers/plants/plant_actions", () => ({ +jest.mock("../../../farm_designer/map/layers/plants/plant_actions", () => ({ dropPlant: jest.fn(), })); let mockIsMobile = false; -jest.mock("../../screen_size", () => ({ +jest.mock("../../../screen_size", () => ({ isMobile: () => mockIsMobile, })); @@ -21,13 +21,13 @@ jest.mock("react", () => ({ })); import React from "react"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { Bed, BedProps } from "../bed"; import { clone } from "lodash"; import { fireEvent, render, screen } from "@testing-library/react"; -import { dropPlant } from "../../farm_designer/map/layers/plants/plant_actions"; -import { Path } from "../../internal_urls"; -import { fakeAddPlantProps } from "../../__test_support__/fake_props"; +import { dropPlant } from "../../../farm_designer/map/layers/plants/plant_actions"; +import { Path } from "../../../internal_urls"; +import { fakeAddPlantProps } from "../../../__test_support__/fake_props"; describe("", () => { const fakeProps = (): BedProps => ({ diff --git a/frontend/three_d_garden/bed.tsx b/frontend/three_d_garden/bed/bed.tsx similarity index 91% rename from frontend/three_d_garden/bed.tsx rename to frontend/three_d_garden/bed/bed.tsx index 295aa883c8..ce95db308d 100644 --- a/frontend/three_d_garden/bed.tsx +++ b/frontend/three_d_garden/bed/bed.tsx @@ -6,28 +6,25 @@ import { DoubleSide, Path as LinePath, Shape, RepeatWrapping, Group as GroupType, } from "three"; import { range } from "lodash"; -import { threeSpace, zZero, getColorFromBrightness } from "./helpers"; -import { Config, detailLevels } from "./config"; -import { ASSETS } from "./constants"; -import { DistanceIndicator } from "./distance_indicator"; -import { FarmbotAxes } from "./farmbot_axes"; -import { Packaging } from "./packaging"; -import { Caster } from "./caster"; -import { UtilitiesPost } from "./utilities_post"; -import { Group, MeshPhongMaterial } from "./components"; -import { getMode, round } from "../farm_designer/map/util"; +import { threeSpace, zZero, getColorFromBrightness } from "../helpers"; +import { Config, detailLevels } from "../config"; +import { ASSETS } from "../constants"; +import { DistanceIndicator } from "../elements/distance_indicator"; +import { FarmbotAxes, Caster, UtilitiesPost, Packaging } from "./objects"; +import { Group, MeshPhongMaterial } from "../components"; +import { getMode, round } from "../../farm_designer/map/util"; import { AxisNumberProperty, Mode, TaggedPlant, -} from "../farm_designer/map/interfaces"; -import { dropPlant } from "../farm_designer/map/layers/plants/plant_actions"; +} from "../../farm_designer/map/interfaces"; +import { dropPlant } from "../../farm_designer/map/layers/plants/plant_actions"; import { TaggedCurve } from "farmbot"; -import { GetWebAppConfigValue } from "../config_storage/actions"; -import { DesignerState } from "../farm_designer/interfaces"; -import { isMobile } from "../screen_size"; +import { GetWebAppConfigValue } from "../../config_storage/actions"; +import { DesignerState } from "../../farm_designer/interfaces"; +import { isMobile } from "../../screen_size"; import { ThreeEvent } from "@react-three/fiber"; -import { Path } from "../internal_urls"; -import { findIcon } from "../crops/find"; -import { DEFAULT_PLANT_RADIUS } from "../farm_designer/plant"; +import { Path } from "../../internal_urls"; +import { findIcon } from "../../crops/find"; +import { DEFAULT_PLANT_RADIUS } from "../../farm_designer/plant"; const soil = ( Type: typeof LinePath | typeof Shape, diff --git a/frontend/three_d_garden/__tests__/caster_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/caster_test.tsx similarity index 91% rename from frontend/three_d_garden/__tests__/caster_test.tsx rename to frontend/three_d_garden/bed/objects/__tests__/caster_test.tsx index 2a9ea5ea19..2c634e932f 100644 --- a/frontend/three_d_garden/__tests__/caster_test.tsx +++ b/frontend/three_d_garden/bed/objects/__tests__/caster_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { Caster, CasterProps } from "../caster"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/__tests__/farmbot_axes_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/farmbot_axes_test.tsx similarity index 90% rename from frontend/three_d_garden/__tests__/farmbot_axes_test.tsx rename to frontend/three_d_garden/bed/objects/__tests__/farmbot_axes_test.tsx index 6173653219..a7923ff13f 100644 --- a/frontend/three_d_garden/__tests__/farmbot_axes_test.tsx +++ b/frontend/three_d_garden/bed/objects/__tests__/farmbot_axes_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { FarmbotAxes, FarmbotAxesProps } from "../farmbot_axes"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; describe("", () => { const fakeProps = (): FarmbotAxesProps => ({ diff --git a/frontend/three_d_garden/__tests__/packaging_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/packaging_test.tsx similarity index 96% rename from frontend/three_d_garden/__tests__/packaging_test.tsx rename to frontend/three_d_garden/bed/objects/__tests__/packaging_test.tsx index 9afd4ef9c3..c49b7fb9a8 100644 --- a/frontend/three_d_garden/__tests__/packaging_test.tsx +++ b/frontend/three_d_garden/bed/objects/__tests__/packaging_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { Packaging, PackagingProps } from "../packaging"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/__tests__/utilities_post_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/utilities_post_test.tsx similarity index 91% rename from frontend/three_d_garden/__tests__/utilities_post_test.tsx rename to frontend/three_d_garden/bed/objects/__tests__/utilities_post_test.tsx index b9dab5bcc6..fd841c2d82 100644 --- a/frontend/three_d_garden/__tests__/utilities_post_test.tsx +++ b/frontend/three_d_garden/bed/objects/__tests__/utilities_post_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { UtilitiesPost, UtilitiesPostProps } from "../utilities_post"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/caster.tsx b/frontend/three_d_garden/bed/objects/caster.tsx similarity index 94% rename from frontend/three_d_garden/caster.tsx rename to frontend/three_d_garden/bed/objects/caster.tsx index 63d962bdb8..fb6b901ac8 100644 --- a/frontend/three_d_garden/caster.tsx +++ b/frontend/three_d_garden/bed/objects/caster.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Extrude, Cylinder } from "@react-three/drei"; import { Shape } from "three"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; +import { Config } from "../../config"; +import { Group, MeshPhongMaterial } from "../../components"; export interface CasterProps { config: Config; diff --git a/frontend/three_d_garden/farmbot_axes.tsx b/frontend/three_d_garden/bed/objects/farmbot_axes.tsx similarity index 78% rename from frontend/three_d_garden/farmbot_axes.tsx rename to frontend/three_d_garden/bed/objects/farmbot_axes.tsx index 275e732b18..1bd0520b65 100644 --- a/frontend/three_d_garden/farmbot_axes.tsx +++ b/frontend/three_d_garden/bed/objects/farmbot_axes.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { Config } from "./config"; -import { Arrow } from "./arrow"; -import { threeSpace, zZero } from "./helpers"; -import { Group } from "./components"; +import { Config } from "../../config"; +import { Arrow } from "../../elements/arrow"; +import { threeSpace, zZero } from "../../helpers"; +import { Group } from "../../components"; export interface FarmbotAxesProps { config: Config; diff --git a/frontend/three_d_garden/bed/objects/index.ts b/frontend/three_d_garden/bed/objects/index.ts new file mode 100644 index 0000000000..f7cac8b7f7 --- /dev/null +++ b/frontend/three_d_garden/bed/objects/index.ts @@ -0,0 +1,4 @@ +export * from "./caster"; +export * from "./farmbot_axes"; +export * from "./utilities_post"; +export * from "./packaging"; diff --git a/frontend/three_d_garden/packaging.tsx b/frontend/three_d_garden/bed/objects/packaging.tsx similarity index 95% rename from frontend/three_d_garden/packaging.tsx rename to frontend/three_d_garden/bed/objects/packaging.tsx index bbe0c8e52e..3f0e05fd21 100644 --- a/frontend/three_d_garden/packaging.tsx +++ b/frontend/three_d_garden/bed/objects/packaging.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Box } from "@react-three/drei"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; -import { Text } from "./text"; +import { threeSpace } from "../../helpers"; +import { Config } from "../../config"; +import { Group, MeshPhongMaterial } from "../../components"; +import { Text } from "../../elements/text"; export interface PackagingProps { config: Config; diff --git a/frontend/three_d_garden/utilities_post.tsx b/frontend/three_d_garden/bed/objects/utilities_post.tsx similarity index 95% rename from frontend/three_d_garden/utilities_post.tsx rename to frontend/three_d_garden/bed/objects/utilities_post.tsx index 8e50f61dca..4b1b1d34fa 100644 --- a/frontend/three_d_garden/utilities_post.tsx +++ b/frontend/three_d_garden/bed/objects/utilities_post.tsx @@ -1,14 +1,14 @@ import React from "react"; import { Box, Cylinder, RoundedBox, Tube, useTexture } from "@react-three/drei"; import { RepeatWrapping } from "three"; -import { ASSETS } from "./constants"; -import { Config } from "./config"; +import { ASSETS } from "../../constants"; +import { Config } from "../../config"; import { threeSpace, getColorFromBrightness, easyCubicBezierCurve3, -} from "./helpers"; -import { outletDepth } from "./power_supply"; +} from "../../helpers"; +import { outletDepth } from "../../bot/power_supply"; import * as THREE from "three"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../../components"; export interface UtilitiesPostProps { config: Config; diff --git a/frontend/three_d_garden/__tests__/bot_test.tsx b/frontend/three_d_garden/bot/__tests__/bot_test.tsx similarity index 97% rename from frontend/three_d_garden/__tests__/bot_test.tsx rename to frontend/three_d_garden/bot/__tests__/bot_test.tsx index 03b0db42a4..5a520e9072 100644 --- a/frontend/three_d_garden/__tests__/bot_test.tsx +++ b/frontend/three_d_garden/bot/__tests__/bot_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { Bot, FarmbotModelProps } from "../bot"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; import { SVGLoader } from "three/examples/jsm/Addons"; diff --git a/frontend/three_d_garden/__tests__/power_supply_test.tsx b/frontend/three_d_garden/bot/__tests__/power_supply_test.tsx similarity index 95% rename from frontend/three_d_garden/__tests__/power_supply_test.tsx rename to frontend/three_d_garden/bot/__tests__/power_supply_test.tsx index 9ded40538d..2fc9d04aaf 100644 --- a/frontend/three_d_garden/__tests__/power_supply_test.tsx +++ b/frontend/three_d_garden/bot/__tests__/power_supply_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { PowerSupply, PowerSupplyProps } from "../power_supply"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/__tests__/x_axis_water_tube_test.tsx b/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx similarity index 91% rename from frontend/three_d_garden/__tests__/x_axis_water_tube_test.tsx rename to frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx index 3641d9b62b..95d8e064ca 100644 --- a/frontend/three_d_garden/__tests__/x_axis_water_tube_test.tsx +++ b/frontend/three_d_garden/bot/__tests__/x_axis_water_tube_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { XAxisWaterTubeProps, XAxisWaterTube } from "../x_axis_water_tube"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; describe("", () => { const fakeProps = (): XAxisWaterTubeProps => ({ diff --git a/frontend/three_d_garden/bot.tsx b/frontend/three_d_garden/bot/bot.tsx similarity index 98% rename from frontend/three_d_garden/bot.tsx rename to frontend/three_d_garden/bot/bot.tsx index 9e193171aa..b03d52319d 100644 --- a/frontend/three_d_garden/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -10,26 +10,26 @@ import { zZero as zZeroFunc, zero as zeroFunc, extents as extentsFunc, -} from "./helpers"; -import { Config } from "./config"; +} from "../helpers"; +import { Config } from "../config"; import { GLTF } from "three-stdlib"; -import { ASSETS, ElectronicsBoxMaterial, LIB_DIR, PartName } from "./constants"; +import { ASSETS, ElectronicsBoxMaterial, LIB_DIR, PartName } from "../constants"; import { SVGLoader } from "three/examples/jsm/Addons.js"; import { range } from "lodash"; -import { CrossSlide, CrossSlideFull } from "./parts/cross_slide"; -import { GantryWheelPlate, GantryWheelPlateFull } from "./parts/gantry_wheel_plate"; -import { RotaryTool, RotaryToolFull } from "./parts/rotary_tool"; -import { DistanceIndicator } from "./distance_indicator"; -import { VacuumPumpCover, VacuumPumpCoverFull } from "./parts/vacuum_pump_cover"; -import { SoilSensor, SoilSensorFull } from "./parts/soil_sensor"; import { + CrossSlide, CrossSlideFull, + GantryWheelPlate, GantryWheelPlateFull, + RotaryTool, RotaryToolFull, + VacuumPumpCover, VacuumPumpCoverFull, + SoilSensor, SoilSensorFull, SeedTroughAssembly, SeedTroughAssemblyFull, -} from "./parts/seed_trough_assembly"; -import { SeedTroughHolder, SeedTroughHolderFull } from "./parts/seed_trough_holder"; + SeedTroughHolder, SeedTroughHolderFull, +} from "./parts"; +import { DistanceIndicator } from "../elements/distance_indicator"; import { PowerSupply } from "./power_supply"; import { XAxisWaterTube } from "./x_axis_water_tube"; -import { Group, Mesh, MeshPhongMaterial } from "./components"; -import { IColor } from "../settings/pin_bindings/model"; +import { Group, Mesh, MeshPhongMaterial } from "../components"; +import { IColor } from "../../settings/pin_bindings/model"; const extrusionWidth = 20; const utmRadius = 35; diff --git a/frontend/three_d_garden/parts/__tests__/cross_slide_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/cross_slide_test.tsx similarity index 91% rename from frontend/three_d_garden/parts/__tests__/cross_slide_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/cross_slide_test.tsx index f622df95f9..c6785999c2 100644 --- a/frontend/three_d_garden/parts/__tests__/cross_slide_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/cross_slide_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { CrossSlideFull, CrossSlide } from "../cross_slide"; import { useGLTF } from "@react-three/drei"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; describe("", () => { it("renders", () => { diff --git a/frontend/three_d_garden/parts/__tests__/gantry_wheel_plate_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/gantry_wheel_plate_test.tsx similarity index 92% rename from frontend/three_d_garden/parts/__tests__/gantry_wheel_plate_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/gantry_wheel_plate_test.tsx index e01cc0c3b9..9a92f5da0f 100644 --- a/frontend/three_d_garden/parts/__tests__/gantry_wheel_plate_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/gantry_wheel_plate_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { GantryWheelPlate, GantryWheelPlateFull } from "../gantry_wheel_plate"; import { useGLTF } from "@react-three/drei"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; describe("", () => { it("renders", () => { diff --git a/frontend/three_d_garden/parts/__tests__/rotary_tool_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/rotary_tool_test.tsx similarity index 91% rename from frontend/three_d_garden/parts/__tests__/rotary_tool_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/rotary_tool_test.tsx index 131df56eca..5ddc8d0ec0 100644 --- a/frontend/three_d_garden/parts/__tests__/rotary_tool_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/rotary_tool_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { RotaryTool, RotaryToolFull } from "../rotary_tool"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; import { useGLTF } from "@react-three/drei"; describe("", () => { diff --git a/frontend/three_d_garden/parts/__tests__/seed_trough_assembly_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/seed_trough_assembly_test.tsx similarity index 92% rename from frontend/three_d_garden/parts/__tests__/seed_trough_assembly_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/seed_trough_assembly_test.tsx index ceabbc21b0..c2e463b4c8 100644 --- a/frontend/three_d_garden/parts/__tests__/seed_trough_assembly_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/seed_trough_assembly_test.tsx @@ -3,7 +3,7 @@ import { mount } from "enzyme"; import { SeedTroughAssembly, SeedTroughAssemblyFull, } from "../seed_trough_assembly"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; import { useGLTF } from "@react-three/drei"; describe("", () => { diff --git a/frontend/three_d_garden/parts/__tests__/seed_trough_holder_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/seed_trough_holder_test.tsx similarity index 92% rename from frontend/three_d_garden/parts/__tests__/seed_trough_holder_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/seed_trough_holder_test.tsx index 66ae320472..888d2cde6c 100644 --- a/frontend/three_d_garden/parts/__tests__/seed_trough_holder_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/seed_trough_holder_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { SeedTroughHolder, SeedTroughHolderFull } from "../seed_trough_holder"; import { useGLTF } from "@react-three/drei"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; describe("", () => { it("renders", () => { diff --git a/frontend/three_d_garden/parts/__tests__/soil_sensor_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/soil_sensor_test.tsx similarity index 91% rename from frontend/three_d_garden/parts/__tests__/soil_sensor_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/soil_sensor_test.tsx index 623e94dbc9..e9d097c372 100644 --- a/frontend/three_d_garden/parts/__tests__/soil_sensor_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/soil_sensor_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { SoilSensor, SoilSensorFull } from "../soil_sensor"; import { useGLTF } from "@react-three/drei"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; describe("", () => { it("renders", () => { diff --git a/frontend/three_d_garden/parts/__tests__/vacuum_pump_cover_test.tsx b/frontend/three_d_garden/bot/parts/__tests__/vacuum_pump_cover_test.tsx similarity index 92% rename from frontend/three_d_garden/parts/__tests__/vacuum_pump_cover_test.tsx rename to frontend/three_d_garden/bot/parts/__tests__/vacuum_pump_cover_test.tsx index 3f56de9075..68132366c2 100644 --- a/frontend/three_d_garden/parts/__tests__/vacuum_pump_cover_test.tsx +++ b/frontend/three_d_garden/bot/parts/__tests__/vacuum_pump_cover_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { VacuumPumpCover, VacuumPumpCoverFull } from "../vacuum_pump_cover"; import { useGLTF } from "@react-three/drei"; -import { ASSETS } from "../../constants"; +import { ASSETS } from "../../../constants"; describe("", () => { it("renders", () => { diff --git a/frontend/three_d_garden/parts/cross_slide.tsx b/frontend/three_d_garden/bot/parts/cross_slide.tsx similarity index 99% rename from frontend/three_d_garden/parts/cross_slide.tsx rename to frontend/three_d_garden/bot/parts/cross_slide.tsx index 4697cd2cfd..40ede04e96 100644 --- a/frontend/three_d_garden/parts/cross_slide.tsx +++ b/frontend/three_d_garden/bot/parts/cross_slide.tsx @@ -3,7 +3,7 @@ import React from "react"; import * as THREE from "three"; import { InstancedBufferAttribute } from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent, InstancedMesh } from "../components"; +import { Group, Mesh as MeshComponent, InstancedMesh } from "../../components"; import { ThreeElements } from "@react-three/fiber"; type Mesh = THREE.Mesh & { instanceMatrix: InstancedBufferAttribute | undefined }; diff --git a/frontend/three_d_garden/parts/gantry_wheel_plate.tsx b/frontend/three_d_garden/bot/parts/gantry_wheel_plate.tsx similarity index 99% rename from frontend/three_d_garden/parts/gantry_wheel_plate.tsx rename to frontend/three_d_garden/bot/parts/gantry_wheel_plate.tsx index d9a4a73d12..f5fc14864a 100644 --- a/frontend/three_d_garden/parts/gantry_wheel_plate.tsx +++ b/frontend/three_d_garden/bot/parts/gantry_wheel_plate.tsx @@ -3,7 +3,7 @@ import React from "react"; import * as THREE from "three"; import { InstancedBufferAttribute } from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent, InstancedMesh } from "../components"; +import { Group, Mesh as MeshComponent, InstancedMesh } from "../../components"; import { ThreeElements } from "@react-three/fiber"; type Mesh = THREE.Mesh & { instanceMatrix: InstancedBufferAttribute | undefined }; diff --git a/frontend/three_d_garden/bot/parts/index.ts b/frontend/three_d_garden/bot/parts/index.ts new file mode 100644 index 0000000000..f85e6f1c09 --- /dev/null +++ b/frontend/three_d_garden/bot/parts/index.ts @@ -0,0 +1,7 @@ +export * from "./cross_slide"; +export * from "./gantry_wheel_plate"; +export * from "./rotary_tool"; +export * from "./seed_trough_assembly"; +export * from "./seed_trough_holder"; +export * from "./soil_sensor"; +export * from "./vacuum_pump_cover"; diff --git a/frontend/three_d_garden/parts/rotary_tool.tsx b/frontend/three_d_garden/bot/parts/rotary_tool.tsx similarity index 99% rename from frontend/three_d_garden/parts/rotary_tool.tsx rename to frontend/three_d_garden/bot/parts/rotary_tool.tsx index 8a9101d0ce..097eb0c1f8 100644 --- a/frontend/three_d_garden/parts/rotary_tool.tsx +++ b/frontend/three_d_garden/bot/parts/rotary_tool.tsx @@ -3,7 +3,7 @@ import React from "react"; import * as THREE from "three"; import { InstancedBufferAttribute } from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent, InstancedMesh } from "../components"; +import { Group, Mesh as MeshComponent, InstancedMesh } from "../../components"; import { ThreeElements } from "@react-three/fiber"; type Mesh = THREE.Mesh & { instanceMatrix: InstancedBufferAttribute | undefined }; diff --git a/frontend/three_d_garden/parts/seed_trough_assembly.tsx b/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx similarity index 91% rename from frontend/three_d_garden/parts/seed_trough_assembly.tsx rename to frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx index 6094f1385e..2757f9c52a 100644 --- a/frontend/three_d_garden/parts/seed_trough_assembly.tsx +++ b/frontend/three_d_garden/bot/parts/seed_trough_assembly.tsx @@ -2,8 +2,8 @@ import React from "react"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent } from "../components"; -import { SeedTroughAssemblyMaterial } from "../constants"; +import { Group, Mesh as MeshComponent } from "../../components"; +import { SeedTroughAssemblyMaterial } from "../../constants"; import { ThreeElements } from "@react-three/fiber"; export type SeedTroughAssemblyFull = GLTF & { diff --git a/frontend/three_d_garden/parts/seed_trough_holder.tsx b/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx similarity index 90% rename from frontend/three_d_garden/parts/seed_trough_holder.tsx rename to frontend/three_d_garden/bot/parts/seed_trough_holder.tsx index 4a40cea920..47c6095d94 100644 --- a/frontend/three_d_garden/parts/seed_trough_holder.tsx +++ b/frontend/three_d_garden/bot/parts/seed_trough_holder.tsx @@ -2,8 +2,8 @@ import React from "react"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent } from "../components"; -import { SeedTroughHolderMaterial } from "../constants"; +import { Group, Mesh as MeshComponent } from "../../components"; +import { SeedTroughHolderMaterial } from "../../constants"; import { ThreeElements } from "@react-three/fiber"; export type SeedTroughHolderFull = GLTF & { diff --git a/frontend/three_d_garden/parts/soil_sensor.tsx b/frontend/three_d_garden/bot/parts/soil_sensor.tsx similarity index 99% rename from frontend/three_d_garden/parts/soil_sensor.tsx rename to frontend/three_d_garden/bot/parts/soil_sensor.tsx index 33ecfc1d3d..d861c4fa4a 100644 --- a/frontend/three_d_garden/parts/soil_sensor.tsx +++ b/frontend/three_d_garden/bot/parts/soil_sensor.tsx @@ -3,7 +3,7 @@ import React from "react"; import * as THREE from "three"; import { InstancedBufferAttribute } from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent, InstancedMesh } from "../components"; +import { Group, Mesh as MeshComponent, InstancedMesh } from "../../components"; import { ThreeElements } from "@react-three/fiber"; type Mesh = THREE.Mesh & { instanceMatrix: InstancedBufferAttribute | undefined }; diff --git a/frontend/three_d_garden/parts/vacuum_pump_cover.tsx b/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx similarity index 89% rename from frontend/three_d_garden/parts/vacuum_pump_cover.tsx rename to frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx index 1d7335ce0d..cddf0632f2 100644 --- a/frontend/three_d_garden/parts/vacuum_pump_cover.tsx +++ b/frontend/three_d_garden/bot/parts/vacuum_pump_cover.tsx @@ -1,8 +1,8 @@ import React from "react"; import * as THREE from "three"; import { GLTF } from "three-stdlib"; -import { Group, Mesh as MeshComponent } from "../components"; -import { VacuumPumpCoverMaterial } from "../constants"; +import { Group, Mesh as MeshComponent } from "../../components"; +import { VacuumPumpCoverMaterial } from "../../constants"; import { ThreeElements } from "@react-three/fiber"; export type VacuumPumpCoverFull = GLTF & { diff --git a/frontend/three_d_garden/power_supply.tsx b/frontend/three_d_garden/bot/power_supply.tsx similarity index 96% rename from frontend/three_d_garden/power_supply.tsx rename to frontend/three_d_garden/bot/power_supply.tsx index a4e84bd1e7..6e9f1ff9df 100644 --- a/frontend/three_d_garden/power_supply.tsx +++ b/frontend/three_d_garden/bot/power_supply.tsx @@ -3,10 +3,10 @@ import React from "react"; import { RepeatWrapping } from "three"; import * as THREE from "three"; import { Box, Tube, useTexture } from "@react-three/drei"; -import { ASSETS } from "./constants"; -import { threeSpace, easyCubicBezierCurve3 } from "./helpers"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; +import { ASSETS } from "../constants"; +import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; +import { Config } from "../config"; +import { Group, MeshPhongMaterial } from "../components"; export interface PowerSupplyProps { config: Config; diff --git a/frontend/three_d_garden/x_axis_water_tube.tsx b/frontend/three_d_garden/bot/x_axis_water_tube.tsx similarity index 90% rename from frontend/three_d_garden/x_axis_water_tube.tsx rename to frontend/three_d_garden/bot/x_axis_water_tube.tsx index d3df13eed8..551cfc1f0e 100644 --- a/frontend/three_d_garden/x_axis_water_tube.tsx +++ b/frontend/three_d_garden/bot/x_axis_water_tube.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Cylinder, Tube } from "@react-three/drei"; -import { Config } from "./config"; -import { threeSpace, easyCubicBezierCurve3 } from "./helpers"; -import { Group, MeshPhongMaterial } from "./components"; +import { Config } from "../config"; +import { threeSpace, easyCubicBezierCurve3 } from "../helpers"; +import { Group, MeshPhongMaterial } from "../components"; export interface XAxisWaterTubeProps { config: Config; diff --git a/frontend/three_d_garden/__tests__/arrow_test.tsx b/frontend/three_d_garden/elements/__tests__/arrow_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/arrow_test.tsx rename to frontend/three_d_garden/elements/__tests__/arrow_test.tsx diff --git a/frontend/three_d_garden/__tests__/button_test.tsx b/frontend/three_d_garden/elements/__tests__/button_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/button_test.tsx rename to frontend/three_d_garden/elements/__tests__/button_test.tsx diff --git a/frontend/three_d_garden/__tests__/distance_indicator_test.tsx b/frontend/three_d_garden/elements/__tests__/distance_indicator_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/distance_indicator_test.tsx rename to frontend/three_d_garden/elements/__tests__/distance_indicator_test.tsx diff --git a/frontend/three_d_garden/elements/__tests__/text_test.tsx b/frontend/three_d_garden/elements/__tests__/text_test.tsx new file mode 100644 index 0000000000..80dad52894 --- /dev/null +++ b/frontend/three_d_garden/elements/__tests__/text_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Text, TextProps } from "../text"; + +describe("", () => { + const fakeProps = (): TextProps => ({ + children: "text", + position: [0, 0, 0], + rotation: [0, 0, 0], + fontSize: 10, + color: "black", + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("text"); + }); +}); diff --git a/frontend/three_d_garden/arrow.tsx b/frontend/three_d_garden/elements/arrow.tsx similarity index 94% rename from frontend/three_d_garden/arrow.tsx rename to frontend/three_d_garden/elements/arrow.tsx index 8040f0af79..570d599a6b 100644 --- a/frontend/three_d_garden/arrow.tsx +++ b/frontend/three_d_garden/elements/arrow.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Extrude } from "@react-three/drei"; import { Shape } from "three"; -import { MeshPhongMaterial } from "./components"; +import { MeshPhongMaterial } from "../components"; export interface ArrowProps { length: number; diff --git a/frontend/three_d_garden/button.tsx b/frontend/three_d_garden/elements/button.tsx similarity index 97% rename from frontend/three_d_garden/button.tsx rename to frontend/three_d_garden/elements/button.tsx index 449d8500c8..48cb250e44 100644 --- a/frontend/three_d_garden/button.tsx +++ b/frontend/three_d_garden/elements/button.tsx @@ -2,7 +2,7 @@ import React from "react"; import * as THREE from "three"; import { Box } from "@react-three/drei"; import { BufferGeometry } from "three"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../components"; import { Text } from "./text"; export interface PresetButtonProps { diff --git a/frontend/three_d_garden/distance_indicator.tsx b/frontend/three_d_garden/elements/distance_indicator.tsx similarity index 97% rename from frontend/three_d_garden/distance_indicator.tsx rename to frontend/three_d_garden/elements/distance_indicator.tsx index 9dc795e374..73e7482210 100644 --- a/frontend/three_d_garden/distance_indicator.tsx +++ b/frontend/three_d_garden/elements/distance_indicator.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Box } from "@react-three/drei"; import { Arrow } from "./arrow"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../components"; import { Text } from "./text"; enum BoxDimension { diff --git a/frontend/three_d_garden/text.tsx b/frontend/three_d_garden/elements/text.tsx similarity index 85% rename from frontend/three_d_garden/text.tsx rename to frontend/three_d_garden/elements/text.tsx index 0a5d413667..5380f7457f 100644 --- a/frontend/three_d_garden/text.tsx +++ b/frontend/three_d_garden/elements/text.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Center, Text3D } from "@react-three/drei"; -import { ASSETS } from "./constants"; -import { MeshPhongMaterial } from "./components"; +import { ASSETS } from "../constants"; +import { MeshPhongMaterial } from "../components"; -interface TextProps { +export interface TextProps { children: React.ReactNode; position: [number, number, number]; rotation: [number, number, number]; diff --git a/frontend/three_d_garden/garden.tsx b/frontend/three_d_garden/garden.tsx deleted file mode 100644 index e5aaf7e583..0000000000 --- a/frontend/three_d_garden/garden.tsx +++ /dev/null @@ -1,298 +0,0 @@ -import React from "react"; -import { ThreeEvent } from "@react-three/fiber"; -import { - GizmoHelper, GizmoViewcube, - OrbitControls, PerspectiveCamera, - Circle, Stats, Image, Clouds, Cloud, OrthographicCamera, - Detailed, Sphere, - useTexture, - Line, - Cylinder, - Billboard, -} from "@react-three/drei"; -import { RepeatWrapping, BackSide, DoubleSide } from "three"; -import { Bot } from "./bot"; -import { AddPlantProps, Bed } from "./bed"; -import { zero as zeroFunc, extents as extentsFunc, threeSpace } from "./helpers"; -import { Sky } from "./sky"; -import { Config, detailLevels, seasonProperties } from "./config"; -import { ASSETS } from "./constants"; -import { useSpring, animated } from "@react-spring/three"; -import { Solar } from "./solar"; -import { Sun, sunPosition } from "./sun"; -import { Lab } from "./lab"; -import { ZoomBeacons } from "./zoom_beacons"; -import { getCamera, Camera as CameraInterface } from "./zoom_beacons_constants"; -import { - AmbientLight, AxesHelper, Group, MeshBasicMaterial, MeshPhongMaterial, -} from "./components"; -import { isDesktop } from "../screen_size"; -import { isUndefined, range } from "lodash"; -import { ICON_URLS } from "../crops/constants"; -import { calculatePlantPositions, convertPlants, ThreeDPlant } from "./plants"; -import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; -import { BooleanSetting } from "../session_keys"; -import { Greenhouse } from "./greenhouse"; - -const AnimatedGroup = animated(Group); - -export interface GardenModelProps { - config: Config; - activeFocus: string; - setActiveFocus(focus: string): void; - addPlantProps?: AddPlantProps; - mapPoints?: TaggedGenericPointer[]; - weeds?: TaggedWeedPointer[]; -} - -// eslint-disable-next-line complexity -export const GardenModel = (props: GardenModelProps) => { - const { config } = props; - const groundZ = config.bedZOffset + config.bedHeight; - const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera; - - const plants = isUndefined(props.addPlantProps) - ? calculatePlantPositions(config) - : convertPlants(config, props.addPlantProps.plants); - - const [hoveredPlant, setHoveredPlant] = - React.useState(undefined); - - const getI = (e: ThreeEvent) => - e.buttons ? -1 : parseInt(e.intersections[0].object.name); - - const setHover = (active: boolean) => { - return config.labelsOnHover - ? (e: ThreeEvent) => { - e.stopPropagation(); - setHoveredPlant(active ? getI(e) : undefined); - } - : undefined; - }; - - const isXL = config.sizePreset == "Genesis XL"; - const { scale } = useSpring({ - scale: isXL ? 1.75 : 1, - config: { - tension: 300, - friction: 40, - }, - }); - - const grassTexture = useTexture(ASSETS.textures.grass + "?=grass"); - grassTexture.wrapS = RepeatWrapping; - grassTexture.wrapT = RepeatWrapping; - grassTexture.repeat.set(24, 24); - const labFloorTexture = useTexture(ASSETS.textures.concrete + "?=labFloor"); - labFloorTexture.wrapS = RepeatWrapping; - labFloorTexture.wrapT = RepeatWrapping; - labFloorTexture.repeat.set(16, 24); - const brickTexture = useTexture(ASSETS.textures.bricks + "?=bricks"); - brickTexture.wrapS = RepeatWrapping; - brickTexture.wrapT = RepeatWrapping; - brickTexture.repeat.set(30, 30); - - const Ground = ({ children }: { children: React.ReactElement }) => - - {children} - ; - - const initCamera: CameraInterface = { - position: isDesktop() ? [2000, -4000, 2500] : [5400, -2500, 3400], - target: [0, 0, 0], - }; - const camera = getCamera(config, props.activeFocus, initCamera); - - const zero = zeroFunc(config); - const gridZ = zero.z - config.soilHeight + 5; - const extents = extentsFunc(config); - - const getGroundProperties = (sceneName: string) => { - switch (sceneName) { - case "Greenhouse": - return { texture: brickTexture, color: "#999", lowDetailColor: "#8c6f64" }; - case "Lab": - return { texture: labFloorTexture, color: "#aaa", lowDetailColor: "gray" }; - default: - return { texture: grassTexture, color: "#ddd", lowDetailColor: "darkgreen" }; - } - }; - - const groundProperties = getGroundProperties(config.scene); - - // eslint-disable-next-line no-null/no-null - return console.log(e.intersections.map(x => x.object.name)) - : undefined}> - {config.stats && } - {config.zoomBeacons && } - - - - - - - - - - {config.viewCube && - - } - - - - - - - - - - - - - - - - - {ICON_URLS.map((url, i) => )} - - - {plants.map((plant, i) => - )} - - - {range(0, config.botSizeX + 100, 100).map(x => - )} - {range(0, config.botSizeY + 100, 100).map(y => - )} - - - {plants.map((plant, i) => - )} - - - {props.mapPoints?.map(point => - - - - - - - - )} - - - {props.weeds?.map(weed => - - - - - - - - )} - - - - - ; -}; diff --git a/frontend/three_d_garden/garden/__tests__/clouds_test.tsx b/frontend/three_d_garden/garden/__tests__/clouds_test.tsx new file mode 100644 index 0000000000..d2884a48c1 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/clouds_test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Clouds, CloudsProps } from "../clouds"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): CloudsProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("clouds"); + }); +}); diff --git a/frontend/three_d_garden/garden/__tests__/grid_test.tsx b/frontend/three_d_garden/garden/__tests__/grid_test.tsx new file mode 100644 index 0000000000..986d9fc40b --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/grid_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Grid, GridProps } from "../grid"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GridProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const p = fakeProps(); + p.config.grid = true; + const { container } = render(); + expect(container).toContainHTML("grid"); + }); +}); diff --git a/frontend/three_d_garden/garden/__tests__/ground_test.tsx b/frontend/three_d_garden/garden/__tests__/ground_test.tsx new file mode 100644 index 0000000000..83e4d63ef2 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/ground_test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Ground, GroundProps } from "../ground"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; + +describe("", () => { + const fakeProps = (): GroundProps => ({ + config: clone(INITIAL), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("ground"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx similarity index 94% rename from frontend/three_d_garden/__tests__/plants_test.tsx rename to frontend/three_d_garden/garden/__tests__/plants_test.tsx index 32bc15c344..7aed3aadb3 100644 --- a/frontend/three_d_garden/__tests__/plants_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx @@ -1,15 +1,15 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { clone } from "lodash"; -import { fakePlant } from "../../__test_support__/fake_state/resources"; -import { INITIAL } from "../config"; +import { fakePlant } from "../../../__test_support__/fake_state/resources"; +import { INITIAL } from "../../config"; import { calculatePlantPositions, convertPlants, ThreeDPlant, ThreeDPlantProps, } from "../plants"; -import { CROPS } from "../../crops/constants"; +import { CROPS } from "../../../crops/constants"; describe("calculatePlantPositions()", () => { it("calculates plant positions", () => { diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx new file mode 100644 index 0000000000..84dd84b877 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Point, PointProps } from "../point"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; +import { fakePoint } from "../../../__test_support__/fake_state/resources"; + +describe("", () => { + const fakeProps = (): PointProps => ({ + config: clone(INITIAL), + point: fakePoint(), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("cylinder"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/sky_test.tsx b/frontend/three_d_garden/garden/__tests__/sky_test.tsx similarity index 100% rename from frontend/three_d_garden/__tests__/sky_test.tsx rename to frontend/three_d_garden/garden/__tests__/sky_test.tsx diff --git a/frontend/three_d_garden/__tests__/solar_test.tsx b/frontend/three_d_garden/garden/__tests__/solar_test.tsx similarity index 92% rename from frontend/three_d_garden/__tests__/solar_test.tsx rename to frontend/three_d_garden/garden/__tests__/solar_test.tsx index 99192e2e2b..aec76f6bb8 100644 --- a/frontend/three_d_garden/__tests__/solar_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/solar_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { Solar, SolarProps } from "../solar"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/__tests__/sun_test.tsx b/frontend/three_d_garden/garden/__tests__/sun_test.tsx similarity index 90% rename from frontend/three_d_garden/__tests__/sun_test.tsx rename to frontend/three_d_garden/garden/__tests__/sun_test.tsx index caff85ec11..695ff31e0d 100644 --- a/frontend/three_d_garden/__tests__/sun_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/sun_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { mount } from "enzyme"; import { Sun, SunProps } from "../sun"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx new file mode 100644 index 0000000000..d202fd1529 --- /dev/null +++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Weed, WeedProps } from "../weed"; +import { INITIAL } from "../../config"; +import { clone } from "lodash"; +import { fakeWeed } from "../../../__test_support__/fake_state/resources"; + +describe("", () => { + const fakeProps = (): WeedProps => ({ + config: clone(INITIAL), + weed: fakeWeed(), + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("weed"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/zoom_beacons_test.tsx b/frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx similarity index 97% rename from frontend/three_d_garden/__tests__/zoom_beacons_test.tsx rename to frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx index a53f42cd48..0f2e3cee7e 100644 --- a/frontend/three_d_garden/__tests__/zoom_beacons_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/zoom_beacons_test.tsx @@ -1,5 +1,5 @@ let mockIsDesktop = true; -jest.mock("../../screen_size", () => ({ +jest.mock("../../../screen_size", () => ({ isDesktop: () => mockIsDesktop, })); @@ -7,7 +7,7 @@ import React from "react"; import { mount } from "enzyme"; import { ZoomBeacons, ZoomBeaconsProps } from "../zoom_beacons"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; describe("", () => { beforeEach(() => { diff --git a/frontend/three_d_garden/garden/clouds.tsx b/frontend/three_d_garden/garden/clouds.tsx new file mode 100644 index 0000000000..4ae51beef3 --- /dev/null +++ b/frontend/three_d_garden/garden/clouds.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Config, seasonProperties } from "../config"; +import { Cloud, Clouds as DreiClouds } from "@react-three/drei"; +import { ASSETS } from "../constants"; + +export interface CloudsProps { + config: Config; +} + +export const Clouds = (props: CloudsProps) => { + const { config } = props; + return + + ; +}; diff --git a/frontend/three_d_garden/garden/grid.tsx b/frontend/three_d_garden/garden/grid.tsx new file mode 100644 index 0000000000..821382507b --- /dev/null +++ b/frontend/three_d_garden/garden/grid.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Config } from "../config"; +import { Group } from "../components"; +import { Line } from "@react-three/drei"; +import { zero as zeroFunc, extents as extentsFunc } from "../helpers"; +import { range } from "lodash"; + +export interface GridProps { + config: Config; +} + +export const Grid = (props: GridProps) => { + const { config } = props; + const zero = zeroFunc(config); + const gridZ = zero.z - config.soilHeight + 5; + const extents = extentsFunc(config); + return + {range(0, config.botSizeX + 100, 100).map(x => + )} + {range(0, config.botSizeY + 100, 100).map(y => + )} + ; +}; diff --git a/frontend/three_d_garden/garden/ground.tsx b/frontend/three_d_garden/garden/ground.tsx new file mode 100644 index 0000000000..75dd2e14be --- /dev/null +++ b/frontend/three_d_garden/garden/ground.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Config, detailLevels } from "../config"; +import { Circle, Detailed, useTexture } from "@react-three/drei"; +import { MeshPhongMaterial } from "../components"; +import { ASSETS } from "../constants"; +import { RepeatWrapping } from "three"; + +export interface GroundProps { + config: Config; +} + +export const Ground = (props: GroundProps) => { + const { config } = props; + const groundZ = config.bedZOffset + config.bedHeight; + + const grassTexture = useTexture(ASSETS.textures.grass + "?=grass"); + grassTexture.wrapS = RepeatWrapping; + grassTexture.wrapT = RepeatWrapping; + grassTexture.repeat.set(24, 24); + const labFloorTexture = useTexture(ASSETS.textures.concrete + "?=labFloor"); + labFloorTexture.wrapS = RepeatWrapping; + labFloorTexture.wrapT = RepeatWrapping; + labFloorTexture.repeat.set(16, 24); + const brickTexture = useTexture(ASSETS.textures.bricks + "?=bricks"); + brickTexture.wrapS = RepeatWrapping; + brickTexture.wrapT = RepeatWrapping; + brickTexture.repeat.set(30, 30); + + const getGroundProperties = (sceneName: string) => { + switch (sceneName) { + case "Greenhouse": + return { texture: brickTexture, color: "#999", lowDetailColor: "#8c6f64" }; + case "Lab": + return { texture: labFloorTexture, color: "#aaa", lowDetailColor: "gray" }; + default: + return { texture: grassTexture, color: "#ddd", lowDetailColor: "darkgreen" }; + } + }; + + const groundProperties = getGroundProperties(config.scene); + + const GroundWrapper = ({ children }: { children: React.ReactElement }) => + + {children} + ; + + return + + + + + + + ; +}; diff --git a/frontend/three_d_garden/garden/index.ts b/frontend/three_d_garden/garden/index.ts new file mode 100644 index 0000000000..564fcf8827 --- /dev/null +++ b/frontend/three_d_garden/garden/index.ts @@ -0,0 +1,10 @@ +export * from "./clouds"; +export * from "./grid"; +export * from "./ground"; +export * from "./plants"; +export * from "./point"; +export * from "./sky"; +export * from "./solar"; +export * from "./sun"; +export * from "./weed"; +export * from "./zoom_beacons"; diff --git a/frontend/three_d_garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx similarity index 90% rename from frontend/three_d_garden/plants.tsx rename to frontend/three_d_garden/garden/plants.tsx index d251917c50..b1a09c9d66 100644 --- a/frontend/three_d_garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -1,12 +1,12 @@ -import { TaggedPlant } from "../farm_designer/map/interfaces"; -import { Config } from "./config"; -import { GARDENS, PLANTS } from "./constants"; +import { TaggedPlant } from "../../farm_designer/map/interfaces"; +import { Config } from "../config"; +import { GARDENS, PLANTS } from "../constants"; import { Billboard, Image } from "@react-three/drei"; import React from "react"; import { Vector3 } from "three"; -import { threeSpace, zZero as zZeroFunc } from "./helpers"; -import { Text } from "./text"; -import { findIcon } from "../crops/find"; +import { threeSpace, zZero as zZeroFunc } from "../helpers"; +import { Text } from "../elements/text"; +import { findIcon } from "../../crops/find"; import { kebabCase } from "lodash"; interface Plant { diff --git a/frontend/three_d_garden/garden/point.tsx b/frontend/three_d_garden/garden/point.tsx new file mode 100644 index 0000000000..e22e5561fc --- /dev/null +++ b/frontend/three_d_garden/garden/point.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { TaggedGenericPointer } from "farmbot"; +import { Config } from "../config"; +import { Group, MeshPhongMaterial } from "../components"; +import { Cylinder, Sphere } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { zero as zeroFunc, threeSpace } from "../helpers"; + +export interface PointProps { + point: TaggedGenericPointer; + config: Config; +} + +export const Point = (props: PointProps) => { + const { point, config } = props; + const RADIUS = 25; + const HEIGHT = 100; + return + + + + + + + + + + ; +}; diff --git a/frontend/three_d_garden/sky.tsx b/frontend/three_d_garden/garden/sky.tsx similarity index 97% rename from frontend/three_d_garden/sky.tsx rename to frontend/three_d_garden/garden/sky.tsx index b406e47447..c2eb96b49d 100644 --- a/frontend/three_d_garden/sky.tsx +++ b/frontend/three_d_garden/garden/sky.tsx @@ -5,7 +5,7 @@ import { Vector3 as Vector3Type } from "@react-three/fiber"; import { Sky as SkyImpl } from "three-stdlib"; import { Vector3 } from "three"; import { ForwardRefComponent } from "@react-three/drei/helpers/ts-utils"; -import { Primitive } from "./components"; +import { Primitive } from "../components"; export type SkyProps = { distance?: number diff --git a/frontend/three_d_garden/solar.tsx b/frontend/three_d_garden/garden/solar.tsx similarity index 94% rename from frontend/three_d_garden/solar.tsx rename to frontend/three_d_garden/garden/solar.tsx index da647d24d9..be72a50ddf 100644 --- a/frontend/three_d_garden/solar.tsx +++ b/frontend/three_d_garden/garden/solar.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Shape } from "three"; import { Extrude, Line } from "@react-three/drei"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, Mesh, BoxGeometry, MeshPhongMaterial } from "./components"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Group, Mesh, BoxGeometry, MeshPhongMaterial } from "../components"; export interface SolarProps { config: Config; diff --git a/frontend/three_d_garden/sun.tsx b/frontend/three_d_garden/garden/sun.tsx similarity index 93% rename from frontend/three_d_garden/sun.tsx rename to frontend/three_d_garden/garden/sun.tsx index fed1db4b85..9079fb57fc 100644 --- a/frontend/three_d_garden/sun.tsx +++ b/frontend/three_d_garden/garden/sun.tsx @@ -1,8 +1,8 @@ import React from "react"; -import { Config, seasonProperties } from "./config"; +import { Config, seasonProperties } from "../config"; import { Vector3 } from "three"; import { useSpring, animated } from "@react-spring/three"; -import { Group, PointLight } from "./components"; +import { Group, PointLight } from "../components"; const AnimatedPointLight = animated(PointLight); diff --git a/frontend/three_d_garden/garden/weed.tsx b/frontend/three_d_garden/garden/weed.tsx new file mode 100644 index 0000000000..609ca6c79d --- /dev/null +++ b/frontend/three_d_garden/garden/weed.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { TaggedWeedPointer } from "farmbot"; +import { Config } from "../config"; +import { ASSETS } from "../constants"; +import { Group, MeshPhongMaterial } from "../components"; +import { Image, Billboard, Sphere } from "@react-three/drei"; +import { DoubleSide } from "three"; +import { zero as zeroFunc, threeSpace } from "../helpers"; + +export interface WeedProps { + weed: TaggedWeedPointer; + config: Config; +} + +export const Weed = (props: WeedProps) => { + const { weed, config } = props; + return + + + + + + + ; +}; diff --git a/frontend/three_d_garden/zoom_beacons.tsx b/frontend/three_d_garden/garden/zoom_beacons.tsx similarity index 94% rename from frontend/three_d_garden/zoom_beacons.tsx rename to frontend/three_d_garden/garden/zoom_beacons.tsx index 690e55e05f..98a8a1536d 100644 --- a/frontend/three_d_garden/zoom_beacons.tsx +++ b/frontend/three_d_garden/garden/zoom_beacons.tsx @@ -1,10 +1,10 @@ import { Sphere, Html, Line } from "@react-three/drei"; import React from "react"; -import { Config } from "./config"; -import { FOCI, getCameraOffset, setUrlParam } from "./zoom_beacons_constants"; +import { Config } from "../config"; +import { FOCI, getCameraOffset, setUrlParam } from "../zoom_beacons_constants"; import { useSpring, animated } from "@react-spring/three"; -import { Group, Mesh, MeshPhongMaterial } from "./components"; -import { isDesktop } from "../screen_size"; +import { Group, Mesh, MeshPhongMaterial } from "../components"; +import { isDesktop } from "../../screen_size"; const beaconColor = "#0266b5"; diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx new file mode 100644 index 0000000000..ce550ff638 --- /dev/null +++ b/frontend/three_d_garden/garden_model.tsx @@ -0,0 +1,161 @@ +import React from "react"; +import { ThreeEvent } from "@react-three/fiber"; +import { + GizmoHelper, GizmoViewcube, + OrbitControls, PerspectiveCamera, + Stats, Image, OrthographicCamera, + Sphere, +} from "@react-three/drei"; +import { BackSide } from "three"; +import { Bot } from "./bot/bot"; +import { AddPlantProps, Bed } from "./bed/bed"; +import { + Sky, Solar, Sun, sunPosition, ZoomBeacons, + calculatePlantPositions, convertPlants, ThreeDPlant, + Point, Grid, Clouds, Ground, Weed, +} from "./garden"; +import { Config } from "./config"; +import { useSpring, animated } from "@react-spring/three"; +import { Lab, Greenhouse } from "./scenes"; +import { getCamera, Camera as CameraInterface } from "./zoom_beacons_constants"; +import { + AmbientLight, AxesHelper, Group, MeshBasicMaterial, +} from "./components"; +import { isDesktop } from "../screen_size"; +import { isUndefined } from "lodash"; +import { ICON_URLS } from "../crops/constants"; +import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; +import { BooleanSetting } from "../session_keys"; + +const AnimatedGroup = animated(Group); + +export interface GardenModelProps { + config: Config; + activeFocus: string; + setActiveFocus(focus: string): void; + addPlantProps?: AddPlantProps; + mapPoints?: TaggedGenericPointer[]; + weeds?: TaggedWeedPointer[]; +} + +export const GardenModel = (props: GardenModelProps) => { + const { config } = props; + const Camera = config.perspective ? PerspectiveCamera : OrthographicCamera; + + const plants = isUndefined(props.addPlantProps) + ? calculatePlantPositions(config) + : convertPlants(config, props.addPlantProps.plants); + + const [hoveredPlant, setHoveredPlant] = + React.useState(undefined); + + const getI = (e: ThreeEvent) => + e.buttons ? -1 : parseInt(e.intersections[0].object.name); + + const setHover = (active: boolean) => { + return config.labelsOnHover + ? (e: ThreeEvent) => { + e.stopPropagation(); + setHoveredPlant(active ? getI(e) : undefined); + } + : undefined; + }; + + const isXL = config.sizePreset == "Genesis XL"; + const { scale } = useSpring({ + scale: isXL ? 1.75 : 1, + config: { + tension: 300, + friction: 40, + }, + }); + + const initCamera: CameraInterface = { + position: isDesktop() ? [2000, -4000, 2500] : [5400, -2500, 3400], + target: [0, 0, 0], + }; + const camera = getCamera(config, props.activeFocus, initCamera); + + // eslint-disable-next-line no-null/no-null + return console.log(e.intersections.map(x => x.object.name)) + : undefined}> + {config.stats && } + {config.zoomBeacons && } + + + + + + + + + + {config.viewCube && + + } + + + + + + + + {ICON_URLS.map((url, i) => )} + + + {plants.map((plant, i) => + )} + + + + {plants.map((plant, i) => + )} + + + {props.mapPoints?.map(point => + )} + + + {props.weeds?.map(weed => + )} + + + + + ; +}; diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx index 82c19b0042..c3ae54f2c4 100644 --- a/frontend/three_d_garden/index.tsx +++ b/frontend/three_d_garden/index.tsx @@ -1,9 +1,9 @@ import { Canvas } from "@react-three/fiber"; import React from "react"; import { Config } from "./config"; -import { GardenModel } from "./garden"; +import { GardenModel } from "./garden_model"; import { noop } from "lodash"; -import { AddPlantProps } from "./bed"; +import { AddPlantProps } from "./bed/bed"; import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; export interface ThreeDGardenProps { diff --git a/frontend/three_d_garden/__tests__/greenhouse_test.tsx b/frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx similarity index 97% rename from frontend/three_d_garden/__tests__/greenhouse_test.tsx rename to frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx index 52f9debe39..4a834e2e66 100644 --- a/frontend/three_d_garden/__tests__/greenhouse_test.tsx +++ b/frontend/three_d_garden/scenes/__tests__/greenhouse_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { Greenhouse, GreenhouseProps } from "../greenhouse"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/__tests__/lab_test.tsx b/frontend/three_d_garden/scenes/__tests__/lab_test.tsx similarity index 96% rename from frontend/three_d_garden/__tests__/lab_test.tsx rename to frontend/three_d_garden/scenes/__tests__/lab_test.tsx index 5ad6c55022..d64521afe1 100644 --- a/frontend/three_d_garden/__tests__/lab_test.tsx +++ b/frontend/three_d_garden/scenes/__tests__/lab_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { Lab, LabProps } from "../lab"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/greenhouse.tsx b/frontend/three_d_garden/scenes/greenhouse.tsx similarity index 88% rename from frontend/three_d_garden/greenhouse.tsx rename to frontend/three_d_garden/scenes/greenhouse.tsx index d7fdf7c331..1d38fc7fcc 100644 --- a/frontend/three_d_garden/greenhouse.tsx +++ b/frontend/three_d_garden/scenes/greenhouse.tsx @@ -1,14 +1,11 @@ import React from "react"; import { Box, useTexture } from "@react-three/drei"; import { DoubleSide, RepeatWrapping } from "three"; -import { ASSETS } from "./constants"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; -import { StarterTray } from "./starter_tray"; -import { PottedPlant } from "./potted_plant"; -import { GreenhouseWall } from "./greenhouse_wall"; -import { People } from "./people"; +import { ASSETS } from "../constants"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Group, MeshPhongMaterial } from "../components"; +import { StarterTray, PottedPlant, GreenhouseWall, People } from "./props"; export interface GreenhouseProps { config: Config; diff --git a/frontend/three_d_garden/scenes/index.ts b/frontend/three_d_garden/scenes/index.ts new file mode 100644 index 0000000000..1a1721be73 --- /dev/null +++ b/frontend/three_d_garden/scenes/index.ts @@ -0,0 +1,2 @@ +export * from "./lab"; +export * from "./greenhouse"; diff --git a/frontend/three_d_garden/lab.tsx b/frontend/three_d_garden/scenes/lab.tsx similarity index 91% rename from frontend/three_d_garden/lab.tsx rename to frontend/three_d_garden/scenes/lab.tsx index 3059f2a2e2..ea2262eaa7 100644 --- a/frontend/three_d_garden/lab.tsx +++ b/frontend/three_d_garden/scenes/lab.tsx @@ -1,12 +1,11 @@ import React from "react"; import { Box, Extrude, useTexture } from "@react-three/drei"; import { DoubleSide, Shape, RepeatWrapping } from "three"; -import { ASSETS } from "./constants"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Desk } from "./desk"; -import { Group, MeshPhongMaterial } from "./components"; -import { People } from "./people"; +import { ASSETS } from "../constants"; +import { threeSpace } from "../helpers"; +import { Config } from "../config"; +import { Desk, People } from "./props"; +import { Group, MeshPhongMaterial } from "../components"; export interface LabProps { config: Config; diff --git a/frontend/three_d_garden/__tests__/desk_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx similarity index 90% rename from frontend/three_d_garden/__tests__/desk_test.tsx rename to frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx index 4e1f3246f6..bfbd4919be 100644 --- a/frontend/three_d_garden/__tests__/desk_test.tsx +++ b/frontend/three_d_garden/scenes/props/__tests__/desk_test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { Desk, DeskProps } from "../desk"; import { clone } from "lodash"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; describe("", () => { const fakeProps = (): DeskProps => ({ diff --git a/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx new file mode 100644 index 0000000000..3072c5dafc --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/greenhouse_wall_test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { GreenhouseWall } from "../greenhouse_wall"; + +describe("", () => { + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("greenhouse-wall"); + }); +}); diff --git a/frontend/three_d_garden/__tests__/people_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx similarity index 91% rename from frontend/three_d_garden/__tests__/people_test.tsx rename to frontend/three_d_garden/scenes/props/__tests__/people_test.tsx index db0e88ccd5..0494efaba6 100644 --- a/frontend/three_d_garden/__tests__/people_test.tsx +++ b/frontend/three_d_garden/scenes/props/__tests__/people_test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { People, PeopleProps } from "../people"; -import { INITIAL } from "../config"; +import { INITIAL } from "../../../config"; import { clone } from "lodash"; describe("", () => { diff --git a/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx new file mode 100644 index 0000000000..d0698f53b6 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/potted_plant_test.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { PottedPlant } from "../potted_plant"; + +describe("", () => { + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("pot-with-plant"); + }); +}); diff --git a/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx b/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx new file mode 100644 index 0000000000..12b115fca6 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/__tests__/starter_tray_test.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { render } from "@testing-library/react"; +import { Desk, DeskProps } from "../desk"; +import { clone } from "lodash"; +import { INITIAL } from "../../../config"; + +describe("", () => { + const fakeProps = (): DeskProps => ({ + config: clone(INITIAL), + activeFocus: "", + }); + + it("renders", () => { + const { container } = render(); + expect(container).toContainHTML("desk"); + }); +}); diff --git a/frontend/three_d_garden/desk.tsx b/frontend/three_d_garden/scenes/props/desk.tsx similarity index 94% rename from frontend/three_d_garden/desk.tsx rename to frontend/three_d_garden/scenes/props/desk.tsx index fe129c5acf..159caa60ce 100644 --- a/frontend/three_d_garden/desk.tsx +++ b/frontend/three_d_garden/scenes/props/desk.tsx @@ -1,10 +1,10 @@ import React from "react"; import { RepeatWrapping } from "three"; import { Box, useTexture } from "@react-three/drei"; -import { ASSETS } from "./constants"; -import { threeSpace } from "./helpers"; -import { Config } from "./config"; -import { Group, MeshPhongMaterial } from "./components"; +import { ASSETS } from "../../constants"; +import { threeSpace } from "../../helpers"; +import { Config } from "../../config"; +import { Group, MeshPhongMaterial } from "../../components"; export interface DeskProps { config: Config; diff --git a/frontend/three_d_garden/greenhouse_wall.tsx b/frontend/three_d_garden/scenes/props/greenhouse_wall.tsx similarity index 97% rename from frontend/three_d_garden/greenhouse_wall.tsx rename to frontend/three_d_garden/scenes/props/greenhouse_wall.tsx index 6c55e31c52..ed186f7539 100644 --- a/frontend/three_d_garden/greenhouse_wall.tsx +++ b/frontend/three_d_garden/scenes/props/greenhouse_wall.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Box } from "@react-three/drei"; import { DoubleSide } from "three"; -import { Group, MeshPhongMaterial } from "./components"; +import { Group, MeshPhongMaterial } from "../../components"; import { range } from "lodash"; const wallLength = 10000; diff --git a/frontend/three_d_garden/scenes/props/index.ts b/frontend/three_d_garden/scenes/props/index.ts new file mode 100644 index 0000000000..2dc0930c90 --- /dev/null +++ b/frontend/three_d_garden/scenes/props/index.ts @@ -0,0 +1,5 @@ +export * from "./desk"; +export * from "./greenhouse_wall"; +export * from "./people"; +export * from "./potted_plant"; +export * from "./starter_tray"; diff --git a/frontend/three_d_garden/people.tsx b/frontend/three_d_garden/scenes/props/people.tsx similarity index 91% rename from frontend/three_d_garden/people.tsx rename to frontend/three_d_garden/scenes/props/people.tsx index 89e8178dab..ef53a3c0aa 100644 --- a/frontend/three_d_garden/people.tsx +++ b/frontend/three_d_garden/scenes/props/people.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Billboard, Image } from "@react-three/drei"; -import { Group } from "./components"; -import { Config } from "./config"; -import { threeSpace } from "./helpers"; +import { Group } from "../../components"; +import { Config } from "../../config"; +import { threeSpace } from "../../helpers"; import { Vector3 } from "three"; -import { ASSETS } from "./constants"; +import { ASSETS } from "../../constants"; export interface PeopleProps { config: Config; diff --git a/frontend/three_d_garden/potted_plant.tsx b/frontend/three_d_garden/scenes/props/potted_plant.tsx similarity index 95% rename from frontend/three_d_garden/potted_plant.tsx rename to frontend/three_d_garden/scenes/props/potted_plant.tsx index 06eb00f3f0..0c4efe3712 100644 --- a/frontend/three_d_garden/potted_plant.tsx +++ b/frontend/three_d_garden/scenes/props/potted_plant.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from "react"; import { Billboard, Circle, Image } from "@react-three/drei"; import * as THREE from "three"; -import { Group, MeshPhongMaterial, Mesh } from "./components"; +import { Group, MeshPhongMaterial, Mesh } from "../../components"; const potHeight = 400; const plantHeight = 500; diff --git a/frontend/three_d_garden/starter_tray.tsx b/frontend/three_d_garden/scenes/props/starter_tray.tsx similarity index 91% rename from frontend/three_d_garden/starter_tray.tsx rename to frontend/three_d_garden/scenes/props/starter_tray.tsx index 5a12cd5091..062341b9ed 100644 --- a/frontend/three_d_garden/starter_tray.tsx +++ b/frontend/three_d_garden/scenes/props/starter_tray.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Box, Billboard, Image } from "@react-three/drei"; import { DoubleSide } from "three"; -import { ASSETS } from "./constants"; -import { Group, MeshPhongMaterial } from "./components"; +import { ASSETS } from "../../constants"; +import { Group, MeshPhongMaterial } from "../../components"; import { range } from "lodash"; const length = 250; diff --git a/package.json b/package.json index ca18a0155a..eee504ed7f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "test-very-slow": "node --expose-gc ./node_modules/.bin/jest -i --colors --coverage", "test-slow": "./node_modules/.bin/jest -w 6 --colors", "test": "./node_modules/.bin/jest -w 5 --no-coverage", + "graph-modules-dot": "./node_modules/.bin/madge --dot ./frontend > module_graph.dot", + "graph-modules-svg": "dot -Tsvg module_graph.dot -o module_graph.svg", "typecheck": "./node_modules/typescript/bin/tsc --noEmit", "dev-typecheck": "./node_modules/typescript/bin/tsc --project tsconfig.dev.json --noEmit", "eslint": "./node_modules/.bin/eslint frontend public/app-resources/languages --ext .ts,.tsx", From a44f9473475d203bbb8db2eaa46e51b4f50930ea Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 7 Feb 2025 09:56:24 -0800 Subject: [PATCH 17/27] add more index files --- frontend/__test_support__/fake_props.ts | 2 +- frontend/three_d_garden/bed/bed.tsx | 2 +- frontend/three_d_garden/bed/index.ts | 1 + frontend/three_d_garden/bed/objects/farmbot_axes.tsx | 2 +- frontend/three_d_garden/bed/objects/packaging.tsx | 2 +- frontend/three_d_garden/bed/objects/utilities_post.tsx | 2 +- frontend/three_d_garden/bot/bot.tsx | 2 +- frontend/three_d_garden/bot/index.ts | 3 +++ frontend/three_d_garden/elements/index.ts | 4 ++++ frontend/three_d_garden/garden/plants.tsx | 2 +- frontend/three_d_garden/garden_model.tsx | 4 ++-- frontend/three_d_garden/index.tsx | 2 +- 12 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 frontend/three_d_garden/bed/index.ts create mode 100644 frontend/three_d_garden/bot/index.ts create mode 100644 frontend/three_d_garden/elements/index.ts diff --git a/frontend/__test_support__/fake_props.ts b/frontend/__test_support__/fake_props.ts index f96f77d378..68a4c9dfa2 100644 --- a/frontend/__test_support__/fake_props.ts +++ b/frontend/__test_support__/fake_props.ts @@ -1,5 +1,5 @@ import { TaggedPlant } from "../farm_designer/map/interfaces"; -import { AddPlantProps } from "../three_d_garden/bed/bed"; +import { AddPlantProps } from "../three_d_garden/bed"; import { fakeDesignerState } from "./fake_designer_state"; export const fakeAddPlantProps = diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx index ce95db308d..8784b9dd97 100644 --- a/frontend/three_d_garden/bed/bed.tsx +++ b/frontend/three_d_garden/bed/bed.tsx @@ -9,7 +9,7 @@ import { range } from "lodash"; import { threeSpace, zZero, getColorFromBrightness } from "../helpers"; import { Config, detailLevels } from "../config"; import { ASSETS } from "../constants"; -import { DistanceIndicator } from "../elements/distance_indicator"; +import { DistanceIndicator } from "../elements"; import { FarmbotAxes, Caster, UtilitiesPost, Packaging } from "./objects"; import { Group, MeshPhongMaterial } from "../components"; import { getMode, round } from "../../farm_designer/map/util"; diff --git a/frontend/three_d_garden/bed/index.ts b/frontend/three_d_garden/bed/index.ts new file mode 100644 index 0000000000..316c67602b --- /dev/null +++ b/frontend/three_d_garden/bed/index.ts @@ -0,0 +1 @@ +export * from "./bed"; diff --git a/frontend/three_d_garden/bed/objects/farmbot_axes.tsx b/frontend/three_d_garden/bed/objects/farmbot_axes.tsx index 1bd0520b65..770eafcbcf 100644 --- a/frontend/three_d_garden/bed/objects/farmbot_axes.tsx +++ b/frontend/three_d_garden/bed/objects/farmbot_axes.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Config } from "../../config"; -import { Arrow } from "../../elements/arrow"; +import { Arrow } from "../../elements"; import { threeSpace, zZero } from "../../helpers"; import { Group } from "../../components"; diff --git a/frontend/three_d_garden/bed/objects/packaging.tsx b/frontend/three_d_garden/bed/objects/packaging.tsx index 3f0e05fd21..e601b3e2e1 100644 --- a/frontend/three_d_garden/bed/objects/packaging.tsx +++ b/frontend/three_d_garden/bed/objects/packaging.tsx @@ -3,7 +3,7 @@ import { Box } from "@react-three/drei"; import { threeSpace } from "../../helpers"; import { Config } from "../../config"; import { Group, MeshPhongMaterial } from "../../components"; -import { Text } from "../../elements/text"; +import { Text } from "../../elements"; export interface PackagingProps { config: Config; diff --git a/frontend/three_d_garden/bed/objects/utilities_post.tsx b/frontend/three_d_garden/bed/objects/utilities_post.tsx index 4b1b1d34fa..78e821d816 100644 --- a/frontend/three_d_garden/bed/objects/utilities_post.tsx +++ b/frontend/three_d_garden/bed/objects/utilities_post.tsx @@ -6,7 +6,7 @@ import { Config } from "../../config"; import { threeSpace, getColorFromBrightness, easyCubicBezierCurve3, } from "../../helpers"; -import { outletDepth } from "../../bot/power_supply"; +import { outletDepth } from "../../bot"; import * as THREE from "three"; import { Group, MeshPhongMaterial } from "../../components"; diff --git a/frontend/three_d_garden/bot/bot.tsx b/frontend/three_d_garden/bot/bot.tsx index b03d52319d..25c4595af3 100644 --- a/frontend/three_d_garden/bot/bot.tsx +++ b/frontend/three_d_garden/bot/bot.tsx @@ -25,7 +25,7 @@ import { SeedTroughAssembly, SeedTroughAssemblyFull, SeedTroughHolder, SeedTroughHolderFull, } from "./parts"; -import { DistanceIndicator } from "../elements/distance_indicator"; +import { DistanceIndicator } from "../elements"; import { PowerSupply } from "./power_supply"; import { XAxisWaterTube } from "./x_axis_water_tube"; import { Group, Mesh, MeshPhongMaterial } from "../components"; diff --git a/frontend/three_d_garden/bot/index.ts b/frontend/three_d_garden/bot/index.ts new file mode 100644 index 0000000000..7877d4480a --- /dev/null +++ b/frontend/three_d_garden/bot/index.ts @@ -0,0 +1,3 @@ +export * from "./bot"; +export * from "./power_supply"; +export * from "./x_axis_water_tube"; diff --git a/frontend/three_d_garden/elements/index.ts b/frontend/three_d_garden/elements/index.ts new file mode 100644 index 0000000000..5c72cace48 --- /dev/null +++ b/frontend/three_d_garden/elements/index.ts @@ -0,0 +1,4 @@ +export * from "./arrow"; +export * from "./button"; +export * from "./distance_indicator"; +export * from "./text"; diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx index b1a09c9d66..adcb35667d 100644 --- a/frontend/three_d_garden/garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -5,7 +5,7 @@ import { Billboard, Image } from "@react-three/drei"; import React from "react"; import { Vector3 } from "three"; import { threeSpace, zZero as zZeroFunc } from "../helpers"; -import { Text } from "../elements/text"; +import { Text } from "../elements"; import { findIcon } from "../../crops/find"; import { kebabCase } from "lodash"; diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx index ce550ff638..c36d145c81 100644 --- a/frontend/three_d_garden/garden_model.tsx +++ b/frontend/three_d_garden/garden_model.tsx @@ -7,8 +7,8 @@ import { Sphere, } from "@react-three/drei"; import { BackSide } from "three"; -import { Bot } from "./bot/bot"; -import { AddPlantProps, Bed } from "./bed/bed"; +import { Bot } from "./bot"; +import { AddPlantProps, Bed } from "./bed"; import { Sky, Solar, Sun, sunPosition, ZoomBeacons, calculatePlantPositions, convertPlants, ThreeDPlant, diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx index c3ae54f2c4..e0a21b32ed 100644 --- a/frontend/three_d_garden/index.tsx +++ b/frontend/three_d_garden/index.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Config } from "./config"; import { GardenModel } from "./garden_model"; import { noop } from "lodash"; -import { AddPlantProps } from "./bed/bed"; +import { AddPlantProps } from "./bed"; import { TaggedGenericPointer, TaggedWeedPointer } from "farmbot"; export interface ThreeDGardenProps { From 5e01872280d5a87eb56554a4fb9e3d643bfefd30 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Fri, 7 Feb 2025 11:34:01 -0800 Subject: [PATCH 18/27] connect bot position updates to 3D model --- .../__tests__/three_d_garden_map_test.tsx | 16 ++++++++++++++++ frontend/farm_designer/index.tsx | 1 + frontend/farm_designer/three_d_garden_map.tsx | 9 ++++++++- .../__tests__/garden_model_test.tsx | 8 ++++++++ frontend/three_d_garden/garden_model.tsx | 4 +++- 5 files changed, 36 insertions(+), 2 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 cff767bba9..25af12c2c8 100644 --- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx +++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx @@ -27,6 +27,7 @@ describe("", () => { curves: [], mapPoints: [], weeds: [], + botPosition: { x: 1, y: 2, z: 3 }, }); it("converts props", () => { @@ -39,6 +40,9 @@ describe("", () => { expectedConfig.bedWidthOuter = 1660; expectedConfig.botSizeX = 3000; expectedConfig.botSizeY = 1500; + expectedConfig.x = 1; + expectedConfig.y = 2; + expectedConfig.z = 3; expectedConfig.ccSupportSize = 1; expectedConfig.beamLength = 1; expectedConfig.columnLength = 1; @@ -61,4 +65,16 @@ describe("", () => { weeds: [], }, {}); }); + + it("converts props: unknown position", () => { + const p = fakeProps(); + p.botPosition = { x: undefined, y: undefined, z: undefined }; + render(); + expect(ThreeDGarden).toHaveBeenCalledWith({ + config: expect.objectContaining({ x: 0, y: 0, z: 0 }), + addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], + }, {}); + }); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index 8958f1b835..ca715fc44f 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -219,6 +219,7 @@ export class RawFarmDesigner curves={this.props.curves} mapPoints={this.props.genericPoints} weeds={this.props.weeds} + botPosition={this.props.botLocationData.position} getWebAppConfigValue={this.props.getConfigValue} /> :

{ @@ -35,6 +37,11 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.bedWidthOuter = gridSize.y + 160; config.bedLengthOuter = gridSize.x + 160; config.zoomBeacons = false; + config.trail = !!props.getWebAppConfigValue(BooleanSetting.display_trail); + + config.x = props.botPosition.x || 0; + config.y = props.botPosition.y || 0; + config.z = props.botPosition.z || 0; const { designer } = props; config.distanceIndicator = designer.distanceIndicator; diff --git a/frontend/three_d_garden/__tests__/garden_model_test.tsx b/frontend/three_d_garden/__tests__/garden_model_test.tsx index 77ed1f2fed..89697546d4 100644 --- a/frontend/three_d_garden/__tests__/garden_model_test.tsx +++ b/frontend/three_d_garden/__tests__/garden_model_test.tsx @@ -58,6 +58,14 @@ describe("", () => { expect(container).toContainHTML(ASSETS.other.weed); }); + it("doesn't render bot", () => { + const p = fakeProps(); + p.addPlantProps = fakeAddPlantProps([]); + p.addPlantProps.getConfigValue = () => false; + const { container } = render(); + expect(container).not.toContainHTML("bot"); + }); + it("renders promo plants", () => { const p = fakeProps(); p.addPlantProps = undefined; diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx index c36d145c81..9c10b507bf 100644 --- a/frontend/three_d_garden/garden_model.tsx +++ b/frontend/three_d_garden/garden_model.tsx @@ -118,7 +118,9 @@ export const GardenModel = (props: GardenModelProps) => { config={config} activeFocus={props.activeFocus} addPlantProps={props.addPlantProps} /> - + {(!props.addPlantProps + || !!props.addPlantProps.getConfigValue(BooleanSetting.show_farmbot)) && + } {ICON_URLS.map((url, i) => )} From 2591c711fe54cf3829f748b2c73a6fd2991ff8af Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Tue, 11 Feb 2025 14:41:43 -0800 Subject: [PATCH 19/27] handle negative z coordinates in 3D --- .../__tests__/three_d_garden_map_test.tsx | 14 ++++++++++++++ frontend/farm_designer/index.tsx | 1 + frontend/farm_designer/three_d_garden_map.tsx | 5 ++++- 3 files changed, 19 insertions(+), 1 deletion(-) 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 25af12c2c8..e894c83bb8 100644 --- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx +++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx @@ -28,6 +28,7 @@ describe("", () => { mapPoints: [], weeds: [], botPosition: { x: 1, y: 2, z: 3 }, + negativeZ: false, }); it("converts props", () => { @@ -77,4 +78,17 @@ describe("", () => { weeds: [], }, {}); }); + + it("converts props: negative z", () => { + const p = fakeProps(); + p.botPosition = { x: undefined, y: undefined, z: -100 }; + p.negativeZ = true; + render(); + expect(ThreeDGarden).toHaveBeenCalledWith({ + config: expect.objectContaining({ x: 0, y: 0, z: 100 }), + addPlantProps: expect.any(Object), + mapPoints: [], + weeds: [], + }, {}); + }); }); diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index ca715fc44f..977207f7ca 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -212,6 +212,7 @@ export class RawFarmDesigner plants={this.props.plants} get3DConfigValue={get3DConfigValueFunction(this.props.farmwareEnvs)} sourceFbosConfig={this.props.sourceFbosConfig} + negativeZ={!!this.props.botMcuParams.movement_home_up_z} gridOffset={gridOffset} mapTransformProps={this.mapTransformProps} botSize={this.props.botSize} diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx index ecd64eb61f..1d9852d5d3 100644 --- a/frontend/farm_designer/three_d_garden_map.tsx +++ b/frontend/farm_designer/three_d_garden_map.tsx @@ -19,6 +19,7 @@ export interface ThreeDGardenMapProps { gridOffset: AxisNumberProperty; get3DConfigValue(key: string): number; sourceFbosConfig: SourceFbosConfig; + negativeZ: boolean; designer: DesignerState; plants: TaggedPlant[]; dispatch: Function; @@ -39,9 +40,11 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.zoomBeacons = false; config.trail = !!props.getWebAppConfigValue(BooleanSetting.display_trail); + const zDir = props.negativeZ ? -1 : 1; + config.x = props.botPosition.x || 0; config.y = props.botPosition.y || 0; - config.z = props.botPosition.z || 0; + config.z = zDir * (props.botPosition.z || 0); const { designer } = props; config.distanceIndicator = designer.distanceIndicator; From 7263a8c313ef6a73a6ffec2e6172f14d29674334 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Wed, 12 Feb 2025 09:46:26 -0800 Subject: [PATCH 20/27] fix crash upon point search: fixes #2479 --- frontend/points/__tests__/point_inventory_test.tsx | 11 +++++++++++ frontend/points/point_inventory.tsx | 1 + 2 files changed, 12 insertions(+) diff --git a/frontend/points/__tests__/point_inventory_test.tsx b/frontend/points/__tests__/point_inventory_test.tsx index f02e5e8504..7912a41ffe 100644 --- a/frontend/points/__tests__/point_inventory_test.tsx +++ b/frontend/points/__tests__/point_inventory_test.tsx @@ -136,6 +136,17 @@ describe("", () => { expect(wrapper.text()).not.toContain("point 1"); }); + it("filters point grids", () => { + const p = fakeProps(); + const gridPoint = fakePoint(); + gridPoint.body.meta.gridId = "123"; + gridPoint.body.name = "mesh"; + p.genericPoints = [gridPoint]; + const wrapper = mount(); + wrapper.setState({ searchTerm: "0" }); + expect(wrapper.text()).not.toContain("mesh"); + }); + it("changes sort term", () => { const wrapper = shallow(); const menu = wrapper.find(SearchField).props().customLeftIcon; diff --git a/frontend/points/point_inventory.tsx b/frontend/points/point_inventory.tsx index 72dae8f014..7a8d40dd8d 100644 --- a/frontend/points/point_inventory.tsx +++ b/frontend/points/point_inventory.tsx @@ -283,6 +283,7 @@ export class RawPoints extends React.Component { dispatch={dispatch} />)} {gridIds.map(gridId => { const gridPoints = points.filter(p => p.body.meta.gridId == gridId); + if (gridPoints.length == 0) { return
; } const pointName = gridPoints[0].body.name; return Date: Wed, 12 Feb 2025 09:46:50 -0800 Subject: [PATCH 21/27] use filtered count for point groups --- frontend/points/point_inventory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/points/point_inventory.tsx b/frontend/points/point_inventory.tsx index 7a8d40dd8d..e181c29b98 100644 --- a/frontend/points/point_inventory.tsx +++ b/frontend/points/point_inventory.tsx @@ -198,7 +198,7 @@ export class RawPoints extends React.Component { dispatch(createGroup({ criteria: { ...DEFAULT_CRITERIA, From 8dd2079c84ea88a3494f812ce3ee614081263832 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Tue, 18 Feb 2025 15:06:38 -0800 Subject: [PATCH 22/27] fix add tool navigation bug --- .../tools/__tests__/add_tool_slot_test.tsx | 4 +++- frontend/tools/__tests__/add_tool_test.tsx | 20 +++++++++++++------ frontend/tools/add_tool.tsx | 12 ++++++----- frontend/tools/add_tool_slot.tsx | 9 ++++++--- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/frontend/tools/__tests__/add_tool_slot_test.tsx b/frontend/tools/__tests__/add_tool_slot_test.tsx index 4d833aa598..a2c428afc3 100644 --- a/frontend/tools/__tests__/add_tool_slot_test.tsx +++ b/frontend/tools/__tests__/add_tool_slot_test.tsx @@ -70,9 +70,11 @@ describe("", () => { it("saves tool slot", () => { const wrapper = shallow(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("SaveBtn").simulate("click"); expect(save).toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("saves on unmount", () => { diff --git a/frontend/tools/__tests__/add_tool_test.tsx b/frontend/tools/__tests__/add_tool_test.tsx index 41023823e9..ac33be80b2 100644 --- a/frontend/tools/__tests__/add_tool_test.tsx +++ b/frontend/tools/__tests__/add_tool_test.tsx @@ -76,12 +76,14 @@ describe("", () => { p.dispatch = mockDispatch(); const wrapper = shallow(); wrapper.setState({ toolName: "Foo" }); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; await wrapper.find(SaveBtn).simulate("click"); expect(init).toHaveBeenCalledWith("Tool", { name: "Foo", flow_rate_ml_per_s: 0, }); expect(wrapper.state().uuid).toEqual(undefined); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("removes unsaved tool on exit", async () => { @@ -90,12 +92,14 @@ describe("", () => { p.dispatch = mockDispatch(); const wrapper = shallow(); wrapper.setState({ toolName: "Foo" }); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; await wrapper.find(SaveBtn).simulate("click"); expect(init).toHaveBeenCalledWith("Tool", { name: "Foo", flow_rate_ml_per_s: 0, }); expect(wrapper.state().uuid).toEqual("fake uuid"); - expect(mockNavigate).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); wrapper.unmount(); expect(destroy).toHaveBeenCalledWith("fake uuid"); }); @@ -113,20 +117,24 @@ describe("", () => { ])("adds peripherals: %s", (firmware, expectedAdds) => { const p = fakeProps(); p.firmwareHardware = firmware; - const wrapper = mount(); + const wrapper = mount(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("button").last().simulate("click"); expect(initSave).toHaveBeenCalledTimes(expectedAdds); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("doesn't add stock tools twice", () => { const p = fakeProps(); p.firmwareHardware = "express_k10"; p.existingToolNames = ["Seed Trough 1"]; - const wrapper = mount(); + const wrapper = mount(); + const navigate = jest.fn(); + wrapper.instance().navigate = navigate; wrapper.find("button").last().simulate("click"); expect(initSave).toHaveBeenCalledTimes(2); - expect(mockNavigate).toHaveBeenCalledWith(Path.tools()); + expect(navigate).toHaveBeenCalledWith(Path.tools()); }); it("copies a tool name", () => { diff --git a/frontend/tools/add_tool.tsx b/frontend/tools/add_tool.tsx index 8a80050808..0e17b03282 100644 --- a/frontend/tools/add_tool.tsx +++ b/frontend/tools/add_tool.tsx @@ -9,7 +9,6 @@ import { SaveBtn } from "../ui"; import { SpecialStatus } from "farmbot"; import { initSave, destroy, init, save } from "../api/crud"; import { Panel } from "../farm_designer/panel_header"; -import { useNavigate } from "react-router"; import { selectAllTools } from "../resources/selectors"; import { betterCompact } from "../util"; import { @@ -27,6 +26,7 @@ import { reduceToolName, ToolName, } from "../farm_designer/map/tool_graphics/all_tools"; import { WaterFlowRateInput } from "./edit_tool"; +import { NavigationContext } from "../routes_helpers"; export const mapStateToProps = (props: Everything): AddToolProps => ({ dispatch: props.dispatch, @@ -54,9 +54,12 @@ export class RawAddTool extends React.Component { newTool = (name: string) => this.props.dispatch(initSave("Tool", { name })); + static contextType = NavigationContext; + context!: React.ContextType; + navigate = this.context; + back = () => { - const navigate = useNavigate(); - navigate(Path.tools()); + this.navigate(Path.tools()); }; save = () => { @@ -139,7 +142,6 @@ export class RawAddTool extends React.Component { AddStockTools = () => { const add = this.state.toAdd.filter(this.filterExisting); - const navigate = useNavigate(); return