Hide menu
Loading...
Searching...
No Matches
Common Usage Scenarios

This guide bridges the generated API reference and the full example applications. It explains how the public MTK Web classes work together in typical workflows: which classes take part, how they relate, in which order to call them, and which caveats to keep in mind. Each scenario links to the detailed API pages and names the example code that shows the complete integration.

MTK Web reads models in the MTKWEB format. Convert the original CAD files with Manufacturing Toolkit first. MTK Web supplies model data, geometry, and computations; it does not render anything and does not depend on a rendering library. The examples use Three.js, but any renderer can consume the same data, see Examples.

The examples are TypeScript. The Location line directly above an example names its source files, relative to the root of the examples project. An example with a Location line shows the code of those files as it is in the repository, limited to the parts that matter for the scenario. Imports contain only names used in the displayed code. Examples written for this guide have no Location line and are explicitly marked as not implemented in the repository.

In This Guide

Load, Navigate, and Traverse a Model

Object Model at a Glance

Model
roots: Assembly or Part
Assembly
instances: Instance one occurrence with an optional transformation
reference: Assembly or Part a definition that several instances can share
Part
bodies: Body
SolidBody.solid: Solid > Shell > Face > Wire > Edge > Vertex
SheetBody.shapes: Shell or Face, with their subshapes
WireframeBody.shapes: Wire or Edge, with their subshapes
MeshBody.shapes: IndexedTriangleSet, PolylineSet, PointSet, Polyline2dSet
drawing: Drawing or null
sheets: Sheet > views: View > elements: drawing Element
layers: Layer groups drawing elements across views
Assembly, Instance, or Part (any ModelElement)
pmi: PMI Data or null
elements: PMIElement > graphical and semantic representations
savedViews: SavedView > camera and elements

Load an MTKWEB Model

Use this when you have a converted .mtkweb file with its companion files and want a Model in memory.

Typical sequence:

  1. Create the model. Create a Model to receive the product structure and optional drawing.
  2. Provide the files. Implement a DataProvider, a function that returns the bytes of a requested file by its name.
  3. Read the model. Create a ModelReader and await read() with the main file name, the model, and the provider.
  4. Handle the result. Check the returned boolean and handle a rejected promise.
  5. Inspect the contents. Check Model.roots for the product structure and Model.drawing for a drawing.

ModelLoader.load() finds the main .mtkweb file among the files of a file source, creates a data provider for all these files with createModelDataProvider(), and reads the model.

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

import {
Model,
ModelReader,
} from '@mtk/web/model-data';
// ...
export class ModelLoader {
// ...
async load(source: FileSource | Files, model?: Model): Promise<Model> {
const fileSource = source instanceof FileSource ? source : new LocalFileSource(source);
const files = await fileSource.getFiles();
const mainFile = files.find((file: FileEntry): boolean => file.name.endsWith(MTKWEB_EXTENSION));
if (!mainFile) {
throw new Error(`Unable to find main .mtkweb file.`);
}
model = model || new Model();
const modelReader = new ModelReader();
const dataProvider = createModelDataProvider(files, mainFile.path);
const res = await modelReader.read(mainFile.path, model, dataProvider);
if (!res) {
throw new Error(`Unable to read model: ${this.formatForError(files)}`);
}
const modelExternalName = this.getModelExternalName(fileSource.name);
if (!model.name && modelExternalName) {
model.name = modelExternalName;
}
return model;
}
// ...
}

Notes and caveats:

  • The provider is called first with the main file name you pass to read(), then with the names of the external data files that the main file references. In converted models these are UUID-named files stored next to the .mtkweb file, so the provider must serve every file of the converted model. Up to eight requests can be in flight at the same time. Each external file is requested once per read; only a failed request can be repeated.
  • All data, including B-Rep, meshes, and PMI, is loaded before read() resolves, so the provider is not needed afterwards.
  • Return an ArrayBuffer that contains exactly the file content. The reader uses the whole buffer, so when your bytes come from a typed-array view such as a Node.js Buffer, pass a copy of the viewed range instead of the underlying ArrayBuffer.
  • read() ordinarily resolves to false when the content is not a valid MTKWEB file or when an external file cannot be loaded or parsed. It may instead reject when the provider for the main file fails, decompression of the main buffer throws, or an unexpected parsing error escapes. Handle the false result and catch any rejection; do not assume that provider errors are the only reason the promise can reject.
  • Reading first clears the target model, including its name. A read that resolves to false leaves the model empty; after a rejected read, the model can contain partially read data, so discard it. Read into a new Model, and do not run two reads into the same model at the same time.
  • Model.isEmpty() checks only roots. A file can contain only a drawing, so check model.drawing as well.
  • Model.open() is equivalent; ModelReader.read() delegates to it. The model name is read from the file and can be null.

Navigate the Product Structure

Use this when you need to list assemblies, parts, and bodies, for example for a model tree or statistics, and to understand which data is shared.

Class Entry point Responsibility
ModelElement @mtk/web/model-data Base class of assemblies, instances, and parts: name, uuid, material, pmi.
Assembly @mtk/web/model-data Groups child instances in instances.
Instance @mtk/web/model-data One occurrence of an assembly or part in reference, with an optional transformation.
Part @mtk/web/model-data Leaf element that owns bodies.
Body @mtk/web/model-data Base class of SolidBody, SheetBody, WireframeBody, and MeshBody.

Typical sequence:

  1. Iterate model.roots; each root is an Assembly or a Part.
  2. For an assembly, iterate assembly.instances; the direct children of an assembly are always instances.
  3. For an instance, continue with instance.reference, which can be null.
  4. For a part, iterate part.bodies and branch on the body class.

Notes and caveats:

  • Parts and assemblies are definitions; instances are occurrences. One Part or Assembly object can be the reference of several instances, so a traversal reaches a shared part once per occurrence. Recognize shared definitions by object identity, for example with a Set.
  • Only Instance carries a transformation. Assemblies, parts, and bodies do not; see Traverse the Model and Compute World Transformations.
  • The Instance.transformation getter creates and stores an identity transformation when none is set and returns the stored object, not a copy. After that, hasTransformation() returns true. Call hasTransformation() first, and clone the transformation before modifying it. The setter stores a copy.
  • Model.roots and Assembly.instances are read-only sets, while Part.bodies is a regular Set. Use roots.size, instances.size, and bodies.size to count elements, and bodies.add() to add a body.
  • Every object derived from BaseObject, including model entities, has an id of type bigint; loaded entities keep the identifiers stored in the file. Convert an id with toString() before putting it into JSON. uuid can be null; Model.assignUuids() assigns new random UUIDs to all unique model elements and drawing entities.
  • The structure can be edited in memory with Model.addRoot(), Assembly.addInstance(), Assembly.removeInstance(), and Model.merge(). To share a part or an assembly, add it through a new instance for each parent; never put the same Instance object into two parents. MTK Web has no MTKWEB writer, so edits stay in memory.

Traverse the Model and Compute World Transformations

Use this when you need every occurrence of a body with its placement (rendering, world-space bounding boxes, totals), or every unique definition once (caches, per-part processing).

Typical sequence:

  1. Implement a visitor. Implement ModelElementVisitor as a class or an object literal; all callbacks are optional. To handle every element through one visitEnter() and visitLeave() pair, extend the abstract CombinedElementVisitor class.
  2. Start the traversal. Call Model.accept(), or accept() on any model element for a subtree. For one visit per unique element, wrap your visitor in ModelElementUniqueVisitor.
  3. Handle traversal callbacks. The traversal enters an assembly, visits each child instance (enter, referenced element, leave), and leaves the assembly; parts receive visitPart(). Returning false from visitAssemblyEnter() or visitInstanceEnter() skips the children; the matching leave method is still called.
  4. Read the bodies. The traversal stops at parts: iterate part.bodies yourself in visitPart().
  5. Compute world placements. For a traversal from the model roots, initialize a stack with an identity Transformation, which combines rotation, translation, and uniform scale. For a subtree, initialize it with the world transformation of the chosen parent occurrence. On every instance enter, push parent.multiplied(instance.transformation) if hasTransformation() returns true; otherwise push parent.clone(). Pop one entry on every instance leave.

The following example is written for this guide and is not implemented in the repository. OccurrenceCounter counts assemblies and instances during a traversal, and collects parts in a Set to count unique definitions, so a shared part can be visited several times while appearing once in uniqueParts.

import {
Assembly,
Instance,
ModelElementVisitor,
Part,
} from '@mtk/web/model-data';
class OccurrenceCounter implements ModelElementVisitor {
assemblies = 0;
instances = 0;
uniqueParts: Set<Part> = new Set();
visitAssemblyEnter(assembly: Assembly): boolean {
this.assemblies++;
return true;
}
visitInstanceEnter(instance: Instance): boolean {
this.instances++;
return true;
}
visitPart(part: Part) {
this.uniqueParts.add(part);
}
}
const counter = new OccurrenceCounter();
model.accept(counter);
console.log(counter.assemblies, counter.instances, counter.uniqueParts.size);

Before a measurement, getCombinedTransformation() computes the world transformation of a selected body from the path to it in the model tree: it walks from the body node up to the root and composes the transformations of the instances on the way.

Location: shared/src/features/model/measurements/ModelMeasurementTargetAdapter.ts

import { Transformation } from '@mtk/web/geom';
import { Instance } from '@mtk/web/model-data';
// ...
function getCombinedTransformation(node: TreeNode): Transformation | null {
let transformation = new Transformation();
for (const currentNode of node.ancestorsAndSelf) {
if (isModelElementNode(currentNode)) {
const modelElement = currentNode.modelElement;
if (modelElement instanceof Instance && modelElement.hasTransformation()) {
transformation = modelElement.transformation.multiplied(transformation);
}
}
}
return transformation.isIdentity() ? null : transformation;
}

The example applications use ModelStructureBuilder to assign instance transformations to Three.js nodes and let the renderer compose them.

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

To process each definition once, wrap the visitor. The import flow creates a PartAnalysisTargetCollector and passes it through ModelElementUniqueVisitor, so each shared part is converted once per model role.

Location: shared/src/features/mtk/import/AnalysisComposer.ts

import {
Model,
ModelElementUniqueVisitor,
} from '@mtk/web/model-data';
// ...
import { PartAnalysisTarget } from '../analysis/base/AnalysisTarget';
import { AnalysisTargetKey } from './AnalysisTargetKey';
import { ImportDiagnostic } from './ImportDiagnostic';
import { PartAnalysisTargetCollector } from './PartAnalysisTargetCollector';
export class AnalysisComposer {
// ...
private async seedAnalysisTargets(state: AnalysisSessionState, diagnostics: ImportDiagnostic[]) {
for (const [role, model] of state.models) {
if (role !== NESTED_ROLE && !state.isRoleSeeded(role)) {
const collector = new PartAnalysisTargetCollector(
this.materialManager,
(key: AnalysisTargetKey, target: PartAnalysisTarget) => {
state.setTarget(key, role, target);
},
);
model.accept(new ModelElementUniqueVisitor(collector));
state.markRoleSeeded(role);
}
// ...
}
}
// ...
}

