diff --git a/EasyEDA-Loader/Component.cs b/EasyEDA-Loader/Component.cs index c6f31d7..c1e763f 100644 --- a/EasyEDA-Loader/Component.cs +++ b/EasyEDA-Loader/Component.cs @@ -149,6 +149,31 @@ public class ComponentInfo [JsonProperty("packageDetail")] public PackageDetail PackageDetail { get; set; } + + // Multi-subpart components (op-amps with multiple gates, big MCUs like STM32MP135 whose + // pins are split across several drawn "parts") carry their real pin/shape data here + // instead of in the top-level dataStr - the top-level one is empty ("shape": []) for + // these parts. See EasyEDALoader.cs for how this gets used. + [JsonProperty("subparts")] + public List Subparts { get; set; } + } + + public class Subpart + { + [JsonProperty("uuid")] + public string Uuid { get; set; } + + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("dataStr")] + public SymbolData DataStr { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("thumb")] + public string Thumb { get; set; } } public class Root diff --git a/EasyEDA-Loader/EEPCB.cs b/EasyEDA-Loader/EEPCB.cs index 084f463..3929b28 100644 --- a/EasyEDA-Loader/EEPCB.cs +++ b/EasyEDA-Loader/EEPCB.cs @@ -1,6 +1,7 @@ using PCB; using System; +using System.Collections.Generic; namespace EasyEDA_Loader { @@ -132,7 +133,24 @@ public static IPCB_Text3 CreateText(IPCB_LibComponent c, TLayerConstant layer, s return textObject; } - public static IPCB_ComponentBody CreateComponentBody(IPCB_LibComponent c, string fileName, double rx, double ry, double rz, double x, double y, double z) + public static IPCB_Region CreateSolidRegion(IPCB_LibComponent c, TLayerConstant layer, List points) + { + var contour = AltiumApi.GlobalVars.PCBServer.PCBContourFactory(); + if (contour == null) return null; + foreach (var p in points) + { + contour.AddPoint(AltiumApi.MmToCoord(p.X) + c.GetState_XLocation(), AltiumApi.MmToCoord(p.Y) + c.GetState_YLocation()); + } + + var region = AltiumApi.GlobalVars.PCBServer.PCBObjectFactory(TObjectId.eRegionObject, TDimensionKind.eNoDimension, TObjectCreationMode.eCreate_Default) as IPCB_Region; + if (region == null) return null; + region.SetState_Kind(TRegionKind.eRegionKind_Copper); + region.SetOutlineContour(contour); + region.SetState_V7Layer(new V7_Layer(layer)); + return region; + } + + public static IPCB_ComponentBody CreateComponentBody(IPCB_LibComponent c, string fileName, double rx, double ry, double rz, double x, double y, double z, TLayerConstant layer) { var stepModel = AltiumApi.GlobalVars.PCBServer.PCBObjectFactory(TObjectId.eComponentBodyObject, TDimensionKind.eNoDimension, TObjectCreationMode.eCreate_Default) as IPCB_ComponentBody; if (stepModel == null) return null; @@ -140,6 +158,11 @@ public static IPCB_ComponentBody CreateComponentBody(IPCB_LibComponent c, string if (model == null) return null; model.SetState(rx, ry, rz, AltiumApi.MmToCoord(z)); stepModel.SetModel(model); + // Every other shape in this file (pads, tracks, arcs, text) sets its own layer via + // SetState_V7Layer - this call was missing entirely for component bodies, leaving a + // freshly-created body at whatever the internal default layer is instead of the side + // (top/bottom) the SVGNODE's own layerid actually specifies. + stepModel.SetState_V7Layer(new V7_Layer(layer)); // Model is created at the bottom-left origin of the board, so we need to offset it stepModel.MoveByXY(AltiumApi.MmToCoord(x) + c.GetState_Board().GetState_XOrigin(), AltiumApi.MmToCoord(y) + c.GetState_Board().GetState_YOrigin()); return stepModel; diff --git a/EasyEDA-Loader/EESCH.cs b/EasyEDA-Loader/EESCH.cs index 78bcb32..e02a96d 100644 --- a/EasyEDA-Loader/EESCH.cs +++ b/EasyEDA-Loader/EESCH.cs @@ -1,5 +1,6 @@ using EDP; using SCH; +using System.Collections.Generic; using System.Windows.Forms; namespace EasyEDA_Loader @@ -161,6 +162,38 @@ public static void CreateLine(ISch_Lib schLib, ISch_Component c, double x1, doub c.AddSchObject(line); } + // For filled schematic graphics (EasyEDA's PL/PG/PT shapes - polylines drawn closed, + // polygons, and paths, e.g. diode arrows), which were previously parsed but never drawn. + // ISch_Polygon (shared by ISch_Polyline) exposes SetState_VerticesCount + SetState_Vertex + // for building the outline point-by-point. Filled the same way CreateRectangle fills itself. + public static void CreatePolygon(ISch_Lib schLib, ISch_Component c, List points) + { + if (points == null || points.Count < 3) + return; + var polygon = AltiumApi.GlobalVars.SCHServer.SchObjectFactory(SCH.TObjectId.ePolygon, SCH.TObjectCreationMode.eCreate_Default) as ISch_Polygon; + if (polygon == null) + return; + + polygon.SetState_VerticesCount(points.Count); + for (int i = 0; i < points.Count; i++) + { + // SetState_Vertex is 1-indexed, not 0-indexed - without the "+ 1" the last vertex + // silently defaults to (0,0) and the rest are shifted one slot from what was set. + polygon.SetState_Vertex(i + 1, new DXP.Point + { + X = AltiumApi.MilsToCoord(points[i].X), + Y = AltiumApi.MilsToCoord(points[i].Y) + }); + } + polygon.SetState_LineWidth(TSize.eSmall); + polygon.SetState_Color(0); + polygon.SetState_AreaColor(0); + polygon.SetState_IsSolid(true); + polygon.SetState_OwnerPartId(schLib.GetState_CurrentSchComponentPartId()); + polygon.SetState_OwnerPartDisplayMode(schLib.GetState_CurrentSchComponentDisplayMode()); + c.AddSchObject(polygon); + } + public static void AssignFootprint(ISch_Component c, string libraryPath, string modelName, string modelMapping) { var modelType = "PCBLIB"; diff --git a/EasyEDA-Loader/EasyEDALoader.cs b/EasyEDA-Loader/EasyEDALoader.cs index ca442e3..212de92 100644 --- a/EasyEDA-Loader/EasyEDALoader.cs +++ b/EasyEDA-Loader/EasyEDALoader.cs @@ -99,7 +99,13 @@ private void Run( var root = selection.Root; var owner_id = root.Component.Owner.Uuid; var ee_footprint = root.Component.PackageDetail.Footprint; + // The top-level Symbol (root.Component.Symbol) has an empty shape list for + // multi-subpart components, but its Head.Parameters (Name/Pre) are still the + // correct ones for the component as a whole - a subpart's own Pre has a + // per-part suffix ("U?.1") that would be wrong here. Real per-part + // pins/shapes come from Subparts instead. var ee_symbol = root.Component.Symbol; + bool isMultiPart = root.Component.Subparts != null && root.Component.Subparts.Count > 0; string package = ee_footprint.Head.Parameters.Package; EeFootprint3dModel model = selection.Include3dModel ? ee_footprint.GetModel() : null; @@ -154,8 +160,22 @@ private void Run( var component = EESCH.CreateComponent(partName, description, ee_symbol.Head.Parameters.Pre); if (schLib != null && component != null) { + // Moved SetState_Current_SchComponent to before drawing, not after. + // SetState_CurrentSchComponentPartId (used per-subpart below to draw + // real multi-part components) needs the library's "current component" + // context to already be this component while per-part IDs are being + // set - otherwise every subpart's pins land in Part A regardless of + // the part ID set. + schLib.SetState_Current_SchComponent(component); AltiumApi.GlobalVars.PCBServer.PreProcess(); - SymbolDrawing.CreateComponent(schLib, component, pcbLibraryPath, package, ee_symbol); + if (isMultiPart) + { + SymbolDrawing.CreateMultiPartComponent(schLib, component, pcbLibraryPath, package, root.Component.Subparts); + } + else + { + SymbolDrawing.CreateComponent(schLib, component, pcbLibraryPath, package, ee_symbol); + } if (productInfo?.Parameters != null) { @@ -165,8 +185,21 @@ private void Run( } } + // Default Comment (shown as "*" until set). Comment isn't a plain + // parameter you add - it's a dedicated, always-present object + // retrieved via GetState_SchComment(), the same pattern already used + // above for the designator (GetState_SchDesignator()). Adding a + // separate parameter named "Comment" would create an unrelated extra + // parameter and leave the real comment field at its "*" default. + if (!string.IsNullOrEmpty(ee_symbol.Head.Parameters.ManufacturerPart)) + { + var comment = component.GetState_SchComment(); + comment.SetState_Text(ee_symbol.Head.Parameters.ManufacturerPart); + comment.SetState_ShowName(false); + comment.SetState_IsHidden(false); + } + AltiumApi.GlobalVars.PCBServer.PostProcess(); - schLib.SetState_Current_SchComponent(component); schLib.GraphicallyInvalidate(); schDocument.DoFileSave("SchLib"); } diff --git a/EasyEDA-Loader/FootprintData.cs b/EasyEDA-Loader/FootprintData.cs index 06eb202..1466ffd 100644 --- a/EasyEDA-Loader/FootprintData.cs +++ b/EasyEDA-Loader/FootprintData.cs @@ -250,7 +250,7 @@ public override EeFootprintShape ReadJson(JsonReader reader, Type objectType, Ee case "SVGNODE": return EeFootprint3dModel.FromString(raw); case "SOLIDREGION": - return null; + return EeFootprintSolidRegion.FromString(raw); } } diff --git a/EasyEDA-Loader/FootprintShapes/EeFootprint3dModel.cs b/EasyEDA-Loader/FootprintShapes/EeFootprint3dModel.cs index e83b785..dc45bf3 100644 --- a/EasyEDA-Loader/FootprintShapes/EeFootprint3dModel.cs +++ b/EasyEDA-Loader/FootprintShapes/EeFootprint3dModel.cs @@ -14,11 +14,13 @@ public static EeFootprint3dModel FromString(string data) { var parts = data.Split(new[] { "~" }, StringSplitOptions.None); SvgNode node = JsonConvert.DeserializeObject(parts[1]); - var originParts = node.Attrs.COrigin.Split(new[] { "," }, StringSplitOptions.None); var rotationParts = node.Attrs.CRotation.Split(new[] { "," }, StringSplitOptions.None); - double CenterX = double.Parse(originParts[0], System.Globalization.CultureInfo.InvariantCulture); - double CenterY = double.Parse(originParts[1], System.Globalization.CultureInfo.InvariantCulture); + // c_origin is wherever the 3D CAD tool's own workspace zero happened to be when the + // STEP model was authored - it has no relationship to where the model should be + // placed on the footprint, so it's not used here. The model is placed at the + // footprint's own local origin instead (the same reference point every other shape + // is anchored to via ConvertX/ConvertY). // Center compute, shouldnt be needed, the GL engine does this for verification of somekind /* @@ -49,12 +51,13 @@ public static EeFootprint3dModel FromString(string data) { Name = node.Attrs.Title, Uuid = node.Attrs.Uuid, + LayerId = node.Attrs.Layerid, Height = ConvertToMM(double.Parse(node.Attrs.CHeight, System.Globalization.CultureInfo.InvariantCulture)), Width = ConvertToMM(double.Parse(node.Attrs.CWidth, System.Globalization.CultureInfo.InvariantCulture)), Translation = new Vec3 { - X = ConvertToMM(CenterX), - Y = ConvertToMM(CenterY), + X = 0, + Y = 0, Z = ConvertToMM(double.Parse(node.Attrs.Z, System.Globalization.CultureInfo.InvariantCulture)) }, Rotation = new Vec3 @@ -105,13 +108,14 @@ public override bool AddToComponent(IPCB_LibComponent c, EeFootprintContext ctx) string temp = Path.Combine(Path.GetTempPath(), $"{Uuid}.step"); File.WriteAllBytes(temp, modelTask.Result); - // The translation is not quite right, the values shown in "3D Model Manager" are available from the Search API as "3D Model Transform" - // The Y axis is slightly off and I cannot figure out the missing piece maybe combination of rotation/y-flip/re-center causing this to be wrong - // Where the mesh starts X,Y in the EE model manager seems to differ from the computed one here // The Z is the lowest Z of the mesh plus the Z offset (hence why we download the Raw mesh and search for the lowest vert.z as this offset is not part of the info) - // Will leave this for now as it's "close enough" most of the time to only need a nudge by a few 10ths of a millimeter - var body = EEPCB.CreateComponentBody(c, temp, Rotation.X, Rotation.Y, Rotation.Z, ConvertX(Translation.X, ctx), ConvertY(Translation.Y, ctx), Translation.Z + heightTask.Result); + // Translation.X/Y are already local-origin coordinates (see FromString) - they land + // directly on the footprint's own origin without going through ConvertX/ConvertY, + // which would otherwise apply the bounding-box centering a second time. + var layer = ctx.Layers.GetLayer(LayerId); + var targetLayer = layer != null ? EEPCB.EELayerToAltium(layer.Name) : TLayerConstant.eTopLayer; + var body = EEPCB.CreateComponentBody(c, temp, Rotation.X, Rotation.Y, Rotation.Z, Translation.X, Translation.Y, Translation.Z + heightTask.Result, targetLayer); EEPCB.AddToPCB(c, body); File.Delete(temp); @@ -127,6 +131,7 @@ public override bool AddToComponent(IPCB_LibComponent c, EeFootprintContext ctx) public string Name { get; set; } public string Uuid { get; set; } + public string LayerId { get; set; } public double Height { get; set; } public double Width { get; set; } public Vec3 Translation { get; set; } diff --git a/EasyEDA-Loader/FootprintShapes/EeFootprintPad.cs b/EasyEDA-Loader/FootprintShapes/EeFootprintPad.cs index f3ecaa4..597ec26 100644 --- a/EasyEDA-Loader/FootprintShapes/EeFootprintPad.cs +++ b/EasyEDA-Loader/FootprintShapes/EeFootprintPad.cs @@ -31,6 +31,8 @@ public static EeFootprintPad FromString(string data) HolePoints = EePoint.ListFromString(parts[14]), IsPlated = ParseBoolean(parts[15]), IsLocked = ParseBoolean(parts[16]), + PasteMaskExpansion = ConvertToMM(double.Parse(parts[17], System.Globalization.CultureInfo.InvariantCulture)), + SolderMaskExpansion = ConvertToMM(double.Parse(parts[18], System.Globalization.CultureInfo.InvariantCulture)), }; } public override List AddToCanvas(Canvas c, EeFootprintContext ctx) @@ -173,6 +175,19 @@ public override bool AddToComponent(IPCB_LibComponent c, EeFootprintContext ctx) } pad.SetState_HoleWidth(AltiumApi.MmToCoord(HoleLength)); } + // Same GetState_Cache/SetState_Cache pattern EeFootprintHole.cs already uses to + // zero a hole's solder mask expansion, reused here for the pad's own solder/paste + // mask expansion overrides (previously-parsed fields that were never read). + // EasyEDA only carries a single solder value (no separate top/bottom), so both + // sides are set the same, matching the convention EeFootprintHole.cs already uses. + var padCache = pad.GetState_Cache(); + padCache.SolderMaskExpansionValid = TCacheState.eCacheManual; + padCache.UseSeparateExpansions = true; + padCache.SolderMaskExpansion = AltiumApi.MmToCoord(SolderMaskExpansion); + padCache.SolderMaskBottomExpansion = AltiumApi.MmToCoord(SolderMaskExpansion); + padCache.PasteMaskExpansionValid = TCacheState.eCacheManual; + padCache.PasteMaskExpansion = AltiumApi.MmToCoord(PasteMaskExpansion); + pad.SetState_Cache(padCache); EEPCB.AddToPCB(c, pad); } catch (Exception ex) @@ -201,6 +216,8 @@ public override bool AddToComponent(IPCB_LibComponent c, EeFootprintContext ctx) public List HolePoints { get; set; } public bool IsPlated { get; set; } public bool IsLocked { get; set; } + public double SolderMaskExpansion { get; set; } + public double PasteMaskExpansion { get; set; } } } diff --git a/EasyEDA-Loader/FootprintShapes/EeFootprintSolidRegion.cs b/EasyEDA-Loader/FootprintShapes/EeFootprintSolidRegion.cs new file mode 100644 index 0000000..b5c43ed --- /dev/null +++ b/EasyEDA-Loader/FootprintShapes/EeFootprintSolidRegion.cs @@ -0,0 +1,106 @@ +using PCB; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace EasyEDA_Loader +{ + public class EeFootprintSolidRegion : EeFootprintShape + { + public static EeFootprintSolidRegion FromString(string data) + { + var parts = data.Split(new[] { "~" }, StringSplitOptions.None); + return new EeFootprintSolidRegion + { + LayerId = parts[1], + PathData = parts[3], + FillRule = parts[4], + Id = parts[5], + }; + } + + // EasyEDA's SOLIDREGION path data is a simple SVG-subset - only "M x y", "L x y" and a + // closing "Z" appear in every real sample (no curves/arcs within a region's own outline). + // The command letter is glued directly onto the first coordinate with no space (e.g. + // "M383.874048 315.27556", not "M 383.874048 ..."). Strips a leading command letter from + // each token instead of comparing whole tokens (which would never match and silently drop + // that coordinate), then pairs up every remaining number regardless of which original + // token it came from. + public static List ParsePath(string pathData) + { + var numbers = new List(); + var tokens = pathData.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var rawToken in tokens) + { + string token = rawToken; + if (token.Length > 0 && (token[0] == 'M' || token[0] == 'L' || token[0] == 'Z' || token[0] == 'z')) + { + token = token.Substring(1); + } + if (string.IsNullOrEmpty(token)) + continue; + if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out double num)) + { + numbers.Add(num); + } + } + + var points = new List(); + for (int i = 0; i + 1 < numbers.Count; i += 2) + { + points.Add(new EePoint { X = numbers[i], Y = numbers[i + 1] }); + } + return points; + } + + public override bool AddToComponent(IPCB_LibComponent c, EeFootprintContext ctx) + { + // "cutout" regions are meant to subtract from the preceding "solid" region (EasyEDA + // emits them as adjacent pairs sharing the same local coordinates, e.g. a paste-mask + // pad split into an outer solid block and an inner cutout to leave a border) - not + // implemented. Rendering a cutout as its own standalone solid shape would be actively + // wrong (a visible extra blob where there should be a hole), so these are skipped + // entirely rather than guessed at. + if (FillRule != "solid") + return true; + + var layer = ctx.Layers.GetLayer(LayerId); + if (layer == null) + return true; + + try + { + var targetLayer = EEPCB.EELayerToAltium(layer.Name); + + var rawPoints = ParsePath(PathData); + if (rawPoints.Count < 3) + return true; // not a real polygon + + var localPoints = new List(rawPoints.Count); + foreach (var p in rawPoints) + { + localPoints.Add(new EePoint { X = ConvertX(ConvertToMM(p.X), ctx), Y = ConvertY(ConvertToMM(p.Y), ctx) }); + } + + var region = EEPCB.CreateSolidRegion(c, targetLayer, localPoints); + if (region != null) + { + EEPCB.AddToPCB(c, region); + } + } + catch (Exception ex) + { + if (ctx.Exception != null && !ctx.Exception(ex)) + { + return false; + } + } + return true; + } + + public string LayerId { get; set; } + public string PathData { get; set; } + public string FillRule { get; set; } + public string Id { get; set; } + } +} diff --git a/EasyEDA-Loader/SymbolDrawing.cs b/EasyEDA-Loader/SymbolDrawing.cs index b379908..5c0e7f1 100644 --- a/EasyEDA-Loader/SymbolDrawing.cs +++ b/EasyEDA-Loader/SymbolDrawing.cs @@ -73,6 +73,8 @@ static public TRotationBy90 FromOrientation(PinOrientation orientation) public double X { get; set; } public double Y { get; set; } + public double RawX { get; set; } + public double RawY { get; set; } public string Designator { get; set; } public string Name { get; set; } public TRotationBy90 Orientation { get; set; } @@ -83,6 +85,99 @@ static public TRotationBy90 FromOrientation(PinOrientation orientation) public class SymbolDrawing { + // Raw space-separated "x y x y ..." pairs, no unit conversion (stays in the same raw + // mil-like JSON units as BBox/rect throughout this file) - deliberately not reusing + // EePoint.ListFromString, which converts to mm internally and would mix units. + static List ParsePointPairs(string data) + { + var points = new List(); + var tokens = data.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i + 1 < tokens.Length; i += 2) + { + if (double.TryParse(tokens[i], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double x) + && double.TryParse(tokens[i + 1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double y)) + { + points.Add(new EePoint { X = x, Y = y }); + } + } + return points; + } + + // Same robust letter-stripping approach as EeFootprintSolidRegion.ParsePath - strips a + // leading SVG command letter from each token individually rather than comparing whole + // tokens, so it's correct whether the letter is glued to the first coordinate (as in PCB + // SOLIDREGION data) or space-separated from it (as seen in this schematic PT/path data, + // e.g. "M 414 280 L 420 290 L 427 280 Z "). Only M/L/Z seen in any real sample - no curves. + static List ParseSvgPath(string pathData) + { + var numbers = new List(); + var tokens = pathData.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var rawToken in tokens) + { + string token = rawToken; + if (token.Length > 0 && (token[0] == 'M' || token[0] == 'L' || token[0] == 'Z' || token[0] == 'z')) + { + token = token.Substring(1); + } + if (string.IsNullOrEmpty(token)) + continue; + if (double.TryParse(token, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double num)) + { + numbers.Add(num); + } + } + var points = new List(); + for (int i = 0; i + 1 < numbers.Count; i += 2) + { + points.Add(new EePoint { X = numbers[i], Y = numbers[i + 1] }); + } + return points; + } + + // Pins are laid out synthetically (grid position by count/order - see LayoutPins below), + // completely independent of the JSON's own raw pin coordinates. Graphic shapes (diode + // arrows, etc.), which ARE only meaningful at their original raw coordinates, can't be + // drawn 1:1 without landing somewhere unrelated to where the (synthetically repositioned) + // pins actually ended up. EasyEDA's own graphic coordinates literally coincide with pin + // raw coordinates in the source data, so calibrating the mapping against the actual pins + // used for a given axis (rather than the whole bounding box) makes shapes align with + // those pins exactly, not approximately. Falls back to a bbox-proportional approach + // per-axis only when there aren't at least 2 pins on that axis with distinct raw + // positions to calibrate from (e.g. a component with only top/bottom pins has nothing to + // calibrate Y from). + // + // Known limitation: simple 2-point (min/max) calibration per axis, not a full + // least-squares fit - fine for pins laid out linearly along one axis (which is how + // LayoutPins always arranges them), but wouldn't average out well for some hypothetical + // component with non-uniform raw pin spacing on one axis. + static (double Scale, double Offset) ComputeAxisCalibration(List raw, List synthetic, double bboxMin, double bboxSize, double rectMin, double rectSize) + { + double rawMin = double.PositiveInfinity, rawMax = double.NegativeInfinity; + double synMin = 0, synMax = 0; + bool found = false; + for (int i = 0; i < raw.Count; i++) + { + if (raw[i] < rawMin) { rawMin = raw[i]; synMin = synthetic[i]; } + if (raw[i] > rawMax) { rawMax = raw[i]; synMax = synthetic[i]; } + found = true; + } + if (found && rawMax > rawMin) + { + double scale = (synMax - synMin) / (rawMax - rawMin); + double offset = synMin - rawMin * scale; + return (scale, offset); + } + // Fallback: bbox-proportional. + double bboxScale = bboxSize > 0 ? rectSize / bboxSize : 0; + double bboxOffset = rectMin - bboxMin * bboxScale; + return (bboxScale, bboxOffset); + } + + static EePoint MapToSyntheticRect(EePoint raw, (double Scale, double Offset) calibX, (double Scale, double Offset) calibY) + { + return new EePoint { X = raw.X * calibX.Scale + calibX.Offset, Y = raw.Y * calibY.Scale + calibY.Offset }; + } + static void DistributeEvenly(List source, List> targets) { // Keep track of how many items are in each target list @@ -306,6 +401,8 @@ static public (AltiumSymbolRectangle, List) LayoutPins(List Shapes) } } - static public void CreateComponent(ISch_Lib schLib, ISch_Component component, string pcbLibraryPath, string package, SymbolData ee_symbol) + // Draws one part's pins/rectangle/graphics onto the component - does NOT assign + // footprint or add the component to the library, since for a multi-part component those + // only happen once, not once per part. + static public void DrawPart(ISch_Lib schLib, ISch_Component component, SymbolData ee_symbol) { (var rect, var pins) = SymbolDrawing.LayoutPins(ee_symbol.Shapes); EESCH.CreateRectangle(schLib, component, rect.X1, rect.Height - rect.Y1, rect.X2, rect.Height - rect.Y2); @@ -343,6 +443,102 @@ static public void CreateComponent(ISch_Lib schLib, ISch_Component component, st { EESCH.CreatePin(schLib, component, pin.X, rect.Height - pin.Y, pin.Designator, pin.Name, pin.Orientation, pin.Length, pin.PinType, pin.ShowName, null); } + + // Calibrate the graphic-shape mapping against the actual pins (see + // ComputeAxisCalibration above for why) instead of the whole bounding box. TOP/BOTTOM + // pins vary in X (Orientation eRotate90/eRotate270), LEFT/RIGHT pins vary in Y + // (eRotate180/eRotate0). + var xCalibRaw = new List(); + var xCalibSyn = new List(); + var yCalibRaw = new List(); + var yCalibSyn = new List(); + foreach (var p in pins) + { + if (p.Orientation == TRotationBy90.eRotate90 || p.Orientation == TRotationBy90.eRotate270) + { + xCalibRaw.Add(p.RawX); + xCalibSyn.Add(p.X); + } + else + { + yCalibRaw.Add(p.RawY); + yCalibSyn.Add(p.Y); + } + } + var calibX = ComputeAxisCalibration(xCalibRaw, xCalibSyn, ee_symbol.BoundingBox.X, ee_symbol.BoundingBox.Width, rect.X1, rect.Width); + var calibY = ComputeAxisCalibration(yCalibRaw, yCalibSyn, ee_symbol.BoundingBox.Y, ee_symbol.BoundingBox.Height, rect.Y1, rect.Height); + + // Previously-ignored graphic shapes (diode arrows, etc. - parsed since the start of + // this project, but never drawn, same gap as PCB SOLIDREGION shapes). See + // MapToSyntheticRect above for why these go through a scale-to-fit mapping instead of + // their raw coordinates directly. + foreach (var shape in ee_symbol.Shapes) + { + // Exact-type check, not is/OfType<>() - EeSymbolPolygon inherits EeSymbolPolyline, + // and a closed/filled polygon must not be drawn as an open polyline outline. + if (shape.GetType() == typeof(EeSymbolPolyline)) + { + var polyline = (EeSymbolPolyline)shape; + var rawPts = ParsePointPairs(polyline.Points); + var mapped = new List(); + foreach (var p in rawPts) + mapped.Add(MapToSyntheticRect(p, calibX, calibY)); + for (int i = 1; i < mapped.Count; i++) + { + EESCH.CreateLine(schLib, component, + mapped[i - 1].X, rect.Height - mapped[i - 1].Y, + mapped[i].X, rect.Height - mapped[i].Y); + } + } + else if (shape is EeSymbolPolygon polygon) + { + var rawPts = ParsePointPairs(polygon.Points); + var mapped = new List(); + foreach (var p in rawPts) + { + var m = MapToSyntheticRect(p, calibX, calibY); + mapped.Add(new EePoint { X = m.X, Y = rect.Height - m.Y }); + } + EESCH.CreatePolygon(schLib, component, mapped); + } + else if (shape is EeSymbolPath path) + { + var rawPts = ParseSvgPath(path.Paths); + var mapped = new List(); + foreach (var p in rawPts) + { + var m = MapToSyntheticRect(p, calibX, calibY); + mapped.Add(new EePoint { X = m.X, Y = rect.Height - m.Y }); + } + EESCH.CreatePolygon(schLib, component, mapped); + } + } + } + + static public void CreateComponent(ISch_Lib schLib, ISch_Component component, string pcbLibraryPath, string package, SymbolData ee_symbol) + { + DrawPart(schLib, component, ee_symbol); + EESCH.AssignFootprint(component, pcbLibraryPath, package, ""); + schLib.AddSchComponent(component); + } + + // Real multi-part support. Each subpart becomes a distinct, selectable Altium "part" + // within the one component (e.g. all 4 gates of a quad op-amp, or STM32MP135's 4 drawn + // pin groups) sharing one footprint - not just the first subpart's pins. + // SetState_PartCountNoPart0 (ISch_Component) and SetState_CurrentSchComponentPartId + // (ISch_Lib) declare the part count / select the part subsequently-created pins/shapes + // belong to. EESCH.CreatePin/CreateRectangle/CreateLine/CreatePolygon already tag + // themselves with schLib.GetState_CurrentSchComponentPartId() at creation time + // (unchanged, pre-existing code) - so setting the current part ID before drawing each + // subpart is sufficient. + static public void CreateMultiPartComponent(ISch_Lib schLib, ISch_Component component, string pcbLibraryPath, string package, List subparts) + { + component.SetState_PartCountNoPart0(subparts.Count); + for (int i = 0; i < subparts.Count; i++) + { + schLib.SetState_CurrentSchComponentPartId(i + 1); // Altium parts are 1-indexed + DrawPart(schLib, component, subparts[i].DataStr); + } EESCH.AssignFootprint(component, pcbLibraryPath, package, ""); schLib.AddSchComponent(component); }