Skip to content

Commit

Permalink
[PLAT-4715] Apply model views using stream payload (#623)
Browse files Browse the repository at this point in the history
* Use latest protos

* Apply model views using update stream payload
  • Loading branch information
danschultz authored Aug 13, 2024
1 parent 9ac8196 commit 6722341
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 211 deletions.
24 changes: 24 additions & 0 deletions packages/stream-api/src/streamApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
UpdateCrossSectioningPayload,
UpdateDimensionsPayload,
UpdateInteractionPayload,
UpdateModelViewPayload,
UpdateStreamPayload,
} from './types';
import { WebSocketClient, WebSocketClientImpl } from './webSocketClient';
Expand Down Expand Up @@ -307,6 +308,29 @@ export class StreamApi {
return this.sendRequest({ updateCrossSectioning: payload }, withResponse);
}

/**
* Sends a request to set or clear the model view of the frame.
*
* The payload accepts an optional `frameCorrelationId` that will be sent
* back on the frame that is associated to this request. Use `onRequest` to
* add a callback that'll be invoked when the server sends a request to draw
* the frame.
*
* Use `withResponse` to indicate if the server should reply with a response.
* If `false`, the returned promise will complete immediately. Otherwise,
* it'll complete when a response is received.
*
* @param payload The payload of the request.
* @param withResponse Indicates if the server should reply with a response.
* Defaults to `true`.
*/
public updateModelView(
payload: UpdateModelViewPayload,
withResponse = true
): Promise<vertexvis.protobuf.stream.IStreamResponse> {
return this.sendRequest({ updateModelView: payload }, withResponse);
}