Notes and caveats:

  • A plain traversal reaches a shared element once per occurrence; an assembly with 5 unique parts can easily produce 18 part visits. Use it for all per-occurrence work: rendering, world coordinates, and totals.
  • ModelElementUniqueVisitor passes each unique assembly, instance, and part to your visitor once. Later occurrences of an already visited element are skipped completely: your visitor receives neither enter nor leave calls for them, and their children are not visited. It therefore cannot provide per-occurrence transformations; use it for per-definition work only.
  • multiplied() returns a new transformation that applies its argument first and then the transformation it is called on, so the world transformation is parentWorld.multiplied(localTransformation), whereas multiply() is the in-place variant.
  • Geometry, triangulations, and PMI are stored in the local coordinates of their part or element. Apply the world transformation of the occurrence when drawing or measuring. Keep one copy of the data per definition and draw it once per occurrence instead of baking transformations into vertices.
  • A transformation can mirror geometry: its scaleFactor can be negative, and isNegative() reports a rotation matrix with a negative determinant.
  • For CombinedElementVisitor, the value returned from visitEnter() controls child visiting for assemblies and instances and is ignored for parts.

Identify Objects with UUIDs

Use this when external data, such as Manufacturing Toolkit process data, review notes, or saved settings, refers to model elements or drawing entities, or when such references must survive a reload of the model.

Use UUIDs for references that must survive a reload. The id inherited from BaseObject identifies an object within the loaded data; ModelElement.uuid provides the UUID of an assembly, instance, or part, or null.

Typical sequence:

  1. Index external data. Read its UUIDs and index the data by UUID. Uuid is a TypeScript template string type in the canonical form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
  2. Match model elements. After loading a model, traverse its unique elements once with ModelElementUniqueVisitor and look up each uuid.
  3. Check missing values. Skip elements without a usable UUID. Check for null and, when all-zero UUIDs must not match external data, use isNullUuid() to detect NULL_UUID.
  4. Identify application objects. Assign a UUID with createRandomUuid(), which creates a random version 4 UUID using Web Crypto when available. Store references to model elements by uuid.
  5. Initialize UUIDs when needed. For a model whose elements have no stable UUIDs yet, Model.assignUuids() assigns random UUIDs to all unique model elements and drawing entities. Call it before storing references: it replaces existing UUIDs.

Manufacturing Toolkit process data refers to parts by UUID. The default contribution provider reads target records from process-data files, extracts partId, and stores it as an AnalysisTargetKey. AnalysisComposer then matches those contribution keys with the targets previously seeded from model parts.

Location: shared/src/features/mtk/import/mtkAnalysisContributionProvider.ts

import { z } from 'zod';
import { AnalysisContribution } from './AnalysisContribution';
import { AnalysisTargetKey } from './AnalysisTargetKey';
import { ImportDiagnostic } from './ImportDiagnostic';
// ...
const TargetRecordIdentitySchema = z.looseObject({ partId: z.string() });
function getTargetRecordKey(record: unknown): AnalysisTargetKey | null {
const parsed = TargetRecordIdentitySchema.safeParse(record);
return parsed.success ? parsed.data.partId : null;
}
function readTargetRecords(
records: readonly unknown[],
path: string,
variantKey: string | undefined,
contributions: AnalysisContribution[],
diagnostics: ImportDiagnostic[],
) {
records.forEach((record: unknown, index: number) => {
const recordPath = `${path}#/parts/${index}`;
const targetKey = getTargetRecordKey(record);
if (targetKey === null) {
reportRecordWithoutTarget(recordPath, diagnostics);
return;
}
contributions.push({
kind: 'target',
targetKey,
variantKey,
...readAnalysisRecordHeader(record),
record,
path: recordPath,
});
});
}

Objects that the application creates itself get new UUIDs.

A design review item, such as a note or a pin, receives its UUID from createRandomUuid().

Location: shared/src/features/design-review/DesignReviewItem.ts

import {
createRandomUuid,
Uuid,
} from '@mtk/web';
// ...
export abstract class DesignReviewItem<
Kind extends string = string,
> {
abstract readonly kind: Kind;
private internalUuid = createRandomUuid();
// ...
get uuid(): Uuid {
return this.internalUuid;
}
// ...
}

Notes and caveats:

  • id is a bigint that identifies an object within the loaded data and links shapes to polygonal ranges. Nothing guarantees that it stays the same when a model is converted again, so use uuid for persistent references.
  • uuid is null when the file stores none. A UUID stored as all zeros is read as NULL_UUID, not as null. The repository example checks only for null; also check isNullUuid(uuid) when all-zero UUIDs must not match external data.
  • A UUID identifies a definition or an occurrence object, not a path. A part shared by several instances has one UUID; to refer to one occurrence, store the UUID of its Instance or the chain of instance UUIDs from the root.
  • Model.assignUuids() replaces every existing UUID with a new random one, so references stored for the previous values no longer resolve. Call it only for models whose elements have no stable UUIDs yet, for example a model built in memory, and before you store references.
  • Sheets, views, layers, drawing elements, and hatch contours also have a uuid; assignUuids() covers them when the model has a drawing.
  • Uuid is a TypeScript type only. Validate UUID strings from external input yourself.

B-Rep Topology and Visualization Data

Explore B-Rep Topology

Use this when you need the faces, edges, and vertices of a body, for example to inspect surface types, to resolve a selection, or to measure individual entities.

Shape is the base class of the B-Rep topology: Solid, Shell, Face, Wire, Edge, and Vertex.

Typical sequence:

  1. Choose a body. Take a body from part.bodies. SolidBody exposes its solid; SheetBody and WireframeBody expose their shapes. Handle MeshBody separately: its shapes are polygonal data without B-Rep topology.
  2. Iterate subshapes. Create a ShapeIterator with new ShapeIterator(body) for direct children. Pass a ShapeType enumeration value, for example new ShapeIterator(body, ShapeType.FACE), to visit all subshapes of that type in depth-first order.
  3. Read geometry. Narrow each result with instanceof. A Face provides surface, an Edge provides curve, and a Vertex provides point.
  4. Collect unique subshapes. Store results in a Set when the same subshape must be processed only once.

AnalysisBuilder.indexShapesOfPart() iterates faces of solid and sheet bodies with a ShapeType.FACE filter, then iterates the edges of each face with a ShapeType.EDGE filter, and stores each shape by its id. Wireframe bodies have no faces, so the method indexes their edges directly.

Location: shared/src/features/mtk/analysis/base/AnalysisBuilder.ts

import {
Shape,
ShapeIterator,
ShapeType,
} from '@mtk/web/brep';
import {
Part,
SheetBody,
SolidBody,
WireframeBody,
} from '@mtk/web/model-data';
import { ModelRole } from '../../import/ModelRole';
import { AnalysisData } from './AnalysisData';
import { TargetAnalysisData } from './TargetAnalysisData';
// ...
export abstract class AnalysisBuilder<
Data extends TargetAnalysisData = TargetAnalysisData,
Sheet extends AnalysisData = AnalysisData,
> {
// ...
protected indexShapesOfPart(part: Part, role: ModelRole) {
let shapes = this.shapesByRole.get(role);
if (!shapes) {
shapes = new Map<bigint, Shape>();
this.shapesByRole.set(role, shapes);
}
for (const body of part.bodies) {
if (body instanceof WireframeBody) {
for (const edge of new ShapeIterator(body, ShapeType.EDGE)) {
shapes.set(edge.id, edge);
}
continue;
}
if (!(body instanceof SolidBody || body instanceof SheetBody)) {
continue;
}
const faceIt = new ShapeIterator(body, ShapeType.FACE);
for (const face of faceIt) {
shapes.set(face.id, face);
const edgeIt = new ShapeIterator(face, ShapeType.EDGE);
for (const edge of edgeIt) {
shapes.set(edge.id, edge);
}
}
}
}
}

Notes and caveats:

  • Without a type, the iterator returns direct children: a SolidBody yields its solid, SheetBody and WireframeBody yield their shapes, a solid yields shells, and so on down to the start and end vertices of an edge.
  • With a type, a subshape is returned each time it is referenced, so edges and vertices shared by several faces repeat. Shared subshapes are the same objects, so a Set removes the duplicates.
  • With a type, the start shape itself is returned when it matches, and subshapes that cannot be reached through the requested level are skipped. For example, new ShapeIterator(sheetBody, ShapeType.SHELL) ignores faces that were added to the body directly.
  • A ShapeIterator can be consumed only once. Create a new iterator for every loop.
  • A MeshBody yields nothing; read MeshBody.shapes instead.
  • Edge.curve is null for a degenerated edge (see isDegenerated()); Face.surface is always set.
  • Shape identifiers (id) come from the file and link shapes to ranges of the polygonal data, see Get Polygonal Data for Visualization.

Get Polygonal Data for Visualization

Use this when you feed a renderer or implement picking. MTK Web provides the vertex, index, normal, and color arrays; the renderer and its scene are your choice.

An individual shape's stored triangulation is described by ShapeTriangulation and its specialized types FaceTriangulation, EdgeTriangulation, and VertexTriangulation. It contains a shared meshShape and the shape's primitives range within it.

Typical sequence:

  1. Collect occurrences. Collect body occurrences with their world transformations, see Traverse the Model and Compute World Transformations.
  2. Prepare reusable render data. For a B-Rep body, call computeCompoundTriangulation(defaultMaterial) on SolidBody, SheetBody, or WireframeBody; the same method is available on Solid, Shell, and Wire. Pass a material or null explicitly. It merges stored triangulations into a ShapeCompoundTriangulation, whose faces, edges, and vertices map meshes to primitive groups. Cache the result by body and effective material when materials differ between occurrences. For a MeshBody, read its shapes directly.
  3. Upload each mesh once. Read vertices, colors, and counts from IndexedTriangleSet, PolylineSet, or PointSet. IndexedTriangleSet also provides vertexIndices, normals, and uvCoordinates.
  4. Draw material groups. Each ShapeCompoundTriangulationPrimitivesGroup defines a range with one material. Its shapesPrimitives entries are ShapePrimitivesGroup ranges for individual shapes.
  5. Place each occurrence. Draw it with its world transformation.
  6. Resolve picking hits. Optionally use a MeshBVH to find a hit triangle, then map it to a face through shapesPrimitives and shapeId.

ModelDataConverter.convertBody() reads the mesh shapes of a MeshBody, computes the compound triangulation of a B-Rep body, and converts each face mesh with its material groups, shape ranges, and bounding volume hierarchy to a Three.js object.

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

import {
ShapeCompoundTriangulation,
ShapePrimitivesGroup,
} from '@mtk/web/brep';
import { VisualMaterial } from '@mtk/web/materials';
import {
IndexedTriangleSet,
MeshBVH,
PointSet,
Polyline2dSet,
PolylineSet,
} from '@mtk/web/mesh';
import {
Body,
MeshBody,
SheetBody,
SolidBody,
WireframeBody,
} from '@mtk/web/model-data';
// ...
export class ModelDataConverter {
// ...
isBVHBuildEnabled = true;
// ...
convertBody(body: Body, material?: VisualMaterial): Group {
material = body.material ?? material;
const root = new Group();
if (body instanceof MeshBody) {
for (const shape of body.shapes) {
let object3d: Object3D | null = null;
if (shape instanceof IndexedTriangleSet) {
object3d = this.converter.convertIndexedTriangleSet(shape, material);
} else if (shape instanceof PolylineSet) {
object3d = this.converter.convertPolylineSet(shape, material);
} else if (shape instanceof Polyline2dSet) {
object3d = this.converter.convertPolyline2dSet(shape, material);
} else if (shape instanceof PointSet) {
object3d = this.converter.convertPointSet(shape, material);
}
// ...
}
} else {
let isClosed = false;
let prs: ShapeCompoundTriangulation<false> | null = null;
if (body instanceof WireframeBody) {
prs = body.computeCompoundTriangulation(material ?? this.materials[MaterialRole.LINE]);
} else if (body instanceof SheetBody) {
prs = body.computeCompoundTriangulation(material ?? this.materials[MaterialRole.MESH]);
} else if (body instanceof SolidBody) {
isClosed = true;
prs = body.computeCompoundTriangulation(material ?? this.materials[MaterialRole.MESH]);
}
if (prs) {
for (const [pvs, groups] of prs.faces) {
const bvh = pvs.bvh ?? (this.isBVHBuildEnabled ? MeshBVH.build(pvs, groups) : null);
const materials: VisualMaterial[] = [];
const geometryGroups: GeometryGroup[] = [];
const shapePrimitives: ShapePrimitivesGroup[] = [];
for (const group of groups) {
let materialIndex = materials.indexOf(group.material);
if (materialIndex === -1) {
materials.push(group.material);
materialIndex = materials.length - 1;
}
geometryGroups.push({ start: group.start, count: group.count, materialIndex });
shapePrimitives.push(...group.shapesPrimitives);
}
const object3d = this.converter.convertIndexedTriangleSet(pvs);
// ...
object3d.geometry.groups = geometryGroups;
object3d.userData['shapePrimitives'] = shapePrimitives;
object3d.raycast = getRaycast(object3d, bvh);
root.add(object3d);
}
// ...
}
}
return root;
}
// ...
}

