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..40ca2189b9 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,10 @@ 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"
+gem "benchmark"
+gem "ostruct"
+gem "bigdecimal"
group :development, :test do
gem "climate_control"
diff --git a/Gemfile.lock b/Gemfile.lock
index afd78be42a..17755a1c50 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)
@@ -109,6 +110,7 @@ GEM
discard (1.4.0)
activerecord (>= 4.2, < 9.0)
docile (1.4.1)
+ drb (2.2.3)
e2mmap (0.1.0)
erubi (1.13.1)
factory_bot (6.5.1)
@@ -128,7 +130,7 @@ GEM
net-http (>= 0.5.0)
globalid (1.2.1)
activesupport (>= 6.1)
- google-apis-core (0.17.0)
+ google-apis-core (0.18.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a)
@@ -136,7 +138,7 @@ GEM
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
- google-apis-iamcredentials_v1 (0.23.0)
+ google-apis-iamcredentials_v1 (0.24.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.51.0)
google-apis-core (>= 0.15.0, < 2.a)
@@ -165,7 +167,7 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
- hashdiff (1.1.2)
+ hashdiff (1.2.0)
hashie (4.1.0)
httpclient (2.9.0)
mutex_m
@@ -227,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)
@@ -245,7 +248,7 @@ GEM
hashie (~> 4.1)
multi_json (~> 1.15)
racc (1.8.1)
- rack (2.2.14)
+ rack (2.2.16)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
@@ -270,7 +273,7 @@ GEM
bundler (>= 1.15.0)
railties (= 6.1.7.10)
sprockets-rails (>= 2.0.0)
- rails-dom-testing (2.2.0)
+ rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
@@ -381,7 +384,7 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
- zeitwerk (2.7.2)
+ zeitwerk (2.7.3)
PLATFORMS
aarch64-linux
@@ -389,6 +392,8 @@ PLATFORMS
DEPENDENCIES
active_model_serializers
+ benchmark
+ bigdecimal
bunny
climate_control
database_cleaner
@@ -396,6 +401,7 @@ DEPENDENCIES
delayed_job_active_record
devise
discard
+ drb
factory_bot_rails
faker
google-cloud-storage (~> 1.11)
@@ -405,6 +411,7 @@ DEPENDENCIES
logger
lograge
mutations
+ ostruct
passenger
pg
pry
@@ -431,7 +438,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/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/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
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 &&