Skip to main content

User Interface

In this tutorial, you will learn the basics of the user interface in DynaMaker. Every button and tab can be connected to any behavior according to the workflow of creating the custom product. As a product example, we will create an app to build custom conveyors from pre-designed parts. Each section of the whole conveyor will be able to be added manually so that any path can be created. Pretty similar to the typical roller coaster builder but without any zero-g rolls, unfortunately.

For this we will follow 5 steps:

  1. Prepare components
  2. UI Editor
  3. Connectors
  4. Polish app
  5. Hotkeys

Don't forget to check the common mistakes if you get stuck. Good luck!

1. Prepare Components

For this project, we will create three components: Roller, Section & Assembly. You can start with a new project with three new components, as done in previous tutorials.

A. Roller

Let's say that the roller models are previously designed (as STL). So, before going into geometry, let's start with the properties.

Properties

The roller will only have one property for its type: cylindrical or multidirectional.

  • In CONSTANTS, make sure you have:
export enum ROLLER_TYPE {
CYLINDER = 'roller-type-cylinder',
MULTIDIRECTIONAL = 'roller-type-multidirectional',
}

export interface Properties {
rollerType: ROLLER_TYPE
}
  • In COMPONENT, make sure you have:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

this.properties = {
rollerType: CONSTANTS.ROLLER_TYPE.MULTIDIRECTIONAL,
}
}

generateGeometry() {
// logic to generate geometry
}
}

Easy. Now the geometry!

Geometry

You can use your own (as STL for now) or download the following for this tutorial:

To be able to use them in your components, you need to import them into your project first.

  • In the app dashboard, upload all files and create static models:

  • Before you can use the STL models, make sure you import ASSETS within the component Roller, in the same way as if it was a subcomponent.

    You should see ASSETS in imports within any Maker as soon as you upload a file in your app dashboard. Refresh the imports or the page (F5) if it does not show up.

  • Once you have imported them, create the models in GEOM3D:

const baseModel = ASSETS.STATIC_MODELS.BASE_ROLLER
const cylinderModel = ASSETS.STATIC_MODELS.CYLINDER_ROLLER
const multidirectionalModel = ASSETS.STATIC_MODELS.MULTIDIRECTIONAL_ROLLER

export function getBaseModel() {
return baseModel
}

export function getRollerModel(rollerType: string) {
switch (rollerType) {
case CONSTANTS.ROLLER_TYPE.CYLINDER:
return cylinderModel
case CONSTANTS.ROLLER_TYPE.MULTIDIRECTIONAL:
return multidirectionalModel
}
}

Create the models outside the functions so that they are created once.

  • In COMPONENT, add these models to the geometry of the component, in MeshModel in generateGeometry(), so that it depends on rollerType:
generateGeometry() {
const { rollerType } = this.properties
const baseRollerModel = GEOM3D.getBaseModel()
const rollerModel = GEOM3D.getRollerModel(rollerType)

const geometryGroup = new SKYCAD.GeometryGroup()

geometryGroup.addGeometry(baseRollerModel, {
materials: [new SKYCAD.Material({ color: 0x444444 })],
rotation: new SKYMATH.Vector3D(0, 0, Math.PI / 2)
})
geometryGroup.addGeometry(rollerModel, {
materials: [new SKYCAD.Material({ color: 0xDDDDDD })],
rotation: new SKYMATH.Vector3D(0, 0, Math.PI / 2)
})

return geometryGroup
}
  • Save & Update to see the roller.

  • In API export the constants so ROLLER_TYPE can be used in oher components:
export * from './component.js'
export * from './constants.js'
  • Save & Update and Publish.

Now that we have the rollers, let's add them to the Section.

B. Section

The conveyor can be divided into multiple sections. Each section will consist of a structure to hold the rollers, and the structure itself will be made of 2 legs and the bed where the rollers are located. For simplicity, the models of the bed and legs can be created within the Section component. Also, the direction or angle will be parametric.