The ranges use different units:

Range IndexedTriangleSet (faces) PolylineSet (edges) PointSet (vertices)
start and count of a compound group, and primitives of Face.triangulation, Edge.triangulation, or Vertex.triangulation Index-buffer entries, three per triangle Polylines Points
start and count of an entry in shapesPrimitives Triangles start is the index of the first point in the flattened point sequence; count is the number of line segments Points

To find the face or edge under a picked primitive, ModelIntersectionMapper uses this sequence:

  1. getShapeIdFromIntersection() takes the triangle index of a face hit or the index of the first point of a hit edge segment.
  2. It reads the flattened ShapePrimitivesGroup[] stored on the render object by convertBody() and returns the shapeId of the range containing that index.
  3. mapToShape() uses ShapeIterator to traverse the hit body's faces or edges and returns the shape with the matching ID, or null if no match is found.

The excerpt below shows the primitive-index-to-ID lookup. The shape traversal uses the same ShapeIterator API introduced in Explore B-Rep Topology.

Location: shared/src/features/model/interaction/ModelIntersectionMapper.ts

import {
ShapePrimitivesGroup,
ShapeType,
} from '@mtk/web/brep';
// ...
export class ModelIntersectionMapper {
// ...
private getShapeIdFromIntersection(intersection: Intersection, shapeType: ShapeType): bigint | null {
const primitiveIndex = ModelIntersectionMapper.primitiveIndex(intersection, shapeType);
if (primitiveIndex == null) {
return null;
}
const userData = intersection.object.userData['shapePrimitives'] as ShapePrimitivesGroup[];
if (!Array.isArray(userData) || userData.length === 0) {
return null;
}
for (const primitivesGroup of userData) {
if (primitiveIndex >= primitivesGroup.start && primitiveIndex < primitivesGroup.start + primitivesGroup.count) {
return primitivesGroup.shapeId;
}
}
return null;
}
// ...
private static primitiveIndex(intersection: Intersection, shapeType: ShapeType): number | null {
if (shapeType === ShapeType.FACE) {
return intersection.faceIndex ?? null;
}
if (shapeType === ShapeType.EDGE
&& ModelIntersectionMapper.isLineSegments(intersection.object)
&& intersection.index !== undefined) {
return intersection.object.geometry.index?.getX(intersection.index) ?? null;
}
return null;
}
// ...
}

Notes and caveats:

  • Several faces can share one IndexedTriangleSet: Face.triangulation holds the shared mesh and the range of the face. Drawing face.triangulation.meshShape for every face without limiting it to the face's primitives range draws the shared mesh repeatedly; draw the compound triangulation instead.
  • computeCompoundTriangulation() is not cached. When a body uses only part of a stored mesh, it builds a new, compacted mesh on every call; the ranges in the returned groups always refer to the meshes in the returned maps.
  • The getters vertices, vertexIndices, normals, colors, and uvCoordinates build a new typed array on every read. Read each of them once per mesh. vertexIndices is a Uint16Array or a Uint32Array, depending on the largest index.
  • normals, colors, and uvCoordinates are empty arrays when the mesh does not define them; colors are RGBA values per vertex. IndexedTriangleSet.triangleNormal() computes a flat normal when normals are missing.
  • Materials: the MTKWEB reader does not assign materials, so material is null on loaded elements and bodies, and per-vertex colors are the only appearance data from the file. The library applies only body.material, or the default material you pass, to all faces, edges, and vertices of the body; there are no per-face materials. Passing null as the default produces groups whose material can be null. Inheriting materials from parts, instances, and assemblies is an application policy: the examples use the nearest material on the path from the root, and a material assigned to the body overrides it.
  • IndexedTriangleSet.bvh can be loaded from the file or be null, and a compacted compound mesh has none. The static MeshBVH.build() creates one, optionally limited to the given groups, or returns null if it cannot be built. See Pick Triangles with a Bounding Volume Hierarchy.

Choose Materials and Colors

Use this when you assign appearance to bodies or model elements, or convert MTK Web materials into the materials of your renderer.

VisualMaterial is the base class for the material of model elements, bodies, and compound groups. Its opacity is in the range [0, 1].

Typical sequence:

  1. Create colors. Construct a Color from RGBA components in [0, 1] or a hex string. Color.fromRGB() and Color.fromRGBA() accept the BaseRGB and BaseRGBA interfaces ({ r, g, b } and { r, g, b, a }), also accepted as color-array entries by mesh constructors. Read individual components through r, g, b, and a; toRGB() and toRGBA() return packed integers in 0xRRGGBB and 0xRRGGBBAA format.
  2. Choose and share a material. Use BasicMaterial for an unlit material with one color, for example new BasicMaterial(new Color(0, 0, 0)) for drawing lines. Use PhongMaterial for shaded bodies, with ambientColor, diffuseColor, specularColor, emissionColor, and shininess. Create each material once and share it.
  3. Assign the material. Set material on a body or model element, or pass it as the default material of computeCompoundTriangulation().
  4. Resolve inheritance. Determine the material of each body occurrence using your application's inheritance policy; a shared body can inherit different materials through different instance paths.
  5. Convert for rendering. Branch on the material class, convert it to your renderer's material, and cache the result.

MaterialManager.createMeshMaterial() converts BasicMaterial and PhongMaterial into Three.js materials, falls back to a default material, and copies the opacity; convertColor() converts a Color; and DrawingStructureBuilder draws drawing elements with a black BasicMaterial.

Location: shared/src/common/materials/convertColor.ts

import { Color } from '@mtk/web/materials';
import { Color as ThreeColor } from 'three';
export function convertColor(color: Color): ThreeColor {
return new ThreeColor(color.toRGB());
}

Location: shared/src/common/materials/MaterialManager.ts

import {
BasicMaterial,
PhongMaterial,
VisualMaterial,
} from '@mtk/web/materials';
// ...
export class MaterialManager {
// ...
createMeshMaterial(material?: VisualMaterial, isMeshClosed?: boolean): CompatibleMeshMaterial {
if (!material) {
return this.defaultMeshMaterial;
}
let threeMaterial: CompatibleMeshMaterial;
if (material instanceof BasicMaterial) {
const threeBasicMaterial = new MeshBasicMaterial();
threeBasicMaterial.color = convertColor(material.color);
threeMaterial = threeBasicMaterial;
} else if (material instanceof PhongMaterial) {
const threePhongMaterial = new MeshPhongMaterial();
threePhongMaterial.color = convertColor(material.diffuseColor);
threePhongMaterial.specular = convertColor(material.specularColor);
threePhongMaterial.emissive = convertColor(material.emissionColor);
threePhongMaterial.shininess = material.shininess;
threeMaterial = threePhongMaterial;
} else if (
material instanceof VisualThreeMaterial && MaterialManager.isCompatibleMeshMaterial(material.threeMaterial)
) {
threeMaterial = material.threeMaterial.clone();
} else {
threeMaterial = this.defaultMeshMaterial;
}
threeMaterial.side = isMeshClosed ? FrontSide : DoubleSide;
threeMaterial.opacity = material.opacity;
MaterialManager.prepare(threeMaterial);
return threeMaterial;
}
// ...
}

Location: shared/src/features/drawing/structure/DrawingStructureBuilder.ts

import {
BasicMaterial,
Color,
VisualMaterial,
} from '@mtk/web/materials';
// ...
export class DrawingStructureBuilder implements ElementVisitor {
// ...
private readonly material = new BasicMaterial(new Color(0, 0, 0));
// ...
}

