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. App Maker
  3. Connectors
  4. Polish app
  5. Hotkeys

This tutorial takes approximately 45 minutes to complete. 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 interface Properties {
rollerType: string
}

export const ROLLER_TYPE = {
CYLINDER: 'roller-type-cylinder',
MULTIDIRECTIONAL: 'roller-type-multidirectional',
}
  • In COMPONENT, make sure you have:
this.properties = {
rollerType: CONSTANTS.ROLLER_TYPE.MULTIDIRECTIONAL
}

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 project dashboard, upload all files and create static models: tutorial-user-interface-roller

  • 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 project 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_STL
const cylinderModel = ASSETS.STATIC_MODELS.CYLINDER_ROLLER_STL
const multidirectionalModel = ASSETS.STATIC_MODELS.MULTIDIRECTIONAL_ROLLER_STL

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
}

tutorial-user-interface-roller

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

tutorial-user-interface-section-1


tutorial-user-interface-section-2

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:

  • In CONSTANTS, make sure you have:
export interface Properties {
depth: number
height: number
width: number,
angle: number,
rollerType: string,
}

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:
this.properties = {
depth: 500,
height: 1200,
width: 1000,
angle: 0,
rollerType: ROLLER.ROLLER_TYPE.CYLINDER,
}

Notice that we have imported the constant from the Roller component:

  • in the component Roller, MASTER, you can export all within constants if you type:
export * from './constants.js'
  • then, import the component Roller into the component Section.
  • you will get its IntelliSense when you type ROLLER. in the component Section.

Try to use this pattern either upstream or downstream as for the level components, but avoid going back and forth with your constants. It might get messy if you start doing a lot of cross-references.

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()

this.properties = {
// ...
}

this.addRollers()
this.update()
}
  • Create addRollers() to simply add the rollers to the componentHandler:
  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()
}
}
  • 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:
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))
})
}
}

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 refPlane = new SKYCAD.Plane(0, 0, 1, 0)
model.addExtrude(legSketch, refPlane, height - CONSTANTS.BED_HEIGHT)
return model
}

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

export function generateBedRevolveModel(radius: number, angle: number, { invertedProfile = false } = {}) {
const model = new SKYCAD.ParametricModel()
const refPlane = new SKYCAD.Plane(-1, 0, 0, 0)
const profileSketch = GEOM2D.generateBedProfileSketch()
if (invertedProfile) profileSketch.mirrorInYaxis()
profileSketch.translate(radius, 0)
model.addRevolve(profileSketch, refPlane, {
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 Maker 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).

tutorial-user-interface-section

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 app maker.

Properties

  • In CONSTANTS, update the properties definition with just rollerType and angle:
export interface Properties {
rollerType: string
angle: number
}
  • In COMPONENT, you can have the following class for now:
export class Component extends STUDIO.BaseComponent<CONSTANTS.Properties> {
constructor() {
super()

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

addSection() {
// logic to be added later
}

removeSection() {
// logic to be added later
}

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

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

2. App Maker

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

tutorial-user-interface-app

In the code editor you will see the following tabs:

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

Keep in mind that the preview in the App Maker is exactly what the user will see and use.

In this tutorial, we will focus on the tab CODE, explaining how to add some buttons/configurators to the UI, trigger specific actions (add or remove sections) and bind those to some 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

Replace the old COMPONENT1 with the new component ASSEMBLY. Notice that the App Maker imports all components by default, so you only need to replace it wherever it's used (COMPONENTS and

ACTIONS)

When you Save & Update, you will "see" the Assembly. There's no geometry yet, but you can see that the configurator of the first app tab Product updated with the configurator of the Assembly component, including both parameters Roller type and Angle (deg).

This done, let's go with 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.

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

The tabs that are generated in generateTabs() and have their own function (i.e. generateProductTab() and generateExportTab()). Let's add two buttons after the configurator in the Product tab:

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

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

const addButton = new SKYUI.Button('Add section', () => {
ACTIONS.addSection()
}, { icon: STUDIO.ICONS.PLUS })

const deleteButton = new SKYUI.Button('Delete section', () => {
ACTIONS.removeSection()
}, { icon: STUDIO.ICONS.TRASH })

const toolbarItems: STUDIO.ToolbarItem[] = [
configurator,
addButton,
deleteButton,
]

return toolbarItems
}

// tabArgs

return new STUDIO.Tab(tabTitle, toolbarGenerator, tabArgs)
}
  • Notice that we trigger an action for each button (addSection() and deleteSection()), these should be created in ACTIONS:
export function addSection() {
const assemblyComponent = getMainComponent()
assemblyComponent.addSection()
}

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

tutorial-user-interface-buttons

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

Great, now that we have connected the button with the respective functions of the assembly, it's time to add the logic in the Assembly component.

Add add- & remove-logic

  • Go back to the Assembly component.

  • In COMPONENT:

    • in addSection(), add a Section component with the current properties of the assembly (remember to import Assembly to be able to use it here):
        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
      })
      }
    • in removeSection(), remove the last section (if any) of the assembly:
        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)
      }
      }
  • Save & Update and Publish.