To have a clear idea of what we are about to build, here are some pictures of what the main dimensions will represent and how the angle will affect the curvature of the rollers, bed and legs positions & rotations. However, you can choose your own design for the profile of the legs and bed. Notice that we will have exactly 8 rollers so that they can fit the section with a specific set of default properties.


This said, let's start with the properties and then go with the geometry.

Properties

To make it simple, height, depth and width of the component Section will remain constant. Also, angle and rollerType will be something that the user will be able to change in the app and therefore, will be part of the properties.

  • First off, import the component Roller to reuse its constant ROLLER_TYPE via edit imports....

  • In CONSTANTS, make sure you have:

export interface Properties {
depth: number
height: number
width: number
angle: number
rollerType: ROLLER.ROLLER_TYPE
}

export const BED_HEIGHT = 150
export const BED_THICKNESS = 25
export const LEG_WIDTH = 50
export const ROLLERS_CC_DISTANCE = 100
  • In COMPONENT try the following default property values:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
// notice CONSTANTS.Properties
constructor() {
super()

this.properties = {
depth: 500,
height: 1200,
width: 1000,
angle: 0,
rollerType: ROLLER.ROLLER_TYPE.CYLINDER,
}
}

generateGeometry() {
// logic to generate geometry
}
}

Subcomponents

With the similar process as we did in My First Assembly:

  • Still in COMPONENT, add this.addRollers() and this.update() within the constructor.
constructor() {
super()

// properties

this.addRollers()
this.update()
}
  • Create addRollers() to simply add the rollers to the componentHandler:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

// properties

this.addRollers()
this.update()
}

addRollers() {
const nrRollers = 8
const rollerComponent = new ROLLER.Component()
for (let i = 0; i < nrRollers; i++) {
this.componentHandler.add(rollerComponent)
// you don't need to handle positions/rotations here since that will be done in update()
}
}

generateGeometry() {
// logic to generate geometry
}
  • Create a function getSectionRadius() to get the radius for a section given its width and angle. This function will be used to place the rollers and legs correctly, and also to give the proper value for the bed width. You decide where to add it (within the class Component, outside or even in CONSTANTS). Here we add it outside the class as:
function getSectionRadius(width: number, angle: number) {
const R = width / angle
return R
}

Be aware of the division by 0 in getSectionRadius(). In Typescript this will give Infinity, a value that can be avoided easily with an if statement to make sure the angle is not 0. Continue to see how it's done.

  • Create update() that handles the position and rotation of the rollers:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

// properties

this.addRollers()
this.update()
}

addRollers() {
// logic to add rollers
}

update() {
const { width, height, rollerType, angle } = this.properties

// Rollers
const rollerComponent: ROLLER.Component = this.componentHandler.getComponents(ROLLER.Component)[0]
rollerComponent.setProperties({ rollerType })
const rollers = this.componentHandler.getInstances(ROLLER.Component)
const nrRollers = rollers.length

const stepPosX = width / nrRollers
let posX = stepPosX / 2

if (angle === 0) {
rollers.forEach((roller) => {
const position = new SKYMATH.Vector3D(posX, 0, height - CONSTANTS.BED_HEIGHT / 2)
roller.setPosition(position)
posX += stepPosX
})
} else {
const R = getSectionRadius(width, angle)
const rollerStepAngle = angle / nrRollers
let rotZ = rollerStepAngle / 2
let posY = (R * (1 - Math.cos(rotZ))) / 2

rollers.forEach((roller) => {
const position = new SKYMATH.Vector3D(posX, posY, height - CONSTANTS.BED_HEIGHT / 2)
const rotation = new SKYMATH.Vector3D(0, 0, rotZ)
roller.setPosition(position)
roller.setRotation(rotation)
rotZ += rollerStepAngle
posX = R * Math.sin(rotZ)
posY = R * (1 - Math.cos(rotZ))
})
}
}

generateGeometry() {
// logic to generate geometry
}
}

In this case, we don't clear the rollers and add them again. Since we always have 8 rollers in each section it's better to add them once (through addRollers()) and change their properties/position in update() for the app's performance.

