Skip to main content

STUDIO

This library has a collection of UI features, camera handling, component handler and more:

Actions

As best practices in DynaMaker, when we are working in the app, we use actions to wrap specific logic into functions, under ACTIONS, so that they can be used easily in UI. Actions refer to something that happens, so it is very common and convenient to use actions when we click in certain buttons and tabs, or when we are done with a configurator in its callback.

Taking the example of the tutorial with the conveyors in the following picture, there are 4 actions:

  • ACTIONS.addSection(): to add a new section to the assembly according to the parameters.
  • ACTIONS.removeSection(): to remove the last added section to the assembly.
  • ACTIONS.generateConfigurator(): to generate the configurator from the assembly component.
  • ACTIONS.generateStlData(): to generate the data the download the proper file.

Actions

Remember to write export before function, so that they can be called in other code-editor tabs, like UI, HOTKEYS, etc.

As any function in TypeScript, actions in DynaMaker can do some logic (like ACTIONS.addSection()) and/or generate something (like ACTIONS.generateConfigurator()).

UI Items

There are other ways to customize the UI apart from adding SKYUI.Button and STUDIO.Tab. In your DynaMaker application it is possible to add:

These can be added in the predefined function onInit() in your application (UI).

You can add a Menu dropdown to the top-right corner. It is usually intended for saving and loading your projects, but any other functionality you want can be added as well as a SKYUI.Button (e.g. change unit system, language, rename project, save as new project, etc).

The following example contains a Menu with Save and Load buttons.

Menu

Studio.setMainMenuItems([
new SKYUI.Button('Save', () => { LoadSaveProject.openPopupSave() }),
new SKYUI.Button('Load', () => { LoadSaveProject.openPopupLoad() }),
])

Quickbar Items

You can add buttons to the Quickbar at the top.

The following example contains a button Home to recenter the camera to an ISO view, and Delete with a custom function to delete any possible selected component.

Quickbar

Studio.setQuickBarItems([
{
name: 'Home',
icon: STUDIO.ICONS.HOME,
action: () => {
Studio.setCameraToFitBounds()
},
tooltip: 'Reset Camera'
},
{
name: 'Delete',
icon: STUDIO.ICONS.TRASH,
action: () => {
ACTIONS.deleteSelectedComponent()
},
disabled: true,
tooltip: 'Delete'
},
])

Visible Metrics

Sometimes in your application you want to let the user know about certain values, while designing. Usually when you are configuring your product, you want to update those values dynamically.

The following example contains information about the material, color & volume of the cube.

Metrics

Studio.setVisibleMetrics([
{
id: 'material',
value: `PVC`,
label: 'Material: '
},
{
id: 'color',
value: `Blue`,
label: 'Color: '
},
{
id: 'volume',
value: `1000 cm³`,
label: 'Volume: '
},
])

How To Update Them Dynamically

In order to update the metrics dynamically, we need to update the metrics when we update the parameters.

For example, the cube volume needs to be updated when changing the size given by the parameters. Since the only configurator that exists in the template is in the Product tab, we can add an addUpdateCallback() so that it updates the metric with id 'volume' through Studio.updateVisibleMetric(). Then, you should have the following:

function generateProductTab(Studio: STUDIO.StudioService): STUDIO.Tab {
const tabTitle: string = 'Product'

const toolbarGenerator: STUDIO.ToolbarGenerator = () => {
const configurator = ACTIONS.configureMainComponent()

configurator.addUpdateCallback((valid, value) => {
if (valid) {
const cubeVolume = ACTIONS.getCubeVolume()
Studio.updateVisibleMetric('volume', `${(cubeVolume / 1000).toFixed(1)} cm³`)
}
})

const toolbarItems: STUDIO.ToolbarItem[] = [
configurator,
]
return toolbarItems
}

const tabArgs: STUDIO.TabArgs = {
... // whatever you have
}

return new STUDIO.Tab(tabTitle, toolbarGenerator, tabArgs)
}

Notice that here we have created previously a function getCubeVolume() in ACTIONS that gives the multiplication of width, depth and height of the component in mm³. Then we divide by 1000 to make it cm³ and leave 1 decimal with .toFixed(1).

Great! Here is an example of the template with dynamic metrics (try different widths):