You can optionally add some presets and debug them to see how the new functions work. You can try to add the following in PRESETS COMP:

mainComponentPreset.addCase([], {
postProcessor: (component: COMPONENT.Component) => {
debugger // open console (F12) before saving the component
component.addSection()
},
label: '1 section added',
})

mainComponentPreset.addCase([], {
postProcessor: (component: COMPONENT.Component) => {
debugger // open console (F12) before saving the component
component.addSection()
component.removeSection()
},
label: '1 section added and removed',
})

Great! If you published the Assembly component properly, you should see this functionality working in the new buttons Add section and Delete section in your app. Give it a go, try different properties and add 1 section and then remove it.

You will probably notice that we want to add a new section after the previous one. We will return on how to do that later, using connectors.

Add new tab

Sometimes we want to show the user the full potential of our app. Instead of implementing the whole functionality at first, it is much more convenient to add buttons/tabs but disabled, so the user could see how the prototype could look. Let's 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.

  • Go back to your app, in UI, add a second tab Edit and leave it as disabled:
export function generateTabs(Studio: STUDIO.StudioService): STUDIO.Tab[] {
const productTab = generateProductTab(Studio)
const editTab = generateEditTab(Studio)
const exportTab = generateExportTab(Studio)

const listOfTabs: STUDIO.Tab[] = [
productTab,
editTab,
exportTab,
]

return listOfTabs
}

function generateEditTab(Studio: STUDIO.StudioService): STUDIO.Tab {
const tabTitle: string = 'Edit'

const toolbarGenerator: STUDIO.ToolbarGenerator = () => {
const toolbarItems: STUDIO.ToolbarItem[] = [
// add any possible configurators/buttons here
]
return toolbarItems
}

const tabArgs: STUDIO.TabArgs = {
disabled: true,
icon: STUDIO.ICONS.PENCIL,
designStep: CONSTANTS.DESIGN_STEP.EDIT, // needs "EDIT" within "DESIGN_STEP" in CODE/Constants
callback: () => {
// add any possible geometry updates
}
}

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

You can also disable the button Download PDF in the Export tab since we won't adjust the drawing in this tutorial.

tutorial-user-interface-tab

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

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

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 have a section visible when the app is open, adjust the camera and add some metrics.

A. Default Component

We can start by adding a straight section by default:

  • Go back to the app.
  • In COMPONENTS, include a section with the new assembly in initComponentHandler():
export function initComponentHandler(handler: STUDIO.ComponentHandler) {
const assemblyComponent = new ASSEMBLY.Component()
assemblyComponent.setProperties({ angle: 0 })
assemblyComponent.addSection()
handler.add(assemblyComponent)
}

Here handler refers to the top-level component handler, meaning that any other component (i.e. Assembly, Roller, etc) can be added directly here and used in the app freely. Similar to how the geometry is done for the assembly with subcomponents, notice that updateGeometry() generates the geometry from the component handler (data.componentHandler here).

B. Camera

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

  • In UI, add the following at the top of the onInit() function:
Studio.setCameraToFitBounds()
  • Also, you can easily disable the ground to be able to pan the camera below it.
Studio.updateCameraSettings({
maxPolarAngle: Math.PI,
})
  • If you want, you can have an orthographic view instead of perspective. You can switch to orthographic view with:
Studio.updateCameraSettings({
orthographic: true,
})

Check more camera functionalities here.

C. 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, as:
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/struct, like:
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 app.

  • Go back to the app.
  • In ACTIONS create a function that gets the metrics from the assembly, like:
export function updateVisibleMetrics() {
const assemblyComponent = getMainComponent()
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, set the visible metrics and update them within onInit():
  Studio.setVisibleMetrics([
{ 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()
  • 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: STUDIO.ICONS.PLUS })

const deleteButton = new SKYUI.Button('Delete section', () => {
ACTIONS.removeSection()
Studio.requestGeometryUpdate()
ACTIONS.updateVisibleMetrics()
}, { icon: STUDIO.ICONS.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()
},
'minus': () => {
ACTIONS.removeSection()
},
'delete': () => {
ACTIONS.removeSection()
},
}
manager.requestGeometryUpdate()
ACTIONS.updateVisibleMetrics()

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 and predefined STL models. 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.