- A Minimal Snapper Example
- File Format
- Nodes
- SymGraph
- Handles
- Components
- Transforms
- Bounds
- Geometry
- Rendering and Appearance
- Picking
- Visibility
- Connectors
- Grouping
- Props
- Files and External References
- Caching
- Updating Graphics
- Parameters and Programs
- Iteration
- Filtering
- Dump
- Debug
- Vocabulary
CmSym is a scene graph API and file format that allows you to create, edit, read and write graphics entities. Most often it's simply referred to as "sym".
Read the FAQ.
A Minimal Snapper Example
This is a minimal example of getting CmSym graphics from a snapper:
private class ExSnapper extends Snapper { public SymNode buildSym() { SymNode root(); root << reps3D(#medium); root << SymGMaterial(plainColorGMaterial3D(255, 255, 0)); root << SymBox(); return root; } }
File Format
The CmSym file format (.cmsym), built on top of Dex, supports streaming of parametric graphics (2D and 3D) and should be considered as the successor to Cm3D.
Meshes are compressed using OpenCTM, which can reduce the file size with a 2-5x factor compared to Cm3D. Other data is compressed using lzma (equivalent to 7-zip default). Always remember that the compression amount depends on the type of data included in the sym.
CmSym files are versioned using semantic versioning which will give a clear meaning for when to change each number in the major.minor.patch versioning scheme:
major: Backwards incompatible API changes minor: New backwards compatible features patch: Backwards compatible bug fixes
This will allow readers to easily check if it can load a newer file version or not by simply comparing the major part. However, no such guarantee can be made before CmSym file version 2.0.0.
Major changes will only be made during the major CET releases.
See the CmSym file structure reference
Nodes
SymNode is the main class of the cmsym package. Nodes are used to create cmsym based scene graphs, also referred to as sym graphs. The graphs are DAGs, directed acyclic graphs, which cannot contain cycles.
Here's an example of how to construct a simple sym graph:
SymNode root("root"); SymNode a("a"); root.addChild(a); SymNode b("b"); root.addChild(b);
Which will create a node "root" with two children; "a" and "b". Each node has an identifier which must be unique among it's siblings, so you cannot have two children named "a" for example. The id can be used to access different parts of a sym graph in a simple manner. A node id should be unique among its siblings. Ids allow you to access children easily:
SymNode a = root.child("a");
If you want an unique id to reference the node you can use the "rid" field, which is unique during runtime. Note that you shouldn't store SymNode on your snapper (or anywhere really) because they're not guaranteed to survive edits.
Nodes can exist several times in a symgraph, they're then referred to as shared. For example:
SymNode x("x"); SymNode root("root"); SymNode a("a"); a.addChild(x); root.addChild(a); SymNode b("b"); b.addChild(x); root.addChild(b);
To see the structure we can use the dump method:
root.dump();
Which shows that "x" is shared by the "a" and "b" nodes:
SymNode(root) SymNode(a) SymNode(x), shared=2 SymNode(b) SymNode(x), shared=2
You can access nodes further below the structure with a path, specifying each node below the current one separated by a dot.
SymNode x = root.node("a.x");
For convenience the sym graph can be constructed with a special syntax. Here's the same example with shared nodes, but with a syntax:
sym SymNode root("root") { SymNode x("x"); symadd SymNode("a") { symadd x; }; symadd SymNode("b") { symadd x; }; }
Here the "sym" prefix begins the sym syntax and child nodes and components are added with the "symadd" prefix. It's not required to use the syntax if you don't like it, but it can make the hierarchy of some sym graphs easier to understand. Especially the overall structure is more visible with the syntax.
SymGraph
SymGraph is the connection between the owning object, such as a Snapper, and the SymNode.
It gets automatically created and you should not store it as a field. You can always access it with the .sym method:
Snapper z = anySnapper(ExSnapper, place=true); SymGraph sym = z.sym(); pln(sym.owner);
sym.owner=ExSnapper((id=3, orig))
If you need to create it yourself you can override the buildSymGraph method:
public SymGraph buildSymGraph() { SymGraph graph(buildSym(), this); graph.disableCacheAndOptimizedGfx(); return graph; }
Handles
A SymHandle specifies a specific node using a starting node as an anchor and a path to the target node.
Sometimes it might be easier to use a handle than a node. See for example this example where a shared node is offset by a transform:
SymNode root("root"); SymNode x("x"); SymNode a("a"); a << SymTransform((1, 0, 0)); a << x; SymNode b("b"); b << SymTransform((2, 0, 0)); b << x; root << a << b;
or with syntax:
sym SymNode root("root") { SymNode x("x"); symadd SymNode("a") { symadd SymTransform((1, 0, 0)); symadd x; }; symadd SymNode("b") { symadd SymTransform((2, 0, 0)); symadd x; }; }
You can then use a handle to get the actual position with:
SymHandle ax = root.handle("a.x"); // AC stands for anchor coordinates, in this case same as root coordinates. pln(#ax.posAC); SymHandle bx = root.handle("b.x"); pln(#bx.posAC);
ax.posAC=(1, 0, 0) bx.posAC=(2, 0, 0)
Even though handles are often more convenient to use than nodes there are pitfalls to be aware of:
- Don't store a handle because the nodes might become invalid. In some cases, you can get away with calling "invalidate()" on the handle, but that comes with performance trade-offs.
- Handles might be slower than using nodes directly. Handle construction introduces some overhead, so constructing handles during iteration should be avoided unless necessary. And especially searching upwards in handles during iteration, for example calculating the anchor coordinate for all leafs, is very slow.
Components
SymComponent(s) are used to assign attributes to nodes.
Here's how to add components to a SymNode:
SymNode root("root"); // A convenient way to define a SymReps component. root.setComponent(reps3D(#medium)); // Append concatenates the transforms. root.appendComponent(SymTransform((1, 0, 0))); root.appendComponent(SymTransform((1, 1, 0))); // Or use the append operator. root << SymGMaterial(coloredGM(255, 0, 125));
You can see the components with the dump method:
root.dump(":c");
SymNode(root) symReps 3D(medium) symTransform (2, 1, 0) symGMaterial colorF(255, 0, 125)
Note that components are unique to each node and cannot be shared. Instead, if you want to reuse a component you need to share nodes.
Components have their own id which you can use, but it's recommended to use access functions instead:
pln(#root.symReps); pln(#root.pos);
root.symReps=SymReps(3D(medium)) root.pos=(2, 1, 0)
You can also use the sym syntax to add components:
sym SymNode root("root") { symadd reps3D(#medium); symadd SymTransform((1, 0, 0)); symadd SymTransform((1, 1, 0)); symadd SymGMaterial(coloredGM(255, 0, 125)); };
The component-based design sym uses do not allow multiple components of the same type per node. This decision was made to simplify and make the implementation more efficient but it can make it more inconvenient to use. To get around the limitation some components, like SymText2D or SymLines2D, support multiple geometry instances. If it's still insufficient you need to create more nodes.
Transforms
SymTransform is used to position, scale and rotate nodes. Transforms of parent nodes affect their children, for example:
SymNode root("root"); root << SymTransform((1, 0, 0)); SymNode a("a"); a << SymTransform((1, 0, 0), (90deg, 0deg, 0deg)); a << SymBox(); root << a;
or with syntax:
sym SymNode root("root") { symadd SymTransform((1, 0, 0)); symadd SymNode("a") { symadd SymTransform((1, 0, 0), (90 deg, 0 deg, 0 deg)); symadd SymBox(); }; };
The box in "a" would be placed on position (2, 0, 0) and with yaw 90°.
Coordinate Systems
If you want the absolute transform of your geometry it's easiest to work with handles:
SymHandle a = root.handle("a"); pln(a.transformAC); // AC is short for "Anchor Coordinates"
Transform(pos=(2, 0, 0), rot=(yaw=90°, pitch=0°, roll=0°), scale=1)
Here anchor coordinates applies all transform from the handle anchor to the node the handle points to (sometimes referred to as root coordinates if the anchor starts from root, which is the common case). Keep in mind that calculating these coordinates may be expensive, because we need to traverse the handle and append the transform of all nodes.
When working with transforms it's important to remember that the transforms are applied from the bottom up, not top-down. This is relevant when calculating pivot points during rotation and scaling. See this example:
SymNode root("root"); root << SymTransform((0, 0, 2)); SymNode a("a"); a << SymTransform((0deg, 45deg, 0deg)); SymNode b("b"); b << SymTransform((1, 0, 0)); b << SymBox(); a << b; root << a;
or with syntax:
sym SymNode root("root") { symadd SymTransform((0, 0, 2)); symadd SymNode("a") { symadd SymTransform((0 deg, 45 deg, 0 deg)); symadd SymNode("b") { symadd SymTransform((1, 0, 0)); symadd SymBox(); }; }; };
The box will first move 1 x-wise, then rotated with 45 degree pitch and then finally moved 2 z-wise. During the rotation, the box will rotate around (1, 0, 0), instead of (0, 0, 2) which would be the case if the transforms were applied top-down.
Local coordinates is the coordinate system of a node before the transform has been applied. This is used when transforming between the coordinate systems of two nodes (used if you want to position geometry relative to another node):
sym SymNode root("root") { symadd SymNode("a", (1, 0, 0)); symadd SymNode("b", (0, 2, 0)); }; SymHandle a = root.handle("a"); SymHandle b = root.handle("b"); // LC is short for "Local Coordinates" pln("LCToLC ", a.transformLCToLC(b).pos);
LCToLC (1, -2, 0)
The local coordinates for the "a" node are without any transform, so the position starts at (0, 0, 0). Then we apply the "a" transform (1, 0, 0) and move upwards towards "root". Then we go down towards "b", applying the inverse of (0, 2, 0) which is (0, -2, 0) and we end up with the resulting transform (1, -2, 0).
Pivots
An important difference between SymTransform and Transform is that you can also modify the pivot of the transform. For example rotation without a pivot:
SymTransform x(90 deg); pln((1, 0, 0).transformed(x.transform));
(~0, 1, 0)
Rotation with a pivot:
SymTransform x(90 deg, pivot=(1, 0, 0)); pln((1, 0, 0).transformed(x.transform));
(1, 0, 0)
During rotation, the pivot defines which point the rotation should rotate around. And during scaling, it defines the point that should remain at the same position.
Note that changing the pivot will change the position of the transform, but not the resulting transformation. So for example if you've positioned everything where you want it and realize that you want to rotate or scale around a different pivot, you don't have to reposition all graphics, just set the pivot. For example:
SymTransform x(90 deg); x.setPivot((1, 0, 0)); pln((1, 0, 0).transformed(x.transform));
(~0, 1, 0)
Note how it's not the same to initialize the pivot in the constructor compared to changing it after.
Bounds
By default, sym calculates and caches all bounds on graphics. It's still recommended to override localBound() on Snapper to prevent the initial calculation cost. It's also possible to manually override the bounds on a SymNode:
public void setLocalBound(SymNode this, box bound) { public void setBound(SymNode this, box bound) {
Or if using a parametric sym you can create a program to update localBound on the root:
sym.addProg(symprog(double width, double depth, double height) { setLocalBound(width, depth, height); });
If you don't want a node to contribute to bounds calculation you can use a gfx setting to turn it off:
node.putGfx(symIgnoreBound, true);
Geometry
There are different types of geometry components. They can be rendered but it's not required. See Rendering and Appearance and Visibility for how to customize them.
Meshes
ATriMeshF defines the meshes in CmSym. ATriMeshF is a size optimized float-based triangle mesh. They are in generally used in 3D but they can be shown in 2D as well.
The SymMesh component holds a raw ATriMeshF:
ATriMeshF mesh(); mesh << boxMesh(box((0, 0, 0), (1, 1, 1))); mesh << boxMesh(box((1, 0, 0), (1.5, 0.2, 0.2))); SymNode node(); node << SymMesh(mesh); pln(node.mesh);
ATriMeshF(triangles=24, vertices=48, normals)
Other components may generate a mesh parametrically:
SymNode node(); node << SymCylinder(1, 0.5); pln(node.mesh);
ATriMeshF(triangles=92, vertices=96, normals, verticalTextureFlip=true)
Shapes
AShape2D defines the shapes in CmSym. It's a general description of shapes. They are generally used in 2D but they can be shown in 3D as well.
The SymShape components hold a raw AShape2D:
APolyline2D shape([point2D: (0, 0), (3, 0), (3, 3), (0, 3)], close=true); SymNode node(); node << SymShape(shape); pln(node.shape);
APolyline2D((0, 0), (3, 0), (3, 3), (0, 3), (0, 0), closed)
Other components may generate a shape parametrically:
SymNode node(); node << SymRect(3); pln(node.shape);
APolyline2D((0, 0), (3, 0), (3, 3), (0, 3), (0, 0), closed)
Points
Points are stored in the SymPoints component, even if you only want to store a single point.
Planes
SymPlane defines a plane in CmSym. It is however never rendered.
Lines
Lines are stored pre transformed in a compact format with vertice and index sequences:
public class SymLines2D extends SymPrimitive2D { /** * Vertices. */ private point2D[] vertices : public readable; /** * Lines (as integer/index pairs) * A single line is defined by two integers/indices that are referring * two vertices from the 'vertices' sequence. */ private int[] lines : public readable;
public class SymLines3D extends SymPrimitive3D { /** * Vertices. */ private point[] vertices : public readable; /** * Lines (as integer/index pairs) * A single line is defined by two integers/indices that are referring * two vertices from the 'vertices' sequence. */ private int[] lines : public readable;
This is similar to how ATriMeshF stores its data. This is a very efficient way to store a large number of lines. For performance reasons, it should be preferred to let SymLines2D and SymLines3D hold as much data as possible, in contrast to using SymShape or creating more nodes.
Text
Text can either be meshed (traces the outline of the text and extrudes to a mesh) or a textured shape.
For example, this is the meshed text:
SymNode node(); node << reps3D(#medium); node << SymText3D("Text", d=0.1, h=0.4); node << SymGMaterial(plainColorGMaterial3D(240, 0, 120));
And this is text with a textured shape (displayed in 3D):
SymNode node(); node << reps3D(#medium); node << SymText2D("Text");
Note that SymText2D is displayed lying down by default as it's most commonly used in 2D.
Primitives
The primitives define common parametric geometry. They will either generate a mesh or a shape.
3D primitives (generates meshes):
- SymBox
A box defined by width (x-dir), depth (y-dir) and height (z-dir). The box has its lower-left corner positioned in origo and the upper right corner is positioned in (width, depth, height).
- SymCylinder
A cylinder defined by radius (x- and y-dir), length (z-dir) (may be closed or opened ended). The cylinder is standing up with the center of one end in origo and the center of the other end at (0, 0, length).
- SymCone
A cone defined by two radiuses (x- and y-dir) end height (z-dir) (may be closed or opened-ended). The cone is standing up with center of r1 end in origo and the center of r2 end at (0, 0, length).
- SymSphere
A sphere defined by a radius. The sphere has its center in position in origo.
2D primitives (generates shapes):
- SymRect
A rectangle defined by width (x-dir) and depth (y-dir). The rectangle has its lower-left corner in origo and its upper right corner in (w, d, 0).
- SymCircle
A circle defined by a radius (x- and y-dir). Defined with the center of sphere placed at origo.
- SymEllipse
An ellipse defined by two radiuses (x- and y-dir). Defined with the center of ellipse placed at origo.
The flat primitives should be preferred for 2D but they work in 3D as well. You may control double sidedness by setting the "symRenderMeshAsDoubleSided" property on SymGfx.
The meshes for curved primitives are generated with different refinement depending on the LOD for speed reasons. For example:
SymNode node(); node << reps3D(#medium, #high); node << SymSphere(1); pln(node.mesh(rep3D(#medium))); pln(node.mesh(rep3D(#high)));
ATriMeshF(triangles=676, vertices=340, normals, uv=340) ATriMeshF(triangles=1560, vertices=782, normals, uv=782)
Notice how the medium mesh contains fewer triangles than the high mesh. The generated meshes are cached on the sym for speed reasons.
Rendering and Appearance
These components control how the sym is rendered. The properties are moved out from all individual components and placed on specialized components on the node.
Material
SymGMaterial holds a GMaterial3D which will affect all nodes below unless overridden. The material will be applied to all meshes. Unless specified via a gfx property, the average color will be used as the shape fill color.
For example to set a nice yellow material on a node:
node << SymGMaterial(plainColorGMaterial3D(255, 255, 0));
If you want to control material per visibility you can create separate nodes with different visibility, but you can also use the SymGMaterialSelector class (similar to MaterialSelector3D). For example:
node << SymGMaterialSelector([symOptionGM(color(0, 170, 100), layer(#architectural)), symOptionGM(color(255, 255, 0))]);
Would provide a green material in the architectural view and a yellow material otherwise. (Make sure the snapper contains the #architectural category for this to be visible.)
UV Mapping
SymUVMapper defines the texture mapping technique intended for the meshes on the node. This is not propagated and only affects the node of the component. If not set the different components to decide which technique to use as default.
Gfx Properties
By setting properties on SymGfx it's possible to control the appearance of the sym. Properties will be propagated down and affect everything below unless overridden. For example:
SymNode node(); node.putGfx(symShapeFillColor, color(255, 123, 123));
The complete list is documented above the definition of the SymGfx class. Default values can be found in the function "defaultSymGfx" in the same file.
You can also add properties inside the sym syntax:
sym SymNode node() { symadd gfx(symShapeFillColor, color(255, 123, 123)); };
To dump all properties you can use:
node.dump(":props");
SymNode(0364edbe-78a0-4c5b-88b1-11d1b339069e) symGfx 1 props shapeFillColor color(255, 123, 123)
Picking
When selecting objects in the 2D/3D-view the mouse position is translated to a ray that we follow to find the intersection between the ray and objects in the scene. This is referred to as picking. By default all geometry (SymShape/SymShape etc) found under a SymNode supports picking, but sometimes it may be useful to disable picking for specific surfaces. For instance, you might want to be able to select the interior of an object by making the exterior transparent.
Here is a simple example:
SymNode node("unpickable"); node << gfx(symEnablePicksurface, false);
Note: As with all other gfx properties this property is inherited but may be overridden by descendants.
If you want information about which nodes you pick you can add the SymPickInfo component:
SymNode node(); node << SymPickInfo("button");
Which you can access for example in the clickAnimation method:
public Animation clickAnimation(StartClickAnimationEnv env) { if (SymPickInfo info = env.pickSymInfo()) { pln(info.key); } }
SymPickInfo will be valid both for the node the component lives on and for nodes below. If you want to pick against the actual node you can instead use the SymPickSurface class in clickAnimation:
public Animation clickAnimation(StartClickAnimationEnv env) { if (SymPickSurface pick = env.pickSym()) { pln(pick.node); } }
If you only want the actual node you pick on you don't need to add a SymPickInfo, but in general, it's good practice to use SymPickInfo for your pick implementation.
Visibility
Each node has visibility controls specifying when they're visible. There are different kinds of controls: 2D and 3D visibility, LOD visibility, category visibility, and the render can be disabled completely.
Reps and LODs
SymReps (reps means representation) controls 2D and 3D visibility as well as LOD visibility.
For example, a node that is only visible in 3D during render (the super LOD):
SymNode node(); node << SymReps(symGfxMode.x3D, detailLevel.super);
Because it's quite verbose to use symGfxMode and detailLevel directly there are shorthand forms for creating SymReps. For example:
reps3D(#super); reps2D(#super); reps3D(#high, #medium, #low);
Several different LODs can exist on one node, and the node can be visible in 2D and 3D at the same time:
SymReps(reps3D(#super), reps2D(#super)); reps(#super); // Same as above // Different LODs for 3D and 2D SymReps(reps3D(#super, #high), reps2D(#super));
CmSym supports five LOD-levels for both 2D and 3D (the other ones found in the detailLevel enum should not be used):
- low
- medium
- high
- super
- base
The base LOD-level is generally not used outside of Model Lab and it refers to the original, non-reduced, model.
Note that while CmSym supports different LOD-levels for 2D, CET always uses the super 2D-LOD. If a model doesn't contain a specific LOD then the closest matching one will be chosen. Therefore it's highly recommended to only specify the LODs you need. If you only need a single LOD-level for the whole sym, consider using medium for 3D and super for 2D.
SymReps only affects the node the component lives on. For example:
sym SymNode root() { symadd SymNode("mid", reps3D(#low)) { symadd SymNode("leaf", reps3D(#medium)); }; };
While it's a strange way to model, the low LOD will only contain geometry from "mid" and the medium LOD will only contain geometry from "leaf".
Look at Model Lab for more information about creating LODs from existing models.
Category Visibility
SymVisibility controls the visibility of the node. By setting a LayerExpr on a node it's possible to control when a part of the sym is visible. For example, two nodes should be visible only in #normal or #architectural:
sym SymNode root() { symadd SymNode("normal") { symadd SymVisibility(layer(#normal)); }; symadd SymNode("architectural") { symadd SymVisibility(layer(#architectural)); }; }
By default all nodes inherit the visibility of the parents so if a node is invisible so will all the children. To override this set "includeParent" to false:
sym SymNode root() { symadd SymNode("normal") { symadd SymVisibility(layer(#normal)); symadd SymNode("architectural") { symadd SymVisibility(layer(#architectural), includeParent=false); }; }; }
In both examples, the node "normal" will only be visible in the normal view mode and the node "architectural" in the architectural view mode.
If used in a Snapper remember to enable any custom categories as well:
/** * Add categories. */ public void addSystemCategoriesTo(symbol{} categories) { super(..); categories << #architectural; }
Disable Render
SymGfx has a setting that can disable the render of a node, and it's subgraph, completely:
SymNode node(); node.putGfx(symRenderDisabled, true);
It serves as a complement to SymVisibility allowing you to turn on and off a subgraph that contains categories.
Connectors
SymConnector means the node and its descendants represent a connector. Keep in mind that this is not the same as a snapper connector. SymConnector is mainly a data carrier that can be used to produce "real connectors".
Grouping
There are currently two ways to group nodes.
LODGroups
When a node has a SymLODGroup component it means that its descendants represent the same object, for instance, an arm rest of a chair. CmSym does not require LODs representing the same object to be arranged under a SymLODGroup but it is considered to be best practice. It makes it possible to perform changes, such as setting a material in a simple manner without the need for programs/parameters.
For example in this case:
sym SymNode() { symadd SymLODGroup(); symadd SymNode("super") { symadd reps3D(#super); }; symadd SymNode("high2") { symadd reps3D(#high); }; symadd SymNode("high3") { symadd reps3D(#high); }; };
Because the nodes are grouped by a LOD group we know that changing the material of node "super" means we should also change the material for nodes "high2" and "high3". Otherwise, we might not know that the nodes in fact represents the same object.
If the sym is generated by code this problem is easy to avoid, but LOD groups are useful when generic tools should manipulate the sym. They're used extensively in Model Lab for example.
Tags
SymTags provide a way define groups for nodes by using a LayerSet. For example:
SymTags(layerSet(#special, #incredible)); SymTags(layerSet(#a, #b, #c), main=#b);
Props
Props on syms have native support for PropObj with the SymProps component.
You can add prop defs allowing you to control how data is copied and streamed. Remember to not store any snapper instance-specific data on the sym since it can and will be shared across snappers!
root."a" = 2; root.put("b", 7, {#copy_null}); root.put(PropDef("c", {#copy_reference}));
By default, streaming is only supported for some basic types. To add your own to/from conversions register functions with:
registerSymPropToDex(MyClass, function toDex); registerSymPropFromDex("MyClass", function fromDex);
Where "MyClass" is the dex type you're returning. It is recommended to use the class name as the type:
private DexObj toDex(Object v, CmSymExportEnv env) { DexObj dex(p.class.name); ... return dex; }
See the SymProps implementation for more details.
Files and External References
Syms can be loaded from files or from streams, but cannot be streamed as fields.
Loading Files
Use loadSym to load from a .cmsym file or from a stream:
Url url = cmFindUrl("custom/modelLab/models/default.cmsym"); SymNode sym = loadSym(url);
By default the result will be cached and loaded lazy. Lazy means nodes and heavy geometry such as meshes won't be loaded until they're requested. For example:
Url url = cmFindUrl("custom/modelLab/models/default.cmsym"); SymNode sym = loadSym(url); sym.node("1.2"); // Loads the node "1" and it's child "2". sym.dump("+symMesh :lazy"); // Without ":lazy" dump will load everything.
SymNode(0), cached SymNode(1) SymNode(2) symMesh not loaded children 4 children 1
Here we can see that the root node "0" has one loaded child and one which hasn't been loaded yet. The node "2" has a SymMesh, but the actual mesh hasn't been requested yet.
It's possible to change the behavior if needed:
loadSym(url, lazy=false, cache=false)
External References
A sym can contain external references to other sym files which can be automatically loaded on demand. For example:
sym SymNode leg("leg") { symadd SymNXRef(cmFindUrl("custom/developer/examples/cmsym/training/leg.cmsym")); };
Here "leg" will contain a child node taken from the file. It's forbidden for "leg" to contain any other children when using an xref component.
With SymNXRef it's possible to combine multiple sym files to form a single sym graph. It's also possible to replace parts of a sym to other parts, for example exchanging an armrest to another type of armrest. See SymChair in custom.developer.examples.cmsym as an example.
Caching
It's a very good idea to use caching when creating syms. Note that a cache, both static and parametric, must always describe everything within the cache block. Changing a parameter that isn't part of the cache key will not be seen when reusing the cached sym.
Static Caching
Static caching, which is the same as the cache3D syntax. Inside a class you can use:
symStaticCache(cw, cd, "other stuff") { ... } symStaticCache(w=1.0, d=2.0) { ... } symStaticCache(props{w=1.0, d=2.0}) { ... }
Which will namespace the cache within the class. Outside a class you can use:
symNoOwnerCache(cw, cd, "my very unique key") { ... }
But please make sure to namespace your key properly if you use the no owner cache.
Also, a simpler unique cache if the cache should only be accessed at a single location, for example wrapping small graphics generating functions:
symUniqueCache { ... }
Which will be reset during reload.
If you want to edit a statically cached sym you must always break it loose from the cache. This happens automatically during beginEdit or symEdit. There is no automatic way to recache the sym again after editing, but you can do it manually:
node.staticRecache("new static cache key");
A cache might be used like this in a Snapper's buildSym():
public SymNode buildSym() { return symStaticCache("d", 2) { SymNode node(); node << reps3D(#medium); node << SymGMaterial(plainColorGMaterial3D(255, 255, 0)); node << SymBox(2, 2, 2); result node; }; }
Parametric Caching
If you want to edit the sym inside a cache then parameters should be used:
symCache("w", "d", "h") { ... }
Where the values are given by extending:
public str->Object symParams() { ... }
If you want to cache on all parameters you can leave it empty, and it will automatically cache on all parameters:
symCache() { ... }
Here's how to use it in a Snapper:
private class ExSnapper extends Snapper { private double w = 2; private double d = 1.5; private double h = 1; public str->Object symParams() { return props{w=w, d=d, h=h}; } public SymNode buildSym() { return symCache() { // same as symCache("w", "d", "h") SymNode node(); node << reps3D(#medium); node << SymGMaterial(plainColorGMaterial3D(255, 255, 0)); node << SymBox(w, d, h); node << symprog(double w, double d, double h) { set(symBox(), w, d, h); }; result node; }; } }
User Materials
To support user materials you need to explicitly include a user materials key in your static cache routine:
symStaticCache("someKeyHere", symUserMaterialsCacheKey()) { ... }
Parameter caching handles user material implicitly:
symCache() { ... }
And to support user materials you should override:
public bool supportsMaterialChange() { return true; }
Updating Graphics
Building and Editing a Sym
The sym system is built around the idea of creating the sym once and then editing the parts you want, instead of throwing away all the graphics just because something small has changed. Therefore the sym API seems more complex, which it is, but it's to enable this approach where you tell the system to only update the parts that changed.
There are of course trade-offs to this approach. If you want to replace the whole graphics with something that already exists somewhere else, the "invalidate-rebuild" approach might fit you better. Therefore you can circumvent this via "forceSymRebuild". Please note that it's generally not a good idea, although it might be for some use-cases.
Changing Parameters
If all you want to do is update parameters of a sym, it's quite simple:
sym.editParams("w", 2, "d", 3);
And it will take the sym prepared for edit (or replacing the sym with a cached result if possible) and updating the graphics. If you don't want to cache the node (for example during stretch) you can use:
sym.editParamsNoCache("w", 2, "d", 3);
But it's also possible to change parameters explicitly:
sym.beginEdit(); sym.setParam("w", 2); sym.setParam("d", 3); sym.endEdit();
But please note that you'll have to be careful to break loose cached child nodes if the programs will change them.
Preparing a Sym for Edit
If you're using a static cache and you want to manually edit your sym, you need to break it loose from the cache. You also need to notify the backend so it can reflect graphics edits, but more on that later.
All edits should be wrapped inside beginEdit/endEdit like so:
sym.beginEdit(); ... sym.endEdit();
Or in symEdit/symUndoableEdit:
symEdit(sym) {
...
}
But note that we only break loose the root node here. If you only use caching on the root, then the above is fine.
But if you want to edit a child node that is cached then you must explicitly target that node to be broken loose:
sym.beginEdit("a"); sym.child("a").xsetMaterial(..); sym.endEdit();
or
SymNode a = sym.beginEdit("a"); a.xsetMaterial(..); sym.endEdit();
or
SymHandle a = sym.beginEditHandle("a"); a.xsetMaterial(..); a.endEdit(); // or sym.endEdit()
Note that if you do beginEdit on node it will make sure all nodes on the path will also be broken loose. Furthermore a non-cached child will also always be copied if a parent is cached.
If you want you can specify several paths or force everything to be broken loose:
sym.beginEdit("a", "b.c.d"); sym.beginEdit(editBelow=true); // Breaks loose everything from the cache
The symEdit syntax behaves the same:
symEdit(sym, "a", "b.c.d") { ... } symEdit(sym, editBelow=true) { ... }
symEdit on a handle only prepares that node for edit:
SymHandle h = sym.handle("a"); symEdit(h) { h.xsetMaterial(..); }
For more examples see SymCachedEditSnapper.cm in cm.core.cmsym.test.
Reflecting Graphics Changes
When manually editing a sym there are two separate systems to be aware of:
- The sym structure itself (nodes and components)
- The backend graphics (compromised of REDShapes)
While you're usually not interacting with REDShapes directly you do need to notify the backend so it knows what to update. Functions that do update the graphics should be prefixed with an 'x' and be wrapped in beginEdit()/endEdit() or the symEdit syntax (which simply wraps a block with beginEdit/endEdit).
For example, if you want to change a mesh via a handle you can do:
symEdit(myHandle) { myHandle.xsetMesh(newMesh); }
or
SymHandle h = sym.beginEditHandle("path.to.node"); h.xsetMaterial(greenGM); h.endEdit();
or
SymGraph sym = sym(); sym.beginEdit("path.to.node"); sym.handle("path.to.node").xsetMesh(newMesh); sym.endEdit();
Please be aware that any handles created before beginEdit may become invalid if the sym is shared between snappers (which it may become implicitly after copy for example). A possible workaround is to call invalidate() on the handle before use, but please note that it might be slow.
You can also make changes to nodes directly:
SymNode node = sym.beginEdit("path.to.node"); b.xsetMesh(newMesh); sym.endEdit();
If you're missing an "x" function to do what you want, you can still explicitly request graphics updates via symRt. For example:
sym.beginEdit(); // Initiates symRt GMaterial3D gm = sym.symGMaterial.material; // ... Change GMaterial3D in some way // Explicitly update the graphics after material has changed. symRt.updateMaterial(sym); sym.endEdit(); // Flushes symRt
For more info see cm/format/cmsym/edit.cm and refer to the implementation of mentioned functions.
Parameters and Programs
Sym allows you to define parameters and have programs reacting to changes of those parameters. They are stored on nodes and will be included in the files you save, making the files contain parametric graphics.
For example:
sym SymNode root("root") { symadd SymGMaterial(plainColorGMaterial3D(255, 255, 0)); symadd param("w", 2); symadd SymNode("x", reps3D(#medium)) { symadd SymBox(); symadd symprog(double w) { set(symBox(), w, w, w); }; }; }; root.dump(":code");
SymNode(root) w = 2 SymNode(x) RProg(RSymNode this, rdouble w) { insns=14})
Where changes to the parameter "w" will run the program on the child node, changing the size of the box. Note that the symProg parameter name needs to be the same as the sym parameter name in order for the symProg to understand it is supposed to react when the sym parameter changing.
The "symprog" directive generates a Z-script program that will check for missing functions and types during compile time. Available functions and types can be found in SymRt.cm in cm.format.cmzym.z.
Here's a full example of a Snapper using parameters and programs:
private class ExSnapper extends Snapper { private double w = 2; public str->Object symParams() { return props{w=w}; } public SymNode buildSym() { return symCache() { result sym SymNode("root") { symadd SymGMaterial(plainColorGMaterial3D(255, 255, 0)); symadd param("w", 2); symadd SymNode("x", reps3D(#medium)) { symadd SymBox(); symadd symprog(double w) { set(symBox(), w, w, w); }; }; }; }; } final public void change() { if (w == 2) { w = 1; } else { w = 2; } sym.editParam("w", w); } } { ?ExSnapper z = anySnapper(ExSnapper); if (z) { z.change(); } else { placeSnapper(ExSnapper()); } }
Iteration
It's often wanted to iterate over the sym graph and visit all, or some, of the nodes. There are efficient and convenient ways of doing this.
Visit Nodes
If you want to visit all nodes in a sym graph, a simple for loops works like you might expect:
sym SymNode root("root") { symadd SymNode("a") { symadd SymNode("b"); }; symadd SymNode("x") { symadd SymNode("y"); }; } for (x in root) pln(x);
SymNode(root) SymNode(a) SymNode(b) SymNode(x) SymNode(y)
Which visits the nodes in a depth-first manner.
Important to note that continue skips the whole subgraph, not just the node we're visiting. So this might not do what you expect:
for (x in root) { if (x.id == "a") continue; pln(x); }
SymNode(root) SymNode(x) SymNode(y)
Continue skipped both the node "a" and it's child "b".
Iteration accepts filters to control which nodes to visit. For example:
sym SymNode root("root") { symadd SymNode("a") { symadd SymNode("b") { symadd reps3D(#high); }; }; symadd SymNode("x") { symadd SymNode("y") { symadd reps3D(#medium); }; }; } for (x in root, filter=SymNodeRepFilter(rep3D(#medium))) { pln(x); }
SymNode(y)
Because information about reps is propagated upwards, the iteration is efficient and aborts as soon as possible. In this case, when checking "a" it notices there are no medium reps there or below and it can abort.
Iteration exists for handles as well:
for (x in root.iter, filter=SymNodeRepFilter(rep3D(#medium))) { pln(x); }
SymHandle(root.x.y)
But please be aware that they're less efficient than iterating over pure nodes.
Track Data
It's often needed to traverse the whole sym graph and calculate data on the fly. For example, during export, you want to know the combined transform to a node from the root. It's possible to do this using handles but it's very inefficient to recalculate the transform from root to all nodes.
Instead, there's an iteration env that does this for you:
for (nodeEnv in SymNodeItEnv(root)) { ... } for (handleEnv in SymHandleItEnv(root)) { ... }
It combines the transform and gfx props and keeps track of the latest material, visibility, tags, and other data efficiently. The distinction between node and handle iteration exists because handle iteration is slightly slower. If you want to combine and store other information simply subclass the env you want.
With the iteration env it can for example be straightforward to implement conversions from sym to another data structure using this skeleton:
sym SymNode root("root") { symadd SymGMaterial(plainColorGMaterial3D(255, 120, 0)); symadd SymNode("a") { symadd SymBox(); }; }; for (env in SymNodeItEnv(root)) { pln(env.node); if (ATriMeshF mesh = env.mesh) { pln(" ", env.material); } }
SymNode(root) SymNode(a) GMaterial3D(106, Diffuse3D, Ambient3D)
Instead of having to search upwards for the material when from node "a" the env keeps track of it during the iteration.
See the CmSym to Graph generation in cm.core.cmsym symToGraph.cm for a real-life example.
Filtering
If you want to collect nodes from a sym graph you can use a filter:
sym SymNode root("root") { symadd SymNode("a", reps3D(#high)); symadd SymNode("b", reps3D(#high)); symadd SymNode("c", reps3D(#medium)); } SymNode[] nodes = root.filter(SymNodeRepFilter(rep3D(#high))); pln(nodes);
[SymNode, count=2: SymNode(b), SymNode(a)]
Or if you want to collect handles:
SymHandle[] handles = root.filterHandles(SymNodeRepFilter(rep3D(#high))); pln(handles);
[SymHandle, count=2: SymHandle(root.b), SymHandle(root.a)]
Keep in mind that this will create a collection and if you only want to visit the nodes it's more efficient to iterate over them directly.
There different kinds of filters you can use, refer to the base class SymNodeFilter in cm.format.cmsym.filter.
Dump
It can be useful to look at how the sym graph is structure. You can do this with the dump() method:
sym SymNode root("root") { symadd prop("customProp", 2); symadd SymTags({#a, #b, #c}, main=#a); symadd SymNode("3D") { symadd reps3D(#medium); symadd SymGMaterial(coloredGM(255, 0, 0)); symadd SymBox(); symadd gfx(symRenderMeshAsWireframe, true); }; symadd SymNode("2D") { symadd reps2D(#super); symadd SymRect(); symadd gfx(symShapeFillColor, color(255, 0, 0)); }; }; root.dump();
SymNode(root) SymNode(2D) SymNode(3D)
The method accepts an optional string to customize its output. A "+" prefix specifies which components to dump, a "-" prefix which components to ignore, and ":" specifies general settings. These settings can be combined as you please. You can get a help text for the options using:
dumpSymDumpOptions();
For example, if you only want to dump the nodes to reach 3D:
root.dump("3D");
SymNode(root) SymNode(3D)
To dump all components:
root.dump(":c");
SymNode(root) symReps 3D(medium) 2D(super) symTags #a, #b, #c symProps 1 props SymNode(2D) symReps 2D(super) symProps 1 props symGfx 1 props symRect SymRect(rect(p0=(0, 0), p1=(1, 1))) SymNode(3D) symReps 3D(medium) symProps 1 props symGfx 1 props symGMaterial colorF(255, 0, 0) symBox SymBox(box(p0=(0, 0, 0), w=1, d=1, h=1))
To ignore some components:
root.dump(":c -symProps -symReps");
SymNode(root) symTags #a, #b, #c SymNode(2D) symGfx 1 props symRect SymRect(rect(p0=(0, 0), p1=(1, 1))) SymNode(3D) symGfx 1 props symGMaterial colorF(255, 0, 0) symBox SymBox(box(p0=(0, 0, 0), w=1, d=1, h=1))
To ignore children:
root.dump(":c -children");
SymNode(root) symReps 3D(medium) 2D(super) symTags #a, #b, #c symProps 1 props
The props (on SymGfx and SymProps) can be dumped, note that there might be some props used internally as well:
root.dump(":props");
SymNode(root) symProps 1 props customProp 2 SymNode(2D) symProps 1 props _features {"shape"->"symRect"} Object {#stream_null} symGfx 1 props shapeFillColor color(255, 0, 0) SymNode(3D) symProps 1 props _features {"solid"->"symBox", "mesh"->"symBox"} Object {#stream_null} symGfx 1 props renderMeshAsWireFrame true
To dump only specific properties you're interested in:
root.dump(":props(customProp shapeFillColor)");
SymNode(root) symProps 1 props customProp 2 SymNode(2D) symGfx 1 props shapeFillColor color(255, 0, 0) SymNode(3D)
There is a couple of other special commands dump can take:
root.dump(":bound"); root.dump(":shape"); root.dump(":mesh");
SymNode(root) bound box(p0=(0, 0, 0), w=1, d=1, h=1) SymNode(2D) bound box(p0=(0, 0, 0), w=1, d=1, h=0) SymNode(3D) bound box(p0=(0, 0, 0), w=1, d=1, h=1) SymNode(root) SymNode(2D) symRect SymRect(rect(p0=(0, 0), p1=(1, 1))) SymNode(3D) SymNode(root) SymNode(2D) SymNode(3D) symBox SymBox(box(p0=(0, 0, 0), w=1, d=1, h=1))
If the output is too verbose you can specify only the components you're interested in:
root.dump("+symTags");
SymNode(root) symTags #a, #b, #c SymNode(2D) SymNode(3D)
Some components may include color-coded information. For example, SymReps codes propagated reps (from the children) as gray while the reps on the node are colored black:
root.dump("+symReps");
SymNode(root) symReps 3D(medium) 2D(super) SymNode(2D) symReps 2D(super) SymNode(3D) symReps 3D(medium)
Sym nodes are loaded on demand. Dump normally force loads everything but you can tell it not to:
SymNode sym = loadSym(cmFindUrl("custom/modelLab/models/default.cmsym")); sym.node("1.2"); // Loads the node "1" and it's child "2". sym.dump("+symMesh :lazy"); // Without ":lazy" dump will load everything.
SymNode(0), cached SymNode(1) SymNode(2) symMesh not loaded children 4 children 1
There are more advanced dumps available for some components. For example parameters and programs:
sym SymNode root("root") { symadd param("w", 2); symadd SymNode("a") { symadd param("d", 3); symadd symprog(double w, double d) { pln(#w; #d); }; }; }; root.dump("+symProgs +symParams");
SymNode(root) symParams (1 param) w 2 SymNode(a) symProgs (1 prog) w, d invalid symParams (1 param) d 3
Their internals can be dumped (only really relevant if you're debugging sym itself):
root.dump("+symProgs(_propagated) +symParams(_propagated internal)");
SymNode(root) symProgs (0 progs) "a" 1 prog symParams (1 param) w 2 deps{"a"} invalidProgs{"a"} "a" {d->3} SymNode(a) symProgs (1 prog) w, d invalid symParams (1 param) d 3 deps{this} invalidProgs{""}
The dump method also accepts a SymNodeFilter for more granular control. See SymDump.cm cm.format.cmsym.debug or the dump methods of individual components for more info.
Debug
You can identify CmSym Snappers via the release debug dialog (Ctrl + Alt + F12).
There is a CmSym debug dialog that can be used to get a deeper understanding of the CmSym usage in a drawing. The tool is started from symDebugDialog.cm found under cm.core.cmsym.debug.
Vocabulary
Here you can find the CmSym vocabulary
Comments
0 comments
Please sign in to leave a comment.