From ceef21f2ba7c18eacc917232da31ecb1f2fcbbc1 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 15 May 2025 10:10:17 -0700 Subject: [PATCH 1/6] only open popup once on controls route --- frontend/controls/controls.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index efd37ac694..3a2295069c 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -26,7 +26,10 @@ import { Navigate } from "react-router"; import { mapStateToProps } from "./state_to_props"; export const RawDesignerControls = (props: DesignerControlsProps) => { - props.dispatch({ type: Actions.OPEN_POPUP, payload: "controls" }); + React.useEffect(() => { + props.dispatch({ type: Actions.OPEN_POPUP, payload: "controls" }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return From 0ae759213e949fdfbc9a75554fdda7823d5c21d7 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 15 May 2025 11:05:32 -0700 Subject: [PATCH 2/6] upgrade deps (ruby, node) --- .ruby-version | 2 +- Gemfile | 3 ++- Gemfile.lock | 4 +++- docker_configs/api.Dockerfile | 4 ++-- package.json | 6 +++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.ruby-version b/.ruby-version index 37d02a6e38..6cb9d3dd0d 100755 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.8 +3.4.3 diff --git a/Gemfile b/Gemfile index d34325c08a..86a1b5ce37 100755 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ source "https://rubygems.org" -ruby "~> 3.3.8" +ruby "~> 3.4.3" gem "rails", "~> 6" gem "active_model_serializers" @@ -28,6 +28,7 @@ gem "tzinfo-data" # For validation of user selected timezone names gem "valid_url" gem "thwait" gem "lograge" # Used to filter repetitive RabbitMQ logs. +gem "drb" group :development, :test do gem "climate_control" diff --git a/Gemfile.lock b/Gemfile.lock index afd78be42a..ccd0dc7e5c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -109,6 +109,7 @@ GEM discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.1) + drb (2.2.1) e2mmap (0.1.0) erubi (1.13.1) factory_bot (6.5.1) @@ -396,6 +397,7 @@ DEPENDENCIES delayed_job_active_record devise discard + drb factory_bot_rails faker google-cloud-storage (~> 1.11) @@ -431,7 +433,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 3.3.8p144 + ruby 3.4.3p32 BUNDLED WITH 2.6.9 diff --git a/docker_configs/api.Dockerfile b/docker_configs/api.Dockerfile index f498481088..74d907a913 100644 --- a/docker_configs/api.Dockerfile +++ b/docker_configs/api.Dockerfile @@ -1,10 +1,10 @@ -FROM ruby:3.3.8 +FROM ruby:3.4.3 RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/apt.postgresql.org.gpg > /dev/null && \ sh -c '. /etc/os-release; echo $VERSION_CODENAME; echo "deb http://apt.postgresql.org/pub/repos/apt/ $VERSION_CODENAME-pgdg main" >> /etc/apt/sources.list.d/pgdg.list' && \ apt-get update -qq && apt-get install -y build-essential libpq-dev postgresql postgresql-contrib && \ mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - sh -c 'echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list' && \ + sh -c 'echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list' && \ apt-get update -qq && \ sh -c 'echo "\nPackage: *\nPin: origin deb.nodesource.com\nPin-Priority: 700\n" >> /etc/apt/preferences' && \ apt-get install -y nodejs && \ diff --git a/package.json b/package.json index ec67544552..f647f17e3b 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Farmbot web frontend.", "engines": { "browsers": "defaults", - "node": "22.x", - "npm": "10.x", + "node": "24.x", + "npm": "11.x", "parcel": "2.x" }, "repository": { @@ -68,7 +68,7 @@ "moment": "2.30.1", "monaco-editor": "0.52.2", "mqtt": "5.13.0", - "npm": "11.3.0", + "npm": "11.4.0", "parcel": "2.15.0", "process": "0.11.10", "promise-timeout": "1.3.0", From e36d430f03c601e9ceee2c6661aaeb3538a1aaf1 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 15 May 2025 13:43:18 -0700 Subject: [PATCH 3/6] update translation metrics --- .../languages/translation_metrics.md | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/public/app-resources/languages/translation_metrics.md b/public/app-resources/languages/translation_metrics.md index b531bd9e2a..e7b575a5ad 100644 --- a/public/app-resources/languages/translation_metrics.md +++ b/public/app-resources/languages/translation_metrics.md @@ -21,21 +21,22 @@ For example, `sudo docker compose run web npm run translation-check`._ See the [README](https://github.com/FarmBot/Farmbot-Web-App#translating-the-web-app) for contribution instructions. -Total number of phrases identified by the language helper for translation: __2373__ +Total number of phrases identified by the language helper for translation: __2388__ |Language|Percent translated|Translated|Untranslated|Other Translations| |:---:|---:|---:|---:|---:| -|da|3%|83|2290|29| -|de|77%|1833|540|614| -|es|57%|1353|1020|556| -|fr|39%|919|1454|464| -|it|96%|2283|90|176| -|ko|96%|2286|87|64| -|nl|3%|61|2312|77| -|pt|2%|55|2318|98| -|ru|18%|436|1937|361| -|th|0%|0|2373|0| -|zh|91%|2159|214|156| +|da|3%|81|2307|31| +|de|75%|1801|587|646| +|es|55%|1323|1065|584| +|fr|38%|896|1492|488| +|it|94%|2245|143|210| +|ko|94%|2248|140|101| +|nl|3%|61|2327|77| +|pt|2%|55|2333|98| +|ru|18%|425|1963|372| +|th|0%|0|2388|0| +|yy|0%|0|2388|0| +|zh|89%|2122|266|192| **Percent translated** refers to the percent of phrases identified by the language helper that have been translated. Additional phrases not identified From fa866b9f2529e1fd1f576dadd9132e613693acb7 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 15 May 2025 16:31:00 -0700 Subject: [PATCH 4/6] add missing gems --- Gemfile | 3 +++ Gemfile.lock | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Gemfile b/Gemfile index 86a1b5ce37..40ca2189b9 100755 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,9 @@ gem "valid_url" gem "thwait" gem "lograge" # Used to filter repetitive RabbitMQ logs. gem "drb" +gem "benchmark" +gem "ostruct" +gem "bigdecimal" group :development, :test do gem "climate_control" diff --git a/Gemfile.lock b/Gemfile.lock index ccd0dc7e5c..124a6df8fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,6 +70,7 @@ GEM amq-protocol (2.3.4) base64 (0.2.0) bcrypt (3.1.20) + benchmark (0.4.0) bigdecimal (3.1.9) builder (3.3.0) bunny (2.24.0) @@ -228,6 +229,7 @@ GEM racc (~> 1.4) orm_adapter (0.5.0) os (1.1.4) + ostruct (0.6.1) passenger (6.0.27) rack (>= 1.6.13) rackup (>= 1.0.1) @@ -390,6 +392,8 @@ PLATFORMS DEPENDENCIES active_model_serializers + benchmark + bigdecimal bunny climate_control database_cleaner @@ -407,6 +411,7 @@ DEPENDENCIES logger lograge mutations + ostruct passenger pg pry From 25eac828dc0f012c3d11220ecf18b25467676459 Mon Sep 17 00:00:00 2001 From: gabrielburnworth Date: Thu, 22 May 2025 16:36:53 -0700 Subject: [PATCH 5/6] add 3D soil surface --- .../__test_support__/additional_mocks.tsx | 9 ++ .../__test_support__/fake_designer_state.ts | 1 + frontend/__test_support__/three_d_mocks.tsx | 53 ++++---- frontend/constants.ts | 1 + .../farm_designer/__tests__/reducer_test.ts | 9 ++ frontend/farm_designer/interfaces.ts | 1 + frontend/farm_designer/reducer.ts | 5 + frontend/farm_designer/three_d_garden_map.tsx | 1 + .../__tests__/components_test.tsx | 12 ++ .../three_d_garden/__tests__/index_test.tsx | 23 ++++ .../__tests__/triangles_test.ts | 110 ++++++++++++++++ .../three_d_garden/bed/__tests__/bed_test.tsx | 8 +- frontend/three_d_garden/bed/bed.tsx | 78 ++++++++---- .../__tests__/pointer_objects_test.tsx | 3 + .../bed/objects/pointer_objects.tsx | 56 ++++----- frontend/three_d_garden/config.ts | 11 +- frontend/three_d_garden/config_overlays.tsx | 1 + .../garden/__tests__/grid_test.tsx | 1 + .../garden/__tests__/plants_test.tsx | 1 + .../garden/__tests__/point_test.tsx | 1 + .../garden/__tests__/weed_test.tsx | 1 + frontend/three_d_garden/garden/grid.tsx | 69 ++++++++--- frontend/three_d_garden/garden/plants.tsx | 3 +- frontend/three_d_garden/garden/point.tsx | 16 +-- frontend/three_d_garden/garden/weed.tsx | 3 +- frontend/three_d_garden/garden_model.tsx | 16 ++- frontend/three_d_garden/helpers.ts | 25 ++++ frontend/three_d_garden/index.tsx | 15 +++ frontend/three_d_garden/triangles.ts | 117 ++++++++++++++++++ package.json | 2 + 30 files changed, 543 insertions(+), 109 deletions(-) create mode 100644 frontend/three_d_garden/__tests__/triangles_test.ts create mode 100644 frontend/three_d_garden/triangles.ts diff --git a/frontend/__test_support__/additional_mocks.tsx b/frontend/__test_support__/additional_mocks.tsx index 0b0bd69bea..e19b26cabf 100644 --- a/frontend/__test_support__/additional_mocks.tsx +++ b/frontend/__test_support__/additional_mocks.tsx @@ -51,3 +51,12 @@ jest.mock("react-router", () => ({ Navigate: ({ to }: { to: string }) =>
{mockNavigate(to)}
, Outlet: jest.fn(() =>
), })); + +jest.mock("delaunator", () => ({ + __esModule: true, + default: { + from: jest.fn(() => ({ + triangles: [0, 1, 2], + })), + }, +})); diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index 56a89232fa..4e61f712d3 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -52,6 +52,7 @@ export const fakeDesignerState = (): DesignerState => ({ distanceIndicator: "", panelOpen: true, threeDTopDownView: false, + threeDExaggeratedZ: false, }); export const fakeHelpState = (): HelpState => ({ diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index d8832210fe..42c1f00fe2 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -9,15 +9,40 @@ import { import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; -import { ThreeElements } from "@react-three/fiber"; +import { ThreeElements, ThreeEvent } from "@react-three/fiber"; import { Cloud, Clouds, Image, Tube } from "@react-three/drei"; const GroupForTests = (props: ThreeElements["group"]) => // @ts-expect-error Property does not exist on type JSX.IntrinsicElements ; +type Event = ThreeEvent; + +const MeshForTests = (props: ThreeElements["mesh"]) => + // @ts-expect-error Property does not exist on type JSX.IntrinsicElements + + props.onPointerMove?.({ + // @ts-expect-error: This spread always overwrites this property. + point: { x: 0, y: 0 }, + ...e, + })} + onClick={(e: Event) => + props.onClick?.({ + // @ts-expect-error: This spread always overwrites this property. + stopPropagation: jest.fn(), + // @ts-expect-error: This spread always overwrites this property. + point: { x: 0, y: 0 }, + ...e, + } as unknown as Event)}> + {props.name} + {props.children} + {/* @ts-expect-error Property does not exist on type JSX.IntrinsicElements */} + ; + jest.mock("../three_d_garden/components", () => ({ ...jest.requireActual("../three_d_garden/components"), + Mesh: (props: ThreeElements["mesh"]) => , Group: (props: ThreeElements["group"]) => props.visible === false ? <> @@ -61,8 +86,6 @@ jest.mock("@react-spring/three", () => ({
{children}
, })); -type Event = React.MouseEvent; - jest.mock("@react-three/drei", () => { const useGLTF = jest.fn((key: string) => ({ [ASSETS.models.crossSlide]: { @@ -581,6 +604,8 @@ jest.mock("@react-three/drei", () => { useGLTF, RoundedBox: ({ name }: { name: string }) =>
{name}
, + Plane: ({ name }: { name: string }) => +
{name}
, Cylinder: ({ name }: { name: string }) =>
{name}
, Torus: ({ name }: { name: string }) => @@ -591,26 +616,8 @@ jest.mock("@react-three/drei", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any Box: (props: any) =>
{props.children}
, - Extrude: ({ name, onClick, onPointerMove }: { - name: string, - onClick: (event: Event) => void, - onPointerMove: (event: Event) => void, - }) => -
- onPointerMove({ - point: { x: 0, y: 0 }, - ...e, - } as unknown as Event)} - onClick={e => - onClick({ - // @ts-expect-error: This spread always overwrites this property. - stopPropagation: jest.fn(), - point: { x: 0, y: 0 }, - ...e, - } as unknown as Event)}> - {name} -
, + Extrude: ({ name }: { name: string }) => +
{name}
, Line: ({ name }: { name: string }) =>
{name}
, Trail: ({ name }: { name: string }) => diff --git a/frontend/constants.ts b/frontend/constants.ts index 8550508d87..718d13ca2f 100644 --- a/frontend/constants.ts +++ b/frontend/constants.ts @@ -2487,6 +2487,7 @@ export enum Actions { // 3D SET_DISTANCE_INDICATOR = "SET_DISTANCE_INDICATOR", TOGGLE_3D_TOP_DOWN_VIEW = "TOGGLE_3D_TOP_DOWN_VIEW", + TOGGLE_3D_EXAGGERATED_Z = "TOGGLE_3D_EXAGGERATED_Z", // Regimens PUSH_WEEK = "PUSH_WEEK", diff --git a/frontend/farm_designer/__tests__/reducer_test.ts b/frontend/farm_designer/__tests__/reducer_test.ts index 950985f050..6b0a2b4498 100644 --- a/frontend/farm_designer/__tests__/reducer_test.ts +++ b/frontend/farm_designer/__tests__/reducer_test.ts @@ -135,6 +135,15 @@ describe("designer reducer", () => { expect(newState.threeDTopDownView).toEqual(true); }); + it("sets exaggerated z", () => { + const action: ReduxAction = { + type: Actions.TOGGLE_3D_EXAGGERATED_Z, + payload: true, + }; + const newState = designer(oldState(), action); + expect(newState.threeDExaggeratedZ).toEqual(true); + }); + it("sets panel open state", () => { const action: ReduxAction = { type: Actions.SET_PANEL_OPEN, diff --git a/frontend/farm_designer/interfaces.ts b/frontend/farm_designer/interfaces.ts index 5982c63e13..291a674407 100644 --- a/frontend/farm_designer/interfaces.ts +++ b/frontend/farm_designer/interfaces.ts @@ -178,6 +178,7 @@ export interface DesignerState { distanceIndicator: string; panelOpen: boolean; threeDTopDownView: boolean; + threeDExaggeratedZ: boolean; } export type TaggedExecutable = TaggedSequence | TaggedRegimen; diff --git a/frontend/farm_designer/reducer.ts b/frontend/farm_designer/reducer.ts index 1bb630526f..b6fd525a6c 100644 --- a/frontend/farm_designer/reducer.ts +++ b/frontend/farm_designer/reducer.ts @@ -60,6 +60,7 @@ export const initialState: DesignerState = { distanceIndicator: "", panelOpen: true, threeDTopDownView: false, + threeDExaggeratedZ: false, }; export const designer = generateReducer(initialState) @@ -244,6 +245,10 @@ export const designer = generateReducer(initialState) s.threeDTopDownView = payload; return s; }) + .add(Actions.TOGGLE_3D_EXAGGERATED_Z, (s, { payload }) => { + s.threeDExaggeratedZ = payload; + return s; + }) .add(Actions.SET_PANEL_OPEN, (s, { payload }) => { s.panelOpen = payload; return s; diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx index 2552158ba1..a2752b5220 100644 --- a/frontend/farm_designer/three_d_garden_map.tsx +++ b/frontend/farm_designer/three_d_garden_map.tsx @@ -49,6 +49,7 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { : "v1.7"; config.negativeZ = props.negativeZ; + config.exaggeratedZ = props.designer.threeDExaggeratedZ; config.x = props.botPosition.x || 0; config.y = props.botPosition.y || 0; diff --git a/frontend/three_d_garden/__tests__/components_test.tsx b/frontend/three_d_garden/__tests__/components_test.tsx index 4383a7560c..d208dfa5e3 100644 --- a/frontend/three_d_garden/__tests__/components_test.tsx +++ b/frontend/three_d_garden/__tests__/components_test.tsx @@ -6,6 +6,7 @@ import React from "react"; import { mount } from "enzyme"; import { AmbientLight, + BoxGeometry, DirectionalLight, Group, Mesh, @@ -25,6 +26,17 @@ describe("", () => { }); }); +describe("", () => { + const fakeProps = (): ThreeElements["boxGeometry"] => ({ + name: "box", + }); + + it("adds props", () => { + const wrapper = mount(); + expect(wrapper.props().name).toEqual("box"); + }); +}); + describe("", () => { const fakeProps = (): ThreeElements["ambientLight"] => ({ intensity: 0.5, diff --git a/frontend/three_d_garden/__tests__/index_test.tsx b/frontend/three_d_garden/__tests__/index_test.tsx index 1b6538f229..55a5766142 100644 --- a/frontend/three_d_garden/__tests__/index_test.tsx +++ b/frontend/three_d_garden/__tests__/index_test.tsx @@ -80,6 +80,29 @@ describe("", () => { }); }); + it("disables exaggerated z", () => { + const p = fakeProps(); + p.designer.threeDExaggeratedZ = true; + render(); + const isoViewButton = screen.getByTitle("normal z"); + fireEvent.click(isoViewButton); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_3D_EXAGGERATED_Z, + payload: false, + }); + }); + + it("enables exaggerated z", () => { + const p = fakeProps(); + render(); + const topDownViewButton = screen.getByTitle("exaggerated z"); + fireEvent.click(topDownViewButton); + expect(p.dispatch).toHaveBeenCalledWith({ + type: Actions.TOGGLE_3D_EXAGGERATED_Z, + payload: true, + }); + }); + it("toggles 3D view", () => { const p = fakeProps(); render(); diff --git a/frontend/three_d_garden/__tests__/triangles_test.ts b/frontend/three_d_garden/__tests__/triangles_test.ts new file mode 100644 index 0000000000..055bd2e87f --- /dev/null +++ b/frontend/three_d_garden/__tests__/triangles_test.ts @@ -0,0 +1,110 @@ +import { computeSurface, getZFunc, precomputeTriangles } from "../triangles"; +import { INITIAL } from "../config"; +import { clone } from "lodash"; +import { fakePoint } from "../../__test_support__/fake_state/resources"; +import { tagAsSoilHeight } from "../../points/soil_height"; +import { TaggedGenericPointer } from "farmbot"; + +describe("precomputeTriangles()", () => { + it("computes triangles: zero", () => { + expect(precomputeTriangles([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ], [0, 1, 2])).toEqual([]); + }); + + it("computes triangles", () => { + expect(precomputeTriangles([ + [1, 1, 0], + [4, 1, 0], + [2, 3, 0], + ], [0, 1, 2])).toEqual([{ + a: [1, 1, 0], + b: [4, 1, 0], + c: [2, 3, 0], + det: 6, + x1: 1, + x2: 4, + x3: 2, + y1: 1, + y2: 1, + y3: 3, + }]); + }); +}); + +describe("getZFunc()", () => { + it("gets Z: falls back", () => { + expect(getZFunc([], -100)(0, 0)).toEqual(-100); + }); + + it("gets Z", () => { + expect(getZFunc([{ + a: [0, 0, 10], + b: [2, 0, 20], + c: [0, 2, 30], + det: 4, + x1: 0, + x2: 2, + x3: 0, + y1: 0, + y2: 0, + y3: 2, + }], -100)(1, 1)).toEqual(25); + }); +}); + +const zs = (items: number[]) => items.filter((_, i) => (i + 1) % 3 == 0); + +describe("computeSurface()", () => { + it("computes surface: zero", () => { + const soilPoints: TaggedGenericPointer[] = []; + const config = clone(INITIAL); + config.soilHeight = 0; + config.bedLengthOuter = 0; + config.bedWidthOuter = 0; + const { vertices } = computeSurface(soilPoints, config); + expect(zs(vertices)).toEqual([-0, -0, -0]); + }); + + it("computes surface: no soil points", () => { + const config = clone(INITIAL); + config.soilHeight = 500; + const { vertices } = computeSurface(undefined, config); + expect(zs(vertices)).toEqual([-500, -500, -500]); + }); + + it("computes surface: soil points", () => { + const point0 = fakePoint(); + tagAsSoilHeight(point0); + point0.body.x = 0; + point0.body.y = 0; + point0.body.z = -400; + const point1 = fakePoint(); + tagAsSoilHeight(point1); + point0.body.x = 100; + point0.body.y = 200; + point0.body.z = -600; + const soilPoints = [point0, point1]; + const config = clone(INITIAL); + config.soilHeight = 500; + const { vertices } = computeSurface(soilPoints, config); + expect(zs(vertices)).toEqual([-600, 0, -500]); + }); + + it("computes surface: exaggerated", () => { + const point = fakePoint(); + tagAsSoilHeight(point); + point.body.x = 100; + point.body.y = 200; + point.body.z = -600; + const soilPoints = [point]; + const config = clone(INITIAL); + config.soilHeight = 500; + config.exaggeratedZ = true; + config.perspective = true; + const { vertices } = computeSurface(soilPoints, config); + expect(zs(vertices)).toEqual([-1500, -500, -500]); + }); +}); diff --git a/frontend/three_d_garden/bed/__tests__/bed_test.tsx b/frontend/three_d_garden/bed/__tests__/bed_test.tsx index 82253b2df9..8f18785a96 100644 --- a/frontend/three_d_garden/bed/__tests__/bed_test.tsx +++ b/frontend/three_d_garden/bed/__tests__/bed_test.tsx @@ -98,6 +98,9 @@ describe("", () => { config: clone(INITIAL), activeFocus: "", mapPoints: [], + vertices: [], + uvs: [], + getZ: () => 0, }); it("renders bed", () => { @@ -182,7 +185,7 @@ describe("", () => { expect(mockSetPlantPosition).toHaveBeenCalledWith(0, 0, 0); expect(p.addPlantProps.dispatch).toHaveBeenCalledWith({ type: Actions.SET_DRAWN_POINT_DATA, - payload: { ...point, cx: 1360, cy: 660, z: -500 }, + payload: { ...point, cx: 1360, cy: 660, z: 0 }, }); expect(p.addPlantProps.dispatch).toHaveBeenCalledTimes(1); }); @@ -225,6 +228,7 @@ describe("", () => { mockXCrosshairRef.current = { position: { set: mockSetXCrosshairPosition } }; mockYCrosshairRef.current = { position: { set: mockSetYCrosshairPosition } }; const p = fakeProps(); + p.config.columnLength = 100; p.addPlantProps = fakeAddPlantProps([]); render(); const soil = screen.getAllByText("soil")[0]; @@ -255,6 +259,7 @@ describe("", () => { mockXCrosshairRef.current = undefined; mockYCrosshairRef.current = undefined; const p = fakeProps(); + p.config.columnLength = 100; p.addPlantProps = fakeAddPlantProps([]); render(); const soil = screen.getAllByText("soil")[0]; @@ -300,6 +305,7 @@ describe("", () => { mockXCrosshairRef.current = { position: { set: mockSetXCrosshairPosition } }; mockYCrosshairRef.current = { position: { set: mockSetYCrosshairPosition } }; const p = fakeProps(); + p.config.columnLength = 100; p.addPlantProps = fakeAddPlantProps([]); const point = fakeDrawnPoint(); point.cx = undefined; diff --git a/frontend/three_d_garden/bed/bed.tsx b/frontend/three_d_garden/bed/bed.tsx index 5f49caf04a..e474395958 100644 --- a/frontend/three_d_garden/bed/bed.tsx +++ b/frontend/three_d_garden/bed/bed.tsx @@ -1,18 +1,20 @@ import React from "react"; -import { Box, Detailed, Extrude, useTexture } from "@react-three/drei"; +import { Box, Detailed, Extrude, Plane, useTexture } from "@react-three/drei"; import { DoubleSide, Path as LinePath, Shape, RepeatWrapping, + BufferGeometry, + Float32BufferAttribute, } from "three"; import { range } from "lodash"; -import { threeSpace, zZero, getColorFromBrightness } from "../helpers"; +import { threeSpace, getColorFromBrightness, zZero } from "../helpers"; import { Config, detailLevels } from "../config"; import { ASSETS } from "../constants"; import { DistanceIndicator } from "../elements"; import { FarmbotAxes, Caster, UtilitiesPost, Packaging } from "./objects"; -import { Group, MeshPhongMaterial } from "../components"; +import { Group, Mesh, MeshPhongMaterial } from "../components"; import { AxisNumberProperty, TaggedPlant, } from "../../farm_designer/map/interfaces"; @@ -28,6 +30,7 @@ import { XCrosshairRef, YCrosshairRef, } from "./objects/pointer_objects"; +import { ThreeElements } from "@react-three/fiber"; const soil = ( Type: typeof LinePath | typeof Shape, @@ -63,6 +66,28 @@ const bedStructure2D = ( return shape; }; +type MeshProps = ThreeElements["mesh"]; + +interface SurfaceProps extends MeshProps { + vertices: number[]; + uvs: number[]; +} + +const Surface = (props: SurfaceProps) => { + const { vertices, uvs } = props; + const geometry = React.useMemo(() => { + const geom = new BufferGeometry(); + geom.setAttribute("position", new Float32BufferAttribute(vertices, 3)); + geom.setAttribute("uv", new Float32BufferAttribute(uvs, 2)); + geom.computeVertexNormals(); + return geom; + }, [vertices, uvs]); + + return + {props.children} + ; +}; + export interface AddPlantProps { gridSize: AxisNumberProperty; dispatch: Function; @@ -77,13 +102,16 @@ export interface BedProps { activeFocus: string; mapPoints: TaggedGenericPointer[]; addPlantProps?: AddPlantProps; + vertices: number[]; + uvs: number[]; + getZ(x: number, y: number): number; } export const Bed = (props: BedProps) => { const { bedWidthOuter, bedLengthOuter, botSizeZ, bedHeight, bedZOffset, legSize, legsFlush, extraLegsX, extraLegsY, bedBrightness, soilBrightness, - soilHeight, ccSupportSize, axes, xyDimensions, + ccSupportSize, axes, xyDimensions, bedXOffset, bedYOffset, } = props.config; const thickness = props.config.bedWallThickness; const botSize = { x: bedLengthOuter, y: bedWidthOuter, z: botSizeZ, thickness }; @@ -117,9 +145,6 @@ export const Bed = (props: BedProps) => { legWoodTexture.wrapT = RepeatWrapping; legWoodTexture.repeat.set(0.02, 0.05); const soilTexture = useTexture(ASSETS.textures.soil + "?=soil"); - soilTexture.wrapS = RepeatWrapping; - soilTexture.wrapT = RepeatWrapping; - soilTexture.repeat.set(0.00034, 0.00068); const Bed = ({ children }: { children: React.ReactElement }) => { const navigate = useNavigate(); const Soil = ({ children, addPlantProps }: SoilProps) => { - const soilDepth = bedHeight + zZero(props.config) - soilHeight; - return { imageRef, xCrosshairRef, yCrosshairRef, + getZ: props.getZ, })} castShadow={true} receiveShadow={true} - args={[ - soil(Shape, botSize) as Shape, - { steps: 1, depth: soilDepth, bevelEnabled: false }, - ]} + vertices={props.vertices} + uvs={props.uvs} position={[ - threeSpace(0, bedLengthOuter), - threeSpace(0, bedWidthOuter), - -bedStartZ, + threeSpace(0, bedLengthOuter) + bedXOffset, + threeSpace(0, bedWidthOuter) + bedYOffset, + zZero(props.config), ]}> {children} - ; + ; + }; + + const commonSoil = { + side: DoubleSide, + shininess: 0, }; return @@ -212,6 +241,15 @@ export const Bed = (props: BedProps) => { + + + { mapPoints={props.mapPoints} />} - + - + {legXPositions.map((x, index) => diff --git a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx index 84241d26c1..491e476da3 100644 --- a/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx +++ b/frontend/three_d_garden/bed/objects/__tests__/pointer_objects_test.tsx @@ -59,6 +59,7 @@ describe("soilClick()", () => { navigate: jest.fn(), addPlantProps: fakeAddPlantProps([]), pointerPlantRef: { current: { position: new Vector3(0, 0, 0) } } as PointerPlantRef, + getZ: () => 0, }); it("creates plant", () => { @@ -81,6 +82,7 @@ describe("soilPointerMove()", () => { const fakeProps = (): SoilPointerMoveProps => ({ config: clone(INITIAL), addPlantProps: fakeAddPlantProps([]), + getZ: () => 0, pointerPlantRef: { current: { position: { set: jest.fn() } } } as unknown as PointerPlantRef, radiusRef: { current: { scale: { set: jest.fn() } } } as unknown as RadiusRef, torusRef: { current: { scale: { set: jest.fn() } } } as unknown as TorusRef, @@ -94,6 +96,7 @@ describe("soilPointerMove()", () => { location.pathname = Path.mock(Path.cropSearch("mint")); mockIsMobile = false; const p = fakeProps(); + p.config.columnLength = 100; const e = { stopPropagation: jest.fn(), point: { x: 100, y: 200 }, diff --git a/frontend/three_d_garden/bed/objects/pointer_objects.tsx b/frontend/three_d_garden/bed/objects/pointer_objects.tsx index 0732ebcab4..17acdc54ce 100644 --- a/frontend/three_d_garden/bed/objects/pointer_objects.tsx +++ b/frontend/three_d_garden/bed/objects/pointer_objects.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Group } from "../../components"; import { Billboard, Line, Image } from "@react-three/drei"; import { findIcon } from "../../../crops/find"; -import { AxisNumberProperty, Mode } from "../../../farm_designer/map/interfaces"; +import { Mode } from "../../../farm_designer/map/interfaces"; import { getMode, round, xyDistance } from "../../../farm_designer/map/util"; import { isMobile } from "../../../screen_size"; import { HOVER_OBJECT_MODES, DRAW_POINT_MODES, RenderOrder } from "../../constants"; @@ -10,7 +10,11 @@ import { DrawnPoint, POINT_CYLINDER_SCALE_FACTOR, WEED_IMG_SIZE_FRACTION, } from "../../garden"; import { - zero as zeroFunc, extents as extentsFunc, threeSpace, + zero as zeroFunc, + extents as extentsFunc, + zZero, + getGardenPositionFunc, + get3DPositionFunc, } from "../../helpers"; import { Config } from "../../config"; import { SpecialStatus, TaggedGenericPointer } from "farmbot"; @@ -27,8 +31,6 @@ import { NavigateFunction } from "react-router"; import { DrawnPointPayl } from "../../../farm_designer/interfaces"; import { Line2 } from "three/examples/jsm/lines/Line2"; -type XY = AxisNumberProperty; - export type PointerPlantRef = React.RefObject; export type RadiusRef = React.RefObject; export type TorusRef = React.RefObject; @@ -47,24 +49,6 @@ interface AllRefs { yCrosshairRef: YCrosshairRef; } -const getGardenPositionFunc = (config: Config) => - (threeDPosition: XY): XY => { - const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config; - return { - x: round(threeSpace(threeDPosition.x, -bedLengthOuter) - bedXOffset), - y: round(threeSpace(threeDPosition.y, -bedWidthOuter) - bedYOffset), - }; - }; - -const get3DPositionFunc = (config: Config) => - (gardenPosition: XY): XY => { - const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config; - return { - x: threeSpace(gardenPosition.x + bedXOffset, bedLengthOuter), - y: threeSpace(gardenPosition.y + bedYOffset, bedWidthOuter), - }; - }; - export interface PointerObjectsProps extends AllRefs { config: Config; mapPoints: TaggedGenericPointer[]; @@ -84,8 +68,6 @@ export const PointerObjects = (props: PointerObjectsProps) => { const { drawnPoint } = addPlantProps.designer; const settingRadius = !(isUndefined(drawnPoint?.cx) || isUndefined(drawnPoint.cy)); - const soilZ = zero.z - config.soilHeight; - const crosshairZ = soilZ + 1; const gridPreview = mapPoints .filter(p => p.specialStatus == SpecialStatus.DIRTY && p.body.meta.gridId) .length > 0; @@ -104,8 +86,8 @@ export const PointerObjects = (props: PointerObjectsProps) => { opacity={0.9} lineWidth={2} points={[ - [zero.x, 0, crosshairZ], - [extents.x, 0, crosshairZ], + [zero.x, 0, 0], + [extents.x, 0, 0], ]} /> { opacity={0.9} lineWidth={2} points={[ - [0, zero.y, crosshairZ], - [0, extents.y, crosshairZ], + [0, zero.y, 0], + [0, extents.y, 0], ]} /> } - + {DRAW_POINT_MODES.includes(getMode()) && !gridPreview && drawnPoint && @@ -151,6 +133,7 @@ export interface SoilClickProps { addPlantProps: AddPlantProps; pointerPlantRef: PointerPlantRef; navigate: NavigateFunction; + getZ(x: number, y: number): number; } export const soilClick = (props: SoilClickProps) => @@ -181,7 +164,7 @@ export const soilClick = (props: SoilClickProps) => ...drawnPoint, cx: cursor.x, cy: cursor.y, - z: -config.soilHeight, + z: mathRound(props.getZ(cursor.x, cursor.y), 1), } : { ...drawnPoint, @@ -209,6 +192,7 @@ export const soilClick = (props: SoilClickProps) => export interface SoilPointerMoveProps extends AllRefs { config: Config; addPlantProps: AddPlantProps; + getZ(x: number, y: number): number; } export const soilPointerMove = (props: SoilPointerMoveProps) => @@ -225,17 +209,19 @@ export const soilPointerMove = (props: SoilPointerMoveProps) => && HOVER_OBJECT_MODES.includes(getMode()) && !isMobile() && pointerPlantRef.current) { - const position = get3DPosition(getGardenPosition(e.point)); - xCrosshairRef.current?.position.set(0, position.y, 0); - yCrosshairRef.current?.position.set(position.x, 0, 0); + const gardenPosition = getGardenPosition(e.point); + const { x, y } = get3DPosition(gardenPosition); + const z = zZero(config) + props.getZ(gardenPosition.x, gardenPosition.y); + xCrosshairRef.current?.position.set(0, y, z); + yCrosshairRef.current?.position.set(x, 0, z); if (getMode() == Mode.clickToAdd) { - pointerPlantRef.current.position.set(position.x, position.y, 0); + pointerPlantRef.current.position.set(x, y, z); } if (DRAW_POINT_MODES.includes(getMode())) { const { drawnPoint } = addPlantProps.designer; if (isUndefined(drawnPoint)) { return; } if (isUndefined(drawnPoint.cx) || isUndefined(drawnPoint.cy)) { - pointerPlantRef.current.position.set(position.x, position.y, 0); + pointerPlantRef.current.position.set(x, y, z); } else { if (drawnPoint.r > 0) { return; } const radius = round(xyDistance( diff --git a/frontend/three_d_garden/config.ts b/frontend/three_d_garden/config.ts index 7d5e765927..63ba1dc851 100644 --- a/frontend/three_d_garden/config.ts +++ b/frontend/three_d_garden/config.ts @@ -71,6 +71,7 @@ export interface Config { distanceIndicator: string; kitVersion: string; negativeZ: boolean; + exaggeratedZ: boolean; waterFlow: boolean; } @@ -102,7 +103,7 @@ export const INITIAL: Config = { extraLegsX: 1, extraLegsY: 0, bedBrightness: 8, - soilBrightness: 6, + soilBrightness: 12, soilHeight: 500, plants: "Spring", labels: false, @@ -147,6 +148,7 @@ export const INITIAL: Config = { distanceIndicator: "", kitVersion: "v1.7", negativeZ: false, + exaggeratedZ: false, waterFlow: false, }; @@ -170,7 +172,7 @@ export const BOOLEAN_KEYS = [ "xyDimensions", "zDimension", "promoInfo", "settingsBar", "zoomBeacons", "solar", "utilitiesPost", "packaging", "lab", "people", "lowDetail", "eventDebug", "cableDebug", "zoomBeaconDebug", "animate", "negativeZ", - "waterFlow", + "waterFlow", "exaggeratedZ", ]; export const PRESETS: Record = { @@ -249,7 +251,7 @@ export const PRESETS: Record = { legSize: 100, legsFlush: false, bedBrightness: 8, - soilBrightness: 6, + soilBrightness: 12, plants: "", labels: false, labelsOnHover: false, @@ -302,7 +304,7 @@ export const PRESETS: Record = { legSize: 100, legsFlush: true, bedBrightness: 8, - soilBrightness: 6, + soilBrightness: 12, plants: "Spring", labels: true, labelsOnHover: false, @@ -365,6 +367,7 @@ const OTHER_CONFIG_KEYS: (keyof Config)[] = [ "solar", "utilitiesPost", "packaging", "lab", "people", "scene", "lowDetail", "eventDebug", "cableDebug", "zoomBeaconDebug", "animate", "distanceIndicator", "kitVersion", "negativeZ", "waterFlow", + "exaggeratedZ", ]; export const modifyConfig = (config: Config, update: Partial) => { diff --git a/frontend/three_d_garden/config_overlays.tsx b/frontend/three_d_garden/config_overlays.tsx index 1117f9969c..26368cce47 100644 --- a/frontend/three_d_garden/config_overlays.tsx +++ b/frontend/three_d_garden/config_overlays.tsx @@ -330,6 +330,7 @@ export const PrivateOverlay = (props: OverlayProps) => { + diff --git a/frontend/three_d_garden/garden/__tests__/grid_test.tsx b/frontend/three_d_garden/garden/__tests__/grid_test.tsx index d996372315..6b826fd3ff 100644 --- a/frontend/three_d_garden/garden/__tests__/grid_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/grid_test.tsx @@ -15,6 +15,7 @@ describe("gridLineOffsets()", () => { describe("", () => { const fakeProps = (): GridProps => ({ config: clone(INITIAL), + getZ: () => 0, }); it("renders", () => { diff --git a/frontend/three_d_garden/garden/__tests__/plants_test.tsx b/frontend/three_d_garden/garden/__tests__/plants_test.tsx index f0341947b0..84febd196c 100644 --- a/frontend/three_d_garden/garden/__tests__/plants_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/plants_test.tsx @@ -93,6 +93,7 @@ describe("", () => { config: config, hoveredPlant: undefined, visible: true, + getZ: () => 0, }; }; diff --git a/frontend/three_d_garden/garden/__tests__/point_test.tsx b/frontend/three_d_garden/garden/__tests__/point_test.tsx index 2830285a02..9c9ff20583 100644 --- a/frontend/three_d_garden/garden/__tests__/point_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/point_test.tsx @@ -17,6 +17,7 @@ describe("", () => { config: clone(INITIAL), point: fakePoint(), visible: true, + getZ: () => 0, }); it("renders", () => { diff --git a/frontend/three_d_garden/garden/__tests__/weed_test.tsx b/frontend/three_d_garden/garden/__tests__/weed_test.tsx index f502840354..7604059cdb 100644 --- a/frontend/three_d_garden/garden/__tests__/weed_test.tsx +++ b/frontend/three_d_garden/garden/__tests__/weed_test.tsx @@ -13,6 +13,7 @@ describe("", () => { config: clone(INITIAL), weed: fakeWeed(), visible: true, + getZ: () => 0, }); it("renders", () => { diff --git a/frontend/three_d_garden/garden/grid.tsx b/frontend/three_d_garden/garden/grid.tsx index 3632f94b1e..bd0639c846 100644 --- a/frontend/three_d_garden/garden/grid.tsx +++ b/frontend/three_d_garden/garden/grid.tsx @@ -1,9 +1,12 @@ 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 { Line, LineProps } from "@react-three/drei"; +import { + zero as zeroFunc, extents as extentsFunc, getGardenPositionFunc, +} from "../helpers"; import { chain, floor, range } from "lodash"; +import { Vector3 } from "three"; export const gridLineOffsets = (botDimension: number): number[] => { const lastRegularOffset = floor(botDimension, -2); @@ -13,39 +16,75 @@ export const gridLineOffsets = (botDimension: number): number[] => { .value(); }; +interface SurfaceLineProps extends Omit { + getZ(x: number, y: number): number; + start: { x: number, y: number }; + end: { x: number, y: number }; + config: Config; +} + +const SurfaceLine = (props: SurfaceLineProps) => { + const { getZ, start, end, config } = props; + const points = React.useMemo(() => + range(101).map(i => { + const t = i / 100; + const x = start.x + (end.x - start.x) * t; + const y = start.y + (end.y - start.y) * t; + const gardenPosition = getGardenPositionFunc(config, false)({ x, y }); + const z = getZ(gardenPosition.x, gardenPosition.y); + return new Vector3(x, y, z); + // eslint-disable-next-line react-hooks/exhaustive-deps + }), [getZ]); + return ; +}; + export interface GridProps { config: Config; + getZ(x: number, y: number): number; } export const Grid = (props: GridProps) => { const { config } = props; const zero = zeroFunc(config); - const gridZ = zero.z - config.soilHeight + 1; const extents = extentsFunc(config); - return + return {gridLineOffsets(config.botSizeX).map(xOffset => { const isOuterLine = xOffset === 0 || xOffset === config.botSizeX; - return ; + lineWidth={2 * (isOuterLine ? 1.5 : 1)} + config={config} + getZ={props.getZ} + start={{ + x: zero.x + xOffset, + y: zero.y, + }} + end={{ + x: zero.x + xOffset, + y: extents.y, + }} />; })} {gridLineOffsets(config.botSizeY).map(yOffset => { const isOuterLine = yOffset === 0 || yOffset === config.botSizeY; - return ; + config={config} + getZ={props.getZ} + start={{ + x: zero.x, + y: zero.y + yOffset, + }} + end={{ + x: extents.x, + y: zero.y + yOffset, + }} />; })} ; }; diff --git a/frontend/three_d_garden/garden/plants.tsx b/frontend/three_d_garden/garden/plants.tsx index 7d27de61f7..4c2a726c1f 100644 --- a/frontend/three_d_garden/garden/plants.tsx +++ b/frontend/three_d_garden/garden/plants.tsx @@ -93,6 +93,7 @@ export interface ThreeDPlantProps { hoveredPlant: number | undefined; dispatch?: Function; visible?: boolean; + getZ(x: number, y: number): number; } export const ThreeDPlant = (props: ThreeDPlantProps) => { @@ -103,7 +104,7 @@ export const ThreeDPlant = (props: ThreeDPlantProps) => { position={new Vector3( threeSpace(plant.x, config.bedLengthOuter), threeSpace(plant.y, config.bedWidthOuter), - zZeroFunc(config) - config.soilHeight + plant.size / 2, + zZeroFunc(config) + props.getZ(plant.x, plant.y) + plant.size / 2, )}> {labelOnly ? { @@ -44,7 +45,7 @@ export const Point = (props: PointProps) => { position={{ x: point.body.x, y: point.body.y, - z: -config.soilHeight, + z: props.getZ(point.body.x, point.body.y), }} onClick={() => { if (point.body.id && !isUndefined(props.dispatch) && props.visible && @@ -138,12 +139,13 @@ const PointBase = (props: PointBaseProps) => { opacity={1 * props.alpha} /> - + {radius > 0 && + } ; }; diff --git a/frontend/three_d_garden/garden/weed.tsx b/frontend/three_d_garden/garden/weed.tsx index dfe8521757..ac60315c65 100644 --- a/frontend/three_d_garden/garden/weed.tsx +++ b/frontend/three_d_garden/garden/weed.tsx @@ -19,6 +19,7 @@ export interface WeedProps { config: Config; dispatch?: Function; visible: boolean; + getZ(x: number, y: number): number; } export const Weed = (props: WeedProps) => { @@ -37,7 +38,7 @@ export const Weed = (props: WeedProps) => { position={{ x: weed.body.x, y: weed.body.y, - z: -config.soilHeight, + z: props.getZ(weed.body.x, weed.body.y), }} config={config} color={weed.body.meta.color} diff --git a/frontend/three_d_garden/garden_model.tsx b/frontend/three_d_garden/garden_model.tsx index 8ec014bed0..2fbbdae6c4 100644 --- a/frontend/three_d_garden/garden_model.tsx +++ b/frontend/three_d_garden/garden_model.tsx @@ -28,6 +28,7 @@ import { BooleanSetting } from "../session_keys"; import { SlotWithTool } from "../resources/interfaces"; import { cameraInit } from "./camera"; import { isMobile } from "../screen_size"; +import { computeSurface, getZFunc, precomputeTriangles } from "./triangles"; const AnimatedGroup = animated(Group); @@ -88,6 +89,12 @@ export const GardenModel = (props: GardenModelProps) => { const showPoints = !!addPlantProps?.getConfigValue(BooleanSetting.show_points); const showWeeds = !!addPlantProps?.getConfigValue(BooleanSetting.show_weeds); + const { vertices, vertexList, uvs, faces } = React.useMemo(() => + computeSurface(props.mapPoints, config), [props.mapPoints, config]); + const triangles = React.useMemo(() => + precomputeTriangles(vertexList, faces), [vertexList, faces]); + const getZ = getZFunc(triangles, -config.soilHeight); + // eslint-disable-next-line no-null/no-null return { @@ -148,9 +158,10 @@ export const GardenModel = (props: GardenModelProps) => { plant={plant} labelOnly={true} config={config} + getZ={getZ} hoveredPlant={hoveredPlant} />)} - + { visible={plantsVisible} config={config} hoveredPlant={hoveredPlant} + getZ={getZ} dispatch={dispatch} />)} { point={point} visible={showPoints} config={config} + getZ={getZ} dispatch={dispatch} />)} { weed={weed} visible={showWeeds} config={config} + getZ={getZ} dispatch={dispatch} />)} diff --git a/frontend/three_d_garden/helpers.ts b/frontend/three_d_garden/helpers.ts index 66987cd1cc..7ef534c7b0 100644 --- a/frontend/three_d_garden/helpers.ts +++ b/frontend/three_d_garden/helpers.ts @@ -1,5 +1,7 @@ import { Config } from "./config"; import * as THREE from "three"; +import { AxisNumberProperty } from "../farm_designer/map/interfaces"; +import { round } from "../farm_designer/map/util"; export const threeSpace = (position: number, max: number): number => position - max / 2; @@ -53,3 +55,26 @@ export const easyCubicBezierCurve3 = ( new THREE.Vector3(x2, y2, z2), ); }; + +type XY = AxisNumberProperty; + +export const getGardenPositionFunc = (config: Config, snap = true) => + (threeDPosition: XY): XY => { + const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config; + const position = { + x: threeSpace(threeDPosition.x, -bedLengthOuter) - bedXOffset, + y: threeSpace(threeDPosition.y, -bedWidthOuter) - bedYOffset, + }; + return snap + ? { x: round(position.x), y: round(position.y) } + : { x: position.x, y: position.y }; + }; + +export const get3DPositionFunc = (config: Config) => + (gardenPosition: XY): XY => { + const { bedLengthOuter, bedWidthOuter, bedXOffset, bedYOffset } = config; + return { + x: threeSpace(gardenPosition.x + bedXOffset, bedLengthOuter), + y: threeSpace(gardenPosition.y + bedYOffset, bedWidthOuter), + }; + }; diff --git a/frontend/three_d_garden/index.tsx b/frontend/three_d_garden/index.tsx index 962f56fd85..fba2b2c591 100644 --- a/frontend/three_d_garden/index.tsx +++ b/frontend/three_d_garden/index.tsx @@ -66,6 +66,7 @@ export interface ThreeDGardenToggleProps { export const ThreeDGardenToggle = (props: ThreeDGardenToggleProps) => { const { navigate, dispatch, threeDGarden } = props; const topDown = props.designer.threeDTopDownView; + const exaggeratedZ = props.designer.threeDExaggeratedZ; const description = isMobile() ? Content.SHOW_3D_VIEW_DESCRIPTION_MOBILE : Content.SHOW_3D_VIEW_DESCRIPTION_DESKTOP; @@ -79,6 +80,20 @@ export const ThreeDGardenToggle = (props: ThreeDGardenToggleProps) => { }}> } + {threeDGarden && + } {threeDGarden &&