Great, now that we have the rollers in the componentHandler we need to generate their geometry, together with the legs and bed.

Geometry

To make it simple, the legs and bed will be generated directly within the component Section and added to the geometry of the Section. Let's start with the 2D sketches, then the 3D models, and finally the geometry.

  • In GEOM2D, create the base of the leg and the profile for the bed:
export function generateLegBaseSketch() {
return SKYCAD.generateHexagonSketch(0, 0, CONSTANTS.LEG_WIDTH)
}

export function generateBedProfileSketch() {
const H = CONSTANTS.BED_HEIGHT
const T = CONSTANTS.BED_THICKNESS
const sketch = new SKYCAD.Sketch()
sketch.moveTo(0, 0)
sketch.lineTo(0, H / 2)
sketch.lineTo(T / 2, H / 2)
sketch.lineTo(T / 2, H)
sketch.lineTo(T, H)
sketch.lineTo(T, 0)
sketch.lineToId(0)
return sketch
}
  • In GEOM3D, create the leg model as an extrude and the bed railing as a revolve for angles different from 0, and as an extrude for angles equal to 0:
export function generateLegModel(height: number) {
const model = new SKYCAD.ParametricModel()
const legSketch = GEOM2D.generateLegBaseSketch()
const plane = new SKYCAD.Plane(0, 0, 1, 0)
model.addExtrude(legSketch, plane, height - CONSTANTS.BED_HEIGHT)
return model
}

export function generateBedStraightModel(width: number, { invertedProfile = false } = {}) {
const model = new SKYCAD.ParametricModel()
const plane = new SKYCAD.Plane(-1, 0, 0, 0)
const profileSketch = GEOM2D.generateBedProfileSketch()
if (invertedProfile) profileSketch.mirrorInYaxis()
model.addExtrude(profileSketch, plane, -width)
return model
}

export function generateBedRevolveModel(radius: number, angle: number, { invertedProfile = false } = {}) {
const model = new SKYCAD.ParametricModel()
const plane = new SKYCAD.Plane(-1, 0, 0, 0)
const profileSketch = GEOM2D.generateBedProfileSketch()
if (invertedProfile) profileSketch.mirrorInYaxis()
profileSketch.translate(radius, 0)
model.addRevolve(profileSketch, plane, {
revolveAngle: angle,
axisDirection: new SKYMATH.Vector2D(0, 1),
})
return model
}
  • In COMPONENT, create the geometry from these models, also add the rollers through the componentHandler as done in previous tutorials:
generateGeometry() {
const { width, depth, height, angle } = this.properties
const model = GEOM3D.generateLegModel(height)

const geometryGroup = new SKYCAD.GeometryGroup()

// Legs
let leftLegPosition = new SKYMATH.Vector3D(width / 2, depth / 2, 0)
let rightLegPosition = new SKYMATH.Vector3D(width / 2, -depth / 2, 0)
if (angle !== 0) {
const R = getSectionRadius(width, angle)
leftLegPosition.x = (R - depth / 2) * Math.sin(angle / 2)
rightLegPosition.x = (R + depth / 2) * Math.sin(angle / 2)
leftLegPosition.y = depth / 2 + (R - depth / 2) * (1 - Math.cos(angle / 2))
rightLegPosition.y = -depth / 2 + (R + depth / 2) * (1 - Math.cos(angle / 2))
}
const legMaterials = [new SKYCAD.Material({ color: 0xCCCCCC })]
geometryGroup.addGeometry(model, {
materials: legMaterials,
position: leftLegPosition,
})
geometryGroup.addGeometry(model, {
materials: legMaterials,
position: rightLegPosition,
})

// Bed
const bedMaterials = [new SKYCAD.Material({ color: 0x8B0000 })]
if (angle === 0) {
geometryGroup.addGeometry(GEOM3D.generateBedStraightModel(width, { invertedProfile: false }), {
materials: bedMaterials,
position: new SKYMATH.Vector3D(0, -depth / 2, height - CONSTANTS.BED_HEIGHT)
})
geometryGroup.addGeometry(GEOM3D.generateBedStraightModel(width, { invertedProfile: true }), {
materials: bedMaterials,
position: new SKYMATH.Vector3D(0, depth / 2, height - CONSTANTS.BED_HEIGHT)
})

} else {
const radius = getSectionRadius(width, angle)
geometryGroup.addGeometry(GEOM3D.generateBedRevolveModel(radius + depth / 2, angle, { invertedProfile: false }), {
materials: bedMaterials,
position: new SKYMATH.Vector3D(0, radius, height - CONSTANTS.BED_HEIGHT)
})
geometryGroup.addGeometry(GEOM3D.generateBedRevolveModel(radius - depth / 2, angle, { invertedProfile: true }), {
materials: bedMaterials,
position: new SKYMATH.Vector3D(0, radius, height - CONSTANTS.BED_HEIGHT)
})
}

// Rollers
geometryGroup.addGeometry(this.componentHandler.generateAllGeometry())

return geometryGroup
}
  • Save & Update and Publish.

