Skip to content

Commit

Permalink
- snapping
Browse files Browse the repository at this point in the history
- adjusts the height of the volum and origin visualization to match the selections height
  • Loading branch information
gunnnnii committed Nov 7, 2024
1 parent e607c41 commit 07d160b
Show file tree
Hide file tree
Showing 11 changed files with 478 additions and 217 deletions.
55 changes: 54 additions & 1 deletion app/components/arcgis/sketch/sketch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import { useSceneView } from "../views/scene-view/scene-view-context";
import { ForwardedRef, PropsWithChildren, createContext, memo, useContext, useEffect } from "react";
import useProvideRef from "~/hooks/useProvideRef";
import { SketchToolManager } from "./tools/create-tool";
import FeatureSnappingLayerSource from "@arcgis/core/views/interactive/snapping/FeatureSnappingLayerSource.js";
import Layer from "@arcgis/core/layers/Layer";
import type BuildingSceneLayer from '@arcgis/core/layers/BuildingSceneLayer';
import type CSVLayer from '@arcgis/core/layers/CSVLayer';
import type FeatureLayer from '@arcgis/core/layers/FeatureLayer';
import type GeoJSONLayer from '@arcgis/core/layers/GeoJSONLayer';
import type GraphicsLayer from '@arcgis/core/layers/GraphicsLayer';
import type MapNotesLayer from '@arcgis/core/layers/MapNotesLayer';
import type SceneLayer from '@arcgis/core/layers/SceneLayer';
import type WFSLayer from '@arcgis/core/layers/WFSLayer';
import { useWatch } from "~/hooks/reactive";
import Collection from '@arcgis/core/core/Collection';

interface SketchProps {
ref?: ForwardedRef<SketchToolManager>;
Expand Down Expand Up @@ -61,7 +73,11 @@ export default function Sketch({ ref, children, disableZ = false }: PropsWithChi
*/
defaultCreateOptions: {
hasZ: !disableZ
}
},
snappingOptions: {
featureEnabled: true,
},
updateOnGraphicClick: false,
}));

