Hide menu
Loading...
Searching...
No Matches
Base Model Viewer Example

The Base Model Viewer example demonstrates how to integrate a basic interactive 3D scene into a web application. This example shows how to load a 3D model, configure a viewer, and render the model within a scene. It serves as the foundational example for developing complex 3D visualization functionality.

User Interface Components

The Base Model Viewer's user interface is built using React and Ant Design components. The UI architecture consists of several key components that work together to create a 3D model viewing experience:

  • Main Layout Component: Orchestrates the overall interface, model upload, and 3D viewport rendering.
  • Model Upload Button Group: Provides file and folder upload options for loading models.
  • Viewport: Renders the 3D scene, grid, and navigation gizmo.

BaseModelViewer.tsx

Location: react/src/pages/base-model-viewer/BaseModelViewer.tsx

This is the main React component that orchestrates the entire base model viewer interface. It creates an instance of the core viewer logic, manages model upload, and composes the layout by combining the upload interface with the viewport.

export const BaseModelViewer = () => {
const viewerRef = useRef<BaseModelViewerRef>(new BaseModelViewerRef());
const onUpload = useAsyncLock(async (files: File[]) => {
await viewerRef.current.loadAndDisplayModel(files);
});
return (
<div className="viewer-page">
<div className="overlay-controls-container">
<ModelUploadButtonGroup onUpload={onUpload} />
</div>
<Viewport viewportRef={viewerRef.current.viewport} isShowGrid={true} />
</div>
);
};

ModelUploadButtonGroup.tsx

Location: react/src/features/model-upload-button-group/ModelUploadButtonGroup.tsx

This component renders the UI elements that allow users to select and upload a 3D model archive or folder. It manages loading state and disables upload buttons during upload.

export const ModelUploadButtonGroup = (props: ModelUploadButtonGroupProps) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const onUpload = async (files: RcFile[]) => {
setIsLoading(true);
await props.onUpload(files);
setIsLoading(false);
};
return (
<Flex vertical gap="small" align="stretch">
<UploadButton
text="Load MTKWEB Model Archive"
uploadType="file"
isEnabled={!isLoading}
onUpload={onUpload}
acceptExtensions=".zip"
/>
<UploadButton
text="Load MTKWEB Model Folder"
uploadType="directory"
isEnabled={!isLoading}
onUpload={onUpload}
/>
</Flex>
);
};

UploadButton.tsx

Location: react/src/common/ui/upload-button/UploadButton.tsx

A reusable button component for uploading files or directories. It handles file selection, disables itself and shows a loading state during upload, and calls the provided upload handler.

export const UploadButton = (props: UploadButtonProps) => {
const [isLoading, setIsLoading] = useState<boolean>(false);
const onBeforeUpload = async (file?: RcFile | null, fileList?: RcFile[] | null) => {
setIsLoading(true);
// ...
return false;
};
return (
<Upload
className="upload-button"
beforeUpload={onBeforeUpload}
directory={props.uploadType === 'directory' ? true : undefined}
showUploadList={false}
accept={props.uploadType === 'file' ? props.acceptExtensions : undefined}
>
<Button
block
icon={<UploadOutlined />}
loading={isLoading}
disabled={isLoading || !props.isEnabled}
>
{props.text}
</Button>
</Upload>
);
};

Viewport.tsx

Location: react/src/features/viewport/Viewport.tsx

Initializes the canvas for rendering the 3D scene with support for grid overlays and gizmo helpers.

export const Viewport = (props: ViewportProps) => {
const gridRotation = new Euler(Math.PI / 2, 0, 0);
const [gridProps, setGridProps] = useState<GridProps>({ position: new Vector3(), fadeDistance: 100 });
// ...
return (
<Canvas gl={{ preserveDrawingBuffer: true }} orthographic frameloop="demand">
<ViewportInitializer viewport={props.viewportRef} />
{props.isShowGrid && (
<Grid position={gridProps.position} rotation={gridRotation} fadeDistance={gridProps.fadeDistance} />
)}
<GizmoHelper />
</Canvas>
);
};

ViewportInitializer.tsx

Location: react/src/features/viewport/ui/viewport-initializer/ViewportInitializer.tsx

This component uses the React Three library, and integrates the core viewport logic with the React 3D scene. It initializes the scene, camera, and renderer, and updates camera controls on each frame.

