Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ the supported geometry objects they contain. In `websocket` mode the standalone
app reads `ws_host`, `ws_port`, and `workspace` from the URL, preserving the
Python package integration.

### Plugins

Add-ons such as authoring tools extend the viewer through `plugins` rather than
through the core package. Each plugin gets a narrow `ViewerExtensionContext`
when the viewer mounts. It can add its own objects to an overlay layer that is
never picked or reset, raycast the pointer onto a horizontal plane or against
backend objects, read object bounds, and take over pointer and keyboard input
for the length of a session:

```ts
import type { ViewerPlugin } from "@compas-dev/compas-threejs-ts";

const plugin: ViewerPlugin = {
id: "my-tool",
install(context) {
const session = context.beginInteraction({
onPointerDown(event) {
const point = context.pointerOnPlane(event, 0);
// ...
},
onKeyDown(event) {
if (event.key === "Escape") session.release();
},
});
return () => session.release(); // runs on viewer.dispose()
},
};

createViewer(container, { plugins: [plugin] });
```

While a session is held, ordinary picking, the transform gizmo and the built-in
keyboard shortcuts are suspended; orbiting keeps working. The scene, camera,
renderer and controls are deliberately not part of this API. See
`examples/embedded_extension_plugin.html` for a complete example.

For a broader visual smoke test, open
`examples/embedded_kitchen_sink.html`. It uses the same public embedded API to
display every geometry and helper type included in the 1.0 support matrix in a
Expand Down
18 changes: 9 additions & 9 deletions docs/support-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ objects fail with `unsupported_message` and do not modify the scene.

## Geometry

| Status | Objects | Notes |
| --------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Included | Box, Capsule, Circle, Cone, Cylinder, Line, Point, Pointcloud, Polyline, Sphere, Torus | Native Three.js representations |
| Included | Mesh, Polyhedron | Polygonal faces use fan triangulation; complex faces should be triangulated upstream |
| Included helpers | Frame, Plane, Vector | Frame and Vector are visual helpers; Plane is displayed as a finite surface |
| Python mesh path | Brep | Python sends its view mesh while retaining the Brep identity for callbacks |
| Deferred | Arc, Bezier, Ellipse, Hyperbola, Parabola, Polygon, Graph | Planned after 1.0 |
| Not top-level objects | MeshFaceList, PolyhedronFace | Internal protobuf helper types |
| Non-renderable data | Projection, Quaternion, Reflection, Rotation, Scale, Shear, Transformation, Translation | Deliberately rejected as scene geometry |
| Status | Objects | Notes |
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Included | Arc, Box, Capsule, Circle, Cone, Cylinder, Line, Point, Pointcloud, Polygon, Polyline, Sphere, Torus | Native Three.js representations |
| Included | Mesh, Polyhedron | Polygonal faces use fan triangulation; complex faces should be triangulated upstream |
| Included helpers | Frame, Plane, Vector | Frame and Vector are visual helpers; Plane is displayed as a finite surface |
| Python mesh path | Brep | Python sends its view mesh while retaining the Brep identity for callbacks |
| Deferred | Bezier, Ellipse, Hyperbola, Parabola, Graph | Planned after 1.0 |
| Not top-level objects | MeshFaceList, PolyhedronFace | Internal protobuf helper types |
| Non-renderable data | Projection, Quaternion, Reflection, Rotation, Scale, Shear, Transformation, Translation | Deliberately rejected as scene geometry |

Circle is currently displayed as a filled disc. Native converters are retained;
objects are not converted to meshes unless their integration explicitly does so,
Expand Down
162 changes: 162 additions & 0 deletions examples/embedded_extension_plugin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embedded COMPAS ThreeJS plugin</title>
<link rel="stylesheet" href="../dist-lib/style.css" />
<style>
html,
body,
#viewer {
width: 100%;
height: 100%;
margin: 0;
}
#marker-tool {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
font:
14px system-ui,
sans-serif;
padding: 6px 12px;
}
</style>
<script type="importmap">
{
"imports": {
"vue": "../node_modules/vue/dist/vue.esm-browser.js",
"three": "../node_modules/three/build/three.module.js",
"three/": "../node_modules/three/",
"@gramaziokohler/compas-pb-ts": "../node_modules/@gramaziokohler/compas-pb-ts/dist/index.js",
"@bufbuild/protobuf/wire": "../node_modules/@bufbuild/protobuf/dist/esm/wire/index.js"
}
}
</script>
</head>

<body>
<div id="viewer" aria-label="3D viewer"></div>
<button id="marker-tool" type="button">Place markers</button>

<script type="module">
import * as THREE from "three";
import { createViewer } from "../dist-lib/index.js";
import { Box, pbDumpBytes } from "@gramaziokohler/compas-pb-ts";

