diff --git a/Elements.Serialization.IFC/src/IFCElementExtensions.cs b/Elements.Serialization.IFC/src/IFCElementExtensions.cs index 78d06159d..5c060a0ad 100644 --- a/Elements.Serialization.IFC/src/IFCElementExtensions.cs +++ b/Elements.Serialization.IFC/src/IFCElementExtensions.cs @@ -5,6 +5,7 @@ using Elements.Geometry.Interfaces; using Elements.Geometry.Solids; using Elements.Interfaces; +using Elements; using IFC; namespace Elements.Serialization.IFC @@ -192,17 +193,17 @@ private static List AddSolidOperationToDocument(Document var ifcRepresentations = new List(); if (op is Sweep sweep) { - // Neither of these entities, which are part of the - // IFC4 specification, and which would allow a sweep - // along a curve, are supported by many applications + // Neither of these entities, which are part of the + // IFC4 specification, and which would allow a sweep + // along a curve, are supported by many applications // which are supposedly IFC4 compliant (Revit). For // Those applications where these entities appear, - // the rotation of the profile is often wrong or + // the rotation of the profile is often wrong or // inconsistent. // geom = sweep.ToIfcSurfaceCurveSweptAreaSolid(doc); // geom = sweep.ToIfcFixedReferenceSweptAreaSolid(geoElement.Transform, doc); - // Instead, we'll divide the curve and create a set of + // Instead, we'll divide the curve and create a set of // linear extrusions instead. Polyline pline; if (sweep.Curve is Line) diff --git a/Elements.Serialization.IFC/src/IFCToHypar/Converters/FromIfcDoorConverter.cs b/Elements.Serialization.IFC/src/IFCToHypar/Converters/FromIfcDoorConverter.cs index d59234f57..878769aeb 100644 --- a/Elements.Serialization.IFC/src/IFCToHypar/Converters/FromIfcDoorConverter.cs +++ b/Elements.Serialization.IFC/src/IFCToHypar/Converters/FromIfcDoorConverter.cs @@ -1,5 +1,6 @@ using Elements.Geometry; using Elements.Serialization.IFC.IFCToHypar.RepresentationsExtraction; +using Elements; using IFC; using System; using System.Collections.Generic; @@ -34,16 +35,18 @@ public GeometricElement ConvertToElement(IfcProduct ifcProduct, RepresentationDa // TODO: Implement during the connections establishment. //var wall = GetWallFromDoor(ifcDoor, allWalls); - var doorWidth = (IfcLengthMeasure) ifcDoor.OverallWidth; - var doorHeight = (IfcLengthMeasure) ifcDoor.OverallHeight; + var doorWidth = (IfcLengthMeasure)ifcDoor.OverallWidth; + var doorHeight = (IfcLengthMeasure)ifcDoor.OverallHeight; var result = new Door(doorWidth, doorHeight, + Door.DOOR_THICKNESS, openingSide, openingType, repData.Transform, repData.Material, new Representation(repData.SolidOperations), + false, IfcGuid.FromIfcGUID(ifcDoor.GlobalId), ifcDoor.Name ); diff --git a/Elements.Serialization.IFC/src/IFCToHypar/IFCExtensions.cs b/Elements.Serialization.IFC/src/IFCToHypar/IFCExtensions.cs index 6a9417ac6..63e52c2f1 100644 --- a/Elements.Serialization.IFC/src/IFCToHypar/IFCExtensions.cs +++ b/Elements.Serialization.IFC/src/IFCToHypar/IFCExtensions.cs @@ -4,6 +4,7 @@ using Elements.Geometry; using Elements.Geometry.Interfaces; using Elements.Geometry.Solids; +using Elements; using IFC; namespace Elements.Serialization.IFC.IFCToHypar @@ -131,7 +132,7 @@ internal static ICurve ToCurve(this IfcParameterizedProfileDef profile) } else if (profile is IfcCircleProfileDef ifcCircle) { - var circle = new Circle((IfcLengthMeasure) ifcCircle.Radius); + var circle = new Circle((IfcLengthMeasure)ifcCircle.Radius); return circle.Transformed(transform); } else diff --git a/Elements.Serialization.IFC/test/IFCTests.cs b/Elements.Serialization.IFC/test/IFCTests.cs index 82dd05832..ad077c6db 100644 --- a/Elements.Serialization.IFC/test/IFCTests.cs +++ b/Elements.Serialization.IFC/test/IFCTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using Elements.Geometry.Profiles; using System.Linq; +using System.Xml.Linq; namespace Elements.IFC.Tests { @@ -28,7 +29,7 @@ public IfcTests(ITestOutputHelper output) // [InlineData("rac_sample", "../../../models/IFC4/rac_advanced_sample_project.ifc")] // [InlineData("rme_sample", "../../../models/IFC4/rme_advanced_sample_project.ifc")] // [InlineData("rst_sample", "../../../models/IFC4/rst_advanced_sample_project.ifc")] - [InlineData("AC-20-Smiley-West-10-Bldg", "../../../models/IFC4/AC-20-Smiley-West-10-Bldg.ifc", 1972, 120, 539, 270, 9, 140, 10, 2)] + [InlineData("AC-20-Smiley-West-10-Bldg", "../../../models/IFC4/AC-20-Smiley-West-10-Bldg.ifc", 1963, 120, 530, 270, 9, 140, 10, 2)] // TODO: Some walls are extracted incorrectly and intersecting the roof. It happens because // IfcBooleanClippingResultParser doesn't handle the boolean clipping operation. // In order to fix it surface support is required. @@ -38,7 +39,7 @@ public IfcTests(ITestOutputHelper output) // TODO: The entrance door has an incorrect representation. It happens because during // the UpdateRepresentation the default representation of a door is created instead of // the extracted one. - [InlineData("AC20-Institute-Var-2", "../../../models/IFC4/AC20-Institute-Var-2.ifc", 1517, 5, 577, 121, 7, 82, 0, 21)] + [InlineData("AC20-Institute-Var-2", "../../../models/IFC4/AC20-Institute-Var-2.ifc", 1506, 5, 570, 121, 7, 82, 0, 21)] // [InlineData("20160125WestRiverSide Hospital - IFC4-Autodesk_Hospital_Sprinkle", "../../../models/IFC4/20160125WestRiverSide Hospital - IFC4-Autodesk_Hospital_Sprinkle.ifc")] public void FromIFC4(string name, string ifcPath, @@ -137,15 +138,18 @@ public void Doors() var wall1 = new StandardWall(wallLine1, 0.2, 3, name: "Wall1"); var wall2 = new StandardWall(wallLine2, 0.2, 2, name: "Wall2"); - model.AddElement(wall1); - model.AddElement(wall2); + var door1 = new Door(wallLine1, 0.5, 1.5, 2.0, Door.DOOR_THICKNESS, DoorOpeningSide.LeftHand, DoorOpeningType.DoubleSwing); + var door2 = new Door(wallLine2, 0.5, 1.5, 1.8, Door.DOOR_THICKNESS, DoorOpeningSide.LeftHand, DoorOpeningType.DoubleSwing); - var door1 = new Door(wall1, wallLine1, 0.5, 1.5, 2.0, DoorOpeningSide.LeftHand, DoorOpeningType.DoubleSwing); - var door2 = new Door(wall2, wallLine2, 0.5, 1.5, 1.8, DoorOpeningSide.LeftHand, DoorOpeningType.DoubleSwing); + wall1.AddDoorOpening(door1); + wall2.AddDoorOpening(door2); + model.AddElement(wall1); + model.AddElement(wall2); model.AddElement(door1); model.AddElement(door2); + model.ToJson(ConstructJsonPath("IfcDoor")); model.ToIFC(ConstructIfcPath("IfcDoor")); } diff --git a/Elements/src/BIM/Door/Door.cs b/Elements/src/BIM/Door/Door.cs new file mode 100644 index 000000000..b69e7f049 --- /dev/null +++ b/Elements/src/BIM/Door/Door.cs @@ -0,0 +1,459 @@ +using Elements.Geometry; +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; +using Elements.Geometry.Solids; + +namespace Elements +{ + /// Definition of a door + public class Door : GeometricElement + { + private const double HANDLE_HEIGHT_POSITION = 42 * 0.0254; + private const double HANDLE_CIRCLE_RADIUS = 1.35 * 0.0254; + private const double HANDLE_CYLINDER_RADIUS = 0.45 * 0.0254; + private const double HANDLE_LENGTH = 5 * 0.0254; + private const double HANDLE_CYLINDER_HEIGHT = 2 * 0.0254; + private readonly Material DEFAULT_MATERIAL = BuiltInMaterials.Wood; + private readonly Material SILVER_MATERIAL = new Material(Colors.Gray, 0.5, 0.25, false, null, false, false, null, false, null, 0, false, default, "Silver Frame"); + + /// Default thickness of a door. + public const double DOOR_THICKNESS = 1.375 * 0.0254; + /// Default thickness of a door frame. + public const double DOOR_FRAME_THICKNESS = 4 * 0.0254; + /// Default width of a door frame. + public const double DOOR_FRAME_WIDTH = 2 * 0.0254; //2 inches + /// Door width without a frame + public double ClearWidth { get; private set; } + /// The opening type of the door that should be placed + public DoorOpeningType OpeningType { get; private set; } + /// The opening side of the door that should be placed + public DoorOpeningSide OpeningSide { get; private set; } + /// Height of a door without a frame. + public double ClearHeight { get; private set; } + /// Door thickness. + public double Thickness { get; private set; } + /// Original position of the door used for override identity + public Vector3 OriginalPosition { get; set; } + + [JsonIgnore] + private double fullDoorWidthWithoutFrame => GetDoorFullWidthWithoutFrame(ClearWidth, OpeningSide); + /// + /// Create a door. + /// + /// The width of a single door. + /// Height of the door without frame. + /// Door thickness. + /// The side where the door opens. + /// The way the door opens. + /// The door's transform. X-direction is aligned with the door, Y-direction is the opening direction. + /// The door's material. + /// The door's representation. + /// Is this an element definition? + /// The door's id. + /// The door's name. + [JsonConstructor] + public Door(double clearWidth, + double clearHeight, + double thickness, + DoorOpeningSide openingSide, + DoorOpeningType openingType, + Transform transform = null, + Material material = null, + Representation representation = null, + bool isElementDefinition = false, + Guid id = default, + string name = "Door" + ) : base( + transform: transform, + representation: representation, + isElementDefinition: isElementDefinition, + id: id, + name: name + ) + { + OpeningSide = openingSide; + OpeningType = openingType; + ClearHeight = clearHeight; + ClearWidth = clearWidth; + Thickness = thickness; + Material = material ?? DEFAULT_MATERIAL; + } + + /// + /// Create a door at the certain point of a line. + /// + /// The line where the door is placed. + /// Relative position on the line where door is placed. Should be in [0; 1]. + /// The width of a single door. + /// Height of the door without frame. + /// Door thickness. + /// The side where the door opens. + /// The way the door opens. + /// The door's material. + /// The door's representation. + /// Is this an element definition? + /// The door's id. + /// The door's name. + public Door(Line line, + double tPos, + double clearWidth, + double clearHeight, + double thickness, + DoorOpeningSide openingSide, + DoorOpeningType openingType, + Material material = null, + Representation representation = null, + bool isElementDefinition = false, + Guid id = default, + string name = "Door" + ) : base( + representation: representation, + isElementDefinition: isElementDefinition, + id: id, + name: name + ) + { + OpeningType = openingType; + OpeningSide = openingSide; + ClearWidth = clearWidth; + ClearHeight = clearHeight; + Thickness = thickness; + Material = material ?? DEFAULT_MATERIAL; + Transform = GetDoorTransform(line.PointAtNormalized(tPos), line); + } + + /// + /// Create an opening for the door. + /// + /// The door's opening depth front. + /// The door's opening depth back. + /// Is the opening flipped? + /// An opening where the door can be inserted. + public Opening CreateDoorOpening(double depthFront, double depthBack, bool flip) + { + var openingWidth = fullDoorWidthWithoutFrame + 2 * DOOR_FRAME_WIDTH; + var openingHeight = ClearHeight + DOOR_FRAME_WIDTH; + + var openingDir = flip ? Vector3.YAxis.Negate() : Vector3.YAxis; + var widthDir = flip ? Vector3.XAxis.Negate() : Vector3.XAxis; + var openingTransform = new Transform(0.5 * openingHeight * Vector3.ZAxis, widthDir, openingDir); + + var openingPolygon = Polygon.Rectangle(openingWidth, openingHeight).TransformedPolygon(openingTransform); + + var opening = new Opening(openingPolygon, openingDir, depthFront, depthBack, Transform); + return opening; + } + + private Transform GetDoorTransform(Vector3 currentPosition, Line wallLine) + { + var adjustedPosition = GetClosestValidDoorPos(wallLine, currentPosition); + var xDoorAxis = wallLine.Direction(); + return new Transform(adjustedPosition, xDoorAxis, Vector3.ZAxis); + } + + /// + /// Checks if the door can fit into the wall with the center line @. + /// + public static bool CanFit(Line wallLine, DoorOpeningSide openingSide, double width) + { + var doorWidth = GetDoorFullWidthWithoutFrame(width, openingSide) + DOOR_FRAME_WIDTH * 2; + return wallLine.Length() - doorWidth > DOOR_FRAME_WIDTH * 2; + } + + /// + /// Update the representations. + /// + public override void UpdateRepresentations() + { + if (RepresentationInstances.Count == 0) + { + DoorRepresentationStorage.SetDoorRepresentation(this); + } + } + + /// + /// Get Hash for representation storage dictionary + /// + public string GetRepresentationHash() + { + return $"{this.GetType().Name}-{this.ClearWidth}-{this.ClearHeight}-{this.Thickness}-{this.OpeningType}-{this.OpeningSide}-{this.Material.Name}"; + } + + public List GetInstances() + { + var representationInstances = new List() + { + this.CreateDoorSolidRepresentation(), + this.CreateDoorFrameRepresentation(), + this.CreateDoorCurveRepresentation(), + this.CreateDoorHandleRepresentation() + }; + + return representationInstances; + } + + private Vector3 GetClosestValidDoorPos(Line wallLine, Vector3 currentPosition) + { + var fullWidth = fullDoorWidthWithoutFrame + DOOR_FRAME_WIDTH * 2; + double wallWidth = wallLine.Length(); + Vector3 p1 = wallLine.PointAt(0.5 * fullWidth); + Vector3 p2 = wallLine.PointAt(wallWidth - 0.5 * fullWidth); + var reducedWallLine = new Line(p1, p2); + return currentPosition.ClosestPointOn(reducedWallLine); + } + + private static double GetDoorFullWidthWithoutFrame(double doorClearWidth, DoorOpeningSide doorOpeningSide) + { + switch (doorOpeningSide) + { + case DoorOpeningSide.LeftHand: + case DoorOpeningSide.RightHand: + return doorClearWidth; + case DoorOpeningSide.DoubleDoor: + return doorClearWidth * 2; + } + return 0; + } + + + private RepresentationInstance CreateDoorCurveRepresentation() + { + var points = CollectPointsForSchematicVisualization(); + var curve = new IndexedPolycurve(points); + var curveRep = new CurveRepresentation(curve, false); + var repInstance = new RepresentationInstance(curveRep, BuiltInMaterials.Black); + + return repInstance; + } + + private RepresentationInstance CreateDoorFrameRepresentation() + { + Vector3 left = Vector3.XAxis * (fullDoorWidthWithoutFrame / 2); + Vector3 right = Vector3.XAxis.Negate() * (fullDoorWidthWithoutFrame / 2); + + var frameLeft = left + Vector3.XAxis * Door.DOOR_FRAME_WIDTH; + var frameRight = right - Vector3.XAxis * Door.DOOR_FRAME_WIDTH; + var frameOffset = Vector3.YAxis * Door.DOOR_FRAME_THICKNESS; + var doorFramePolygon = new Polygon(new List() { + left + Vector3.ZAxis * this.ClearHeight - frameOffset, + left - frameOffset, + frameLeft - frameOffset, + frameLeft + Vector3.ZAxis * (this.ClearHeight + Door.DOOR_FRAME_WIDTH) - frameOffset, + frameRight + Vector3.ZAxis * (this.ClearHeight + Door.DOOR_FRAME_WIDTH) - frameOffset, + frameRight - frameOffset, + right - frameOffset, + right + Vector3.ZAxis * this.ClearHeight - frameOffset }); + var doorFrameExtrude = new Extrude(new Profile(doorFramePolygon), Door.DOOR_FRAME_THICKNESS * 2, Vector3.YAxis); + + var solidRep = new SolidRepresentation(doorFrameExtrude); + var repInstance = new RepresentationInstance(solidRep, SILVER_MATERIAL, true); + return repInstance; + } + + private RepresentationInstance CreateDoorSolidRepresentation() + { + Vector3 left = Vector3.XAxis * (fullDoorWidthWithoutFrame / 2); + Vector3 right = Vector3.XAxis.Negate() * (fullDoorWidthWithoutFrame / 2); + + var doorPolygon = new Polygon(new List() { + left + Vector3.YAxis * this.Thickness, + left - Vector3.YAxis * this.Thickness, + right - Vector3.YAxis * this.Thickness, + right + Vector3.YAxis * this.Thickness}); + + var doorPolygons = new List(); + + if (this.OpeningSide == DoorOpeningSide.DoubleDoor) + { + doorPolygons = doorPolygon.Split(new Polyline(new Vector3(0, this.Thickness, 0), new Vector3(0, -this.Thickness, 0))); + } + else + { + doorPolygons.Add(doorPolygon); + } + + var doorExtrusions = new List(); + + foreach (var polygon in doorPolygons) + { + var doorExtrude = new Extrude(new Profile(polygon.Offset(-0.005)[0]), this.ClearHeight, Vector3.ZAxis); + doorExtrusions.Add(doorExtrude); + } + + var solidRep = new SolidRepresentation(doorExtrusions); + var repInstance = new RepresentationInstance(solidRep, this.Material, true); + return repInstance; + } + + private List CollectPointsForSchematicVisualization() + { + var points = new List(); + + if (this.OpeningSide == DoorOpeningSide.Undefined || this.OpeningType == DoorOpeningType.Undefined) + { + return points; + } + + if (this.OpeningSide != DoorOpeningSide.LeftHand) + { + points.AddRange(CollectSchematicVisualizationLines(this, false, false, 90)); + } + + if (this.OpeningSide != DoorOpeningSide.RightHand) + { + points.AddRange(CollectSchematicVisualizationLines(this, true, false, 90)); + } + + if (this.OpeningType == DoorOpeningType.SingleSwing) + { + return points; + } + + if (this.OpeningSide != DoorOpeningSide.LeftHand) + { + points.AddRange(CollectSchematicVisualizationLines(this, false, true, 90)); + } + + if (this.OpeningSide != DoorOpeningSide.RightHand) + { + points.AddRange(CollectSchematicVisualizationLines(this, true, true, 90)); + } + + return points; + } + + private List CollectSchematicVisualizationLines(Door door, bool leftSide, bool inside, double angle) + { + // Depending on which side door in there are different offsets. + var doorOffset = leftSide ? fullDoorWidthWithoutFrame / 2 : -fullDoorWidthWithoutFrame / 2; + var horizontalOffset = leftSide ? door.Thickness : -door.Thickness; + var verticalOffset = inside ? door.Thickness : -door.Thickness; + var widthOffset = inside ? door.ClearWidth : -door.ClearWidth; + + // Draw open door silhouette rectangle. + Vector3 corner = Vector3.XAxis * doorOffset; + var c0 = corner + Vector3.YAxis * verticalOffset; + var c1 = c0 + Vector3.YAxis * widthOffset; + var c2 = c1 - Vector3.XAxis * horizontalOffset; + var c3 = c0 - Vector3.XAxis * horizontalOffset; + + // Rotate silhouette is it's need to be drawn as partially open. + if (!angle.ApproximatelyEquals(90)) + { + double rotation = 90 - angle; + if (!leftSide) + { + rotation = -rotation; + } + + if (!inside) + { + rotation = -rotation; + } + + Transform t = new Transform(); + t.RotateAboutPoint(c0, Vector3.ZAxis, rotation); + c1 = t.OfPoint(c1); + c2 = t.OfPoint(c2); + c3 = t.OfPoint(c3); + } + List points = new List() { c0, c1, c1, c2, c2, c3, c3, c0 }; + + // Calculated correct arc angles based on door orientation. + double adjustedAngle = inside ? angle : -angle; + double anchorAngle = leftSide ? 180 : 0; + double endAngle = leftSide ? 180 - adjustedAngle : adjustedAngle; + if (endAngle < 0) + { + endAngle = 360 + endAngle; + anchorAngle = 360; + } + + // If arc is constructed from bigger angle to smaller is will have incorrect domain + // with max being smaller than min and negative length. + // ToPolyline will return 0 points for it. + // Until it's fixed angles should be aligned manually. + bool flipEnds = endAngle < anchorAngle; + if (flipEnds) + { + (anchorAngle, endAngle) = (endAngle, anchorAngle); + } + + // Draw the arc from closed door to opened door. + Arc arc = new Arc(c0, door.ClearWidth, anchorAngle, endAngle); + var tessalatedArc = arc.ToPolyline((int)(Math.Abs(angle) / 2)); + for (int i = 0; i < tessalatedArc.Vertices.Count - 1; i++) + { + points.Add(tessalatedArc.Vertices[i]); + points.Add(tessalatedArc.Vertices[i + 1]); + } + + return points; + } + + private RepresentationInstance CreateDoorHandleRepresentation() + { + var solidOperationsList = new List(); + + if (OpeningSide == DoorOpeningSide.DoubleDoor) + { + var handlePair1 = CreateHandlePair(-3 * 0.0254, false); + solidOperationsList.AddRange(handlePair1); + + var handlePair2 = CreateHandlePair(3 * 0.0254, true); + solidOperationsList.AddRange(handlePair2); + } + else if (OpeningSide != DoorOpeningSide.Undefined) + { + var xPos = OpeningSide == DoorOpeningSide.LeftHand ? -(fullDoorWidthWithoutFrame / 2 - 2 * 0.0254) : (fullDoorWidthWithoutFrame / 2 - 2 * 0.0254); + var handle = CreateHandlePair(xPos, OpeningSide == DoorOpeningSide.LeftHand); + solidOperationsList.AddRange(handle); + } + + var solidRep = new SolidRepresentation(solidOperationsList); + var repInst = new RepresentationInstance(solidRep, SILVER_MATERIAL); + return repInst; + } + + private List CreateHandlePair(double xRelPos, bool isCodirectionalToX) + { + var xOffset = xRelPos * ClearWidth * Vector3.XAxis; + var yOffset = Thickness * Vector3.YAxis; + var zOffset = HANDLE_HEIGHT_POSITION * Vector3.ZAxis; + + var solidOperationsList = new List(); + var handleDir = isCodirectionalToX ? Vector3.XAxis : Vector3.XAxis.Negate(); + + var handleOrigin1 = xOffset + yOffset + zOffset; + var handle1Ops = CreateHandle(handleOrigin1, handleDir, Vector3.YAxis); + solidOperationsList.AddRange(handle1Ops); + + var handleOrigin2 = xOffset - yOffset + zOffset; + var handle2Ops = CreateHandle(handleOrigin2, handleDir, Vector3.YAxis.Negate()); + solidOperationsList.AddRange(handle2Ops); + + return solidOperationsList; + } + + private List CreateHandle(Vector3 origin, Vector3 handleDir, Vector3 yDir) + { + var circleTransform = new Transform(origin, handleDir, yDir); + var circle = new Circle(circleTransform, HANDLE_CIRCLE_RADIUS).ToPolygon(); + var circleOperation = new Extrude(circle, 0.1 * HANDLE_CYLINDER_HEIGHT, yDir); + + var cyl1Transform = new Transform(origin + 0.1 * HANDLE_CYLINDER_HEIGHT * yDir, handleDir, yDir); + var cyl1Circle = new Circle(cyl1Transform, HANDLE_CYLINDER_RADIUS).ToPolygon(); + var cyl1Operation = new Extrude(cyl1Circle, 0.9 * HANDLE_CYLINDER_HEIGHT, yDir); + + var cyl2Origin = cyl1Transform.Origin + cyl1Operation.Height * yDir + handleDir.Negate() * HANDLE_CYLINDER_RADIUS; + var cyl2Transform = new Transform(cyl2Origin, handleDir); + var cyl2Circle = new Circle(cyl2Transform, HANDLE_CYLINDER_RADIUS).ToPolygon(); + var cyl2Operation = new Extrude(cyl2Circle, HANDLE_LENGTH, handleDir); + + var handleSolids = new List() { circleOperation, cyl1Operation, cyl2Operation }; + return handleSolids; + } + } +} diff --git a/Elements/src/DoorOpeningSide.cs b/Elements/src/BIM/Door/DoorOpeningSide.cs similarity index 85% rename from Elements/src/DoorOpeningSide.cs rename to Elements/src/BIM/Door/DoorOpeningSide.cs index 0acfcbc57..b33dbe5fe 100644 --- a/Elements/src/DoorOpeningSide.cs +++ b/Elements/src/BIM/Door/DoorOpeningSide.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Elements { public enum DoorOpeningSide @@ -15,4 +11,4 @@ public enum DoorOpeningSide [System.Runtime.Serialization.EnumMember(Value = @"Double Door")] DoubleDoor } -} +} \ No newline at end of file diff --git a/Elements/src/DoorOpeningType.cs b/Elements/src/BIM/Door/DoorOpeningType.cs similarity index 82% rename from Elements/src/DoorOpeningType.cs rename to Elements/src/BIM/Door/DoorOpeningType.cs index 19c17e2f5..85d893788 100644 --- a/Elements/src/DoorOpeningType.cs +++ b/Elements/src/BIM/Door/DoorOpeningType.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace Elements { public enum DoorOpeningType @@ -13,4 +9,4 @@ public enum DoorOpeningType [System.Runtime.Serialization.EnumMember(Value = @"Double Swing")] DoubleSwing } -} +} \ No newline at end of file diff --git a/Elements/src/BIM/Door/DoorRepresentationStorage.cs b/Elements/src/BIM/Door/DoorRepresentationStorage.cs new file mode 100644 index 000000000..2b01c4b56 --- /dev/null +++ b/Elements/src/BIM/Door/DoorRepresentationStorage.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Elements.Geometry.Solids; + +namespace Elements +{ + static class DoorRepresentationStorage + { + private static readonly Dictionary> _doors = new Dictionary>(); + public static Dictionary> Doors => _doors; + + public static void SetDoorRepresentation(Door door) + { + var hash = door.GetRepresentationHash(); + if (!_doors.ContainsKey(hash)) + { + _doors.Add(hash, door.GetInstances()); + } + door.RepresentationInstances = _doors[hash]; + } + } + +} \ No newline at end of file diff --git a/Elements/src/Door.cs b/Elements/src/Door.cs deleted file mode 100644 index 75309cc67..000000000 --- a/Elements/src/Door.cs +++ /dev/null @@ -1,416 +0,0 @@ -using Elements.Geometry.Solids; -using Elements.Geometry; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Elements -{ - /// Definition of a door - public class Door : GeometricElement - { - private readonly Material DEFAULT_MATERIAL = new Material("Door material", Colors.White); - - /// - /// Default thickness of a door. - /// - public const double DOOR_THICKNESS = 0.125; - /// - /// Default thickness of a door frame. - /// - public const double DOOR_FRAME_THICKNESS = 0.15; - /// - /// Default width of a door frame. - /// - public const double DOOR_FRAME_WIDTH = 2 * 0.0254; //2 inches - - /// Door width without a frame - public double ClearWidth { get; private set; } - /// The opening type of the door that should be placed - public DoorOpeningType OpeningType { get; private set; } - /// The opening side of the door that should be placed - public DoorOpeningSide OpeningSide { get; private set; } - /// The wall on which a door is placed. - public Wall Wall { get; private set; } - /// Height of a door without a frame. - public double ClearHeight { get; private set; } - /// Position where door was placed originally. - public Vector3 OriginalPosition { get; private set; } - /// Opening for a door. - public Opening Opening { get; private set; } - - /// - /// Create a door that attaches to the closest point on a certain wall. - /// - /// The wall the door is attached to. - /// A center line of a wall that door is attached to. - /// The position where the door was plased originally. - /// The current door's position. - /// The with of a single door. - /// The door's height. - /// The side where the door opens. - /// The way the door opens. - /// The door's material. - /// The door's representation. - /// The door's id. - /// The door's name. - /// The door's opening depth front. - /// The door's opening depth back. - /// Is the door flipped? - public Door(Wall wall, - Line wallLine, - Vector3 originalPosition, - Vector3 currentPosition, - double width, - double height, - DoorOpeningSide openingSide, - DoorOpeningType openingType, - Material material = null, - Representation representation = null, - Guid id = default, - string name = "Door", - double depthFront = 1, - double depthBack = 1, - bool flip = false) - { - Wall = wall; - OpeningType = openingType; - OpeningSide = openingSide; - OriginalPosition = originalPosition; - ClearWidth = WidthWithoutFrame(width, openingSide); - ClearHeight = height; - Material = material ?? DEFAULT_MATERIAL; - Transform = GetDoorTransform(currentPosition, wallLine, flip); - Representation = representation ?? new Representation(new List() { }); - Opening = new Opening(Polygon.Rectangle(width, height), depthFront, depthBack, GetOpeningTransform()); - Id = id != default ? id : Guid.NewGuid(); - Name = name; - } - - /// - /// Create a door that is not attached to a wall. - /// - /// The with of a single door. - /// The door's height. - /// The side where the door opens. - /// The way the door opens. - /// The door's transform. X-direction is aligned with the door, Y-direction is the opening direction. - /// The door's material. - /// The door's representation. - /// The door's id. - /// The door's name. - /// The door's opening depth front. - /// The door's opening depth back. - public Door(double width, - double height, - DoorOpeningSide openingSide, - DoorOpeningType openingType, - Transform transform = null, - Material material = null, - Representation representation = null, - Guid id = default, - string name = "Door", - double depthFront = 1, - double depthBack = 1 - ) - { - Wall = null; - Transform = transform; - OpeningSide = openingSide; - OpeningType = openingType; - ClearHeight = height; - ClearWidth = WidthWithoutFrame(width, openingSide); - Material = material ?? DEFAULT_MATERIAL; - Representation = representation ?? new Representation(new List() { }); - Opening = new Opening(Polygon.Rectangle(width, height), depthFront, depthBack, GetOpeningTransform()); - OriginalPosition = Transform.Origin; - Id = id != default ? id : Guid.NewGuid(); - Name = name; - } - - /// - /// Create a door at the certain point of a wall. - /// - /// The wall the door is attached to. - /// A center line of a wall that door is attached to. - /// Relative position on the wall where door is placed. Should be in [0; 1]. - /// The with of a single door. - /// The door's height. - /// The side where the door opens. - /// The way the door opens. - /// The door's material. - /// The door's representation. - /// The door's id. - /// The door's name. - /// The door's opening depth front. - /// The door's opening depth back. - /// Is the door flipped? - public Door(Wall wall, - Line wallLine, - double tPos, - double width, - double height, - DoorOpeningSide openingSide, - DoorOpeningType openingType, - Material material = null, - Representation representation = null, - Guid id = default, - string name = "Door", - double depthFront = 1, - double depthBack = 1, - bool flip = false) - : this(wall, - wallLine, - wallLine.PointAtNormalized(tPos), - wallLine.PointAtNormalized(tPos), - width, - height, - openingSide, - openingType, - material, - representation, - id, - name, - depthFront, - depthBack, - flip) - { - } - - private Transform GetOpeningTransform() - { - var halfHeightDir = 0.5 * (ClearHeight + DOOR_FRAME_THICKNESS) * Vector3.ZAxis; - var openingTransform = new Transform(Transform.Origin + halfHeightDir, Transform.XAxis, Transform.XAxis.Cross(Vector3.ZAxis)); - return openingTransform; - } - - private Transform GetDoorTransform(Vector3 currentPosition, Line wallLine, bool flip) - { - var adjustedPosition = GetClosestValidDoorPos(wallLine, currentPosition); - var xDoorAxis = flip ? wallLine.Direction().Negate() : wallLine.Direction(); - return new Transform(adjustedPosition, xDoorAxis, Vector3.ZAxis); - } - - /// - /// Checks if the door can fit into the wall with the center line @. - /// - public static bool CanFit(Line wallLine, DoorOpeningSide openingSide, double width) - { - var doorWidth = WidthWithoutFrame(width, openingSide) + DOOR_FRAME_WIDTH * 2; - return wallLine.Length() - doorWidth > DOOR_FRAME_WIDTH * 2; - } - - /// - /// Get graphics buffers and other metadata required to modify a GLB. - /// - /// - /// True if there is graphicsbuffers data applicable to add, false otherwise. - /// Out variables should be ignored if the return value is false. - /// - public override bool TryToGraphicsBuffers(out List graphicsBuffers, out string id, out glTFLoader.Schema.MeshPrimitive.ModeEnum? mode) - { - var points = CollectPointsForSchematicVisualization(); - GraphicsBuffers buffer = new GraphicsBuffers(); - Color color = Colors.Black; - for (int i = 0; i < points.Count; i++) - { - buffer.AddVertex(points[i], default, default, color); - buffer.AddIndex((ushort)i); - } - - id = $"{Id}_door"; - // Only one type is allowed, since line are not linked into one loop, LINES is used. - // This mean that each line segment need both endpoints stored, often duplicated. - mode = glTFLoader.Schema.MeshPrimitive.ModeEnum.LINES; - graphicsBuffers = new List { buffer }; - return true; - } - - // TODO: Move visualization logic out of the class in case of DoorOpeningType enum extension. - private List CollectPointsForSchematicVisualization() - { - var points = new List(); - - if (OpeningSide == DoorOpeningSide.Undefined || OpeningType == DoorOpeningType.Undefined) - { - return points; - } - - if (OpeningSide != DoorOpeningSide.LeftHand) - { - points.AddRange(CollectSchematicVisualizationLines(false, false, 90)); - } - - if (OpeningSide != DoorOpeningSide.RightHand) - { - points.AddRange(CollectSchematicVisualizationLines(true, false, 90)); - } - - if (OpeningType == DoorOpeningType.SingleSwing) - { - return points; - } - - if (OpeningSide != DoorOpeningSide.LeftHand) - { - points.AddRange(CollectSchematicVisualizationLines(false, true, 90)); - } - - if (OpeningSide != DoorOpeningSide.RightHand) - { - points.AddRange(CollectSchematicVisualizationLines(true, true, 90)); - } - - return points; - } - - private List CollectSchematicVisualizationLines(bool leftSide, bool inside, double angle) - { - var doorWidth = OpeningSide == DoorOpeningSide.DoubleDoor ? ClearWidth / 2 : ClearWidth; - - // Depending on which side door in there are different offsets. - var doorOffset = leftSide ? ClearWidth / 2 : -ClearWidth / 2; - var horizontalOffset = leftSide ? DOOR_THICKNESS : -DOOR_THICKNESS; - var verticalOffset = inside ? DOOR_THICKNESS : -DOOR_THICKNESS; - var widthOffset = inside ? doorWidth : -doorWidth; - - // Draw open door silhouette rectangle. - Vector3 corner = Vector3.XAxis * doorOffset; - var c0 = corner + Vector3.YAxis * verticalOffset; - var c1 = c0 + Vector3.YAxis * widthOffset; - var c2 = c1 - Vector3.XAxis * horizontalOffset; - var c3 = c0 - Vector3.XAxis * horizontalOffset; - - // Rotate silhouette is it's need to be drawn as partially open. - if (!angle.ApproximatelyEquals(90)) - { - double rotation = 90 - angle; - if (!leftSide) - { - rotation = -rotation; - } - - if (!inside) - { - rotation = -rotation; - } - - Transform t = new Transform(); - t.RotateAboutPoint(c0, Vector3.ZAxis, rotation); - c1 = t.OfPoint(c1); - c2 = t.OfPoint(c2); - c3 = t.OfPoint(c3); - } - List points = new List() { c0, c1, c1, c2, c2, c3, c3, c0 }; - - // Calculated correct arc angles based on door orientation. - double adjustedAngle = inside ? angle : -angle; - double anchorAngle = leftSide ? 180 : 0; - double endAngle = leftSide ? 180 - adjustedAngle : adjustedAngle; - if (endAngle < 0) - { - endAngle = 360 + endAngle; - anchorAngle = 360; - } - - // If arc is constructed from bigger angle to smaller is will have incorrect domain - // with max being smaller than min and negative length. - // ToPolyline will return 0 points for it. - // Until it's fixed angles should be aligned manually. - bool flipEnds = endAngle < anchorAngle; - if (flipEnds) - { - (anchorAngle, endAngle) = (endAngle, anchorAngle); - } - - // Draw the arc from closed door to opened door. - Arc arc = new Arc(c0, doorWidth, anchorAngle, endAngle); - var tessalatedArc = arc.ToPolyline((int)(Math.Abs(angle) / 2)); - for (int i = 0; i < tessalatedArc.Vertices.Count - 1; i++) - { - points.Add(tessalatedArc.Vertices[i]); - points.Add(tessalatedArc.Vertices[i + 1]); - } - - return points; - } - - /// - /// Update the representations. - /// - public override void UpdateRepresentations() - { - Vector3 left = Vector3.XAxis * ClearWidth / 2; - Vector3 right = Vector3.XAxis.Negate() * ClearWidth / 2; - - var doorPolygon = new Polygon(new List() { - left + Vector3.YAxis * DOOR_THICKNESS, - left - Vector3.YAxis * DOOR_THICKNESS, - right - Vector3.YAxis * DOOR_THICKNESS, - right + Vector3.YAxis * DOOR_THICKNESS}); - - var doorPolygons = new List(); - - if (OpeningSide == DoorOpeningSide.DoubleDoor) - { - doorPolygons = doorPolygon.Split(new Polyline(new Vector3(0, DOOR_THICKNESS, 0), new Vector3(0, -DOOR_THICKNESS, 0))); - } - else - { - doorPolygons.Add(doorPolygon); - } - - var doorExtrusions = new List(); - - foreach (var polygon in doorPolygons) - { - var doorExtrude = new Extrude(new Profile(polygon.Offset(-0.005)[0]), ClearHeight, Vector3.ZAxis); - doorExtrusions.Add(doorExtrude); - } - - var frameLeft = left + Vector3.XAxis * DOOR_FRAME_WIDTH; - var frameRight = right - Vector3.XAxis * DOOR_FRAME_WIDTH; - var frameOffset = Vector3.YAxis * DOOR_FRAME_THICKNESS; - var doorFramePolygon = new Polygon(new List() { - left + Vector3.ZAxis * ClearHeight - frameOffset, - left - frameOffset, - frameLeft - frameOffset, - frameLeft + Vector3.ZAxis * (ClearHeight + DOOR_FRAME_WIDTH) - frameOffset, - frameRight + Vector3.ZAxis * (ClearHeight + DOOR_FRAME_WIDTH) - frameOffset, - frameRight - frameOffset, - right - frameOffset, - right + Vector3.ZAxis * ClearHeight - frameOffset }); - var doorFrameExtrude = new Extrude(new Profile(doorFramePolygon), DOOR_FRAME_THICKNESS * 2, Vector3.YAxis); - - Representation.SolidOperations.Clear(); - Representation.SolidOperations.Add(doorFrameExtrude); - foreach (var extrusion in doorExtrusions) - { - Representation.SolidOperations.Add(extrusion); - } - } - - private Vector3 GetClosestValidDoorPos(Line wallLine, Vector3 currentPosition) - { - var fullWidth = ClearWidth + DOOR_FRAME_WIDTH * 2; - double wallWidth = wallLine.Length(); - Vector3 p1 = wallLine.PointAt(0.5 * fullWidth); - Vector3 p2 = wallLine.PointAt(wallWidth - 0.5 * fullWidth); - var reducedWallLine = new Line(p1, p2); - return currentPosition.ClosestPointOn(reducedWallLine); - } - - private static double WidthWithoutFrame(double internalWidth, DoorOpeningSide openingSide) - { - switch (openingSide) - { - case DoorOpeningSide.LeftHand: - case DoorOpeningSide.RightHand: - return internalWidth; - case DoorOpeningSide.DoubleDoor: - return internalWidth * 2; - } - return 0; - } - } -} diff --git a/Elements/src/Opening.cs b/Elements/src/Opening.cs index a14069e16..80be2d6e6 100644 --- a/Elements/src/Opening.cs +++ b/Elements/src/Opening.cs @@ -90,7 +90,8 @@ public override void UpdateRepresentations() { this.Representation.SolidOperations.Clear(); var depth = this.DepthFront + this.DepthBack; - var op = new Extrude(this.Perimeter, depth, Normal, true); + var depthBackTransform = new Transform(-DepthBack * Normal); + var op = new Extrude(this.Perimeter.TransformedPolygon(depthBackTransform), depth, Normal, true); this.Representation.SolidOperations.Add(op); } } diff --git a/Elements/src/StandardWall.cs b/Elements/src/StandardWall.cs index 40b75851d..bebe933e4 100644 --- a/Elements/src/StandardWall.cs +++ b/Elements/src/StandardWall.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Elements.Geometry; using Elements.Geometry.Solids; +using Elements; namespace Elements { @@ -117,7 +118,8 @@ public Opening AddOpening(Polygon perimeter, double x, double y, double depthFro private Transform GetOpeningTransform(double x, double y) { var xAxis = this.CenterLine.Direction(); - var openingTransform = new Transform(this.CenterLine.Start + xAxis * x + Vector3.ZAxis * y, xAxis, xAxis.Cross(Vector3.ZAxis)); + var outOfPlane = xAxis.Cross(Vector3.ZAxis); + var openingTransform = new Transform(this.CenterLine.Start + xAxis * x + Vector3.ZAxis * y - outOfPlane, xAxis, xAxis.Cross(Vector3.ZAxis)); return openingTransform; } @@ -132,5 +134,16 @@ public override void UpdateRepresentations() var profile = new Polygon(new[] { e1.Start, e1.End, e2.End, e2.Start }); this.Representation.SolidOperations.Add(new Extrude(profile, this.Height, Vector3.ZAxis, false)); } + + /// + /// Creates an opening that suits . + /// + /// Properties of will be used to create an opening. + public void AddDoorOpening(Door door) + { + var halfThickness = 0.5 * Thickness; + var opening = door.CreateDoorOpening(halfThickness, halfThickness, false); + Openings.Add(opening); + } } } \ No newline at end of file diff --git a/Elements/test/DoorTest.cs b/Elements/test/DoorTest.cs new file mode 100644 index 000000000..a4ee8ace6 --- /dev/null +++ b/Elements/test/DoorTest.cs @@ -0,0 +1,26 @@ +using Elements.Geometry; +using Elements.Tests; +using Elements; +using Xunit; + +namespace Elements +{ + public class DoorTest : ModelTest + { + [Fact, Trait("Category", "Examples")] + public void MakeDoorElement() + { + this.Name = nameof(MakeDoorElement); + + var line = new Line(new Vector3(0, 0, 0), new Vector3(10, 10, 0)); + var wall = new StandardWall(line, 0.1, 3.0); + var door = new Door(wall.CenterLine, 0.5, 2.0, 2.0, Door.DOOR_THICKNESS, DoorOpeningSide.LeftHand, DoorOpeningType.SingleSwing); + wall.AddDoorOpening(door); + + Assert.Single(wall.Openings); + + this.Model.AddElement(wall); + Model.AddElement(door); + } + } +}