Perfect! You can complement this Component Editor with some presets, using different roller types and angles to see the edge cases (e.g. if the rollers collide with each other for angle = ±90 degrees).

C. Assembly

The entire conveyor or assembly will consist of multiple sections. This component should handle the adding and removing of sections, which will be subcomponents in this case, and with which properties, i.e. roller type & angle. Then a configurator is needed for the application. However, the logic to add & remove sections will be explained later, together with the UI Editor.

Properties

  • First import the subcomponents Roller and Section via edit imports....
  • In CONSTANTS, update the properties definition with just rollerType and angle:
export interface Properties {
rollerType: ROLLER.ROLLER_TYPE
angle: number
}
  • In COMPONENT, you can have the following class with addSection() and removeSection(), which will be triggered in the UI.
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

this.properties = {
rollerType: ROLLER.ROLLER_TYPE.CYLINDER,
angle: 0,
}
}

addSection() {
const { rollerType, angle } = this.properties
const newSectionComponent = new SECTION.Component()
newSectionComponent.setProperties({ rollerType, angle })
this.componentHandler.add(newSectionComponent, {
// position for multiple sections will be added later
})
}

removeSection() {
const sections = this.componentHandler.getInstances(SECTION.Component)
if (sections.length > 0) {
const lastSection = sections[sections.length - 1]
const sectionsToRemove = [lastSection]
this.componentHandler.removeInstances(sectionsToRemove)
}
}

generateGeometry() {
return this.componentHandler.generateAllGeometry()
}
}

If you Save & Update you won't see an empty preview because Assembly doesn't contain any section or rollers.

Configurator

Keep in mind that the angle should be always in radians, because of the Math.sin(angle) and Math.cos(angle) used previously. However, the displayed value should be in degrees to make it easier to read and update for the user.

  • In PARAMETERS, create a parameter for each property.
Try yourself first and compare it with this new generateConfigurator():
export function generateConfigurator(component: COMPONENT.Component): SKYPARAM.Configurator {
const rollerTypeParameter = SKYPARAM.generateDropdown('rollerType', 'Roller type', [
new SKYPARAM.DropdownItem('Cylinders', ROLLER.ROLLER_TYPE.CYLINDER),
new SKYPARAM.DropdownItem('Multidirectional', ROLLER.ROLLER_TYPE.MULTIDIRECTIONAL),
])
rollerTypeParameter.setValue(component.getProperty('rollerType'))

const angleParameter = SKYPARAM.generateSlider('angle', 'Angle (deg)', {
defaultValue: -Math.round((component.getProperty('angle') * 180) / Math.PI),
maxValue: 90,
minValue: -90,
stepSize: 1,
})

const configurableParameters = [rollerTypeParameter, angleParameter]

const configurator = new SKYPARAM.Configurator(configurableParameters)

configurator.addCompletionCallback((validUserInput, values) => {
if (validUserInput) {
component.setProperties({
rollerType: values.rollerType,
angle: (-values.angle * Math.PI) / 180,
})
}
})

return configurator
}

