OnShape: A Design Overview

OnShape utilizes a client server split in which the client updates state on the server via commands and the result (geometry) is sent back to the client to be rendered.

Commands

OnShape serializes operations which act on a set of features (Extrude, Sketch, ...) this is known as the operation log (op log). The server then runs the features and produces a NURBS representation of the model. This is then tessellated and the vertices with associated data is sent to the frontend.

sequenceDiagram
	participant Client
	participant Server

	Client->>+Server: Modifiy Extrude 10mm -> 25mm

	Note right of Server: Update operation log
	Note right of Server: Process operation to modify FeatureScript
	Note right of Server: Process FeatureScript to update NURBS representation
	Note right of Server: Tessellate NURBS into triangles

	Server->>-Client: Geometry to render

There are some hidden challenges here

  • The tessellation must be sent over the network and that means that we are limited in the quality.
    • The tradeoff here is that if it were local OnShape would be dependent on the users machine.
    • However by knowing what hardware the compute is running on, OnShape can hyper optimize the codebase around that.
    • I believe the way that OnShape handles this is with a custom client-server protocol over web sockets, that minimally updates the geometry stored on the client.
  • The client will see a delay until the server is done processing
    • This means complex parts can cause slower interactions
  • Centralized "source of truth"

Multi-user Interactions

Using commands has some really interesting properties. If two users are editing the same part, the user interactions don't change. Both users simply write to the operation log and it is processed like normal. The clients are updated at the same time in the same way.

sequenceDiagram
	participant John
	participant Alice
	participant Server

	Alice->>+Server: Modifiy Extrude 10mm -> 25mm

	Note right of Server: Update operation log
	Note right of Server: Process operation to modify FeatureScript
	Note right of Server: Process FeatureScript to update NURBS representation
	Note right of Server: Tessellate NURBS into triangles

	par Send Data back
		Server->>John: Geometry to render
		Server->>Alice: Geometry to render
		deactivate Server
	end

This opens up some really interesting opportunities.

  • Since adding to the op log is "atomic" the FeatureScript update step can process multiple operations at once, reducing the amount of work needed to be done.
  • Conflicts, for example one user removing a sketch, while the other uses that sketch to extrude are race conditions but provide an way to error out.
    • If the extrude happens before the delete, then the extrude breaks
    • If the sketch deletes before the extrude then the extrude fails and the user can be notified

Branches

Another way that OnShape allows multiple users to interact with the same design is by having branches. This is a "fork" in the operation log not a snapshot of the FeatureScript.

gitGraph TB:
commit id: "Create Sketch"
commit id: "Extrude Sketch"

branch "Longer Extrude"
commit id: "Modify Sketch Point 1"
commit id: "Modify Extrude"

checkout main
commit id: "Add Rotation"
merge "Longer Extrude"
commit id: "Modify Apperance"

The fact that its not a snapshot of the FeatureScript means that user intent can be more effectively captured.

Features of branching the op log rather than the FeatureScript

  • History and Intent are preserved.
  • The FeatureScript and 3D Model can be generated from the OP log meaning you can still diff them.
  • Merges are effectively inserting a set of operations into a branch.
    • Merged are only ever "rebased" merges where the set of operations are applied on top of the base branch.

Merge Conflicts

When an OnShape merge occurs the operations are effectively rewritten on top of the base branch. If some of these operations are no longer valid they may be able to be rewritten to become valid or deleted if they no longer make sense. This effectively makes OnShape merges an operation in themselves.

An example of an operation which is no longer valid is a extrude of a base sketch which no longer exists.

gitGraph TB:
	commit id: "Create Document"
	commit id: "Create Sketch"
	branch "Extrude"
	commit id: "Extrude Sketch"

	checkout main
	commit id: "Remove Sketch"
	merge "Extrude"

OnShape will still add the extrude but since the sketch doesn't exist the operation fails.

Here is what it looks like in OnShape before the merge

OnShape before the merge

When we begin the merge OnShape gives us the option to determine how we merge for each part studio.

Choosing how to merge each part studio

And finally the state after the merge. The extrude is broken (see the red on the right side) but still added, its now up to the user to add operations which fix the state.

State after the merge with a broken extrude

OnShape supplements this with a repair feature, which takes the broken state (merge conflict) and allows you to view older versions of the part studio at the same time to provide context when fixing broken links between features.

Direct Conflicts

This rebasing approach applies to direct conflicts as well. If we create a branch and modify the same feature on main and the new branch the same way the merged branches changes will win.

gitGraph LR:
	commit id: "Create Document"
	commit id: "Create Sketch"
	commit id: "Extrude Sketch to 25mm"

	branch "B2"
	commit id: "Modify Extrude Length to 35mm"

	checkout main
	commit id: "Modify Extrude Length to 30mm"

	merge "B2"
	commit id: "Extruded to 35mm (merged operation won)"

OnShape direct merge

This is something that I believe is a weakness of OnShape, the op log gets appended but its not always clear that the merged branch should win. I think an improvement would be to detect when operations conflict (edit the same feature directly) and force the user to handle the change.

However this could cause another issue. If a branch has a bunch of edits

  • modify extrude length 32
  • modify extrude length 31
  • modify extrude length 35

you could get into a situation where conflict resolution happens at each operation. This is similar to rebasing a stack of commits where you hit a conflict on each commit.

To solve for this you could rewrite the set of operations into equivalent ones before the merge. The operations above would just become

  • modify extrude length 35

This would be like rewriting your stack of commits to remove all the "temp" commits (I see you).

The implementation of this may be more difficult than I am thinking as you would have to define all the dependencies for a set of operations and ensure that you don't remove any operations that are required for other operations.

What about merging the FeatureScript?

Features are the representation generated from the operations and used to generate the NURBS model.

Common features are

  • Sketch
  • Extrude
  • FIlet
  • Chamfer

We can think of the features as a snapshot of the generated state of operations. In this sense we could compare it to a commit in git (a snapshot of state).

A simplified model of an Extrude feature might look like this

struct Extrude {
	id: ExtrudeId, // this is probably a UUID
	face: FaceId,
	length: Length,
}

If we try to merge the example from Direct Conflicts using features rather than the op log we can see a difference in the resolution. Here is what the representations of the (simplified) extrudes look like on both branches.

// On Main branch
Extrude {
	id: 1,
	face: FaceId(Sketch1.FaceId1),
	length: 35mm,
}

// On B2 branch
Extrude {
	id: 1, // note the id is the same
	face: FaceId(Sketch1.FaceId1),
	length: 30mm,
}

The process for determining conflicts looks similar to a 3 way merge in git.

  1. Find the common ancestor
  2. Find list of things that changed on Main
  3. Find list of things that changed on B2
  4. Create new commit with changes that don't conflict and force user to address conflicts
gitGraph LR:
	commit id: "Create Document"
	commit id: "Create Sketch"
	commit id: "Extrude Sketch to 25mm"

	branch "B2"
	commit id: "Modify Extrude Length to 35mm"

	checkout main
	commit id: "Modify Extrude Length to 30mm"

	merge "B2"
	commit id: "Extruded to 35mm (merged operation won)"

In our case the ancestor would be the state of the FeatureScript at Extrude Sketch to 25mm. The changes on main would be Extrude with Id 1 and the changes on B2 would be Extrude with Id 1. We would notice a conflict and force the user to update the feature with the length they want.

Why does OnShape merge operations rather than features?

These are mostly my guesses.

Since OnShape is "always on" is means that the documents are constantly changing (accepting new operations). Putting the document into a special "merging" or locked state would prevent further work from happening and complicate the implementation.