// A minimal add-on built only on the public extension API: while its
// session is active, a ghost follows the cursor on the ground plane, a left
// click drops a marker there, and Escape hands input back to the viewer.
function markerPlugin() {
let context = null;
let session = null;
let removeGhost = null;
const ghost = new THREE.Mesh(
new THREE.SphereGeometry(0.15, 16, 12),
new THREE.MeshBasicMaterial({ color: 0xff6a00 }),
);
const markers = [];

function stop() {
session?.release();
session = null;
removeGhost?.();
removeGhost = null;
}

return {
id: "example-markers",
install(ctx) {
context = ctx;
return () => {
stop();
ghost.geometry.dispose();
ghost.material.dispose();
for (const marker of markers) {
marker.geometry.dispose();
marker.material.dispose();
}
};
},
start() {
if (!context || session) return;
ghost.visible = false;
removeGhost = context.addOverlay(ghost);
session = context.beginInteraction({
onPointerMove(event) {
const point = context.pointerOnPlane(event, 0);
ghost.visible = point !== null;
if (point) ghost.position.set(point.x, point.y, point.z);
},
onPointerDown(event) {
if (event.button !== 0) return;
const point = context.pointerOnPlane(event, 0);
if (!point) return;
const marker = new THREE.Mesh(
new THREE.SphereGeometry(0.15, 16, 12),
new THREE.MeshBasicMaterial({ color: 0x2f7de1 }),
);
marker.position.set(point.x, point.y, point.z);
context.addOverlay(marker);
markers.push(marker);
const [hit] = context.pickObjects(event);
console.log(
"marker placed",
point,
"over",
hit?.guid ?? "ground",
);
},
onKeyDown(event) {
if (event.key === "Escape") stop();
},
onInterrupt: stop,
});
},
};
}

const container = document.querySelector("#viewer");
if (!(container instanceof HTMLElement)) {
throw new Error("Viewer container was not found");
}

const markers = markerPlugin();
const viewer = createViewer(container, {
mode: "embedded",
defaultLighting: true,
showToolbar: false,
plugins: [markers],
onError(error) {
console.error(error.code, error.message, error.details);
},
});

const box = new Box({
data: {
guid: crypto.randomUUID(),
name: "Box",
frame: {
point: { x: 0, y: 0, z: 0.5 },
xaxis: { x: 1, y: 0, z: 0 },
yaxis: { x: 0, y: 1, z: 0 },
},
xsize: 3,
ysize: 3,
zsize: 1,
},
});
viewer.dispatch(pbDumpBytes(box));

document
.querySelector("#marker-tool")
.addEventListener("click", () => markers.start());
window.addEventListener("pagehide", () => viewer.dispose(), {
once: true,
});
</script>
</body>
</html>
19 changes: 17 additions & 2 deletions scripts/test-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,23 @@ try {

await writeFile(
join(consumerRoot, "consumer.ts"),
`import { createViewer, CompasViewerError, type CompasViewerOptions } from "${packageName}";\n` +
`const options: CompasViewerOptions = { mode: "embedded" };\n` +
`import { createViewer, CompasViewerError, type CompasViewerOptions, type ViewerPlugin } from "${packageName}";\n` +
`const plugin: ViewerPlugin = {\n` +
` id: "consumer",\n` +
` install(context) {\n` +
` const session = context.beginInteraction({ onKeyDown: (event) => void event.key });\n` +
` void context.pointerOnPlane({ clientX: 0, clientY: 0 }, 0)?.z;\n` +
` void context.objectBounds()[0]?.guid;\n` +
` void context.objectVertices()[0]?.kind;\n` +
` const stop = context.onSelectionChange((guid) => {\n` +
` if (guid) void context.getMaterial(guid)?.color;\n` +
` });\n` +
` context.send({ dispatch: "create_geometry", type: "point", point: [0, 0, 0] });\n` +
` context.setTransformSnap({ grid: 0.5, angle: Math.PI / 12 });\n` +
` return () => { stop(); session.release(); };\n` +
` },\n` +
`};\n` +
`const options: CompasViewerOptions = { mode: "embedded", plugins: [plugin] };\n` +
`void createViewer; void CompasViewerError; void options;\n`,
);
await writeFile(
Expand Down
14 changes: 5 additions & 9 deletions src/conversions/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,7 @@ import * as THREE from "three";
import * as GEOCONV from "./geometry";
import * as DATASTRUCTCONV from "./datastructures";

const UNIMPLEMENTED_RENDERABLES = [
Arc,
Bezier,
Ellipse,
Graph,
Hyperbola,
Parabola,
Polygon,
];
const UNIMPLEMENTED_RENDERABLES = [Bezier, Ellipse, Graph, Hyperbola, Parabola];
const NON_RENDERABLES = [
Projection,
Quaternion,
Expand Down Expand Up @@ -91,6 +83,8 @@ export function convertToThreeJSGeometry(object: unknown): THREE.Object3D {
}

switch (true) {
case object instanceof Arc:
return GEOCONV.arcToThreeJS(object);
case object instanceof Box:
return GEOCONV.boxToThreeJS(object);
case object instanceof Capsule:
Expand All @@ -111,6 +105,8 @@ export function convertToThreeJSGeometry(object: unknown): THREE.Object3D {
return GEOCONV.pointToThreeJS(object);
case object instanceof Pointcloud:
return GEOCONV.pointcloudToThreeJS(object);
case object instanceof Polygon:
return GEOCONV.polygonToThreeJS(object);
case object instanceof Polyline:
return GEOCONV.polylineToThreeJS(object);
case object instanceof Sphere:
Expand Down
Loading