useEffect(() => {
Expand All @@ -75,6 +91,18 @@ export default function Sketch({ ref, children, disableZ = false }: PropsWithChi
sketch.layer = layer;
}, [view, layer, sketch]);

useWatch(() => {
const layers = view.map.allLayers.filter(layer => isSnappableLayer(layer)) as Collection<SnappableLayer>;
const sources = layers.map(layer => new FeatureSnappingLayerSource({
layer,
enabled: true
}))

return sources
}, (layers, previous) => {
if (previous) sketch.snappingOptions?.featureSources.removeMany(previous);
sketch.snappingOptions?.featureSources.addMany(layers);
})

useEffect(() => {
const handle = sketch.on("create", (event) => {
Expand All @@ -92,3 +120,28 @@ export default function Sketch({ ref, children, disableZ = false }: PropsWithChi
</SketchContext.Provider>
);
}

const snappableLayerTypes = [
"building-scene",
"csv",
"feature",
"geojson",
"graphics",
"map-notes",
"scene",
"wfs"
] as const satisfies Layer['type'][];

type SnappableLayer =
| BuildingSceneLayer
| CSVLayer
| FeatureLayer
| GeoJSONLayer
| GraphicsLayer
| MapNotesLayer
| SceneLayer
| WFSLayer

function isSnappableLayer(layer: Layer): layer is SnappableLayer {
return snappableLayerTypes.includes(layer.type as any);
}
13 changes: 9 additions & 4 deletions app/components/arcgis/sketch/tools/create-extent-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import * as reactiveUtils from "@arcgis/core/core/reactiveUtils";
import { Point, Polygon } from "@arcgis/core/geometry";
import CoreGraphic from "@arcgis/core/Graphic";
import { CreateTool, ToolEvent } from "./create-tool";
import { ReactNode, useEffect } from "react";
import { forwardRef, ReactNode, useEffect } from "react";
import useInstance from "~/hooks/useInstance";
import { useSketch } from "../sketch";
import { useAccessorValue } from "~/hooks/reactive";
import useProvideRef from "~/hooks/useProvideRef";

interface ExtentToolProps {
onStart?: (point?: Polygon) => void;
Expand All @@ -16,13 +17,13 @@ interface ExtentToolProps {
children: ({ start }: { start: () => void; cancel: () => void, state: CreateExtentToolManager['state'] }) => ReactNode;
}

export default function CreateExtentTool({
const CreateExtentTool = forwardRef<CreateExtentToolManager, ExtentToolProps>(function CreateExtentTool({
children,
onStart,
onActive,
onComplete,
onCancel,
}: ExtentToolProps) {
}, ref) {
const sketch = useSketch();
const manager = useInstance(() => new CreateExtentToolManager());

Expand All @@ -43,14 +44,18 @@ export default function CreateExtentTool({

const state = useAccessorValue(() => manager.state) ?? 'disabled'

useProvideRef(manager, ref);

return (
children({
start: () => manager.start(),
cancel: () => manager.cancel(),
state
})
);
}
})

export default CreateExtentTool;

@subclass()
class CreateExtentToolManager extends CreateTool {
Expand Down
4 changes: 4 additions & 0 deletions app/components/arcgis/sketch/tools/create-point-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class CreatePointToolManager extends CreateTool {
"active",
{ tool: "point", state: 'active', graphic: this.manager!.createGraphic!, toolEventInfo: null!, type: 'create' }
)
if (geometry == null && previous != null) {
this.manager.snappingOptions.enabled = false;
}
}),
])
}
Expand All @@ -87,6 +90,7 @@ class CreatePointToolManager extends CreateTool {
if (this.state === 'ready') {
this.manager!.pointSymbol = this.createSymbol!;
this.manager!.activeToolId = this.id;
this.manager!.snappingOptions.enabled = true;
this.manager!.create("point", options)
}
}
Expand Down
30 changes: 21 additions & 9 deletions app/components/selection/selection-graphic.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { memo, useMemo } from "react";
import { memo, useDeferredValue, useMemo } from "react";
import { Polygon } from "@arcgis/core/geometry";
import Graphic from "~/components/arcgis/graphic";
import { ExtrudeSymbol3DLayer, FillSymbol3DLayer, PolygonSymbol3D } from "@arcgis/core/symbols";
import FeatureFilterHighlights from "./scene-filter-highlights";
import GraphicsLayer from "../arcgis/graphics-layer";
import { OriginSymbol, SymbologyColors } from "~/symbology/symbology";
import { createOriginSymbol, SymbologyColors } from "~/symbology/symbology";
import { useSelectionState } from "~/data/selection-store";
import { useOriginElevationInfo, useSelectionElevationInfo } from "../../hooks/queries/elevation-query";
import { useOriginElevationInfo, useSelectionVolumeExtent } from "../../hooks/queries/elevation-query";
import SolidEdges3D from "@arcgis/core/symbols/edges/SolidEdges3D.js";
import { useAccessorValue } from "~/hooks/reactive";

Expand Down Expand Up @@ -39,28 +39,40 @@ function InternalSelectionGraphic() {
function Origin() {
const store = useSelectionState();
const origin = useAccessorValue(() => store.modelOrigin ?? store.selectionOrigin);
const selection = useAccessorValue(() => store.selection);
const originElevationInfo = useOriginElevationInfo().data;

const elevatedOrigin = origin?.clone();
if (originElevationInfo && elevatedOrigin) elevatedOrigin.z = originElevationInfo.z
if (originElevationInfo && elevatedOrigin) elevatedOrigin.z = originElevationInfo.z;

const { data } = useSelectionVolumeExtent();

const volumeExtent = useDeferredValue(data ?? selection?.extent)
const height = (volumeExtent?.zmax ?? 0) - (volumeExtent?.zmin ?? 0);
const symbol = useMemo(() => createOriginSymbol(height), [height])

return (
elevatedOrigin ? (
<Graphic geometry={elevatedOrigin} symbol={OriginSymbol} />
<Graphic
geometry={elevatedOrigin}
symbol={symbol}
/>
) : null
)
}

function Volume() {
const store = useSelectionState();
const selection = useAccessorValue(() => store.selection);
const elevationQuery = useSelectionElevationInfo()

const zmin = elevationQuery.data?.minElevation ?? 0;
const zmax = elevationQuery.data?.maxElevation ?? 0;
const { data } = useSelectionVolumeExtent();

const zmin = data?.zmin ?? 0
const zmax = data?.zmax ?? 0

const bufferedZmin = zmin - 100;
const height = (zmax - bufferedZmin) + 200;
const bufferedZmax = zmax + 5;
const height = (bufferedZmax - bufferedZmin);

const VolumeSymbol = useMemo(() => new PolygonSymbol3D({
symbolLayers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SketchTooltip } from "~/components/arcgis/sketch/sketch"
import CreateExtentTool from "~/components/arcgis/sketch/tools/create-extent-tool"
import { useSceneView } from "~/components/arcgis/views/scene-view/scene-view-context"
import { useSelectionState } from "~/data/selection-store"
import { useAccessorValue } from "~/hooks/reactive"
import { useAccessorValue, useWatch } from "~/hooks/reactive"
import { useReferenceElementId } from "../walk-through-context"

export function CreateSelectionTool() {
Expand All @@ -20,11 +20,21 @@ export function CreateSelectionTool() {

const store = useSelectionState();

const toolRef = useRef<any>(null)
useWatch(() => store.editingState, (next,) => {
if (next === 'creating') {
toolRef.current?.start();
} else {
toolRef.current?.cancel()
}
}, { initial: false })

const previousSelection = useRef<Polygon | null>(null);

return (
<>
<CreateExtentTool
ref={toolRef}
onStart={() => {
store.editingState = 'creating'
previousSelection.current = store.selection;
Expand Down
29 changes: 22 additions & 7 deletions app/components/selection/selection-tools/update-origin-tool.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
import { Point } from "@arcgis/core/geometry";
import { CalciteDropdownGroup, CalciteDropdownItem, CalciteSplitButton } from "@esri/calcite-components-react";
import { useRef } from "react";
import { useRef, useMemo, useDeferredValue } from "react";
import { SketchTooltip } from "~/components/arcgis/sketch/sketch";
import CreatePointTool from "~/components/arcgis/sketch/tools/create-point-tool";
import { useSelectionState } from "~/data/selection-store";
import { OriginSymbol } from "~/symbology/symbology";
import { createOriginSymbol } from "~/symbology/symbology";
import { useSelectionVolumeExtent } from "~/hooks/queries/elevation-query";
import { useAccessorValue } from "~/hooks/reactive";

export function UpdateOriginTool() {
const store = useSelectionState();
const previousSelection = useRef<Point | null>(null);
const selection = useAccessorValue(() => store.selection);
const hasSelection = selection != null;

const previousOrigin = useRef<Point | null>(null);
const previousEditingState = useRef(store.editingState);

const { data } = useSelectionVolumeExtent();
// Get elevation info for dynamic symbol height

const volumeExtent = useDeferredValue(data ?? selection?.extent)
const height = (volumeExtent?.zmax ?? 0) - (volumeExtent?.zmin ?? 0);
// Create symbol with dynamic height
const symbol = useMemo(() => createOriginSymbol(height), [height]);

return (
<CreatePointTool
onStart={() => {
console.log(store.editingState);
previousEditingState.current = store.editingState;
previousSelection.current = store.modelOrigin;
previousOrigin.current = store.modelOrigin;
store.editingState = 'updating-origin';
}}
onActive={(point) => {
store.modelOrigin = point;
}}
onComplete={() => {
store.editingState = 'updating-selection';
store.editingState = hasSelection
? 'updating-selection'
: 'creating'
}}
onCancel={() => {
store.editingState = previousEditingState.current;
store.modelOrigin = previousSelection.current;
store.modelOrigin = previousOrigin.current;
}}
createSymbol={OriginSymbol}
createSymbol={symbol}
>
{({ start, cancel, state }) => (
<>
Expand Down
42 changes: 39 additions & 3 deletions app/hooks/queries/elevation-query.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,50 @@
import { useSceneView } from "~/components/arcgis/views/scene-view/scene-view-context";
import { useSelectionState } from "~/data/selection-store";
import { useAccessorValue } from "~/hooks/reactive";
import { Multipoint, Point } from "@arcgis/core/geometry";
import { useSceneView } from "~/components/arcgis/views/scene-view/scene-view-context";
import { Extent, Multipoint, Point } from "@arcgis/core/geometry";
import { useQuery } from '@tanstack/react-query';
import { useSceneLayerViews } from "../useSceneLayers";

export function useSelectionVolumeExtent() {
const view = useSceneView();
const ground = useAccessorValue(() => view.groundView.elevationSampler)!;
const store = useSelectionState();
const selection = useAccessorValue(() => store.selection) ?? null

const lvs = useSceneLayerViews() ?? [];

const query = useQuery({
queryKey: ['selection', 'elevation', selection?.toJSON()],
queryFn: async ({ signal }) => {
let extent: Extent | null = null;
for (const lv of lvs) {
const query = lv.createQuery();
query.spatialRelationship = 'intersects'
query.geometry = selection!.extent;

const results = await lv.queryExtent(query, { signal });
if (results.count > 0) {
extent ??= results.extent;
extent!.union(results.extent);
}
}

return extent;
},
enabled: ground != null && selection != null,
});

return query;
}

export function useSelectionElevationInfo() {
const view = useSceneView();

const ground = useAccessorValue(() => view.map.ground)!;
const store = useSelectionState();
const selection = useAccessorValue(() => store.selection) ?? null

return useQuery({
const query = useQuery({
queryKey: ['selection', 'elevation', ground?.toJSON(), selection?.toJSON()],
queryFn: async ({ signal }) => {
const multipoint = new Multipoint({
Expand All @@ -36,6 +70,8 @@ export function useSelectionElevationInfo() {
},
enabled: ground != null && selection != null,
});

return query;
}

export function useOriginElevationInfo() {
Expand Down
Loading

0 comments on commit 07d160b

Please sign in to comment.