在 FeatureScript 簡介的影片中, 可以看到 Onshape 打造了一個客製化特徵功能的程式語言與社群分享機制, 任何人利用 FeatureScript 建立的延伸功能, 都能夠透過 Add custom features 指令與其他使用者分享.
FeatureScript 是 Onshape 發明, 一種可用來定義客製化參數特徵用的程式語言, 具有下列特點:
- 各 Document 中用戶所建立的 FeatureScript 程式碼, 位於 Feature Studio (特徵工房) 分頁中.
- FeatureScript 的整合開發環境 (Integrated Development Environment) 內建於 Onshape 中.
- Onshape 原本系統中使用的特徵功能, 其對應的 FeatureScript 程式碼, 已經採開放源 (Open Source) 模式釋出.
- Onshape 同時提供 FeatureScript 參考手冊 與 FeatureScript 論壇
登入 Onshape, 建立一個 Document 之後, 就可以透過左下角的 + 號中的 Create Feature Studio, 進入 FeatureScript 的整合開發環境, 開始編寫客製化的特徵程式碼, IDE 則提供下列相關功能:
Parameter (參數)
Length, Angle, Count, Query, Enum, Boolean and String
Query (查詢)
Everything, Nth element, Entity filter, Created by, Intersection, Subtraction, Symetric difference, Own by body, Own by body filter, Owner body, Entities adjacent to edge, Geometry type filter, Contains point, Intersects plane and Query evaluation
Evaluation (評量)
Tangent plane, Tangent line, Vertex point, Length measurement, Area measurement, Volume measurement, Query evaluation
Sketch, Line segment, Circle, Arc, Ellipse, Rectangle, Line segment chain
Cuboid, Cylinder, Extrude, Revolute, Fillet, Boolean, Transform, Import
Import (導入)
Format feature studio (整理特徵工房編輯格式)
Commit (提交)
FeatureScript 程式基本架構如下:
FeatureScript 355; import(path : "onshape/std/geometry.fs", version : "355.0"); annotation { "Feature Type Name" : "My Feature" } export const myFeature = defineFeature(function(context is Context, id is Id, definition is map) precondition { // Define the parameters of the feature type } { // Define the function's action });
上層宣告, 列舉值與指令敘述都可以加上 annotations (註解). FeatureScript 的註解使用格式類似 Python 的 Dictionary, 但是 annotation 的索引值型別必須為字串, 而且索引值為 "Feature Type Name" 的 annotation 為每一個特徵指令的必要註解.
export 則可視為上述的程式碼中, 使用者所宣告的 myFeature 對應特徵物件會被優先置入 FeatureScript Template 中.
此外 Onshape 任一頁面送出時, 都帶有以下標頭設置, 除了利用 X-Frame-Options:SAMEORIGIN, 不允許使用者將頁面納入 iframe 或 object 標註中外, 也避免遭受可能的網路攻擊:
Cache-Control:must-revalidate,no-cache,no-store Strict-Transport-Security:max-age=31536000; includeSubDomains X-Content-Type-Options:nosniff X-Frame-Options:SAMEORIGIN X-XSS-Protection:1; mode=block
以下則為目前 Onshape 官方釋出的參考應用範例:
Point Pattern FeatureScript 程式 Document
Spur Gear FeatureScript 程式 Document
Wave Spring FeatureScript 程式 Document
Port feature 程式碼 (建立 SAE J1926 螺孔):
FeatureScript 336; import(path : "onshape/std/geometry.fs", version : "336.0"); export enum PortSize { annotation { "Name" : "5/16-24" } fiveSixteen, annotation { "Name" : "3/8-24" } threeEights, annotation { "Name" : "7/16-20" } sevenSixteen, annotation { "Name" : "1/2-20" } half, annotation { "Name" : "9/16-18" } nineSixteen, annotation { "Name" : "3/4-16" } threeFour, annotation { "Name" : "7/8-14" } sevenEights, annotation { "Name" : "1 1/16-12" } oneSixteen } annotation { "Feature Type Name" : "Port Feature" } export const portFeature = defineFeature(function(context is Context, id is Id, definition is map) precondition { annotation { "Name" : "Port Size" } definition.portSize is PortSize; annotation { "Name" : "Points", "Filter" : EntityType.VERTEX } definition.points is Query; annotation { "Name" : "Depth" } isLength(definition.depth, DEPTH_BOUNDS); } { var ports; var depth = definition.depth; //here the vector points for each port are added to an array depending on size chosen if (definition.portSize == PortSize.fiveSixteen) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .019 * inch, -.031 * inch), vector(-.631, -.031) * inch, vector(-.53, -.132) * inch, vector(-.167, -.132) * inch, vector(-.136, -.163) * inch, vector(-.062, -.179) * inch, vector(-.062, -.336) * inch, vector(0, -.336) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.threeEights) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .038 * inch, -.063 * inch), vector(-.629, -.063) * inch, vector(-.53, -.162) * inch, vector(-.169, -.162) * inch, vector(-.136, -.195) * inch, vector(-.062, -.211) * inch, vector(-.062, -.375) * inch, vector(0, -.375) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.sevenSixteen) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .052 * inch, -.086 * inch), vector(-.663, -.086) * inch, vector(-.547, -.202) * inch, vector(-.176, -.202) * inch, vector(-.155, -.224) * inch, vector(-.062, -.243) * inch, vector(-.062, -.414) * inch, vector(0, -.414) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.half) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .07 * inch, -.117 * inch), vector(-.71, -.117) * inch, vector(-.607, -.22) * inch, vector(-.188, -.22) * inch, vector(-.153, -.255) * inch, vector(-.062, -.275) * inch, vector(-.062, -.453) * inch, vector(0, -.453) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.nineSixteen) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .089 * inch, -.149 * inch), vector(-.773, -.149) * inch, vector(-.671, -.251) * inch, vector(-.195, -.251) * inch, vector(-.159, -.287) * inch, vector(-.062, -.308) * inch, vector(-.062, -.485) * inch, vector(0, -.485) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.threeFour) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .117 * inch, -.196 * inch), vector(-.928, -.196) * inch, vector(-.782, -.341) * inch, vector(-.237, -.341) * inch, vector(-.194, -.384) * inch, vector(-.094, -.406) * inch, vector(-.094, -.594) * inch, vector(0, -.594) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.sevenEights) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .145 * inch, -.242 * inch), vector(-1.032, -.242) * inch, vector(-.875, -.399) * inch, vector(-.245, -.399) * inch, vector(-.194, -.45) * inch, vector(-.094, -.471) * inch, vector(-.094, -.672) * inch, vector(0, -.672) * inch, vector(0, 0) * inch]; } if (definition.portSize == PortSize.oneSixteen) { ports = [vector(0, 0) * inch, vector(-depth, 0 * inch), vector(-depth + .183 * inch, -.304 * inch), vector(-1.22, -.304) * inch, vector(-1, -.525) * inch, vector(-.246, -.525) * inch, vector(-.224, -.546) * inch, vector(-.094, -.574) * inch, vector(-.094, -.813) * inch, vector(0, -.813) * inch, vector(0, 0) * inch]; } const checkLength = (ports[3] - ports[2]); if (checkLength[0] < 0) // check the depth entered and throw error if depth causes feature to invert { throw regenError(ErrorStringEnum.SKETCH_DIMENSION_FAILED, ["depth"]); } var points = evaluateQuery(context, definition.points); var numberOfPoints = size(points); var sketchId = id + "sketch"; var portId = "port1"; for (var i = 0; i < numberOfPoints; i += 1) //for each point selected create a sketch using port vertices and revolve cut { sketchId = sketchId + i; var point = points[i]; var sketchPlane = evOwnerSketchPlane(context, { "entity" : point }); var cSys = planeToCSys(sketchPlane); var pointVertex = evVertexPoint(context, { "vertex" : point }); var sketchPlane2 = plane(pointVertex, cSys.xAxis, cSys.zAxis); var sketch = newSketchOnPlane(context, sketchId, { "sketchPlane" : sketchPlane2 }); skPolyline(sketch, portId, { "points" : ports, "constrained" : false }); skSolve(sketch); var axisQuery = sketchEntityQuery(sketchId, EntityType.EDGE, portId ~ ".line0"); revolveCut(context, id, sketch, sketchId, axisQuery); } opDeleteBodies(context, id + "delete_sketch", { "entities" : qCreatedBy(id + "sketch", EntityType.BODY) }); // delete the sketch }, { /* default parameters */ }); function revolveCut(context is Context, id is Id, sketch is Sketch, sketchId is Id, axisQuery is Query) { var sketchQuery = qSketchRegion(sketchId, false); //revolve cut revolve(context, sketchId + "revolve", { "operationType" : NewBodyOperationType.REMOVE, "entities" : qUnion([sketchQuery]), "axis" : qUnion([axisQuery]), "revolveType" : RevolveType.FULL, "defaultScope" : true }); } const DEPTH_BOUNDS = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.0381, 500], (centimeter) : 3.81, (millimeter) : 38.1, (inch) : 1.5 // set the default depth to 1.5 inch } as LengthBoundSpec;
Screw Boss FeatureScript 原始碼:
/* Screw Boss This custom feature creates a common fastening feature in plastic part design. The Screw Boss is just one version of this type of fastening feature and could be easily extended to include many other types. This was built to show that you can create complex, compound features easily. Version 1 - April 26, 2016 - Neil Cooke, Onshape Inc. */ FeatureScript 336; import(path : "onshape/std/geometry.fs", version : "336.0"); annotation { "Feature Type Name" : "Screw Boss" } export const ScrewBoss = defineFeature(function(context is Context, id is Id, definition is map) precondition { annotation { "Name" : "Sketch points to place bosses", "Filter" : EntityType.VERTEX && SketchObject.YES && ConstructionObject.NO } definition.locations is Query; annotation { "Name" : "Boss style" } definition.style is BossStyle; if (definition.style == BossStyle.BLIND) { annotation { "Name" : "Boss height", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.height, BOSS_HEIGHT); } else { annotation { "Name" : "Parallel face or plane", "Filter" : EntityType.FACE, "MaxNumberOfPicks" : 1 } definition.parallelFace is Query; } annotation { "Name" : "Boss diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.diameter, BOSS_DIA); annotation { "Name" : "Ribs", "Default" : true, "UIHint" : "DISPLAY_SHORT" } definition.hasRibs is boolean; if (definition.hasRibs == true) { annotation { "Name" : "Number of ribs (max 6)", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] } isInteger(definition.ribCount, RIB_COUNT_BOUNDS); annotation { "Name" : "Flip rib direction", "UIHint" : "OPPOSITE_DIRECTION" } definition.ribFlipDirection is boolean; annotation { "Name" : "Edge to define rib direction", "Filter" : EntityType.EDGE, "MaxNumberOfPicks" : 1 } definition.ribDirection is Query; annotation { "Name" : "Rib diameter at top", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.ribDiameter, RIB_DIA); annotation { "Name" : "Rib distance from top", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.ribHeight, RIB_HEIGHT); annotation { "Name" : "Rib thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.ribThickness, RIB_THK); annotation { "Name" : "Chamfer", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"], "Default" : true } definition.hasChamfer is boolean; if (definition.hasChamfer == true) { annotation { "Name" : "Chamfer size", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] } isLength(definition.chamferSize, CHAMFER_SIZE); } } annotation { "Name" : "Hole diameter", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.holeDiam, HOLE_DIA); annotation { "Name" : "Wall thickness", "UIHint" : "REMEMBER_PREVIOUS_VALUE" } isLength(definition.wallThickness, WALL_THK); annotation { "Name" : "Draft", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"], "Default" : true } definition.hasDraft is boolean; if (definition.hasDraft == true) { annotation { "Name" : "Draft angle", "UIHint" : ["DISPLAY_SHORT", "REMEMBER_PREVIOUS_VALUE"] } isAngle(definition.draftAngle, DRAFT_ANGLE); } annotation { "Name" : "Merge scope", "Filter" : EntityType.BODY && BodyType.SOLID } definition.booleanScope is Query; } { // get all the user selected locations const locations = evaluateQuery(context, definition.locations); // if a solid body intersects the first point in the list, automatically use that in the merge scope const targetBody = evaluateQuery(context, qContainsPoint(qBodyType(qEverything(EntityType.BODY), BodyType.SOLID), evVertexPoint(context, { "vertex" : locations[0] }))); if (size(targetBody) == 0 && definition.booleanScope != undefined) definition.targetBody = definition.booleanScope; // if not, get user to select merge scope else definition.targetBody = targetBody[0]; var sketchPlane is Plane = evOwnerSketchPlane(context, { "entity" : locations[0] }); var topPlane; var ribPlane; // define the plane for the top of the boss if (definition.style == BossStyle.PLANE && definition.parallelFace != undefined) topPlane = evPlane(context, { "face" : definition.parallelFace }); else topPlane = plane(sketchPlane.origin + definition.height * sketchPlane.normal, sketchPlane.normal); var nameId = 1; definition.sketch = newSketchOnPlane(context, id + "sketch1", { "sketchPlane" : topPlane }); // Build first feature - extruded circle for (var location in locations) { var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location })); skCircle(definition.sketch, "circle" ~ nameId, { "center" : vector(point[0], point[1]), "radius" : definition.diameter / 2 }); nameId += 1; } skSolve(definition.sketch); extrude(context, id + ("extrude1"), { "entities" : qSketchRegion(id + "sketch1"), "endBound" : BoundingType.UP_TO_BODY, "endBoundEntityBody" : definition.targetBody, "oppositeDirection" : true, "hasDraft" : definition.hasDraft, "draftAngle" : definition.draftAngle, "draftPullDirection" : false, "operationType" : NewBodyOperationType.ADD, "defaultScope" : false, "booleanScope" : definition.targetBody }); // Build second feature - extruded ribs if (definition.hasRibs) { // define top of ribs ribPlane = plane(topPlane.origin - definition.ribHeight * topPlane.normal, topPlane.normal); var ribVector = vector(0, 1); // by default pointing up in Y // if user has defined rib direction, work out the vector if (definition.ribDirection != undefined) { const directionResult = try(evAxis(context, { "axis" : definition.ribDirection })); if (directionResult != undefined) ribVector = normalize(vector(directionResult.direction[0], directionResult.direction[1])); } if (definition.ribFlipDirection) ribVector = ribVector * -1; definition.sketch = newSketchOnPlane(context, id + "sketch2", { "sketchPlane" : ribPlane }); const ribPlaneCSys = planeToCSys(ribPlane); var chamferPoints = []; nameId = 1; for (var location in locations) { var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location })); const center = vector(point[0], point[1]); // Build a closed "star" shaped sketch to represent the ribs for (var j = 0; j < definition.ribCount; j += 1) { var angle = (360 / definition.ribCount) * j * degree; // The angle for each rib var angledRibVector = vector(ribVector[0] * cos(angle) - ribVector[1] * sin(angle), ribVector[0] * sin(angle) + ribVector[1] * cos(angle)); var perpRibVector = vector(angledRibVector[1] * -1, angledRibVector[0]); var ribOffset = definition.ribThickness / 2 / tan(180 / definition.ribCount * degree); if (definition.ribCount == 1) ribOffset = 0 * meter; var points = [ center - (definition.ribThickness / 2) * perpRibVector + (ribOffset) * angledRibVector, center - (definition.ribThickness / 2) * perpRibVector + (definition.ribDiameter / 2) * angledRibVector, center + (definition.ribThickness / 2) * perpRibVector + (definition.ribDiameter / 2) * angledRibVector, center + (definition.ribThickness / 2) * perpRibVector + (ribOffset) * angledRibVector]; for (var i = 0; i < size(points); i += 1) { skLineSegment(definition.sketch, "line" ~ nameId, { "start" : points[i], "end" : points[(i + 1) % size(points)] }); nameId += 1; } // Keep a list of the centerpoints of the edges where the chamfers may go var chamferPoint2d = center + (definition.ribDiameter / 2) * angledRibVector; chamferPoints = append(chamferPoints, toWorld(ribPlaneCSys, vector(chamferPoint2d[0], chamferPoint2d[1], 0 * meter))); } nameId += 1; } skSolve(definition.sketch); extrude(context, id + ("extrude2"), { "entities" : qSketchRegion(id + "sketch2"), "endBound" : BoundingType.UP_TO_BODY, "endBoundEntityBody" : definition.targetBody, "oppositeDirection" : true, "hasDraft" : definition.hasDraft, "draftAngle" : definition.draftAngle, "draftPullDirection" : false, "operationType" : NewBodyOperationType.ADD, "defaultScope" : false, "booleanScope" : definition.targetBody }); // Build third feature - chamfers if (definition.hasChamfer) { var chamferEdges = []; for (var i = 0; i < size(chamferPoints); i += 1) { // Find the edges that intersect the points previously collected chamferEdges = append(chamferEdges, qContainsPoint(qCreatedBy(id + "extrude2", EntityType.EDGE), chamferPoints[i])); } try(opChamfer(context, id + "chamfer1", { "entities" : qUnion(chamferEdges), "chamferType" : ChamferType.EQUAL_OFFSETS, "width" : definition.chamferSize })); } } nameId = 1; // Build fourth feature - through hole to outside of part var holePlane = plane(topPlane.origin - definition.wallThickness * topPlane.normal, topPlane.normal); definition.sketch = newSketchOnPlane(context, id + "sketch3", { "sketchPlane" : holePlane }); for (var location in locations) { var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location })); skCircle(definition.sketch, "circle" ~ nameId, { "center" : vector(point[0], point[1]), "radius" : definition.diameter / 2 - definition.wallThickness }); nameId += 1; } skSolve(definition.sketch); extrude(context, id + ("extrude3"), { "entities" : qSketchRegion(id + "sketch3"), "endBound" : BoundingType.UP_TO_BODY, "endBoundEntityBody" : definition.targetBody, "oppositeDirection" : true, "hasDraft" : definition.hasDraft, "draftAngle" : definition.draftAngle, "draftPullDirection" : false, "operationType" : NewBodyOperationType.REMOVE, "defaultScope" : false, "booleanScope" : definition.targetBody }); nameId = 1; // Build fifth feature - screw hole definition.sketch = newSketchOnPlane(context, id + "sketch4", { "sketchPlane" : topPlane }); for (var location in locations) { var point is Vector = worldToPlane(topPlane, evVertexPoint(context, { "vertex" : location })); skCircle(definition.sketch, "circle" ~ nameId, { "center" : vector(point[0], point[1]), "radius" : definition.holeDiam / 2 }); nameId += 1; } skSolve(definition.sketch); extrude(context, id + ("extrude4"), { "entities" : qSketchRegion(id + "sketch4"), "endBound" : BoundingType.UP_TO_BODY, "endBoundEntityBody" : definition.targetBody, "oppositeDirection" : true, "hasDraft" : definition.hasDraft, "draftAngle" : definition.draftAngle, "draftPullDirection" : false, "operationType" : NewBodyOperationType.REMOVE, "defaultScope" : false, "booleanScope" : definition.targetBody }); // Remove sketch entities - no longer required var sketches = [qCreatedBy(id + "sketch1"), qCreatedBy(id + "sketch2"), qCreatedBy(id + "sketch3"), qCreatedBy(id + "sketch4")]; opDeleteBodies(context, id + "delete", { "entities" : qUnion(sketches) }); }, {}); const BOSS_HEIGHT = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.02, 500], (centimeter) : 2.0, (millimeter) : 20.0, (inch) : 0.8 } as LengthBoundSpec; const BOSS_DIA = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.008, 500], (centimeter) : 0.8, (millimeter) : 8.0, (inch) : 0.3125 } as LengthBoundSpec; const RIB_COUNT_BOUNDS = { "min" : 1, "max" : 6, (unitless) : [1, 4, 6] } as IntegerBoundSpec; const RIB_DIA = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.015, 500], (centimeter) : 1.5, (millimeter) : 15.0, (inch) : 0.6 } as LengthBoundSpec; const RIB_HEIGHT = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [0, 0.005, 500], (centimeter) : 0.5, (millimeter) : 5.0, (inch) : 0.2 } as LengthBoundSpec; const RIB_THK = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.001, 500], (centimeter) : 0.1, (millimeter) : 1.0, (inch) : 0.04 } as LengthBoundSpec; const CHAMFER_SIZE = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.002, 500], (centimeter) : 0.2, (millimeter) : 2.0, (inch) : 0.08 } as LengthBoundSpec; const HOLE_DIA = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.003, 500], (centimeter) : 0.3, (millimeter) : 3.0, (inch) : 0.12 } as LengthBoundSpec; const WALL_THK = { "min" : -TOLERANCE.zeroLength * meter, "max" : 500 * meter, (meter) : [1e-5, 0.0015, 500], (centimeter) : 0.15, (millimeter) : 1.5, (inch) : 0.06 } as LengthBoundSpec; const DRAFT_ANGLE = { "min" : -TOLERANCE.zeroAngle * radian, "max" : (2 * PI + TOLERANCE.zeroAngle) * radian, (degree) : [0, 2, 360], (radian) : 0.035 } as AngleBoundSpec; export enum BossStyle { annotation { "Name" : "Blind" } BLIND, annotation { "Name" : "Up to face" } PLANE }