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

When we begin the merge OnShape gives us the option to determine how we merge for 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.

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

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 32modify extrude length 31modify 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.
- Find the common ancestor
- Find list of things that changed on
Main - Find list of things that changed on
B2 - 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.