An easy to use 2D engine to load model with objects that you can select, highlight, hide, update shapes...
This simple example shows how to load the engine with two simple objects and interact (select, highlight) with them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>BIMData 2D Engine</title>
<style>
* {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body style="background-color: gainsboro;">
<canvas id="canvas2d" style="width: 100vw; height: 100vh;"></canvas>
<script type="module">
import makeViewer from "https://www.unpkg.com/@bimdata/2d-engine@1.0.0/dist/2d-engine.esm.js";
const canvas = document.getElementById("canvas2d");
const viewer = makeViewer({ canvas });
// Select object on click, fitview on right-click.
viewer.ui.on("click", ({ object, rightClick }) => {
if (object) {
if (rightClick) {
viewer.camera.fitView(object);
} else {
object.selected = !object.selected;
}
}
});
// Highlight object on hover.
viewer.ui.on("mousemove", ({ object, previousObject }) => {
if (object !== previousObject) {
if (object) {
viewer.scene.highlightObjects([object.id]);
}
if (previousObject) {
viewer.scene.unhighlightObjects([previousObject.id]);
}
}
});
viewer.ui.on("mouseleave", () => {
const highlightedObjectIds = viewer.scene.highlightedObjects.map(
o => o.id
);
viewer.scene.unhighlightObjects(highlightedObjectIds);
});
// Add a model with two simple objects. A line and a circle.
const model = await viewer.scene.addModel({
objects: [
{
shapes: [
{
style {
lineWidth: 2,
lineOpacity: 0.8,
lineColor: 0x0000ff,
},
paths: [
[0, 0, 50, 50],
[0, 50, 50, 0],
],
}
]
},
{
shapes: [
{
style {
textureOpacity: 0.5,
texture: "solid",
textureTint: 0xff00ff,
},
paths: [
{
type: "arc",
x: 25,
y: 25,
radius: 20,
},
],
}
]
},
],
});
// Fit view the model once loaded.
viewer.camera.fitView(model);
</script>
</body>
</html>
The result:
Of course you can do more that just drawing circles and lines. In this example, a complete building storey is drawn and doors and space names are displayed using addons. These addons are simple javascript code that use the 2D Engine API.
To see more about these addons:
The 2D Engine provide tools to customise and interact with models/objects you load.
- Draw object shapes with line and texture cutsomization.
- Show, hide, select and highlight objects.
- The camera can scale, rotate, pan and fitview objects/models.
- ...
See the API reference to know all available features.
npm i @bimdata/2d-engine
import makeViewer from "@bimdata/2d-engine";
<script src="https://www.unpkg.com/@bimdata/2d-engine@1.0.0"></script>
Add makeViewer
available on the window object.
Or on script type module:
<script type="module">
import makeViewer from "https://www.unpkg.com/@bimdata/2d-engine@1.0.0/dist/2d-engine.esm.js";
// have fun
</script>
2D Engine is created using a factory you get as default export from the package:
import makeViewer from "@bimdata/2d-engine"; // webpack / rollup
const canvas = document.getElementById("canvas");
const viewer = makeViewer({ canvas });
canvas
is mandatory and another properties can be passed to customized the 2D Engine behavior:
Property | Type | Description |
---|---|---|
canvas |
HTMLCanvasElement |
Required. |
autoStart |
boolean |
Default = true . If false, you must call viewer.ticker.start() to start rendering. |
The factory returns the viewer
with the following interface:
interface Viewer extends {
canvas: HTMLCanvasElement;
scene: Scene;
ui: UI;
camera: Camera;
destroy(): void;
renderer: Renderer;
settings: Settings;
constants: Constants;
ticker: Ticker;
picker: Picker;
utils: {
vector2D: Vector2DUtils;
};
}
The viewer scene is where model and objects lives. You can add/remove models and objects and getters and setters allow read/write objects in batch.
It has the following interface:
interface Scene extends Readonly<EventHandler> {
readonly viewer: Viewer;
// Models
readonly models: Model[];
readonly modelsMap: Map<number, Model>;
addModel(modelData: ModelData): Promise<Model>;
removeModel(id: number): boolean;
// Objects
readonly objects: SceneObject[];
readonly objectsMap: Map<number, SceneObject>;
addObject(objectData: SceneObjectData, model?: Model): SceneObject;
removeObject(objectId: number): boolean;
// Objects setters
showObjects(ids: number[]): void;
hideObjects(ids: number[]): void;
selectObjects(ids: number[]): void;
deselectObjects(ids: number[]): void;
highlightObjects(ids: number[]): void;
unhighlightObjects(ids: number[]): void;
setObjectsPickable(ids: number[]): void;
setObjectsUnpickable(ids: number[]): void;
// Object getters
readonly shownObjects: SceneObject[];
readonly hiddenObjects: SceneObject[];
readonly selectedObjects: SceneObject[];
readonly unselectedObjects: SceneObject[];
readonly highlightedObjects: SceneObject[];
readonly unhighlightedObjects: SceneObject[];
readonly pickableObjects: SceneObject[];
readonly unpickableObjects: SceneObject[];
}
It also contains styles and the texture manager.
model
and object
have the following interfaces:
interface Model {
readonly id: number;
readonly scene: Scene;
readonly objects: SceneObject[];
addObject(objectData: SceneObjectData): SceneObject;
removeObject(objectId: number): boolean;
// Related with its objects gemetries
readonly aabb: AABB;
readonly center: Point;
}
interface SceneObject {
readonly id: number;
readonly scene: Scene;
readonly model: Model;
readonly shapes: Shape[];
readonly zIndex: number;
readonly pickingZIndex: number;
// Scene state properties
visible: boolean;
selected: boolean;
highlighted: boolean;
pickable: boolean;
}
Objects have shapes. An shape have the following interface:
interface Shape {
readonly paths: Path[];
getPath(index?: number): Path | undefined;
addPath(pathData: PathData | PointsData, index?: number): Path;
removePath(index?: number): Path | undefined;
readonly aabb: AABB;
readonly center: Point;
}
The viewer handle three shape types: line
, arc
and curve
.
interface Path {
type: PathType;
close: boolean; // If true, the shape draw is closed after this path and the next path restart the draw. Equivalent of the svg command "Z".
start: boolean; // If true, a new draw is started before drawing this path. Equivalent of the svg command "M".
}
By default, a draw is automatically started before the first path of a shape, and after a close path command. This behaviour can be changed using the start
property.
interface Line {
readonly type: PathType.line;
readonly points: Point[];
getPoint(index?: number): Point | undefined;
addPoint(point: Point, index?: number): Point;
removePoint(index?: number): Point | undefined;
}
interface Arc {
readonly type: PathType.arc;
x: number;
y: number;
radius: number;
startAngle: number;
endAngle: number;
anticlockwise?: boolean;
}
interface Curve extends Shape {
readonly type: PathType.curve;
x: number;
y: number;
cpX1: number;
cpY1: number;
cpX2: number;
cpY2: number;
toX: number;
toY: number;
}
When adding objects on a scene, shapes can be provide to object using the shapes property. By default, array of numbers will be displayed as lines.
const model = await viewer.scene.addModel({
objects: [
{
shapes: [
{
paths: [
[0, 0, 50, 50], // a line
{
type: "line", // another line
points: [0, 50, 50, 0],
},
{
type: "arc", // a default arc => full circle
x: 25,
y: 25,
radius: 20,
},
]
}
],
},
],
});
The following events are emitted by the 2D Engine scene :
- "model-added", payload: the added model.
- "model-removed", payload: the removed model.
- "object-added", payload: the added object.
- "object-removed", payload: the removed object.
- "object-update", payload: { object, property, value?, oldValue? }. No value and oldValue for the "shapes" property.
Events can be listened using scene.on
:
viewer.scene.on("model-added", model =>
console.log(`A model is loaded with the id ${model.id}`)
);
The UI is the only component connected to a DOM element, listening to events. It has the following interface:
interface UI extends EventHandler<UIHandlerEvents> {
connect(el: HTMLElement): void;
disconnect(): boolean;
}
The camera
and the picker
listen to the UI events. Disconnecting the UI will make them not reactive to user interactions.
- "click", payload: { canvasPosition, keys }
- "right-click", payload: { canvasPosition, keys }
- "move", payload: { canvasPosition, keys }
- "draw", payload: { dx, dy, keys }
- "drag", payload: { dx, dy, keys }
- "scroll", payload: { canvasPosition, dx, dy, keys }
- "exit", no payload, when the mouse leave
el
.
The camera is binded on the mouse events. It has the following interface:
/**
* A Camera that allows to see the world.
*/
interface Camera extends Readonly<EventHandler<{ update: PIXITransform }>> {
fitView(positionable: Positionable | Positionable[]): void;
destroy(): void;
controller: CameraController;
/**
* @param factor ]-Infinity, +Infinity[
* @param origin the zoom transform center.
*/
zoomIn(factor: number, origin: Point): void;
/**
* @param factor ]-Infinity, +Infinity[
* @param origin the zoom transform center.
*/
zoomOut(factor: number, origin: Point): void;
/**
* @param angle in degree clockwise.
* @param origin the rotation transform center.
*/
rotate(angle: number, origin: Point): void;
translate(dx: number, dy: number): void;
/**
* @param ds delta scale ]0, +Infinity[
* @param origin the scale transform center.
*/
scale(ds: number, origin: Point): void;
move(position: Point): void;
transform: PIXIMatrix;
position: Point;
/**
* @returns { number } the camera rotation angle in degree clockwise.
*/
rotation: number;
/**
* @returns { number } ]-Infinity, +Infinity[
*/
getScale(): number;
/**
* @returns { number } ]0, +Infinity[
*/
zoom: number;
/**
* Returns the position of the given canvas position.
*/
getPosition(canvasPosition: Point): Point;
/**
* Returns the canvas position of the given position.
*/
getCanvasPosition(position: Point): Point;
getViewpoint(): Viewpoint;
setViewpoint(viewpoint: Viewpoint): Viewpoint;
}
It is possible to customise the camera behaviour by changing the properties of the camera.controller
that has the following interface:
interface CameraController {
translatable: boolean;
rotatable: boolean;
scallable: boolean;
}
All properties are true
by default.
Settings are predefined values/keys that define the 2D Engine behaviour. It has the following interface:
interface Settings {
dragKey: string; // default "shift"
rotateKey: string; // default "shift"
clickMaxOffset: number; // default 5
scaleSpeed: number; // default 0.002
rotateSpeed: number; // default 0.1
maxScale: number; // default 100
minScale: number; // default 0.1
fitViewRatio: number; // 1
curves: {
adaptive: boolean;
maxLength: number;
maxSegments: number;
minSegments: number;
};
}
Use viewer.ticker.start/stop()
to start or stop the rendering.
Use viewer.ticker.add/addOnce(name: string, callBack: Function)
to schedule a callback on the next tick. The callback will be called with dt
, the delta time since the next tick. WARNING: adding many tasks with the same name will overide the previous tasks and only the last added will be called on the next tick. This can be used to do not overdue a callback that must be done only once per tick.
Renderer emits events you can use for addons:
- "resize", payload: { dx: number, dy: number }. Emited when the renderer resizes.
- "pre-draw". Emited before the draw phase.
- "post-draw". Emited after the draw phase.
Styles define how objects are drawn by default when they are visible, selected and highlighted. It can be customized by changing the default styles (WARNING: updating styles.default won't affect already loaded objects) or loading objects with non default style.
A style has the following interface:
interface Style {
// TEXTURE
texture?: string;
textureTint?: number;
textureOpacity?: number;
// LINE
lineWidth?: number;
lineColor?: number;
lineOpacity?: number;
lineDash?: number[];
lineCap?: Constants.LINE_CAP;
lineJoin?: Constants.LINE_JOIN;
}
You can change textureTint
and lineColor
using hexadecimal numbers.
viewer.styles.default.textureTint = 0xff0000; // For red. (RGB)
textureOpacity
and lineOpacity
are numbers between 0 and 1.
lineDash
is an array of numbers describing the dash pattern you want to use. See here for more informations.
The Texture Manager has the following interface:
interface TextureManager {
textureMatrix: TextureMatrix;
load(name: string, src: string, width?: number, height?: number): Object;
}
// and the interface of the texture matrix
interface TextureMatrix {
rotate(angle: number): void;
scale(x: number, y?: number): void;
translate(x: number, y: number): void;
}
The viewer only have "solid" texture built in but it is possible to load more:
await viewer.textureManager.load(
"myTextureName",
"./my/textures/folder/myTexture.png"
);
NOTE: this is async code and must be awaited in order to the texture to ba available in the viewer.
This is what you can get: (wall are textured using a cross hatch texture)
Notice that the textrure is aligned with the axis. If the model is not aligned with the axis, you can use the Texture Manager textureMatrix to rotate the textures.
viewer.textureManager.textureMatrix.rotate(30);
It exposes some methods to work with 2D vectors:
interface Vector2DUtils {
distance(v1: Vector2D, v2: Vector2D): number;
sub(v1: Vector2D, v2: Vector2D): Vector2D;
add(v1: Vector2D, v2: Vector2D): Vector2D;
normalize(v: Vector2D): Vector2D;
length(v: Vector2D): number;
dot(v1: Vector2D, v2: Vector2D): number;
cross(v1: Vector2D, v2: Vector2D): number;
rotateAround(v: Vector2D, center: Vector2D, angle: number): Vector2D;
angle(v1: Vector2D, v2: Vector2D): number;
}
Build on change for development and serve:
npm run dev
Unit tests: (jest)
npm run test:unit
E2e tests: (cypress)
npm run build:prod
npm run test:e2e
To e2e test on development (Runs a build in production mode and opens a chromium window instead of a headless run):
npm run test:e2e-dev