Notes and caveats:

  • The MTKWEB reader does not assign materials; appearance from the file is available only as per-vertex colors of meshes.
  • Color components are numbers from 0 to 1, not from 0 to 255; calling new Color() without arguments creates a color with unset components for which isNull() returns true. Pass at least one component or a hex string such as ‘’#9f9fa6'; an invalid hex string also leaves the color unset. -toRGB()drops the alpha component; opacity is carried byVisualMaterial.opacity, and the example copies it to the renderer material.
  • The constructors and color setters ofBasicMaterialandPhongMaterialcopy the colors you pass, while the color getters return the stored objects, so modifying a returned color changes the material. Clone it first when you derive a color for another material.
  • AVisualMaterialthat is neither aBasicMaterialnor aPhongMaterialis an application subclass; the examples useVisualThreeMaterialto carry a Three.js material through the MTK Web API. Keep a fallback branch for such materials. -Color.toJSON()returns aBaseRGBA` object, which is convenient for storing colors in application state.

Pick Triangles with a Bounding Volume Hierarchy

Use this when you implement picking or other spatial queries on large meshes and a linear scan over all triangles is too slow.

MeshBVH provides a hierarchy over the triangles, polylines, or points of one mesh shape; its primitives are their indices. It derives from the abstract BaseBVH, which provides traversal over primitives.

Typical sequence:

  1. Choose build settings when needed. BVHBuildOptions provides optional maxLeafSize, maxDepth, and strategy settings. BVHSplitStrategy selects CENTER, AVERAGE, or SAH (surface area heuristic) splitting.
  2. Prepare the hierarchy once. For each face mesh of a compound triangulation, use the loaded mesh.bvh, or build one with MeshBVH.build(mesh, groups, options) and keep it with the render object. The options argument is optional. Check the result before traversal: build() returns null when it cannot build a hierarchy for the supplied mesh and groups.
  3. Transform the pick ray. Use the inverse world matrix of the occurrence to put the ray in the mesh's local coordinates.
  4. Collect candidate primitives. Call bvh.traverse() with a predicate that tests a node's Box (minCorner, maxCorner) against the ray and a callback that receives each candidate primitive index. The BVHNode interface describes bounds, children, and primitive ranges for hierarchy implementations; applications do not receive nodes.
  5. Find the nearest hit. Intersect the ray with each candidate triangle, keep the nearest hit, and map it to a face, see Get Polygonal Data for Visualization.

ModelDataConverter.convertBody(), shown in Get Polygonal Data for Visualization, reuses a loaded hierarchy or builds one per face mesh and passes it to getRaycast(). getRaycast() transforms the Three.js ray into the local coordinates of the mesh, and raycast() traverses the hierarchy with a ray-box test and intersects the candidate triangles.

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

import { BaseBVH } from '@mtk/web/base';
import { Box } from '@mtk/web/geom';
import { MeshBVH } from '@mtk/web/mesh';
// ...
function raycast(object: Mesh, bvh: MeshBVH, raycaster: Raycaster, ray: Ray, intersections: Intersection[]) {
const material = object.material;
const geometry = object.geometry;
const groups = geometry.groups;
const index = geometry.index;
const intersects = (box: Box): boolean => {
BOX.min.set(box.minCorner.x, box.minCorner.y, box.minCorner.z);
BOX.max.set(box.maxCorner.x, box.maxCorner.y, box.maxCorner.z);
return ray.intersectsBox(BOX);
};
const intersect = (triangleIndex: number) => {
const offset = triangleIndex * 3;
let a = offset + 0;
let b = offset + 1;
let c = offset + 2;
if (index) {
a = index.getX(a);
b = index.getX(b);
c = index.getX(c);
}
// ...
};
bvh.traverse(intersects, intersect);
}
type Raycast = (raycaster: Raycaster, intersections: Intersection[]) => void;
export function getRaycast(object: Object3D, bvh?: BaseBVH<unknown> | null): Raycast {
if (object instanceof Mesh && bvh instanceof MeshBVH) {
return (raycaster: Raycaster, intersections: Intersection[]) => {
INVERSE_MATRIX.copy(object.matrixWorld).invert();
RAY.copy(raycaster.ray).applyMatrix4(INVERSE_MATRIX);
raycast(object as Mesh, bvh, raycaster, RAY, intersections);
};
}
return object.raycast.bind(object);
}

The example applications use the default build options: maxLeafSize is 10, maxDepth is 40, and strategy is BVHSplitStrategy.CENTER. Pass a BVHBuildOptions object as the third argument of MeshBVH.build() to change these values, for example to select BVHSplitStrategy.SAH.

Notes and caveats:

  • Node bounds and primitives are in the local coordinates of the mesh. Transform the ray, not the hierarchy, as getRaycast() does with the inverse world matrix.
  • For an IndexedTriangleSet, a primitive is a triangle index; its vertices are at index-buffer positions 3 * triangle, 3 * triangle + 1, and 3 * triangle + 2. The groups passed to build() use the units of MeshShapePrimitivesGroup, that is, index-buffer entries for triangle meshes.
  • traverse() calls the primitive callback for every primitive in a node whose bounds pass the predicate; it does not sort by distance or stop early. The example collects every hit and leaves sorting to the Three.js raycaster; without such a raycaster, keep the nearest hit yourself.
  • The predicate receives one reused Box object for all nodes. Read its corners inside the predicate and do not keep a reference to it.
  • A hierarchy describes the mesh at build time. Rebuild it after changing vertices, and do not share it between different meshes.
  • MeshBVH.build() returns null when the mesh has no primitives or a group range is invalid. The example then falls back to the renderer's own raycasting.
  • BaseBVH is the extension point for hierarchies over other primitives: a subclass supplies primitive bounds and builds its tree through protected members. Applications normally use MeshBVH.

Exact Geometry and Geometric Primitives

Inspect Surface and Curve Geometry

Use this when a selected face or edge must be classified by its exact geometry, for example to decide which measurements apply, to read a radius or an axis, or to place an annotation on the carrier of a shape.

Geometry is the base class of curves, surfaces, and polylines.

Typical sequence:

  1. Read the carrier geometry. Take face.surface, a Surface, or edge.curve, a Curve. The curve is null for a degenerated edge.
  2. Identify its kind. Branch on the class with instanceof, or read type when you only need the kind of geometry. Surface types use SurfaceType; curve types use CurveType.
  3. Inspect analytic surfaces. ElementarySurface provides a position axis placement, a location, and a direction. Its concrete classes include Plane, CylindricalSurface, ConicalSurface, SphericalSurface, and ToroidalSurface. Read the applicable parameters, such as radius, semiAngle, majorRadius, or minorRadius.
  4. Inspect curves. A Line represents a straight line. Conic provides a position axis placement for Circle, Ellipse, Hyperbola, and Parabola. Read the applicable radius, majorRadius, minorRadius, or focal parameter.
  5. Place the geometry. An Axis3d describes a 3D coordinate system with location, axis, xDirection, and yDirection; use isRightHanded() to check its handedness. An Axis1d describes a location and one direction. Transform positions with the occurrence's world transformation before combining them with data from other occurrences.
  6. Handle 2D curves. Curve2d is their base class. Line2d stores a location and direction. Conic2d provides an Axis2d position for Circle2d, Ellipse2d, Hyperbola2d, and Parabola2d.

For a bounded solid cylinder, use Cylinder, defined by an Axis1d through its center, a radius, and a height. It provides volume, surfaceArea, and isContaining(); it is not a surface of the model.

Use instanceof to read the parameters of a concrete surface or curve. Compare CurveType and SurfaceType separately; their numeric values overlap.

RadialDimensionsBuilder reads the placement of a conic curve and transforms it.

Location: shared/src/features/measurements/radial/RadialDimensionsBuilder.ts

import {
Edge,
Face,
} from '@mtk/web/brep';
import {
Axis3d,
Conic,
Curve,
Transformation,
} from '@mtk/web/geom';
// ...
export class RadialDimensionsBuilder {
constructor(
private readonly shape: Face | Edge,
private readonly transformation: Transformation | null,
) { }
// ...
private getCarrierPosition(curve: Curve): Axis3d | null {
// Every conic carries the placement its radii are measured in - a hyperbola is
// the carrier of BOTH its radii, so testing for a circle or an ellipse alone
// would drop every dimension a hyperbolic edge yields.
if (!(curve instanceof Conic)) {
return null;
}
return this.transformation
? curve.position.transformed(this.transformation)
: curve.position;
}
}

getRadialPlacementSpace() branches on spherical and cylindrical carrier surfaces.

Location: shared/src/features/measurements/radial/RadialPlacementSpace.ts

import { Face } from '@mtk/web/brep';
import {
CylindricalSurface,
SphericalSurface,
Transformation,
} from '@mtk/web/geom';
// ...
export function getRadialPlacementSpace(
targets: MeasurementTarget[],
dimension: RadialMeasurementDimensionData,
): RadialPlacementSpace {
const carrier = getCarrierFace(targets);
if (!carrier) {
return PLANAR_PLACEMENT_SPACE;
}
const surface = carrier.face.surface;
if (surface instanceof SphericalSurface) {
return { kind: 'spherical', isTextStandingUp: true };
}
if (!(surface instanceof CylindricalSurface)) {
return PLANAR_PLACEMENT_SPACE;
}
// ...
}

computeFlatPatternRotation() reads the direction of elementary and planar surfaces.

Location: shared/src/features/mtk/analysis/flat-pattern/computeFlatPatternRotation.ts

import {
Face,
ShapeIterator,
ShapeType,
} from '@mtk/web/brep';
import {
ElementarySurface,
Plane,
} from '@mtk/web/geom';
import { Body } from '@mtk/web/model-data';
import { Vector3 } from 'three';
// ...
function surveyBodyFaces(body: Body): NormalCandidates {
// ...
for (const shape of new ShapeIterator(body, ShapeType.FACE)) {
const face = shape as Face;
// ...
if (face.surface instanceof ElementarySurface) {
const normal = new Vector3().copy(face.surface.direction);
if (isValidNormal(normal)) {
keepLargest(candidates.elementarySurfaceNormal, normal, area);
}
}
if (face.surface instanceof Plane) {
const normal = new Vector3().copy(face.surface.direction);
if (isValidNormal(normal)) {
keepLargest(candidates.planeNormal, normal, area);
}
}
}
// ...
}

Cylinder is also useful independently of a loaded face. The following mathematical example is written for this guide and is not implemented in the repository: it constructs a cylinder from a radius and a height and reads its volume.

import {
Axis1d,
Cylinder,
} from '@mtk/web/geom';
const cylinder = new Cylinder(new Axis1d(), 2, 5);
console.log(cylinder.volume); // 62.83185307179586 (20 * Math.PI)
const flat = new Cylinder(new Axis1d(), 2, 0);
console.log(flat.volume); // 0

Notes and caveats:

  • CurveType and SurfaceType values overlap numerically; for example, CurveType.PARABOLA and SurfaceType.TORUS are both 4. Never put curve and surface types into one list or compare a curve type with a surface type. Compare type values only within one enumeration, or use instanceof.
  • Only analytic geometry has concrete classes. Other kinds, such as B-spline or offset surfaces, are reported by type on a plain Surface or Curve without parameters, so always keep a fallback branch.
  • Surfaces and curves are stored in the local coordinates of their part. Axis3d.transformed() returns a transformed copy; parameters such as radii are not scaled, so multiply them by Math.abs(scaleFactor) as RadialDimensionsBuilder does.
  • Face.surface, Edge.curve, and their position objects belong to the model. Clone them before you modify them.
  • Cylinder is a standalone primitive. Its axis location is the center, not a base point, and negative radius and height values are clamped to 0.
  • The MTKWEB reader does not produce 2D curves: drawing CurveSet and PiecewiseContour elements and the PMI Curve2dOutline expose only polygonal triangulation data. Use Curve2d and its subclasses for exact 2D geometry that your application builds; the drawing example samples the arc of an angular dimension into a Polyline2d instead, see Read Dimensions, Hatches, and Text.
  • Radius and Angle in @mtk/web/measurements already evaluate these classes; see Measure Geometry and Compute Validation Properties before reimplementing a measurement.

Work with Transformations, Matrices, and Vectors

Use this when you pass MTK Web placements to a renderer, compose placements yourself, or compute positions in 3D or in 2D overlays.

Typical sequence:

  1. Read or compose a placement. A Transformation combines rotation, translation, and a uniform scaleFactor; static factories such as scaleTransformation() construct placements. See Traverse the Model and Compute World Transformations.
  2. Convert the matrix layout. For your renderer, the linear part is rotation multiplied by scaleFactor, and the last column is translation. Matrix3d and Matrix2d represent transformation rotations; their Matrix base class provides value(row, column), setRow(), and setColumn() for square matrices.
  3. Choose geometric values. Use Point for a position, Vector for a displacement, and Direction for a unit vector. new Vector(start, end) builds the displacement between two points.
  4. Compute intermediate coordinates. Use mutable XYZ triples or XY pairs for temporary arithmetic in loops. The BaseXYZ ({ x, y, z }) and BaseXY ({ x, y }) interfaces describe the values accepted by fromXYZ() and fromXY() factories.
  5. Place 2D overlays. Build a Transformation2d with a Matrix2d rotation, a Vector2d translation, and a scale factor, and apply it to Point2d values. Direction2d and Axis2d provide a 2D unit vector and coordinate system.
  6. Read point sequences. Polyline and Polyline2d store their points in points; obtain them from PolylineSet.polyline() and Polyline2dSet.polyline().

The following mathematical example is written for this guide and is not implemented in the repository. A Vector keeps the displacement length, a Direction keeps only its orientation, and a translation moves a point by the displacement.

import {
Direction,
Point,
Transformation,
Vector,
} from '@mtk/web/geom';
const displacement = new Vector(3, 4, 0);
const direction = new Direction(displacement.x, displacement.y, displacement.z);
const translation = Transformation.translateTransformation(displacement);
const moved = new Point(1, 2, 3).transformed(translation);
console.log(direction.x, direction.y, direction.z); // 0.6, 0.8, 0
console.log(moved.toJSON()); // { x: 4, y: 6, z: 3 }

The convertTransformation() function creates a Three.js Matrix4 from these values.

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

import { Transformation } from '@mtk/web/geom';
// ...
import { Matrix4 } from 'three';
// ...
export function convertTransformation(transformation: Transformation): Matrix4 {
const rotation = transformation.rotation;
const translation = transformation.translation;
const scale = transformation.scaleFactor;
return new Matrix4(
rotation.value(0, 0) * scale, rotation.value(0, 1) * scale, rotation.value(0, 2) * scale, translation.x,
rotation.value(1, 0) * scale, rotation.value(1, 1) * scale, rotation.value(1, 2) * scale, translation.y,
rotation.value(2, 0) * scale, rotation.value(2, 1) * scale, rotation.value(2, 2) * scale, translation.z,
0, 0, 0, 1,
);
}

The following mathematical example is written for this guide and is not implemented in the repository. It scales a point about the origin, then translates it. In translation.multiplied(scale), the right-hand transformation is applied first. The copying methods leave point, scale, and translation unchanged.

import {
Point2d,
Transformation2d,
Vector2d,
} from '@mtk/web/geom';
const point = new Point2d(1, 2);
const scale = Transformation2d.scaleTransformation(new Point2d(0, 0), 2);
const translation = Transformation2d.translateTransformation(new Vector2d(5, -3));
const placement = translation.multiplied(scale);
const result = point.transformed(placement);
console.log(result.toJSON()); // { x: 7, y: 1 }
console.log(point.toJSON()); // { x: 1, y: 2 }

Notes and caveats:

  • Most geometry classes offer an in-place method and a copying method: add() and added(), multiply() and multiplied(), transform() and transformed(). The in-place variant modifies the object it is called on, which is a common source of bugs when that object belongs to the model.
  • point.subtracted(otherPoint) returns the Vector from the other point, while point.subtracted(vector) and point.added(vector) return a Point; a Direction is always normalized, and its constructor throws a RangeError when the length is not greater than 1e-5. Check the length before constructing a direction from a vector that may be zero.
  • The linear part of a Transformation is rotation * scaleFactor. A negative scaleFactor or a rotation with a negative determinant mirrors geometry, see isNegative().
  • Matrix.value() takes a row and a column. Renderers differ in the order of their matrix storage; Three.js Matrix4 accepts row-major values in its constructor but stores them column-major.
  • XYZ and XY are containers without geometric meaning. Converting them with Point.fromXYZ(), Vector.fromXYZ(), Point2d.fromXY(), or Direction2d.fromXY() creates new objects.
  • Polyline and Polyline2d store their points in the points array.

Measurements and Plane Sections

Geometry Conventions

These rules apply to measurements and plane sections:

  • Units: results use the units of the model. Model carries no unit information. Areas are in square units, volumes in cubic units, and angles in radians.
  • Coordinates: shapes, bodies, and meshes are in the local coordinates of their part. For a Model input, results include all instance transformations, that is, they are in the coordinates of the model roots. For an assembly, instance, or part input, results include the instance transformations inside that subtree; for an instance input, its own transformation is included as well. For a body, shape, or mesh input, results are local unless a transformation argument is accepted and passed. BoundingBox, Distance, Angle, and PlaneSection accept transformations; SurfaceArea, Volume, and both ValidationProperties methods do not.
  • Accuracy: SurfaceArea, Volume, and PlaneSection work on the stored triangulation and have no accuracy parameter. BoundingBox and Distance use stored mesh data where available, but without triangulation they can fall back to exact B-Rep vertex points; Distance also represents an untriangulated edge by the straight segment between its endpoints, not by its exact curve. ValidationProperties uses stored polygonal data and can use exact vertex points and untriangulated straight edges. Length, Radius, and Angle use exact curves and surfaces where they are available. Faces without a triangulation contribute nothing to areas and sections and can make a volume or validation result unavailable or invalid.
  • Tolerance: fixed absolute tolerances of 1e-7 are used; for example, lengths, areas, and volumes at or below this value are reported as not measurable.

Measure Geometry and Compute Validation Properties

Use this when you need sizes, distances, angles, radii, areas, volumes, or mass properties. The classes in @mtk/web/measurements provide static methods that do not modify the model; ValidationPropertyData is the result type of ValidationProperties.compute(). Read Geometry Conventions first.

Whole-object properties:

Method Accepts Result and failure value
BoundingBox.compute() Model, ModelElement, Body, Shape, or MeshShape; optional transformation except for Model; optional output Box Box; an infinite box when nothing is measurable, check isInfinity()
SurfaceArea.compute() The same five input types; no transformation overload Area, or -1
Volume.compute() The same five input types; no transformation overload; only solids and closed triangle meshes have a volume Volume, or -1
ValidationProperties.compute() The same five input types; no transformation overload ValidationPropertyData: volume, surface area, centroid, axes of inertia, and isValid; volume or surfaceArea is -1 when the input has no volume or no surface
ValidationProperties.computeCentroid() The same five input types; no transformation overload Point; the origin when the centroid cannot be computed

Entity measurements:

Method Accepts Result and failure value
Distance.compute() Two objects, each a Body, Shape, or MeshShape; optional transformation per object; optional [Point, Point] output Minimum distance, or -1
Angle.compute() Two planar faces, two linear edges, three vertices, or three points; optional transformation per object Angle in radians, or NaN
Length.compute() Edge, Wire, PolylineSet, or Polyline2dSet Length, or -1
Radius.compute() A circular, elliptic, or hyperbolic edge, or a cylindrical, conical, spherical, or toroidal face; optional curves and points outputs Array of radii, empty when unsupported

Typical sequence:

  1. Choose the input level: a model or an element for placed totals, a body or a shape for local values.
  2. When you compare objects from different occurrences, pass each object's world transformation from the traversal.
  3. Check the failure value before you use a result.

The following example is written for this guide and is not implemented in the repository. It computes the distance between two point sets, first in local coordinates, then with a translation applied to each input.

import {
Point,
Transformation,
Vector,
} from '@mtk/web/geom';
import { Distance } from '@mtk/web/measurements';
import { PointSet } from '@mtk/web/mesh';
const first = new PointSet([new Point(0, 0, 0)]);
const second = new PointSet([new Point(0, 0, 7)]);
console.log(Distance.compute(first, second)); // 7
const third = new PointSet([new Point(0, 0, 0)]);
const fourth = new PointSet([new Point(10, 0, 0)]);
const firstTransformation = Transformation.translateTransformation(new Vector(2, 0, 0));
const secondTransformation = Transformation.translateTransformation(new Vector(-4, 0, 0));
console.log(Distance.compute(third, fourth, firstTransformation, secondTransformation)); // 4

Notes and caveats:

  • Totals for a model or an element count every occurrence: a part placed four times contributes four times.
  • SurfaceArea, Volume, ValidationProperties.compute(), and ValidationProperties.computeCentroid() cannot take a transformation for a body, shape, or mesh input. Use a Model or an enclosing Instance when you need occurrence-aware results. When only uniform scale affects a scalar result, you can adjust a local area by scaleFactor * scaleFactor and a local volume by Math.abs(scaleFactor ** 3); use an occurrence-aware input for full validation properties because placement also affects the centroid and axes of inertia.
  • Length and Radius accept no transformation, so their values ignore the scale of the occurrence. For a scaled occurrence, multiply the value by Math.abs(world.scaleFactor), as the radial measurement example does.
  • Distance writes the closest points only on success. Radius appends to the curves and points arrays without clearing them, and the returned curves can be objects of the model; do not modify them.
  • The getters of ValidationPropertyData return stored objects; clone them before modifying. isValid is false when invalid entities were skipped, so treat the values with care in that case. A valid result can still report -1: volume is -1 for inputs without a volume, such as a sheet or a face, and both volume and surfaceArea are -1 for edges, wires, and vertices.
  • computeCentroid() returns the origin when the centroid cannot be computed, which cannot be told apart from a real centroid at the origin. isValid does not distinguish this case either: for an input without any geometry, such as an empty body, part, or model, compute() returns a valid result with the centroid at the origin and a volume and surface area of 0.
  • Angle supports only faces on a Plane surface and edges on a Line curve. See the API page for the value ranges of each input combination.
  • Ellipses, hyperbolas, and tori yield two radii, the minor one first. A conical face with two circular ends yields both end radii, the smaller one first; a conical face that ends at its apex yields one radius.

Compute Plane Sections

Use this when you need the contour of a shape or a mesh in a plane, for example for section views or clipping outlines.

Class Entry point Responsibility
Plane @mtk/web/geom Section plane, for example new Plane(location, normal) or new Plane(a, b, c, d).
PlaneSection @mtk/web/operations Static compute() of one shape or mesh shape with an optional transformation.
PlaneSectionData @mtk/web/operations Result: polylines, points, and toShapes().

Typical sequence:

  1. Create the plane in the coordinate system you want the results in, usually the coordinates of the model roots.
  2. Map each body to section inputs: SolidBody.solid, the shapes of SheetBody and WireframeBody, or the shapes of MeshBody.
  3. Either call PlaneSection.compute(input, plane, world) with the occurrence transformation, or convert the plane to body-local coordinates and omit the transformation.
  4. Read polylines and points of the result, or convert it to edges and vertices with toShapes().

The following example is written for this guide and is not implemented in the repository. It intersects one triangle with the XY plane. The result contains one segment, from (0.5, 0, 0) to (0, 0.5, 0), and no isolated points.

import {
Direction,
Plane,
Point,
} from '@mtk/web/geom';
import { IndexedTriangleSet } from '@mtk/web/mesh';
import { PlaneSection } from '@mtk/web/operations';
const triangleSet = new IndexedTriangleSet(
new Float32Array([
0, 0, -1,
1, 0, 1,
0, 1, 1,
]),
new Uint16Array([0, 1, 2]),
);
const plane = new Plane(new Point(0, 0, 0), new Direction(0, 0, 1));
const data = PlaneSection.compute(triangleSet, plane);
console.log(data.polylines.numberOfPolylines, data.points.numberOfPoints); // 1, 0

Notes and caveats:

  • PlaneSection.compute() accepts a Shape or a MeshShape only, not a Body, a Part, or a Model.
  • The transformation is applied to the input, so the plane and the results are in the transformed coordinates. Alternatively, transform the plane into the local coordinates of the body and pass no transformation; the results are then in the local coordinates of the body.
  • The section is computed from the stored triangulation, so it follows the tessellation rather than the exact surfaces, and faces without a triangulation contribute nothing. See Geometry Conventions.
  • No intersection is not an error: the result then contains no polylines and no points.
  • A closed contour repeats its first point at the end, and collinear intermediate points are removed. Single-point intersections, such as a vertex touching the plane, are reported in points; the points array can also contain points that lie on a section polyline. For B-Rep input, the points where triangulated edges cross the plane are reported there, and a triangulation vertex that lies exactly in the plane can be reported as well. Do not treat points as isolated points only.

PMI and Drawings

Read PMI and Saved Views

Use this when you need Product Manufacturing Information (PMI): dimensions, tolerances, datums, and notes, either as displayable graphics or as semantic values, and the saved views defined for it.

Read PMI through ModelElement.pmi on an assembly, instance, or part. This property contains a Data container or null. The container provides PMI elements and savedViews.

Typical sequence:

  1. Find PMI. Traverse the model and check each element's pmi property. Skip elements without PMI.
  2. Read annotations. Iterate pmi.elements. Each PMIElement has a type and can have graphical and semantic representations; either representation may be null. Branch on the type or on the available representations.
  3. Display graphics. A GraphicalRepresentation contains outlined, text, and triangulated components; its plane places 2D content in 3D. Iterate graphicalRepresentation.components, or call its accept() with a GraphicalComponentVisitor. See Convert PMI Graphics.
  4. Read semantic values. A SemanticRepresentation contains dimension, geometric tolerance, datum, and surface finish components. Iterate semanticRepresentation.components and call each SemanticComponent's accept() with a SemanticComponentVisitor. Inside a component callback, iterate its attributes and call each attribute's accept() with a SemanticAttributeVisitor. See Collect Semantic PMI Values.
  5. Apply a saved view. Iterate pmi.savedViews. Each SavedView provides a Camera (location, target, up) and the PMI elements to show; its name can be null. Apply the camera and show only those elements.

PMIStructureBuilder.collectElements() reads PMI elements both from the data container and from its saved views. A Set avoids adding the same element twice. PMISource belongs to the example application; its data field is the MTK Web Data container.

Location: shared/src/features/pmi/structure/PMIStructureBuilder.ts

import {
Data as PMIData,
PMIElement,
SavedView,
} from '@mtk/web/pmi';
type PMISource = {
data: PMIData;
// ...
};
export class PMIStructureBuilder {
// ...
private readonly internalSavedViews = new Set<SavedView>();
// ...
private collectElements(source: PMISource): PMIElement[] {
const elements = new Set<PMIElement>();
for (const element of source.data.elements) {
elements.add(element);
}
for (const savedView of source.data.savedViews) {
this.internalSavedViews.add(savedView);
for (const element of savedView.elements) {
elements.add(element);
}
}
return Array.from(elements);
}
// ...
}

Notes and caveats:

  • Semantic components decide at run time whether accept() received a component visitor or an attribute visitor by inspecting the visitor object's own properties. Pass plain object literals whose properties are only callbacks of one interface, as shown in Collect Semantic PMI Values. A fieldless class-based SemanticComponentVisitor can work because its empty own-property set is classified as a component visitor and its prototype callback is then called. A class-based SemanticAttributeVisitor passed through component.accept() is unreliable because it can be classified as a component visitor before the attributes are traversed; additional instance fields can also make either classification fail. TypeScript does not report these dispatch problems. Iterating component.attributes and calling attribute.accept(visitor) on each attribute avoids the component-level check. Graphical, outline, and drawing visitors are not affected.
  • ModelElement.pmi is the public way to reach PMI from a model. The reader also associates PMI with B-Rep shapes, but the public API provides no way to read these associations from a Shape.
  • PMI geometry is in the local coordinates of the element that owns it; place it with that element's world transformation. 2D content (TextComponent, Poly2dOutline, and Curve2dOutline) lies in the representation's plane. The PMI example reads PMI of assemblies and parts and maps 2D content to 3D through that plane.
  • PMIElementType.NOTE has the value 0; compare type with === instead of testing it for truthiness.
  • The PMI example collects the elements of Data.elements together with the elements of all saved views. Data.addView() ignores a view whose name already exists.
  • Read and modify PMI content through the collection properties elements, savedViews, components, and attributes.

Convert PMI Graphics

Use this when you display the graphical representation of PMI elements: lines, curves, filled areas, and text of dimensions, tolerances, and notes.

To dispatch through callbacks, use GraphicalComponentVisitor for each component kind, or extend GraphicalComponentCombinedVisitor to handle all kinds through visitGraphicalComponent(). OutlineVisitor dispatches by outline kind, with visitEnter() and visitLeave() around a composite outline's children.

Typical sequence:

  1. Read the representation. Check the PMI element's graphicalRepresentation and skip it when it is null. A GraphicalRepresentation contains GraphicalComponent objects and the plane that places 2D content in 3D.
  2. Build the plane placement. Compute it once from representation.plane.position.
  3. Convert each component. Branch on its class. An OutlinedComponent provides an outline, which can be null. A TextComponent provides text, fontSize, origin, and direction in the representation plane. A TriangulatedComponent provides filled areas in triangleSet, an IndexedTriangleSet or null.
  4. Walk nested outlines. Outline is the base class. Recurse through a CompositeOutline's outlines, which can include other composite outlines.
  5. Read outline geometry. PolyOutline and Poly2dOutline provide a lineSet: a 3D PolylineSet or a Polyline2dSet in the representation plane. CurveOutline and Curve2dOutline provide curves as polygonal triangulation in the corresponding dimensions. Both lineSet and triangulation can be null; skip missing geometry.
  6. Place the converted content. Place 3D content (PolyOutline, CurveOutline, TriangulatedComponent) directly, and 2D content (Poly2dOutline, Curve2dOutline, TextComponent) through the plane placement.
  7. Apply the model placement. Draw the result with the world transformation of the element that owns the PMI data.

PMIConverter builds the plane placement once, and its helper classes PMIGraphicalComponentConverter and PMIOutlineConverter dispatch components and outlines with instanceof, recurse into composite outlines, and wrap 2D content in an object placed by the plane matrix.

Location: shared/src/common/helpers/pmi/PMIConverter.ts

import { Axis3d } from '@mtk/web/geom';
import { VisualMaterial } from '@mtk/web/materials';
import {
CompositeOutline,
Curve2dOutline,
CurveOutline,
GraphicalComponent,
GraphicalRepresentation,
Outline,
OutlinedComponent,
Poly2dOutline,
PolyOutline,
TextComponent,
TriangulatedComponent,
} from '@mtk/web/pmi';
// ...
const PARAGRAPH_BREAK_PATTERN = /\\P/g;
export class PMIConverter {
// ...
async convertGraphicalRepresentation(source: GraphicalRepresentation, material?: VisualMaterial): Promise<Object3D> {
const object3d = new Group();
const matrix = createPlaneMatrix(source.plane.position);
for (const component of source.components) {
addObject(object3d, await this.componentConverter.convert(component, matrix, material));
}
return object3d;
}
}
class PMIGraphicalComponentConverter {
// ...
async convert(
source: GraphicalComponent,
matrix: Matrix4,
material?: VisualMaterial,
): Promise<Object3D | null> {
if (source instanceof OutlinedComponent) {
return this.convertOutlinedComponent(source, matrix, material);
}
if (source instanceof TextComponent) {
return this.convertTextComponent(source, matrix, material);
}
if (source instanceof TriangulatedComponent) {
return this.convertTriangulatedComponent(source, material);
}
return null;
}
// ...
private async createTextObject(source: TextComponent, material?: VisualMaterial): Promise<TText> {
// ...
textObject.text = source.text.replace(PARAGRAPH_BREAK_PATTERN, '\n');
textObject.fontSize = source.fontSize * PMIGraphicalComponentConverter.FONT_SIZE_COEFFICIENT;
// ...
textObject.position.set(source.origin.x, source.origin.y, 0);
textObject.rotateZ(Math.atan2(source.direction.y, source.direction.x));
// ...
}
private convertTriangulatedComponent(source: TriangulatedComponent, material?: VisualMaterial): Object3D | null {
return source.triangleSet
? this.meshConverter.convertIndexedTriangleSet(source.triangleSet, material)
: null;
}
}
class PMIOutlineConverter {
// ...
convert(
source: Outline | null,
matrix: Matrix4,
material?: VisualMaterial,
): Object3D | null {
if (!source) {
return null;
}
if (source instanceof CompositeOutline) {
return this.convertCompositeOutline(source, matrix, material);
}
if (source instanceof PolyOutline) {
return this.convertPolyOutline(source, material);
}
if (source instanceof Poly2dOutline) {
return this.convertPoly2dOutline(source, matrix, material);
}
if (source instanceof CurveOutline) {
return this.convertCurveOutlineTriangulation(source, material);
}
if (source instanceof Curve2dOutline) {
return this.convertCurve2dOutlineTriangulation(source, matrix, material);
}
return null;
}
private convertCompositeOutline(
source: CompositeOutline,
matrix: Matrix4,
material?: VisualMaterial,
): Object3D | null {
const object3d = new Group();
for (const outline of source.outlines) {
addObject(object3d, this.convert(outline, matrix, material));
}
return object3d.children.length > 0 ? object3d : null;
}
// ...
private convertCurve2dOutlineTriangulation(
source: Curve2dOutline,
matrix: Matrix4,
material?: VisualMaterial,
): Object3D | null {
return createPlaneTransformedObject(
source.triangulation ? this.meshConverter.convertPolyline2dSet(source.triangulation, material) : null,
matrix,
);
}
}
function createPlaneMatrix(axis: Axis3d): Matrix4 {
const { xDirection, yDirection, location, axis: zDirection } = axis;
return new Matrix4().set(
xDirection.x, yDirection.x, zDirection.x, location.x,
xDirection.y, yDirection.y, zDirection.y, location.y,
xDirection.z, yDirection.z, zDirection.z, location.z,
0, 0, 0, 1,
);
}

The visitor interfaces express the same dispatch without instanceof. No example application uses them yet. The following example is written for this guide and is not implemented in the repository: a GraphicalComponentCombinedVisitor receives every graphical component through one method, and an OutlineVisitor brackets the children of a composite outline between visitEnter() and visitLeave().

import {
CompositeOutline,
CurveOutline,
GraphicalComponent,
GraphicalComponentCombinedVisitor,
OutlineVisitor,
TextComponent,
} from '@mtk/web/pmi';
class ComponentCounter extends GraphicalComponentCombinedVisitor {
count = 0;
override visitGraphicalComponent(component: GraphicalComponent) {
this.count++;
}
}
const counter = new ComponentCounter();
new TextComponent().accept(counter);
console.log(counter.count); // 1
class OutlineNameCollector implements OutlineVisitor {
names: string[] = [];
visitEnter(): boolean {
this.names.push('composite');
return true;
}
visitLeave() {}
visitCurveOutline() {
this.names.push('curve');
}
}
const composite = new CompositeOutline();
composite.outlines.push(new CurveOutline());
const collector = new OutlineNameCollector();
composite.accept(collector);
console.log(collector.names); // ['composite', 'curve']

Notes and caveats:

  • 2D content is expressed in the coordinate system of representation.plane.position: its xDirection, yDirection, and axis form the rotation columns and its location the translation. 3D content is already in the local coordinates of the owning element.
  • A TextComponent stores only the text, its size, and its placement. Choose a font and a text rendering method in your application; the example multiplies fontSize by a coefficient and replaces the line-break sequence \P with a newline.
  • OutlinedComponent.outline, TriangulatedComponent.triangleSet, PolyOutline.lineSet, and CurveOutline.triangulation can be null; skip such entries.
  • CompositeOutline.accept() calls visitEnter() before its children and visitLeave() after them. Returning false from visitEnter() skips the children; when visitEnter() is not defined, the children are visited.
  • Unlike semantic components, graphical components and outlines dispatch only by the optional methods of the visitor, so class-based and object-literal visitors both work.
  • Graphics and semantics are independent: a PMI element can have only one of the two representations.

Collect Semantic PMI Values

Use this when you need the machine-readable meaning of PMI, for example a table of nominal values, tolerances, datum references, and surface texture requirements.

Semantic components, reached through semanticRepresentation.components:

Class Entry point Main values
DimensionComponent @mtk/web/pmi nominalValue and type (DimensionType).
GeometricToleranceComponent @mtk/web/pmi magnitude, type (ToleranceType), and toleranceZoneForm (ToleranceZoneFormType).
DatumComponent @mtk/web/pmi label.
SurfaceFinishComponent @mtk/web/pmi materialRemoval (MaterialRemovalType), layDirection (LayDirectionType), isAllAround, and manufacturingMethod.

Semantic attributes, reached through component.attributes; SemanticAttribute is their base class:

Class Entry point Main values
ModifierAttribute, ModifierWithValueAttribute @mtk/web/pmi modifier, and modifier with value.
QualifierAttribute @mtk/web/pmi qualifier.
PlusMinusBoundsAttribute, RangeAttribute @mtk/web/pmi lowerBound and upperBound, or lowerLimit and upperLimit.
LimitsAndFitsAttribute @mtk/web/pmi type and value of a fit designation.
MaximumValueAttribute, DisplacementAttribute @mtk/web/pmi maxValue, or displacement.
DatumTargetAttribute @mtk/web/pmi index and description.
DatumRefAttribute @mtk/web/pmi precedence and targetLabel.
DatumRefCompartmentAttribute @mtk/web/pmi Nested references and modifierAttributes.
LengthUnitAttribute, AngleUnitAttribute @mtk/web/pmi unit (LengthUnit or AngleUnit from @mtk/web/measurements).
MachiningAllowanceAttribute @mtk/web/pmi value, upperBound, and lowerBound.
SurfaceTextureRequirementAttribute @mtk/web/pmi precedence, surfaceParameter (SurfaceParameterType), limitValue, specificationLimit (SpecificationLimitType), filters, evaluationLength, and comparisonRule (ComparisonRuleType).

Typical sequence:

  1. Keep only elements whose semanticRepresentation has components.
  2. Call accept() on each component with an object-literal SemanticComponentVisitor.
  3. In each component callback, read the component values, then call accept() on each attribute of component.attributes with an object-literal SemanticAttributeVisitor.
  4. For a datum reference compartment, visit its references and modifierAttributes with the same attribute visitor.
  5. Format enumeration values for display with the reverse mapping of the numeric enumeration, for example ToleranceType[component.type].

SemanticInformationRowCollector visits all four component kinds and all fifteen attribute kinds and converts every value to a table row.

Location: react/src/pages/pmi-explorer-viewer/semantic-information/SemanticInformationRowCollector.ts

import { LengthUnit } from '@mtk/web/measurements';
import {
type DatumRefCompartmentAttribute,
type DimensionComponent,
DimensionType,
type GeometricToleranceComponent,
type LengthUnitAttribute,
type PMIElement,
type SemanticAttribute,
type SemanticAttributeVisitor,
type SemanticComponent,
type SemanticComponentVisitor,
SpecificationLimitType,
SurfaceParameterType,
type SurfaceTextureRequirementAttribute,
ToleranceType,
ToleranceZoneFormType,
} from '@mtk/web/pmi';
export type SemanticTableRow = {
key: string;
semantic: string;
property: string;
value: string;
};
export class SemanticInformationRowCollector implements SemanticComponentVisitor, SemanticAttributeVisitor {
private rows: SemanticTableRow[] = [];
// ...
private context = '';
private readonly componentVisitor: SemanticComponentVisitor = {
visitDimensionComponent: (dimensionComponent: DimensionComponent) => {
this.visitDimensionComponent(dimensionComponent);
},
visitGeometricToleranceComponent: (geometricToleranceComponent: GeometricToleranceComponent) => {
this.visitGeometricToleranceComponent(geometricToleranceComponent);
},
// ...
};
private readonly attributeVisitor: SemanticAttributeVisitor = {
// ...
visitDatumRefCompartmentAttribute: (datumRefCompartmentAttribute: DatumRefCompartmentAttribute) => {
this.visitDatumRefCompartmentAttribute(datumRefCompartmentAttribute);
},
// ...
visitLengthUnitAttribute: (lengthUnitAttribute: LengthUnitAttribute) => {
this.visitLengthUnitAttribute(lengthUnitAttribute);
},
// ...
};
collect(element: PMIElement): SemanticTableRow[] {
this.rows = [];
// ...
for (const component of element.semanticRepresentation?.components ?? []) {
component.accept(this.componentVisitor);
}
return this.rows;
}
visitDimensionComponent(dimensionComponent: DimensionComponent) {
const semantic = 'Dimension';
this.addRow(semantic, 'Nominal Value', dimensionComponent.nominalValue);
this.addRow(semantic, 'Type', dimensionComponent.type, DimensionType);
this.collectComponentAttributes(dimensionComponent, semantic);
}
visitGeometricToleranceComponent(geometricToleranceComponent: GeometricToleranceComponent) {
const semantic = 'Geometric Tolerance';
this.addRow(semantic, 'Magnitude', geometricToleranceComponent.magnitude);
this.addRow(semantic, 'Type', geometricToleranceComponent.type, ToleranceType);
this.addRow(
semantic,
'Tolerance Zone Form',
geometricToleranceComponent.toleranceZoneForm,
ToleranceZoneFormType,
);
this.collectComponentAttributes(geometricToleranceComponent, semantic);
}
// ...
visitDatumRefCompartmentAttribute(datumRefCompartmentAttribute: DatumRefCompartmentAttribute) {
const semantic = `${this.context} / Datum Reference Compartment`;
// ...
for (const reference of datumRefCompartmentAttribute.references) {
this.collectAttribute(reference, semantic);
}
for (const modifierAttribute of datumRefCompartmentAttribute.modifierAttributes) {
this.collectAttribute(modifierAttribute, semantic);
}
}
// ...
visitLengthUnitAttribute(lengthUnitAttribute: LengthUnitAttribute) {
this.addRow(`${this.context} / Length Unit`, 'Unit', lengthUnitAttribute.unit, LengthUnit);
}
// ...
visitSurfaceTextureRequirementAttribute(
surfaceTextureRequirementAttribute: SurfaceTextureRequirementAttribute,
) {
const semantic = `${this.context} / Surface Texture Requirement`;
// ...
this.addRow(
semantic,
'Surface Parameter',
surfaceTextureRequirementAttribute.surfaceParameter,
SurfaceParameterType,
);
// ...
this.addRow(
semantic,
'Specification Limit',
surfaceTextureRequirementAttribute.specificationLimit,
SpecificationLimitType,
);
// ...
}
private collectComponentAttributes(component: SemanticComponent, context: string) {
for (const attribute of component.attributes) {
this.collectAttribute(attribute, context);
}
}
private collectAttribute(attribute: SemanticAttribute, context: string) {
const previousContext = this.context;
this.context = context;
attribute.accept(this.attributeVisitor);
this.context = previousContext;
}
// ...
}
// ...
function formatSemanticEnumValue(value: unknown, enumType?: Record<number, string>): string | null {
if (typeof value !== 'number') {
return null;
}
const name = enumType?.[value];
return typeof name === 'string' ? formatSemanticEnumName(name) : null;
}
// ...

Notes and caveats:

  • Keep the component visitor and the attribute visitor separate object literals whose own properties are only callbacks, as above. The dispatch rules and the problems of class-based visitors are described in Read PMI and Saved Views. The example class implements both interfaces itself but passes private object literals to accept().
  • DatumRefCompartmentAttribute is the only attribute with nested attributes. Visit them yourself; the attribute's accept() does not visit its children.
  • Attributes are optional and repeatable. Do not assume that a dimension has bounds or that a tolerance has exactly one datum reference.
  • Semantic values are plain numbers without unit metadata of their own. When a component carries a LengthUnitAttribute or an AngleUnitAttribute, present that unit together with the values instead of assuming the model units.
  • Enumerations contain UNDEFINED members such as ToleranceType.UNDEFINED_TOLERANCE; show them as "not specified" instead of hiding the row.

Read 2D Drawings

Use this when a model contains 2D drawing data and you want to list or display its sheets, views, and elements.

Typical sequence:

  1. Read the drawing. Check model.drawing; it is null when the file has no drawing. A Drawing contains sheets and layers.
  2. Explore sheets and views. Iterate drawing.sheets, then sheet.views. Each Sheet has paper size and orientation, and each View contains drawing elements. Their Element base class covers geometries such as CurveSet, PiecewiseContour, and DrawingPointSet, as well as Hatch, Text, and dimensions.
  3. Visit the elements. Call view.accept(visitor) with an ElementVisitor to dispatch by element type, or extend DrawingCombinedElementVisitor to handle every element through visitElement().
  4. Place each view. Use its position and scale to place the content on the sheet.
  5. Filter by layer. A Layer groups elements across views. Check isVisible, test membership with isContaining(), or visit the layer's elements with accept().

DrawingVisualizationChecker counts drawing elements through one callback, regardless of their concrete type.

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

import { DrawingCombinedElementVisitor as CombinedElementVisitor } from '@mtk/web/drawing';
export class DrawingVisualizationChecker extends CombinedElementVisitor {
private count = 0;
get isVisualizable(): boolean {
return this.count > 0;
}
override visitElement() {
this.count += 1;
}
}

In BaseModelViewer.prepare(), drawing is taken from model.drawing and checked for null. With a DrawingVisualizationChecker assigned to checker, the viewer visits every view:

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

for (const sheet of drawing.sheets) {
for (const view of sheet.views) {
view.accept(checker);
}
}

View placement is separate from traversal: apply view.scale in view-local coordinates, then use the axes and location in view.position to place the result on the sheet. See Work with Transformations, Matrices, and Vectors for transformation composition.

Notes and caveats:

  • The drawing is a separate data model from the 3D product structure. A file can contain 3D data, a drawing, or both.
  • Element coordinates are 2D and relative to their view. The examples place each view on its sheet with its position, an Axis2d, and its scale, a Ratio whose toNumber() returns numerator / denominator.
  • A Sheet reports its size in width and height, and its format in paperSize (PaperSizeType) and orientation (OrientationType), while PaperSizeType.USER_DEFINED marks a custom size.
  • CurveSet exposes only a polygonal approximation of its curves in triangulation, which can be null.
  • The element collection of a layer is not public; use accept() or isContaining(). An element belongs to at most one layer; to find it, test the layers in drawing.layers with isContaining().
  • Read and modify drawing content through the collection properties sheets, layers, views, elements, and points.

Read Dimensions, Hatches, and Text

Use this when you show the properties of drawing elements or draw dimensions, hatches, and text yourself.

Class Entry point Responsibility
DrawingGeometry @mtk/web/drawing Base class of the geometric elements CurveSet, PiecewiseContour, and DrawingPointSet.
Dimension @mtk/web/drawing Base class of dimensions: measurementValue, isTextAlignedHorizontally, arrowheadPlacement (ArrowheadPlacementType), firstArrowType and secondArrowType (ArrowType), and the dimension text.
LinearDimension @mtk/web/drawing Two reference points, a dimension line point, and rotation (RotationType).
AngularDimension @mtk/web/drawing anglePoint, two reference points, and dimensionArcPoint.
RadialDimension, DiametricDimension @mtk/web/drawing centerPoint and chordPoint, or two chord points, plus dimensionEndPoint.
Text, TextProperties @mtk/web/drawing text and origin, and properties with fontSize in points, rotation in degrees, and justification (JustificationType).
Hatch @mtk/web/drawing Filled area: pattern (PatternType), style (StyleType), scale, angle, contours with contour() and typeOfContour() (ContourType), and definitionLines.
HatchDefinitionLine @mtk/web/drawing One line family of a hatch pattern: angle, basePoint, offset, and dashes.
Polyline2dSet @mtk/web/mesh 2D polylines of curve sets, contours, and application-built annotation lines.

Typical sequence:

  1. Visit the elements of a view, see Read 2D Drawings.
  2. Group elements by their base class: DrawingGeometry, Dimension, Hatch, or Text.
  3. Read the properties of the concrete class; format enumeration values for display.
  4. To draw a dimension, compute its extension lines, dimension line or arc, arrowheads, and text from its points.
  5. To draw a hatch, fill a solid pattern from its contours, or clip the pattern lines of the other patterns against the contours.

DrawingStructureBuilder.addElementToCategoryNode() groups drawing elements into geometries, dimensions, hatches, and texts by their base classes.

Location: shared/src/features/drawing/structure/DrawingStructureBuilder.ts

import {
Dimension,
DrawingGeometry as Geometry,
Element,
ElementVisitor,
Hatch,
Text,
} from '@mtk/web/drawing';
// ...
export class DrawingStructureBuilder implements ElementVisitor {
// ...
private addElementToCategoryNode(element: Element, parent: DrawingStructureNode): DrawingStructureNode {
if (element instanceof Geometry) {
const geometriesNode = this.getOrCreateCategoryNode(
parent, isGeometriesNode, (): GeometriesNode => new GeometriesNode(new Group()),
);
geometriesNode.geometries.add(element);
return geometriesNode;
} else if (element instanceof Dimension) {
const dimensionsNode = this.getOrCreateCategoryNode(
parent, isDimensionsNode, (): DimensionsNode => new DimensionsNode(new Group()),
);
dimensionsNode.dimensions.add(element);
return dimensionsNode;
} else if (element instanceof Hatch) {
const hatchesNode = this.getOrCreateCategoryNode(
parent, isHatchesNode, (): HatchesNode => new HatchesNode(new Group()),
);
hatchesNode.hatches.add(element);
return hatchesNode;
} else if (element instanceof Text) {
const textsNode = this.getOrCreateCategoryNode(
parent, isTextsNode, (): TextsNode => new TextsNode(new Group()),
);
textsNode.texts.add(element);
return textsNode;
}
throw new Error(`Unknown element type: ${DrawingStructureBuilder.getTypeLabel(element)}.`);
}
// ...
}

The property panel of the model explorer example lists the properties of the selected drawing element, and formatEnumValue() converts enumeration values to title-case names.

Location: react/src/pages/model-explorer-viewer/ui/property-panel/PropertyPanel.tsx

import {
ArrowType,
CurveSet,
Dimension,
DrawingPointSet,
Hatch,
JustificationType,
PatternType,
PiecewiseContour,
Text,
} from '@mtk/web/drawing';
// ...
function formatEnumValue<T extends object>(
target: T,
value: T[keyof T],
): string {
const key = Object.keys(target).find((k) => target[k as keyof T] === value);
if (!key) {
throw new Error(`No enum key found for value: ${String(value)}`);
}
return key
.split('_')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
// ...
const getDrawingProperties = (node: TreeNode): NodeProperties | null => {
const properties: DescriptionsItemType[] = [];
if (isElementNode(node)) {
const element = node.element;
if (element instanceof CurveSet || element instanceof PiecewiseContour) {
if (element.triangulation) {
properties.push({
key: 'number-of-polylines',
label: 'Number of polylines',
children: `${element.triangulation.numberOfPolylines}`,
});
}
} else if (element instanceof Hatch) {
properties.push({
key: 'number-of-contours',
label: 'Number of contours',
children: `${element.numberOfContours}`,
});
// ...
properties.push({
key: 'pattern',
label: 'Pattern',
children: formatEnumValue(PatternType, element.pattern),
});
} else if (element instanceof DrawingPointSet) {
properties.push({
key: 'number-of-points',
label: 'Number of points',
children: `${element.points.length}`,
});
} else if (element instanceof Dimension) {
properties.push({
key: 'value',
label: 'Value',
children: `${element.measurementValue}`,
});
// ...
properties.push({
key: 'first-arrow-type',
label: 'First arrow type',
children: formatEnumValue(ArrowType, element.firstArrowType),
});
// ...
} else if (element instanceof Text) {
properties.push({
key: 'text',
label: 'Text',
children: element.text,
});
// ...
properties.push({
key: 'font-size',
label: 'Font size',
children: `${element.properties.fontSize}`,
});
properties.push({
key: 'rotation',
label: 'Rotation',
children: `${element.properties.rotation}`,
});
properties.push({
key: 'justification',
label: 'Justification',
children: formatEnumValue(JustificationType, element.properties.justification),
});
}
}
// ...
};

To draw the arc of an angular dimension:

The getConvertedAngularDimension() method of DrawingConverter shows the value in degrees, computes the arc radius from dimensionArcPoint, orders the reference points counterclockwise, and samples the arc into a Polyline2d.

Location: shared/src/common/helpers/drawing/DrawingConverter.ts

import { AngularDimension } from '@mtk/web/drawing';
import {
Point2d,
Polyline2d,
XY,
} from '@mtk/web/geom';
import { VisualMaterial } from '@mtk/web/materials';
// ...
export class DrawingConverter {
// ...
async getConvertedAngularDimension(element: AngularDimension, material?: VisualMaterial): Promise<Object3D> {
// ...
/* Create text object with rectangle object */
const textObject = await this.createTextObject(
element.text,
`${(element.measurementValue * 180 / Math.PI).toFixed(2)}°`,
material,
);
// ...
/* Extract key points from the dimension element */
const p1 = XY.fromXY(element.anglePoint);
let p2 = XY.fromXY(element.firstReferencePoint);
let p3 = XY.fromXY(element.secondReferencePoint);
const p4 = XY.fromXY(element.dimensionArcPoint);
/* Ensure points are in counter-clockwise order for consistent calculations */
if (p2.crossed(p3) < 0) {
[p2, p3] = [p3, p2];
}
/* Calculate additional parameters */
const distance = p1.subtracted(p4).modulus();
const p2end = p2.subtracted(p1).normalize().multiply(distance);
const p3end = p3.subtracted(p1).normalize().multiply(distance);
const angle = p2end.angle(p3end);
p2end.add(p1);
p3end.add(p1);
// ...
const polyline = new Polyline2d();
const numberOfSectorSegments = Math.max(Math.ceil(angle / Math.PI * 60), 6);
const rotation = p2end.subtracted(p1);
for (let i = 0; i <= numberOfSectorSegments; i++) {
const segment = i / numberOfSectorSegments * angle;
const c = Math.cos(segment);
const s = Math.sin(segment);
const x = rotation.x * c - rotation.y * s;
const y = rotation.x * s + rotation.y * c;
polyline.points.push(new Point2d(p1.x + x, p1.y + y));
}
polylines.push(polyline);
// ...
}
// ...
}

Hatch patterns other than SOLID and CUSTOM are standard ANSI and ISO patterns.

getHatchDefinitionLines() stores the definition of each predefined pattern as numbers (angle, base point, offset, and dashes) and creates the corresponding HatchDefinitionLine objects.

Location: shared/src/common/helpers/drawing/getHatchDefinitionLines.ts

import {
HatchDefinitionLine,
PatternType,
} from '@mtk/web/drawing';
type PredefinedPatternType = Exclude<PatternType, PatternType.SOLID | PatternType.CUSTOM>;
const HATCH_PATTERN_DEFINITIONS: Record<PredefinedPatternType, number[][]> = {
[PatternType.ANSI_31]: [
[45, 0, 0, 0, 3.175],
],
// ...
[PatternType.ANSI_37]: [
[45, 0, 0, 0, 3.175],
[135, 0, 0, 0, 3.175],
],
// ...
} as const;
export function getHatchDefinitionLines(pattern: PredefinedPatternType): HatchDefinitionLine[] {
const lines: HatchDefinitionLine[] = [];
for (const data of HATCH_PATTERN_DEFINITIONS[pattern]) {
const line = new HatchDefinitionLine();
line.angle = data[0];
line.basePoint.setCoord(data[1], data[2]);
line.offset.setCoord(data[3], data[4]);
line.dashes.push(...data.slice(5));
lines.push(line);
}
return lines;
}

Notes and caveats:

  • Dimension points and text origins are in view coordinates, like all drawing elements.
  • measurementValue is the value to display. The example shows an angular dimension value in degrees after multiplying it by 180 / Math.PI, so treat angular values as radians. The dimension text carries the text properties of the label.
  • Text.properties.rotation is in degrees, while angles in @mtk/web/geom are in radians. The font size is properties.fontSize.
  • The example fills a SOLID hatch from the triangulations of its contours. For CUSTOM it draws the definitionLines of the element; for the predefined ANSI and ISO patterns it adds the generated definition lines. It appends them to hatch.definitionLines, which modifies the model; collect them in a separate array instead when a hatch can be converted more than once.
  • The scale and angle of a hatch apply to all definition lines. The example rotates each line by hatch.angle + line.angle, both in degrees, multiplies base points, offsets, and dashes by scale, and mirrors the lines for the MIRRORED and DOUBLE styles.
  • Hatch.numberOfContours, contour(), and typeOfContour() are the way to read contours; definition lines and their dashes are in the definitionLines and dashes arrays.
  • Enumeration names such as ArrowType.CLOSED_FILLED are available through the reverse mapping of numeric enumerations, which formatSemanticEnumValue() in Collect Semantic PMI Values uses. The formatEnumValue() function of the property panel instead finds the enumeration key of a value and converts it to title case.

Patterns and Pitfalls

Example Infrastructure Is Not Library API

The example applications contain helper classes with names that look like library classes. They are not part of @mtk/web; copy and adapt them if they fit your application. Paths are relative to the root of the examples project.

Example class or function Location Purpose
ModelLoader, createModelDataProvider shared/src/features/model/import/ Finds the main .mtkweb file, creates a DataProvider, and calls ModelReader.
FileSource, LocalFileSource, RemoteFileSource, FileEntry shared/src/common/file-source/ Enumerate and read files from a folder, an archive, or a server.
ModelStructureBuilder, ModelStructure shared/src/features/model/structure/ Build the model tree and the Three.js scene graph, including material inheritance.
ModelDataConverter, convertTransformation shared/src/common/helpers/ThreeHelper.ts Convert bodies and transformations to Three.js objects.
MeshShapeConverter, PMIConverter, DrawingConverter shared/src/common/helpers/ Convert mesh shapes, PMI graphics, and drawing elements to Three.js objects.
MaterialManager, VisualThreeMaterial, convertColor shared/src/common/materials/ Cache Three.js materials, carry them in a VisualMaterial, and convert colors.
getRaycast shared/src/common/helpers/getRaycast.ts Raycast a Three.js mesh through a MeshBVH.
getHatchDefinitionLines, getTextAnchor shared/src/common/helpers/drawing/ Definition lines of predefined hatch patterns and text anchors for drawing text justification.
Viewport, Scene, CameraControls, SceneRaycaster shared/src/features/viewport/ Rendering, camera control, and raycasting.
ModelIntersectionMapper, ModelHighlightManager shared/src/features/model/interaction/ Map raycast hits to bodies and B-Rep shapes, and highlight them.
SectionPlane, SectionPlaneManager shared/src/features/section-plane/ Interactive section planes.
PMIStructureBuilder shared/src/features/pmi/structure/ Build the PMI tree and place PMI graphics.
DrawingStructureBuilder shared/src/features/drawing/structure/ Build the drawing tree and its graphics.
MeasurementManager, Measurement and its subclasses, RadialDimensionsBuilder, CylinderAxisOffsetLimitsBuilder shared/src/features/measurements/ Visual measurements built on @mtk/web/measurements and on the exact geometry of faces and edges.
DesignReviewItem shared/src/features/design-review/ Review notes and pins used by the example.
BaseModelViewer, ModelStructureViewer, SelectionHandlingViewer, PMIExplorerViewer, and other viewers shared/src/viewers/ Complete example applications.
SemanticInformationRowCollector react/src/pages/pmi-explorer-viewer/semantic-information/ Table rows for semantic PMI in the React PMI example.
PropertyPanel react/src/pages/model-explorer-viewer/ui/property-panel/ Property table of model and drawing elements in the React model explorer example.

Common Mistakes