If you wonder how to update the metrics instantly, read more here.

Modals

Use modals to get the attention of the user in certain moments. Giving short tips when entering a complex selection mode, forcing to select certain parameters that heavily affect the model or filling a form to complete a quotation with the user's email with an attached custom drawing, are some examples of use. There are many predefined modals built in DynaMaker that are already to use:

Info Modal

Use an info modal to give any information to the user as a pure description or warning. For example when clicking a button that allows a special selection mode or a warning about contraints from the parameters selected.

Studio.openInfoModal(
'Title',
'Description of the info modal',
'This is the body of an info modal',
)

Modals-1

For the experienced developer, it is possible to have a custom header and footer in HTML, so anything can be included in those, like text, pictures, videos, links, iframes, etc (see HTML examples here). Simply use the optional arguments headerHtml and footerHtml.

Studio.openInfoModal(
'Title',
'Description of the info modal',
'This is the body of an info modal',
{
headerHtml: `<p>This is a custom header</p>`,
footerHtml: `<p>This is a custom footer</p>`,
})

Modals-2

Keep in mind that the custom header replaces the title and description, and the custom footer the default close button.

Items Modal

As a variation of the info modal, the items modal allows you to show information in multiple rows by default. It is also possible to add extra functionality at the top-right of the pop-up by using the optional argument headerActions. Also, it allows you to show/hide and rename the OK & Cancel button at the bottom-right.

 Studio.openItemsModal(
'Title',
'This is a description',
['item 1', 'item 2', 'item 3'],
{
okButtonText: 'Send Quotation',
hideOkButton: false,

cancelButtonText: 'Cancel',
hideCancelButton: false,

headerActions: [
{
label: 'Help',
action: () => { window.open('https://docs.dynamaker.com') },
icon: STUDIO.ICONS.QUESTION_SIGN
},
{
label: 'Close',
action: (modalControl) => { modalControl.close() },
icon: STUDIO.ICONS.REMOVE
}
],
})

Modals-3

You could also use the optional arguments bodyHtml, headerHtml and footerHtml to have any custom HTML, but keep in mind that these replace the item list, title & description, and ok & cancel buttons respectively. headerActions are still available.

Modals-4

Parameter Modal

With the parameter modal you can show all parameters of a configurator in a pop-up. A difference from showing configurators in a tab as usual is that the thumbnail or icon is visible to easily identify what the parameter is related to for example. Also, you can disable the default visual transition when open with the optional boolean argument animation. See that headerActions, footerHtml and headerHtml can be used as well.

const configurator = ACTIONS.configureMainComponent()

Studio.openParameterModal(configurator, 'Configure cube', 'Choose width, depth and height', {
animation: true,
headerActions: [ ... ], // same as item modals
// footerHtml: 'This is a custom footer',
// headerHtml: 'This is a custom header',
})

Modals-5

Keep in mind that this configurator still follors the rules and callbacks of the configurator from ACTIONS.configureMainComponent().

However, we usually want to update the geometry right after we are done with the configurator to see the changes, or perhaps we want to go into a second configurator that depends on this one. Since openParameterModal() is a promise, these examples require .then() so the functionality mentioned can be easily implemented.

Update Geometry After Configurator

const configurator = ACTIONS.configureMainComponent()

Studio.openParameterModal(configurator, 'Configure cube', 'Choose width, depth and height')
.then((result) => {
if (result !== undefined){
Studio.requestGeometryUpdate()
Studio.requestLightweightGeometryUpdate()
// other functions
}
})

Notice that result is undefined when clicking Cancel. On the other hand, when the configurator is valid, ie. all parameters are valid, clicking OK gives an object as result as in the following case, giving all the information of the configurator and its parameters:

result: {
success: boolean,
configurator: SKYPARAM.Configurator(),
parameters: [
SKYPARAM.SliderParameter,
SKYPARAM.InputParameter,
SKYPARAM.DropdownParameter,
]
}

In the example used before, you can see that when result !== undefined, we will update the geometry. However, more functions can be added there of course.

Nested Configurators

Another common example is when we have two nested configurators, called eg. configuratorA and configuratorB, with the following conditions:

  • configuratorA should be the first configurator to be open.
  • clicking OK in configuratorA should open configuratorB.
  • geometry and properties should be updated only if OK is clicked in both configurators.
  • clicking Cancel in configuratorB should not apply changes from configuratorA.
  • configuratorB depends on the parameters from configuratorA.