/**
* Sends a request to perform hit detection.
*
Expand Down
5 changes: 5 additions & 0 deletions packages/stream-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export type UpdateCrossSectioningPayload = DeepRequired<
['frameCorrelationId']
>;

export type UpdateModelViewPayload = DeepRequired<
vertexvis.protobuf.stream.IUpdateModelViewPayload,
['frameCorrelationId']
>;

export type HitItemsPayload = DeepRequired<
vertexvis.protobuf.stream.IHitItemsPayload,
[]
Expand Down
14 changes: 7 additions & 7 deletions packages/viewer/src/components/viewer/viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -495,13 +495,6 @@ export class Viewer {
this.annotationStateChanged.emit(state)
);

this.modelViews = new ModelViewController(
client,
() => this.token,
() => this.deviceId,
() => this.scene()
);

this.stream =
this.stream ??
new ViewerStream(new WebSocketClientImpl(), {
Expand All @@ -510,6 +503,13 @@ export class Viewer {
});
this.addStreamListeners();

this.modelViews = new ModelViewController(
client,
this.stream,
() => this.token,
() => this.deviceId
);

this.updateStreamAttributes();
this.stateMap.cursorManager.onChanged.on(() => this.handleCursorChanged());
}
Expand Down
13 changes: 13 additions & 0 deletions packages/viewer/src/lib/mappers/corePbJs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { vertexvis } from '@vertexvis/frame-streaming-protos';
import { UUID } from '@vertexvis/utils';
import { Mapper as M } from '@vertexvis/utils';
import Long from 'long';

export const fromPbJsUuid: M.Func<vertexvis.protobuf.core.IUuid, UUID.UUID> =
M.defineMapper(M.read(M.requiredProp('hex')), ([uuid]) => uuid);
Expand All @@ -16,3 +17,15 @@ export const fromPbJsUuid2l: M.Func<
return UUID.fromMsbLsb(m, l);
}
);

export const toPbJsUuid2l: M.Func<string, vertexvis.protobuf.core.IUuid2l> =
M.defineMapper(
M.compose(
(str) => UUID.toMsbLsb(str),
M.read(
M.mapProp('msb', Long.fromString),
M.mapProp('lsb', Long.fromString)
)
),
([msb, lsb]) => ({ msb, lsb })
);
97 changes: 25 additions & 72 deletions packages/viewer/src/lib/model-views/__tests__/controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
jest.mock(
'@vertexvis/scene-view-protos/sceneview/protos/scene_view_api_pb_service'
);
jest.mock('@vertexvis/stream-api');

import { Dimensions, Point } from '@vertexvis/geometry';
import { SceneViewAPIClient } from '@vertexvis/scene-view-protos/sceneview/protos/scene_view_api_pb_service';
import { StreamApi } from '@vertexvis/stream-api';
import { UUID } from '@vertexvis/utils';

import { mockGrpcUnaryResult } from '../../../testing';
import { makePerspectiveFrame } from '../../../testing/fixtures';
import {
makeListItemModelViewsResponse,
makeUpdateSceneViewRequest,
} from '../../../testing/modelViews';
import { makeListItemModelViewsResponse } from '../../../testing/modelViews';
import { random } from '../../../testing/random';
import { fromPbFrameOrThrow } from '../../mappers';
import { Scene } from '../../scenes';
import { Orientation } from '../../types';
import { ModelViewController } from '../controller';
import { mapListItemModelViewsResponseOrThrow } from '../mapper';
import {
mapItemModelViewOrThrow,
mapListItemModelViewsResponseOrThrow,
} from '../mapper';

describe(ModelViewController, () => {
const jwt = random.string();
const deviceId = random.string();

const sceneId = random.guid();
const sceneViewId = random.guid();
const itemId = random.guid();
const modelViewId = random.guid();
const sceneItemId = UUID.create();
const modelViewId = UUID.create();
const itemModelView = mapItemModelViewOrThrow({ modelViewId, sceneItemId });

describe(ModelViewController.prototype.listByItem, () => {
it('fetches page of model views', async () => {
Expand All @@ -37,7 +33,7 @@ describe(ModelViewController, () => {
mockGrpcUnaryResult(expected)
);

const res = await controller.listByItem(itemId);
const res = await controller.listByItem(sceneItemId);
expect(res).toEqual(
mapListItemModelViewsResponseOrThrow(expected.toObject())
);
Expand All @@ -46,53 +42,32 @@ describe(ModelViewController, () => {

describe(ModelViewController.prototype.load, () => {
it('updates the scene view with the provided model view id', async () => {
const { controller, client } = makeModelViewController(jwt, deviceId);
const expected = makeUpdateSceneViewRequest(
sceneViewId,
itemId,
modelViewId
);
const { controller, streamApi } = makeModelViewController(jwt, deviceId);

(client.updateSceneView as jest.Mock).mockImplementationOnce(
mockGrpcUnaryResult({})
(streamApi.updateModelView as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({})
);

await controller.load(itemId, modelViewId);
await controller.load(sceneItemId, modelViewId);

expect(client.updateSceneView).toHaveBeenCalledWith(
expected,
expect.anything(),
expect.any(Function)
expect(streamApi.updateModelView).toHaveBeenCalledWith(
expect.objectContaining({ itemModelView }),
true
);
});
});

describe(ModelViewController.prototype.unload, () => {
it('updates the scene view with an empty model view id and resets the scene', async () => {
const { controller, client, streamApi } = makeModelViewController(
jwt,
deviceId
);
const expected = makeUpdateSceneViewRequest(sceneViewId);
const { controller, streamApi } = makeModelViewController(jwt, deviceId);

(client.updateSceneView as jest.Mock).mockImplementationOnce(
mockGrpcUnaryResult({})
(streamApi.updateModelView as jest.Mock).mockImplementationOnce(() =>
Promise.resolve({})
);
streamApi.resetSceneView = jest.fn();

await controller.unload();

expect(client.updateSceneView).toHaveBeenCalledWith(
expected,
expect.anything(),
expect.any(Function)
);
expect(streamApi.resetSceneView).toHaveBeenCalledWith(
expect.objectContaining({
includeCamera: true,
}),
true
);
expect(streamApi.updateModelView).toHaveBeenCalledWith({}, true);
});
});

Expand All @@ -102,39 +77,17 @@ describe(ModelViewController, () => {
): {
controller: ModelViewController;
client: SceneViewAPIClient;
scene: Scene;
streamApi: StreamApi;
} {
const client = new SceneViewAPIClient('https://example.com');
const { scene, streamApi } = makeScene();
const streamApi = new StreamApi();
return {
client,
controller: new ModelViewController(
client,
() => jwt,
() => deviceId,
() => Promise.resolve(scene)
),
scene,
streamApi,
};
}

function makeScene(): {
scene: Scene;
streamApi: StreamApi;
} {
const streamApi = new StreamApi();

return {
scene: new Scene(
streamApi,
makePerspectiveFrame(),
fromPbFrameOrThrow(Orientation.DEFAULT),
() => Point.create(1, 1),
Dimensions.create(50, 50),
sceneId,
sceneViewId
() => jwt,
() => deviceId
),
streamApi,
};
Expand Down
72 changes: 10 additions & 62 deletions packages/viewer/src/lib/model-views/controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Pager } from '@vertexvis/scene-view-protos/core/protos/paging_pb';
import { Uuid2l } from '@vertexvis/scene-view-protos/core/protos/uuid_pb';
import { ItemModelView } from '@vertexvis/scene-view-protos/sceneview/protos/domain_pb';
import {
ListItemModelViewsRequest,
ListItemModelViewsResponse,
UpdateSceneViewRequest,
} from '@vertexvis/scene-view-protos/sceneview/protos/scene_view_api_pb';
import { SceneViewAPIClient } from '@vertexvis/scene-view-protos/sceneview/protos/scene_view_api_pb_service';
import { StreamApi } from '@vertexvis/stream-api';
import { UUID } from '@vertexvis/utils';
import { FieldMask } from 'google-protobuf/google/protobuf/field_mask_pb';

import { createMetadata, JwtProvider, requestUnary } from '../grpc';
import { Scene } from '../scenes';
import { mapListItemModelViewsResponseOrThrow } from './mapper';
import {
mapItemModelViewOrThrow,
mapListItemModelViewsResponseOrThrow,
} from './mapper';
import { ModelViewListResponse } from './types';

export interface ListByItemOptions {
Expand All @@ -26,9 +26,9 @@ export interface ListByItemOptions {
export class ModelViewController {
public constructor(
private client: SceneViewAPIClient,
private stream: StreamApi,
private jwtProvider: JwtProvider,
private deviceIdProvider: () => string | undefined,
private sceneProvider: () => Promise<Scene>
private deviceIdProvider: () => string | undefined
) {}

/**
Expand Down Expand Up @@ -79,38 +79,8 @@ export class ModelViewController {
sceneItemId: UUID.UUID,
modelViewId: UUID.UUID
): Promise<void> {
const scene = await this.sceneProvider();

await requestUnary(async (handler) => {
const deviceId = this.deviceIdProvider();
const meta = await createMetadata(this.jwtProvider, deviceId);
const req = new UpdateSceneViewRequest();

const svUuid = UUID.toMsbLsb(scene.sceneViewId);
const svUuid2l = new Uuid2l();
svUuid2l.setMsb(svUuid.msb);
svUuid2l.setLsb(svUuid.lsb);
req.setSceneViewId(svUuid2l);

const siUuid = UUID.toMsbLsb(sceneItemId);
const siUuid2l = new Uuid2l();
siUuid2l.setMsb(siUuid.msb);
siUuid2l.setLsb(siUuid.lsb);
const mvUuid = UUID.toMsbLsb(modelViewId);
const mvUuid2l = new Uuid2l();
mvUuid2l.setMsb(mvUuid.msb);
mvUuid2l.setLsb(mvUuid.lsb);
const mv = new ItemModelView();
mv.setSceneItemId(siUuid2l);
mv.setModelViewId(mvUuid2l);
req.setItemModelView(mv);

const mask = new FieldMask();
mask.addPaths('item_model_view');
req.setUpdateMask(mask);

this.client.updateSceneView(req, meta, handler);
});
const itemModelView = mapItemModelViewOrThrow({ modelViewId, sceneItemId });
this.stream.updateModelView({ itemModelView }, true);
}

/**
Expand All @@ -119,28 +89,6 @@ export class ModelViewController {
* the scene view.
*/
public async unload(): Promise<void> {
const scene = await this.sceneProvider();

await requestUnary(async (handler) => {
const deviceId = this.deviceIdProvider();
const meta = await createMetadata(this.jwtProvider, deviceId);
const req = new UpdateSceneViewRequest();

const svUuid = UUID.toMsbLsb(scene.sceneViewId);
const svUuid2l = new Uuid2l();
svUuid2l.setMsb(svUuid.msb);
svUuid2l.setLsb(svUuid.lsb);
req.setSceneViewId(svUuid2l);

const mask = new FieldMask();
mask.addPaths('item_model_view');
req.setUpdateMask(mask);

this.client.updateSceneView(req, meta, handler);
});

await scene.reset({
includeCamera: true,
});
this.stream.updateModelView({}, true);
}
}
Loading

0 comments on commit 6722341

Please sign in to comment.