Notice the negative sign in defaultValue and component.setProperties(). A negative angle means that the section goes to the left, and a positive angle means that it goes to the right. Doing this, we avoid using opposite angles for the calculations in update().

  • Save & Update and Publish.

Add and remove Sections

2. UI Editor

Once we have all the components, it's time to use the app. Open the UI Editor by clicking on the thumbnail, name or UI Studio in your app dashboard.

In the code editor you will see the following tabs:

  • TESTS for adding tests regarding the app
  • UI where the tabs and buttons are created
  • STYLE to override the current css (color of buttons, style, UI sizes, etc)
  • COMPONENTS meant for handling the components and geometries will be used in the app
  • SELECTION for handling all selection events (not part of this tutorial)
  • HOTKEYS to assign keyboard shortcuts to specific (chains of) actions
  • ACTIONS that trigger any behavior (e.g. when clicking a button)
  • CONSTANTS to store any constant
  • ADVANCED for the experienced user, including many functionalities (e.g. scene lighting & shadows, model edges, etc)

Keep in mind that the preview in the UI Editor is exactly what the user will see and use.

In this tutorial, we will focus on the tabs UI, explaining how to add some buttons/configurators to the UI, trigger specific actions (add or remove sections) in ACTIONS and bind those to some hotkeys in HOTKEYS.

Notice that the app is connected by default to the default cube component of the template project. Let's start by fixing that and then continue with the UI.

A. Connect Assembly

Similarly as done in My First App, we will modify the UI accordingly, but in order to keep UI clean, we will do it the help of the tab ACTIONS. But first:

  • import your component Assembly to be able to use it in the app via edit imports...
  • add ACTIONS.addAssembly() that will handle adding the component at the beginning
  • rename tab to Product.
  • leave tabContent empty for now.
  • doing so, you should end up having something like the following:
export function onInit() {
ACTIONS.addAssembly()

const productTab = new SKYUI.Tab({
title: 'Product',
icon: 'cog',
onInit: () => {
const tabContent = []

Studio.setTabContent(tabContent)
Studio.requestGeometryUpdate().then(() => updateCamera())
},
})

const tabs = [productTab]

Studio.setTabs(tabs)
}

Great. Before saving, let's start adding details to the UI!

B. Prepare UI

In all apps, the user interface plays a critical role since it must be defined by the workflow of the design of the product. The app should give the user a feeling of customizing the product usually from main features to details, without breaking any rule. Even if the user goes back to a previous step to adjust any parameter, the product should behave accordingly without destroying the progress made so far (if possible).

In this case, we will prepare a simple UI consisting of 3 tabs:

  • Product with the assembly configurator and the add & delete buttons.
  • Edit intended for selecting already built sections and editing them (we will skip this and disable the tab instead).
  • Export to export the conveyor as STL and more.

We will do a quick walkthrough of the functions as we are modifying them.

Add buttons & actions

Let's add the configurator and two buttons in the Product tab:

const productTab = new SKYUI.Tab({
title: 'Product',
icon: 'cog',
onInit: () => {
const configurator = ACTIONS.getAssemblyConfigurator()

const addButton = new SKYUI.Button(
'Add section',
() => {
ACTIONS.addSection()
Studio.requestGeometryUpdate()
},
{ icon: 'plus' },
)

const deleteButton = new SKYUI.Button(
'Delete section',
() => {
ACTIONS.removeSection()
Studio.requestGeometryUpdate()
},
{ icon: 'trash' },
)

const tabContent = [configurator, addButton, deleteButton]

Studio.setTabContent(tabContent)
Studio.requestGeometryUpdate().then(() => updateCamera())
},
})
  • In ACTIONS, create those functions like:
export function addAssembly() {
const componentHandler = manager.getComponentHandler()
const assemblyComponent = new ASSEMBLY.Component()
assemblyComponent.setProperties({ angle: 0 })
assemblyComponent.addSection()
componentHandler.add(assemblyComponent)
}