let savedResultA
const configuratorA = ACTIONS.getConfiguratorA()
Studio.openParameterModal(configuratorA, 'Configure cube', 'Choose width, depth and height')
.then((resultA) => {
if (resultA !== undefined) {
savedResultA = resultA

const configuratorB = ACTIONS.getConfiguratorB(savedResultA.parameters)
Studio.openParameterModal(configuratorB, 'Configure appearance', 'Choose material and color')
.then((resultB) => {

if (resultB !== undefined) {
ACTIONS.setComponentProperties(savedResultA, resultB)
Studio.requestGeometryUpdate()
Studio.requestLightweightGeometryUpdate()
}

})

}
})

Notice that resultA needs to be saved as savedResultA to be able to use it in ACTIONS.setComponentProperties().

Modals-6

Custom Modal

Do you want to have a totally customized modal that doesn't fit any of the above? Perhaps filling a form for a complex quotation that automatically sends emails with attached drawings? Send us a request to support@dynamaker.com with the features you need and we can help you to get you started.

Camera Handling

In our application sometimes we have a 2D step in which we don't want to allow the user to rotate the camera. Usually we want to update the camera when we update the parameters so the camera fits the model or even do some cool camera animations when we trigger certain actions. All this can be done and here we explain how, but before let's go through the basic concepts.

Basics

  • Rotate: rotate the camera by holding right-click
  • Pan: move the camera by moving the mouse while holding the mouse wheel
  • Zoom: zoom in/out with the mouse wheel
  • Camera position: 3D point in which the user's eye should be in reality
  • Camera target: 3D point where the camera is looking at
  • Travel time: time in milliseconds that the camera takes to move from the current position-target to new position-target

If your mouse has no mouse wheel button, you can use the arrows on your keyboard as an alternative

Camera Controls

// Move the position and/or target
Studio.setCameraToFitBounds()
Studio.setCameraPosition()
Studio.setCameraTarget()

// Settings
Studio.updateCameraSettings({
allowPan?: boolean
allowRotate?: boolean
allowZoom?: boolean
groundLock?: boolean
maxPolarAngle?: number
maxTargetDistance?: number
maxZoom?: number
minPolarAngle?: number
minTargetDistance?: number
minZoom?: number
orthographic?: boolean
travelTime?: number
zoomToMouse?: boolean
})

// Move in steps, commonly used when clicking a button in the UI
Studio.cameraPan()
Studio.cameraZoomIn()
Studio.cameraZoomOut()

Easings

Easings can be applied to camera transitions. You can read more about easings here.

Pictures

Do you want to take a picture of your project? There is an easy to use function in the Studio for that! Here is an example of downloading a picture in the callback of a button.

const downloadImageButton = new SKYUI.Button('Take picture', () => {
return Studio.requestImage({ type: 'png' }).then((img) => {
Studio.downloadFile(`My project.png`, img)
})
}, { icon: STUDIO.ICONS.DOWNLOAD })

Selection Handling

There are up to 5 different types of event you can reach through clicking or moving the mouse. Below we describe how to get them, and must not be mixed with the camera controls:

Selection handling

They can be found only in the Application, which the user will interact with, under

SELECTION, in the function `selectionEvent()`. The specific type can be found in first argument like `event.type` and it is common to follow this pattern using a `switch` statement:
export function selectionEvent(event, selectionPosition: SKYMATH.Vector3D, manager: STUDIO.Manager) {
switch (event.type) {
case 'mouse-move': // moving the mouse without clicking
break
case 'selection-start': // left-clicking the mouse (down click)
break
case 'selection-move': // left-clicking & moving mouse at the same time (down & drag click)
break
case 'selection-end': // releasing the left-button the mouse (up click)
break
case 'selection-empty': // double left-clicking the mouse quickly
break
}
}

selection-empty empty can be reached also by:

  • pressing key Esc.
  • single left-clicking outside the world.