export const ViewportInitializer = (props: ViewportInitializerProps) => {
const camera = useThree((state) => state.camera);
const renderer = useThree((state) => state.gl);
const scene = useThree((state) => state.scene);
const invalidate = useThree((state) => state.invalidate);
useEffect(() => {
props.viewport.init(scene, renderer, camera, () => invalidate());
const handleResize = () => {
props.viewport.cameraControls.handleResize();
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
props.viewport.dispose();
};
}, [props.viewport, scene, renderer, camera, invalidate]);
useFrame((_state: RootState, delta: number) => {
props.viewport.cameraControls.update(delta);
});
return null;
};

Grid.tsx

Location: react/src/features/viewport/ui/grid/Grid.tsx

Displays a reference grid in the 3D scene, with a customizable position, rotation, and fade distance.

export const Grid = (props: GridProps) => (
<ReactThreeGrid
position={props.position}
rotation={props.rotation}
fadeDistance={props.fadeDistance}
cellSize={10}
sectionSize={100}
infiniteGrid={true}
/>
);

GizmoHelper.tsx

Location: react/src/features/viewport/ui/gizmo-helper/GizmoHelper.tsx

Renders a 3D navigation gizmo in the bottom-right corner of the viewport, providing users with orientation and navigation aids.

export const GizmoHelper = () => (
<ReactThreeGizmoHelper alignment="bottom-right" margin={[80, 80]}>
<GizmoViewport
axisColors={['#9d4b4b', '#2f7f4f', '#3b5b9d']}
labelColor="white"
/>
</ReactThreeGizmoHelper>
);

useAsyncLock.ts

Location: react/src/common/hooks/useAsyncLock.ts

A custom React hook that prevents overlapping asynchronous operations, such as multiple simultaneous file uploads.

export const useAsyncLock = <T extends any[]>(func: (...args: T) => Promise<void>) => {
const isInProgress = useRef<boolean>(false);
return useCallback(async (...args: T): Promise<void> => {
if (isInProgress.current) {
return;
}
isInProgress.current = true;
try {
return await func(...args);
} finally {
isInProgress.current = false;
}
}, [func]);
};

Core Logic Components

The Base Model Viewer core logic layer provides the foundation for 3D model loading, structure management, and scene rendering. The architecture is designed around several key component categories:

  • Viewer Management: Central orchestration of model loading and scene setup.
  • Model Structure: Hierarchical data management and scene graph construction.
  • Model Loading: File and archive handling, model reading, and error management.
  • 3D Scene and Camera: Scene, camera, and controls management.
  • Three.js Helpers: Utilities for converting model data to Three.js objects.

BaseModelViewer.ts

Location: shared/src/viewers/base-model-viewer/BaseModelViewer.ts

This class orchestrates the core functionality for loading and displaying 3D models. It manages the viewport, model structure, and model loading process.

export class BaseModelViewer {
readonly viewport = new Viewport();
readonly modelStructure = new ModelStructure();
async loadAndDisplayModel(files: File | File[]) {
this.clear();
try {
const model = new Model();
await this.modelLoader.load(files, model);
this.onModelLoaded(model);
} catch (error) {
console.log(`Unable to load model. ${error}`);
alert(`Unable to load model. ${error}`);
}
this.setCameraPosition();
this.viewport.fitAll();
}
// ...
}

Viewport.ts

Location: shared/src/features/viewport/Viewport.ts

Responsible for establishing the rendering environment. Manages the Three.js scene, camera, controls, and event handling, ensuring that the scene is properly initialized and adjusted for display.

export class Viewport extends EventTarget {
protected internalScene!: Scene;
protected internalRenderer!: WebGLRenderer;
protected scheduleRender: () => void = () => {};
protected internalCamera!: OrthographicCamera | PerspectiveCamera;
protected internalCameraControls!: CameraControls;
private isInitialized = false;
init(scene?: ThreeScene, renderer?: WebGLRenderer, camera?: OrthographicCamera | PerspectiveCamera, scheduleRender?: () => void): void {
this.internalCamera = camera ? camera : new OrthographicCamera();
this.initScene(scene);
this.initRenderer(renderer);
this.initScheduleRender(scheduleRender);
this.initCameraControls();
this.raycaster.init(this);
this.isInitialized = true;
this.raycaster.isHandlePointerEvents = true;
this.dispatchEvent(new Event('initialized'));
}
// ...
}

Scene.ts

Location: shared/src/features/viewport/scene/Scene.ts

Manages the Three.js scene by initializing the object and light roots. Sets up default lights and provides methods to add or remove objects from the scene.

