Skip to content

Commit

Permalink
- gives individual mesh components separate names making them easier …
Browse files Browse the repository at this point in the history
…to work with in external software

- gives the terrain a different material then other mesh components
  • Loading branch information
gunnnnii committed Jan 13, 2025
1 parent 724e9c4 commit 2ef6427
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 48 deletions.
16 changes: 14 additions & 2 deletions app/components/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class ToastStore extends Accessor {
@property()
messages = [] as ToastMessage[];

toast = (message: ToastMessage) => {
toast = (message: ToastMessage | ToastableError) => {
if (message instanceof ToastableError && message.originalError) {
console.error(message.originalError);
}

if (this.messages.some(m => m.key === message.key)) return;

this.messages = [...this.messages, message];
Expand Down Expand Up @@ -92,6 +96,7 @@ export class ToastableError extends Error {
message: string;
title: string;
severity: ComponentProps<typeof CalciteAlert>["kind"];
originalError?: unknown

get toast(): ToastMessage {
return {
Expand All @@ -102,11 +107,18 @@ export class ToastableError extends Error {
}
}

constructor(props: { key: string, message: string, title: string, severity: ComponentProps<typeof CalciteAlert>["kind"] }, options?: ErrorOptions) {
constructor(props: {
key: string,
message: string,
title: string,
severity: ComponentProps<typeof CalciteAlert>["kind"],
originalError?: unknown
}, options?: ErrorOptions) {
super(props.message, options);
this.key = props.key;
this.message = props.message;
this.title = props.title;
this.severity = props.severity;
this.originalError = props.originalError;
}
}
118 changes: 87 additions & 31 deletions app/hooks/queries/download/create-mesh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,66 +17,114 @@ import MeshLocalVertexSpace from "@arcgis/core/geometry/support/MeshLocalVertexS
import MeshGeoreferencedVertexSpace from "@arcgis/core/geometry/support/MeshGeoreferencedVertexSpace";
import * as meshUtils from "@arcgis/core/geometry/support/meshUtils";
import type Ground from '@arcgis/core/Ground';
import { type Extent, Point } from "@arcgis/core/geometry";
import { type Extent, Point, type SpatialReference } from "@arcgis/core/geometry";
import type WebScene from "@arcgis/core/WebScene";
// import convertGLBToOBJ from "./obj-conversion";
import * as projection from "@arcgis/core/geometry/projection";
import { createOriginMarker } from "~/symbology/symbology";
import { createOriginMarker, ExportColors } from "~/symbology/symbology";
import MeshMaterial from "@arcgis/core/geometry/support/MeshMaterial.js";
import type { MeshGraphic } from "./export-query";

async function extractElevation(ground: Ground, extent: __esri.Extent) {
return await meshUtils.createFromElevation(ground, extent, {
const mesh = await meshUtils.createFromElevation(ground, extent, {
demResolution: "finest-contiguous"
});

for (const component of mesh.components) {
component.name = "elevation";
component.material ??= new MeshMaterial({
color: ExportColors.terrain()
})
}

return mesh;
}

async function createLayerMeshes({
layer,
features,
vertexSpace,
signal,
}: {
layer: __esri.SceneLayer,
features: MeshGraphic[],
vertexSpace: MeshLocalVertexSpace | MeshGeoreferencedVertexSpace,
signal?: AbortSignal
}) {
const meshPromises = features
.map(async (feature) => {
const { geometry: mesh } = feature;

await mesh.load();

const objectId = feature.getObjectId();
for (const component of mesh.components) {
component.name = `${layer.title}-${objectId}`;
// if the feature already has a material, we use that instead
component.material ??= new MeshMaterial({
color: ExportColors.feature()
});
}
return meshUtils.convertVertexSpace(mesh, vertexSpace, { signal });
})

const meshes = await Promise.all(meshPromises)
return meshes;
}

async function mergeSliceMeshes(
{
elevation,
features,
features: featureMap,
origin,
includeOriginMarker = true,
spatialReference,
signal,
}: {
elevation: Mesh,
features: Mesh[],
features: Map<__esri.SceneLayer, MeshGraphic[]>
origin: Point,
includeOriginMarker?: boolean,
spatialReference: SpatialReference;
signal?: AbortSignal
}) {
const originSpatialReference = origin.spatialReference;
const featureSpatialReference = features.at(0)?.spatialReference ?? originSpatialReference;

let projectedOrigin = origin;
if (originSpatialReference.wkid !== featureSpatialReference.wkid) {
await projection.load();
projectedOrigin = projection.project(origin, featureSpatialReference) as Point;
}

const VertexSpace = projectedOrigin.spatialReference.isWGS84 || projectedOrigin.spatialReference.isWebMercator
const VertexSpace = spatialReference.isWGS84 || spatialReference.isWebMercator
? MeshLocalVertexSpace
: MeshGeoreferencedVertexSpace

const vertexSpace = new VertexSpace({
origin: [projectedOrigin.x, projectedOrigin.y, projectedOrigin.z],
origin: [origin.x, origin.y, origin.z],
});

const meshPromises = features
.map(async (mesh) => {
await mesh.load();
return meshUtils.convertVertexSpace(mesh, vertexSpace, { signal });
})
.concat(meshUtils.convertVertexSpace(elevation, vertexSpace, { signal }));
const promises: Promise<Mesh[] | Mesh>[] = [];
for (const [layer, features] of featureMap.entries()) {
if (layer.spatialReference.wkid !== origin.spatialReference.wkid) {
console.warn(`Layer ${layer.title} has a different spatial reference than previous layers. Skipping.`);
continue;
}

const meshes = createLayerMeshes({
layer,
features,
vertexSpace,
signal,
});
promises.push(meshes);
}

promises.push(meshUtils.convertVertexSpace(elevation, vertexSpace, { signal }));

if (includeOriginMarker) {
const zmax = features.reduce((max, next) => next.extent.zmax > max ? next.extent.zmax : max, elevation.extent.zmax);
const zmin = features.reduce((min, next) => min > next.extent.zmin ? next.extent.zmin : min, elevation.extent.zmin);
const features = Array.from(featureMap.values()).flat();
const zmax = features.reduce((max, { geometry: next }) => next.extent.zmax > max ? next.extent.zmax : max, elevation.extent.zmax);
const zmin = features.reduce((min, { geometry: next }) => min > next.extent.zmin ? next.extent.zmin : min, elevation.extent.zmin);
const height = zmax - zmin;

const originMesh = await createOriginMarker(projectedOrigin, height);
meshPromises.push(meshUtils.convertVertexSpace(originMesh, vertexSpace, { signal }))
const originMesh = await createOriginMarker(origin, height);
promises.push(meshUtils.convertVertexSpace(originMesh, vertexSpace, { signal }))
}

const meshes = await Promise.all(meshPromises)
const meshes = await Promise.all(promises).then((meshes) => meshes.flat());

const slice = meshUtils.merge(meshes.filter((mesh): mesh is Mesh => mesh != null));

Expand All @@ -93,21 +141,29 @@ export async function createMesh({
}: {
scene: WebScene,
extent: Extent,
features: Mesh[],
features: Map<__esri.SceneLayer, MeshGraphic[]>
signal?: AbortSignal,
origin: Point,
includeOriginMarker?: boolean
}) {
const ground = scene.ground;
const sr = features.at(0)?.spatialReference ?? extent.spatialReference;
const projectedExtent = projection.project(extent, sr) as Extent;
const originSpatialReference = origin.spatialReference;
const sr = features.keys().next().value?.spatialReference ?? originSpatialReference;

let projectedExtent = extent;
if (extent.spatialReference.wkid !== sr.wkid) {
await projection.load();
projectedExtent = projection.project(extent, sr) as Extent;
}

const elevation = await extractElevation(ground, projectedExtent);

const slice = await mergeSliceMeshes({
elevation,
features,
features: features,
origin,
includeOriginMarker,
spatialReference: sr,
signal,
});

Expand Down
24 changes: 16 additions & 8 deletions app/hooks/queries/download/export-query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useIsMutating, useMutation, useQuery } from '@tanstack/react-query';
import { useAccessorValue } from "~/arcgis/reactive-hooks";
import { ToastableError, useToast } from "~/components/toast";
import WebScene from "@arcgis/core/WebScene";
import type Graphic from "@arcgis/core/Graphic";

export function useExportSizeQuery({ enabled = false, includeOriginMarker = true }: { enabled?: boolean, includeOriginMarker?: boolean }) {
const scene = useScene()
Expand Down Expand Up @@ -71,7 +72,7 @@ export function useExportSizeQuery({ enabled = false, includeOriginMarker = true
const blob = await createModelBlob({
scene,
extent: selection!.extent,
meshes: features.map(f => f.geometry as Mesh),
features: featureQuery.data!,
signal,
origin: modelOrigin!,
includeOriginMarker,
Expand Down Expand Up @@ -101,7 +102,7 @@ export function useDownloadExportMutation() {
filename: string,
scene: WebScene,
extent: Extent,
meshes: Mesh[],
features: Map<__esri.SceneLayer, MeshGraphic[]>
origin: Point,
signal?: AbortSignal
}) => {
Expand Down Expand Up @@ -131,20 +132,21 @@ async function createModelBlob(args: {
filename: string,
scene: WebScene,
extent: Extent,
meshes: Mesh[],
features: Map<__esri.SceneLayer, MeshGraphic[]>
origin: Point,
signal?: AbortSignal
}) {
const {
includeOriginMarker = false,
scene,
extent,
meshes,
features,
origin,
signal
} = args;

if (meshes.length > MAX_FEATURES) {
const featureCount = Array.from(features.values()).flat().length;
if (featureCount > MAX_FEATURES) {
throw new ToastableError({
key: 'too-many-features',
message: 'Too many features have been selected',
Expand All @@ -157,22 +159,28 @@ async function createModelBlob(args: {
// eslint-disable-next-line no-var
var mesh = await createMesh({
scene,
extent: extent,
features: meshes,
extent,
features,
origin,
includeOriginMarker,
signal,
});
} catch (_error) {
} catch (error) {
throw new ToastableError({
key: 'mesh-creation-failed',
message: 'Failed to create mesh',
title: 'Export error',
severity: 'danger',
originalError: error
})
}

const file = await mesh.toBinaryGLTF();
const blob = new Blob([file], { type: 'model/gltf-binary' });
return blob
}

export type MeshGraphic = Omit<Graphic, 'geometry'> & { geometry: Mesh }
export function filterMeshGraphicsFromFeatureSet(featureSet: __esri.FeatureSet): MeshGraphic[] {
return featureSet.features.filter(feature => feature.geometry.type === "mesh") as any as MeshGraphic[]
}
5 changes: 3 additions & 2 deletions app/hooks/queries/feature-query.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useSceneLayerViews } from "../useSceneLayers";
import { useQuery } from '@tanstack/react-query';
import { useAccessorValue } from "../../arcgis/reactive-hooks";
import { useDebouncedValue } from "../useDebouncedValue";
import { filterMeshGraphicsFromFeatureSet, type MeshGraphic } from "./download/export-query";

export function useSelectedFeaturesFromLayerViews(key?: string) {
const store = useSelectionState();
Expand Down Expand Up @@ -107,15 +108,15 @@ export function useSelectedFeaturesFromLayers(enabled = false) {
const query = useQuery({
queryKey: ['selected-features', 'layers', sceneLayerViews?.map(lv => lv.layer.id), deferredPolygon?.rings],
queryFn: async ({ signal }) => {
const featureMap = new Map<SceneLayer, __esri.FeatureSet['features']>();
const featureMap = new Map<SceneLayer, MeshGraphic[]>();
const promises: Promise<unknown>[] = [];
for (const { layer } of sceneLayerViews!) {
const query = layer.createQuery();
query.geometry = deferredPolygon!.extent;
query.spatialRelationship = 'intersects'
const queryPromise = layer.queryFeatures(query, { signal })
.then((featureSet) => {
featureMap.set(layer, featureSet.features)
featureMap.set(layer, filterMeshGraphicsFromFeatureSet(featureSet))
});
promises.push(queryPromise);
}
Expand Down
5 changes: 2 additions & 3 deletions app/routes/_root.$scene/sidebar/export-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import { useSelectionState } from "~/routes/_root.$scene/selection/selection-sto
import { useReferenceElementId } from "../selection/walk-through-context";
import { useHasTooManyFeatures, useSelectedFeaturesFromLayers } from "~/hooks/queries/feature-query";
import { usePreciseOriginElevationInfo } from "~/hooks/queries/elevation-query";
import { Mesh } from "@arcgis/core/geometry";

interface ExportSettingsProps {
state: BlockState['state'];
Expand All @@ -52,7 +51,7 @@ export default function ExportSettings({ dispatch, state }: ExportSettingsProps)


const featureQuery = useSelectedFeaturesFromLayers(editingState === 'idle');
const features = Array.from(featureQuery.data?.values() ?? []).flat();

const modelOrigin = usePreciseOriginElevationInfo().data;

const hasTooManyFeatures = useHasTooManyFeatures();
Expand Down Expand Up @@ -156,7 +155,7 @@ export default function ExportSettings({ dispatch, state }: ExportSettingsProps)
mutation.mutateAsync({
scene,
extent: selection!.extent,
meshes: features.map(f => f.geometry as Mesh),
features: featureQuery.data!,
origin: modelOrigin!,
includeOriginMarker,
filename,
Expand Down
Loading

0 comments on commit 2ef6427

Please sign in to comment.