In order to the see/change the world:

  • Go to CONTROLLER.
  • In the function productConfigurationFactory(), check two things:
    • sceneSelection to toggle the visibility of the ground, which can be:
      • WORLD.SCENES.STANDARD: to make visible the ground.
      • WORLD.SCENES.CLEAN: to make invisible the ground.
    • worldSize: make sure is bigger than your product, so worldSize: { x: 20000, y: 20000, z: 300 } could be a good size for now to avoid strange selection behavior.

Notice that the component always starts in the origin of the world size (0, 0, 0). But don't worry, we will make this simpler in the future, so you don't have to care about the world and its size anymore.

Display Messages

When interacting with the canvas, sometimes the visual feedback given by layouts or block zones is not enough and we want to know whether an action took place successfully or not. We can use a display message to help the user to know what happened at a specific point. There are four different types to use and here we show how to implement them in the application.

How To Use

  • In the Application, go to UI.
  • Create the types by adding the following into the function onInit() :
Studio.addEventListener('warning-message', (args) => { Studio.displayWarningMessage('', args.content) })
Studio.addEventListener('success-message', (args) => { Studio.displaySuccessMessage('', args.content) })
Studio.addEventListener('info-message', (args) => { Studio.displayInfoMessage('', args.content) })
Studio.addEventListener('danger-message', (args) => { Studio.displayDangerMessage('', args.content) })
  • Wherever the manager exists, trigger the pop-up with the message with the following example:
manager.dispatchEvent({ type: 'danger-message', content: 'Invalid position' })

Display messages

Check if the manager exists in the function you want to use it as argument (like in ACTIONS). Otherwise, you can get it from the Studio as Studio.getManager(). Remember that this Studio has to be given as argument in the function and has the type STUDIO.StudioService.

Examples

As inspiration, these are the most common examples to use the display messages on:

  • Info message:
    • to describe briefly the functionality of a tab/button that has been clicked
  • Success message:
    • files have been downloaded
    • order has been put in the production queue
    • an algorithm, that automatically adds multiple components, has finished
  • Warning message:
    • a component has been added but due to some constraints, it doesn't have a specific subcomponent
    • when changing a parameter that removes subcomponents automatically due to internal design rules
  • Danger message:
    • when placing components in invalid positions

Remember that the goal of these messages should help the user when designing and should not give information about invalid or improducible designs. Then, we are talking about bugs in the application and these should never be shown to the end user!

Components

When writing dynamic products it is common to have a lot of logic in the application. This leads to the problem of large files which are hard to maintain and understand. We use components to handle this complexity without having large files. Each component should be a self-contained piece of logic.

Update & Publish

A component exists in two versions:

  • the one that you can see and edit from the Component Maker. Use Save & Update to overwrite the saved version.
  • another so called published version, which is used when a component is imported into another component or application. Use Publish to overwrite the current published version with the latest saved version.

The reason why we have separated this behavior in two is to let you try new features in your subcomponent without changing the behavior in your other components. When you are satisfied with your change you can publish it to your other components.

As an example here (see video below):

  1. we write the code needed to make a hole in a box.
  2. then we Save & Update to apply the changes.
  3. we see the hole in the component, but not in the app since it is not published.
  4. then we Publish to apply the latest changes throught the project (not only app, but possibly other components if used there)
  5. we now see the hole in the app.

Make sure to publish a component when you have tested its behavior enough, eg. presets are meant for that kind of testing.

Components Into Components

You can design the component structure of your project as you want. A good pattern to start with is to have a top-level component, typically an assembly, that can contain every other component, so it's easier to use in the app. Of course any other complex compoenent structure is valid, eg. multiple top-level components can be used simultaneously in the app.

Click on show/hide imports... to import other components into the current component.

Notice that the application (App Maker) imports all the components automatically. However, the Component Maker and Drawing Maker have no imports at first and it has to be done manually as shown in the video above.

Component Handler

In DynaMaker the Component Handler takes care of all the interactions between components. Typically found in top-level components, you can create copies of the components easily called instances. While a component has properties and geometries, an instance refers to a component with a position in the world. Therefore, what you see in an assembly is always instances, which are copies of its subcomponents with a defined position.

The component handler keeps track of components and component instances. You can add components with the add method and the component handler will automatically create a corresponding instance , which is used to place your component in the scene. Here we explain:

How To Use

We can take the app from the tutorial My First Assembly, the Assembly contains two subcomponents: Step and Railing:


As given in the default template, the componentHandler already exists in the component as long as it's created from the default STUDIO.BaseComponent:

export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

this.properties = {
// ...
}
}

update () {
const componentHandler = this.componentHandler
// ...
}
}

Then, it allows you to:

  • add copies of components, place them accordingly and tag them for other purposes:

    // Steps
    const newStepComponent = new STEP.Component()
    newStepComponent.setProperties({ width: 800 })

    // --- step instance 1
    this.componentHandler.add(newStepComponent)

    // --- step instance 2 with plane
    this.componentHandler.add(newStepComponent, {
    plane: new SKYCAD.Plane(0, 0, 1, 50),
    positionOnPlane: new SKYMATH.Vector2D(50, 100),
    rotationOnPlane: Math.PI / 4
    })

    // Railings
    const newRailingComponent = new RAILING.Component()
    newRailingComponent.setProperties({ width: 3000, height: 2500 })

    // --- railing instance 1 with position, rotation and tag
    this.componentHandler.add(newRailingComponent, {
    position: new SKYMATH.Vector3D(-50, 0, 0),
    rotation: new SKYMATH.Vector3D(0, 0, Math.PI / 2),
    tags: ['railing', 'right-side']
    })

    // --- railing instance 2 with connector and tag
    const connector2 = new SKYCAD.Connector3D({
    position: new SKYMATH.Vector3D(-50, 300, 0),
    rotation: new SKYMATH.Vector3D(0, 0, Math.PI / 2),
    })
    this.componentHandler.add(newRailingComponent, {
    connector: connector2
    tags: ['railing', 'left-side']
    })
  • get specific components and instances by class type, optionally by tag:

    const allComponents = this.componentHandler.getAllComponents() // railing and step components
    const railingComponents = this.componentHandler.getComponents(RAILING.Component)

    const allInstances = this.componentHandler.getAllInstances() // railing and step instances
    const railingInstances = this.componentHandler.getInstances(RAILING.Component)
    const leftRailingInstances = this.componentHandler.getInstances(RAILING.Component, { withTags: ['left-side'] })
  • clear copies of components, optionally by tag:

    this.componentHandler.clearAll() // removes all instances

    this.componentHandler.clear(RAILING.Component) // removes all railing instances

    this.componentHandler.clear(RAILING.Component, { // removes all railing instances with specified tags
    withTags: ['left-side']
    })
  • remove specific components and instances

    this.componentHandler.removeComponent(railingComponentToRemove)

    this.componentHandler.removeInstances([railingInstance1, railingInstance2])
  • get its subcomponents geometry, typically to be used in the assembly component's class functions generateGeometry() and generateLightweightGeometry():

    const subcomponentsGeometry = this.componentHandler.generateAllGeometry()
    const subcomponentsLightweightGeometry = this.componentHandler.generateAllLightweightGeometry()

Instance

An instance therefore is a copy of a component, which includes a reference to the component and places it in the world with a certain position and more. These are the most common functions used with an instance (see that an instance is always retrieved from the componentHandler and it is never created as you would do with regular components).

const railingInstances = this.componentHandler.getInstances(RAILING.Component)
const firstRailingInstance = railingInstances[0]

const railingComponent = firstRailingInstance.getComponent()
const clonedRailingInstance = firstRailingInstance.clone()

const instancePosition = firstRailingInstance.getPosition() // returns SKYMATH.Vector3D
const instanceRotation = firstRailingInstance.getRotation() // returns SKYMATH.Vector3D

const newPosition = new SKYMATH.Vector3D(50, 0, 50)
firstRailingInstance.setPosition(newPosition)

const newRotation = new SKYMATH.Vector3D(Math.PI / 2, 0, 0)
firstRailingInstance.setRotation(newRotation)

firstRailingInstance.addTag('glass')
const hasGlassTag = firstRailingInstance.hasTag('glass') // returns true or false
const instanceTags = firstRailingInstance.getTags()

const areInstancesEqual = firstRailingInstance.isEqual(secondRailingInstance) // returns true or false

There are more functionalities for the componentHandler and instances, like componentHandler.generateJSON() and so on that you can check in its intellisense (Ctrl+Space or by typing componentHandler.). Although these are used for advanced patterns that are not typically used, you can contact us at support@dynamaker.com to see how you can benefit from them.