export class Scene extends EventTarget {
constructor(readonly scene = new ThreeScene()) {
super();
this.setGradientBackground(new Color().setHex(0xd9d9d9), new Color().setHex(0xffffff));
this.initLights();
scene.add(this.lightRoot);
scene.add(this.objectRoot);
// ...
}
// ...
}

CameraControls.ts

Location: shared/src/features/viewport/interaction/CameraControls.ts

Sets up interactive camera controls using either OrbitControls or TrackballControls. Allows for adjusting zoom, target, and rotation speeds to support smooth navigation within the 3D scene.

export class CameraControls {
constructor(camera: OrthographicCamera | PerspectiveCamera, domElement: HTMLCanvasElement, type: CameraControlsType = CameraControlsType.TRACKBALL) {
switch (type) {
case CameraControlsType.ORBIT: {
this.internalControls = new OrbitControls(camera, domElement);
break;
}
case CameraControlsType.TRACKBALL: {
this.internalControls = new TrackballControls(camera, domElement);
break;
}
default: {
throw new Error(`Undefined camera controls type: ${type}.`);
}
}
this.setupControls();
}
// ...
}

ThreeJsHelper.ts

Location: shared/src/common/helpers/ThreeJsHelper.ts

Provides conversion functions that transform MTK Web-specific transformation, color, material, and model data into Three.js entities. Includes the ModelDataConverter class for converting model bodies and shapes into Three.js objects.

ModelLoader.ts

Location: shared/src/features/model/upload/ModelLoader.ts

Handles loading of model files by creating a model source from uploaded files and reading model data to initialize a Model instance. Cleans up model names and manages errors.

export class ModelLoader {
async load(files: File | File[], model?: Model): Promise<Model> {
const modelSource = await new ModelSourceFactory().createModelSourceFromFiles(files);
if (!modelSource) {
throw new Error(`Unsupported model source: ${files}`);
} else if (!modelSource.mainFilename) {
throw new Error(`Unable to find main .mtkweb file.`);
}
model = model || new Model();
const modelReader = new ModelReader();
const res = await modelReader.read(modelSource.mainFilename, model, modelSource.dataProvider);
if (!res) {
throw new Error(`Unable to read model: ${files}`);
}
// ...
return model;
}
// ...
}

ModelSource.ts

Location: shared/src/features/model/upload/ModelSource.ts

Abstracts model file access and supports both folder and archive sources. Used by ModelLoader to provide a unified interface for reading model data.

export abstract class ModelSource {
constructor(
public readonly name: string | null,
public readonly mainFilename: string | null,
public readonly dataProvider: DataProvider,
) {}
}
export class FolderModelSource extends ModelSource { /* ... */ }
export class ArchiveModelSource extends ModelSource { /* ... */ }
export class ModelSourceFactory { /* ... */ }

ModelArchive.ts

Location: shared/src/features/model/upload/ModelArchive.ts

Wraps a JSZip archive for model loading. Finds the main .mtkweb file in the archive and provides methods to check for files and retrieve their contents as ArrayBuffer.

export class ModelArchive {
readonly mainFilename: string | null;
constructor(private zipArchive: JSZip, public readonly name: string | null = null) {
const mtkwebFiles = this.zipArchive.file(/\.mtkweb$/);
this.mainFilename = mtkwebFiles.length > 0 ? mtkwebFiles[0].name : null;
}
static async fromFile(file: File): Promise<ModelArchive> { /* ... */ }
hasFile(filename: string) { /* ... */ }
async getFileArrayBuffer(filename: string): Promise<ArrayBuffer> { /* ... */ }
}

ModelStructure.ts

Location: shared/src/features/model/structure/ModelStructure.ts

Maintains the hierarchical structure of the model. Manages the internal scene graph by clearing old data and adding new model nodes as they are constructed. Provides utility functions for naming and type detection.

ModelStructureBuilder.ts

Location: shared/src/features/model/structure/ModelStructureBuilder.ts

Implements a visitor pattern to traverse the model’s data and build a hierarchical structure (tree) of ModelStructure nodes that contain Three.js objects for visualization.

export class ModelStructureBuilder implements ModelElementVisitor {
buildFromModel(model: Model): ModelNode {
const node = this.createModelStructureNode(model, new Group());
this.nodeStack = [{ node: node, material: null }];
model.accept(this);
return node;
}
// ...
}

Viewer

Base Model Viewer