export function getAssemblyConfigurator() {
const assemblyComponent = getAssemblyComponent()
const configurator = ASSEMBLY.generateConfigurator(assemblyComponent)
return configurator
}

export function addSection() {
const assemblyComponent = getAssemblyComponent()
assemblyComponent.addSection()
}

export function removeSection() {
const assemblyComponent = getAssemblyComponent()
assemblyComponent.removeSection()
}

function getAssemblyComponent() {
const componentHandler = manager.getComponentHandler()
const assemblyComponents = componentHandler.getComponents(ASSEMBLY.Component)
if (assemblyComponents.length > 0) {
return assemblyComponents[0]
} else {
throw new Error(`No assemblies found in componentHandler`)
}
}
  • Save & Update to apply changes to the UI.

You can read more about buttons and their optional arguments like icon in the SKYUI library.

Great. You can try adding and removing sections with the buttons but you will see that they are not connected. Now that we have connected the button with the respective functions of the assembly, let's add a new tabs before diving into the button actions.

Add new tab

Sometimes we want to show the customer the full potential of our app without implementing it. Instead of doing it at first, it is much more convenient to add buttons/tabs but disabled, so the customer could see how the prototype could look. So:

  • Go back to your app, in UI
  • Add a disabled Edit tab so the user could see that in the future the app could let them select any section and configure it individually.
  • Add a Export tab with some disabled buttons to download drawings, like:
export function onInit() {
ACTIONS.addAssembly()

const productTab = new SKYUI.Tab({
// previous settings and functions
})

const editTab = new SKYUI.Tab({
title: 'Edit',
icon: 'pencil',
disabled: true,
})

const exportTab = new SKYUI.Tab({
title: 'Export',
icon: 'download',
onInit: () => {
const downloadPdfButton = new SKYUI.Button('Download PDF', () => {}, { disabled: true, icon: 'download' })

const downloadStlButton = new SKYUI.Button('Download STL', () => {}, { disabled: true, icon: 'download' })

const tabContent = [downloadPdfButton, downloadStlButton]

Studio.setTabContent(tabContent)
},
})

const tabs = [productTab, editTab, exportTab]

Studio.setTabs(tabs)
}

You can read more about tab args like disabled in the SKYUI library.

Now that we have all the logic connected properly, we can add the connectors to add multiple sections in a row.

3. Connectors

To automatically add a second section at the end of an already-built one, we can use connectors, which are essentially a bundle of position and rotation with internal handling of local and global coordinate systems. We can add a connector to the Section component and then, the Assembly component makes sure that the new section is added to the last section's connector.

  • Go back to the Section component.
  • In COMPONENT, within the class create getConnectors() to add a connector at the end of the section (on the ground):
getConnectors() {
const { width, angle } = this.properties
if (angle === 0) {
return [
new SKYCAD.Connector3D({
position: new SKYMATH.Vector3D(width, 0, 0),
rotation: new SKYMATH.Vector3D(0, 0, angle),
})
]
} else {
const R = getSectionRadius(width, angle)
return [
new SKYCAD.Connector3D({
position: new SKYMATH.Vector3D(R * Math.sin(angle), R * (1 - Math.cos(angle)), 0),
rotation: new SKYMATH.Vector3D(0, 0, angle),
})
]
}
}
  • Save & Update and Publish this Section component.

getConnectors() is a standard function in a component, meaning that it makes the connectors visible as a small blue-ish coordinate system in the Component Editor.

  • Go back to the Assembly component.
  • In COMPONENT, modify addSection() so that the new section is added to the previous section's connector, like:
addSection() {
const { rollerType, angle } = this.properties

let previousSectionConnector = new SKYCAD.Connector3D()
const sections = this.componentHandler.getInstances(SECTION.Component)
if (sections.length > 0) {
const lastSection = sections[sections.length - 1]
const lastSectionConnector = lastSection.getConnectors()[0]
previousSectionConnector = lastSectionConnector
}

const newSectionComponent = new SECTION.Component()
newSectionComponent.setProperties({ rollerType, angle })
this.componentHandler.add(newSectionComponent, {
connector: previousSectionConnector,
})
}
  • Save & Update and Publish this Assembly component.
