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 on 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 to download the proper file.

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 SKYUI.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.

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.

Studio.setQuickBarItems([
{
name: 'Home',
icon: 'home',
action: () => {
Studio.setCameraToFitBounds()
},
tooltip: 'Reset Camera'
},
{
name: 'Delete',
icon: '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.

Studio.defineVisibleMetrics([
{
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): SKYUI.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 SKYUI.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.

HTML Elements

In DynaMaker it's possible to add HTML elements directly to the tab. As an example of adding some text into the app's template:

const tab1 = new SKYUI.Tab({
title: 'Configure',
icon: 'cog',
onInit: () => {
// widthInput & updateButton

const divElement = document.createElement('div')
divElement.innerText = `Update to change cube's width`

const tabContent = [
widthInput,
updateButton,
divElement
]
Studio.setTabContent(tabContent)

// other functions (e.g. camera and geometry updates)
}
})

You can find more info about HTML elements here.

Icons

Icons can be added to other UI elements, mainly used in tabs and buttons. In order to use them, take the name of the icon and add it as a string in the optional argument icon of your tab or button, like:

The default set of icons available in DynaMaker can be found here.

Use the IntelliSense to assist you with the code completion, press Ctrl+Space within the string to see them like:

Alternatively, pictures from ASSETS can be used as well instead, like: icon: ASSETS.IMAGES.MY_COG. Additional adjustments might need to be done with a custom CSS snippet in STYLE.

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 constraints from the parameters selected.

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

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>`,
})

Keep in mind that the custom header replaces the title and description, and the custom footer replaces 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: 'question-sign'
},
{
label: 'Close',
action: (modalControl) => { modalControl.close() },
icon: 'remove'
}
],
})

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.

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',
})

Keep in mind that this configurator still follows 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, i.e. 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 e.g. 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().

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
mouseButtons?: {
left: 'select'| 'pan' | 'rotate' | 'zoom',
right: 'select'| 'pan' | 'rotate' | 'zoom'
middle: 'select'| 'pan' | 'rotate' | 'zoom'
scroll: 'select'| 'pan' | 'rotate' | 'zoom'
}
}
})

// 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.

Editor Settings

In any of the DynaMaker code editors, there are a few toggles and buttons at the bottom-right corner:

These are (starting from above):

  • Set Thumbnail to update the thumbnail of the UI, components and drawings in the app dashboard.
  • Lights to switch between dark and light mode.
  • Preview to show/hide the preview.
  • TDD (Test Driven Development) to show and run the tests (if any) of the current editor.

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: 'download' })

Selection Handling

There are up to 5 different types of events 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:

Read more here to rebind the camera default buttons.

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 the 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 the ground visible.
      • WORLD.SCENES.CLEAN: to make the ground invisible.
    • worldSize: make sure it 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 more simple in the future, so you don't have to care about the world and its size anymore.

Notifications

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 notification 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

Wherever the Studio or manager exists:

Studio.displayNotification('Action completed', { type: 'success' })
manager.displayNotification('Invalid position', { type: 'danger' })

Examples

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

  • Info:
    • to briefly describe the functionality of a tab/button that has been clicked
  • Success:
    • files have been downloaded
    • order has been put in the production queue
    • an algorithm, that automatically adds multiple components, has finished
  • Warning:
    • 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:
    • when trying to place 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 be handled and not 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 Editor. 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 throughout the project (not only the app but also 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, e.g. 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 component structure is valid, e.g. 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 (UI Editor) imports all the components automatically. However, the Component Editor and Drawing Editor 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 is a reference to the component and places it in the world with a certain position and rotation. Below are the most common functions used with an instance. Note that an instance is always retrieved from the componentHandler and not created directly as you would do with 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.

Configuration Info

You can see the information regarding your configuration by using the following in UI of the app, which contains:

  • id that is unique for all the configurations saved in the app.
  • isUnsaved as a boolean to check if the configuration has been saved previously or not.
  • name given when saving the configuration
  • revision as the saved version of the configuration (starting in 0). E.g. a configuration saved 3 times will have revision: 3.
const configurationInfo = Studio.getConfigurationInfo() // returns { id: string, isUnsaved: boolean, name: string, revision: number }

The most common example of use of this can be found in the drawing, e.g. to show the name and revision in the header. Also remember that you can access the previous versions of saved configurations through the URL, read more about it here.

Advanced UI Settings

The editor tab ADVANCED can be found in the UI of your application. Although this tab is intented for advanced settings (e.g. light direction), here are some of the options that are easy to update:

  • showEdges to show/hide the edges of the model.
  • ui.disableTabCollapse to disable/enable the SKYUI.Tab to be collapsible when clicking on it.
  • sceneOptions with the alternatives:
    • outdoor (with 1 light source position), with clearColor as the background color and sunPosition as the light source position, as:
      { type: 'outdoor', clearColor: 0xFFFFFF, sunPosition: new SKYMATH.Vector3D(5000, 0, 5000) },
    • room (with multiple light sources), with clearColor as the background color and lightRotation as the angle of the light sources, as:
      { type: 'room', clearColor: 0xFFFFFF, lightRotation: 0 },

Function productConfigurationFactory() with these arguments (other arguments not shown here should not be removed):

export function productConfigurationFactory(): STUDIO.IProductConfiguration {
return {
sceneOptions: { type: 'outdoor' }, // replaces current default "sceneSelection"
showEdges: true,
ui: {
disableTabCollapse: true,
},
}
}

If you would like to explore the other available optional arguments or need help understanding them reach us at support@skymaker.com.