Optionally add some presets (in PRESETS COMP) and debug them to see how these new functions work. For example:
const mainComponentPreset = new PRESET.ComponentPreset(COMPONENT.Component, {
setCameraOnUpdate: false,
})

mainComponentPreset.addCase([], { label: 'Default (empty)' })

mainComponentPreset.addCase([], {
postProcessor: (component: COMPONENT.Component) => {
component.setProperties({
rollerType: ROLLER.ROLLER_TYPE.CYLINDER,
angle: (30 * Math.PI) / 180,
})
component.addSection()
},
label: '1 section (cylinders, 30 deg)',
})

mainComponentPreset.addCase([], {
postProcessor: (component: COMPONENT.Component) => {
component.setProperties({
rollerType: ROLLER.ROLLER_TYPE.CYLINDER,
angle: (-60 * Math.PI) / 180,
})
component.addSection() // section 1

component.setProperties({
rollerType: ROLLER.ROLLER_TYPE.MULTIDIRECTIONAL,
angle: (30 * Math.PI) / 180,
})
component.addSection() // section 2

component.addSection() // section 3

component.removeSection()
},
label: '3 sections added and 1 removed',
})

export const presetsComponent: PRESET.ComponentPreset<any>[] = [mainComponentPreset]

Nice, we have a fully functional app! We will polish it in the following section. The camera, more UI items and the initial component are some of the things that remain to improve.

4. Polish App

As general polishing, we can adjust the camera and add some metrics.

A. Camera

We can also adjust the camera when opening the app, so it's automatically adjusted to the geometry.

  • In UI, notice the default function updateCamera(), that contains Studio.setCameraToFitBounds() which updates the camera based on the current bounds of the instances seen in the preview:
function updateCamera() {
const cameraDirection = new SKYMATH.Vector3D(1, 1, -1)
Studio.setCameraToFitBounds({
direction: cameraDirection,
})
}
  • At the beginning of onInit(), you can change the camera settings with Studio.updateCameraSettings() and add:
    • groundLock: false to disable the ground and be able to pan the camera freely.
    • maxPolarAngle: Math.PI to unlock the camera rotation up to the bottom (or nadir angle).
    • orthographic: true to have an orthographic view instead of perspective.
export function onInit() {
Studio.updateCameraSettings({
groundLock: false,
maxPolarAngle: Math.PI,
orthographic: true,
})

// rest of the function
}

Check more camera functionalities here.

B. Metrics

In the previous tutorial, we showed how to prepare some component values/properties so that the app can easily extract them and use them in the app. We will replicate that but with the current number of rollers including type and total conveyor length.

Since we are going to read the rollers within a section from the assembly, we could create a function that returns the component handler of the section. For this, simply:

  • Go to the Section component.
  • Create a function within the class that returns the component handler (in COMPONENT within the class):
getComponentHandler() {
return this.componentHandler
}

Great, now we can read easily the subcomponents of the section from the assembly:

  • Go to the Assembly component.
  • Create a function within the class that gets those values, you can wrap them in an object (in COMPONENT within the class):
getMetrics() {
let totalConveyorLength = 0
let nrCylinderRollers = 0
let nrMultidirectionalRollers = 0
const sections = this.componentHandler.getInstances(SECTION.Component)
sections.forEach(section => {
const sectionComponent = section.getComponent()

// Length
const sectionLength = sectionComponent.getProperty('width')
totalConveyorLength += sectionLength

// Rollers
const rollers = sectionComponent.getComponentHandler().getInstances(ROLLER.Component)
const nrRollers = rollers.length
const rollerType = sectionComponent.getProperty('rollerType')
if (rollerType === ROLLER.ROLLER_TYPE.CYLINDER) {
nrCylinderRollers += nrRollers
} else if (rollerType === ROLLER.ROLLER_TYPE.MULTIDIRECTIONAL) {
nrMultidirectionalRollers += nrRollers
}
})

return {
totalConveyorLength,
nrCylinderRollers,
nrMultidirectionalRollers
}
}

Now, this function can be called in the UI.

  • Go back to the UI.
  • In ACTIONS create a function that gets the metrics from the assembly, like:
export function updateVisibleMetrics() {
const assemblyComponent = getAssemblyComponent()
const metrics = assemblyComponent.getMetrics()

manager.updateVisibleMetric('totalConveyorlength', `${Math.round(metrics.totalConveyorLength / 1000)}`)
manager.updateVisibleMetric('nrCylinderRollers', `${metrics.nrCylinderRollers}`)
manager.updateVisibleMetric('nrMultidirectionalRollers', `${metrics.nrMultidirectionalRollers}`)
}

Remember that if you are using secret formulas, i.e. async functions, you would need to update the function accordingly:

export async function updateVisibleMetrics() {
// assemblyComponent
const metrics = await assemblyComponent.getMetrics()
// update metrics
}
  • In UI, define the visible metrics and update them within onInit():
export function onInit() {
// camera settings

ACTIONS.addAssembly()

Studio.defineVisibleMetrics([
{ id: 'totalConveyorlength', label: 'Total conveyor length (m):', value: 0 },
{ id: 'nrCylinderRollers', label: 'Nr cylinder rollers:', value: 0 },
{ id: 'nrMultidirectionalRollers', label: 'Nr multidirectional rollers:', value: 0 },
])
ACTIONS.updateVisibleMetrics()

// rest of the function
}
  • To update them dynamically, you need to update their values when clicking on Add section and Delete section, so that:
const addButton = new SKYUI.Button(
'Add section',
() => {
ACTIONS.addSection()
Studio.requestGeometryUpdate()
ACTIONS.updateVisibleMetrics()
},
{ icon: 'plus' },
)

const deleteButton = new SKYUI.Button(
'Delete section',
() => {
ACTIONS.removeSection()
Studio.requestGeometryUpdate()
ACTIONS.updateVisibleMetrics()
},
{ icon: 'trash' },
)

Give it a go again, and see how the metrics update accordingly. Also, notice the importance of using light models for the app performance: while the cylinder is just 18 kB, the multidirectional is 1436 kB. Almost 80 times the size!

As the final step, you will learn how to create shortcuts or hotkeys in your app.

5. Hotkeys

In DynaMaker you can assign action events to hotkeys and have your customized key-bindings, meaning faster designing and easier user experience in your application. Let's take advantage of having an action event for adding and deleting the sections and assign this behavior to the keys.

You can easily find the hotkey value here. Give it a try and check that Delete has the value of 46.

We will add sections if we press +, and remove sections with - or Delete.

  • In HOTKEYS, add the following code within hotkeyEvent():
export function hotkeyEvent(event, manager: STUDIO.Manager) {
const keyMap = {
107: 'plus',
109: 'minus',
46: 'delete',
}

const key = keyMap[event.keyCode]

const keyFunctions = {
plus: () => {
ACTIONS.addSection()
ACTIONS.updateVisibleMetrics()
},
minus: () => {
ACTIONS.removeSection()
ACTIONS.updateVisibleMetrics()
},
delete: () => {
ACTIONS.removeSection()
ACTIONS.updateVisibleMetrics()
},
}
manager.requestGeometryUpdate()

return keyFunctions[key] && keyFunctions[key]()
}

Now you should be able to add and remove sections with these hotkeys.


Congratulations! You have learned the basics of the user interface, with a quick walkthrough of connectors, predefined STL models, hotkeys and camera updates. The final application should look something like this:


We have added a button to take a picture of the current view. Read more here if interested.

Do you want to have a copy of this app in your team? Let us know at support@dynamaker.com! Remember that everyone has their own way of developing and there are multiple valid ways to do things as long as anyone can understand the code!