From b47a3f521fe9abb6ae87652a22702e8317634b86 Mon Sep 17 00:00:00 2001 From: xtof Date: Fri, 18 Sep 2026 17:17:41 +0200 Subject: [PATCH 1/3] working on shapes --- .gitignore | 1 + .../najaeda_server.cpython-314.pyc | Bin 0 -> 30131 bytes .../__pycache__/test_server.cpython-314.pyc | Bin 0 -> 5864 bytes scripts/gate_shapes_example.v | 38 +++ scripts/najaeda_server.py | 34 +++ src/AppLogic.cpp | 1 + src/EquipotentialView.cpp | 26 +-- src/EquipotentialView.h | 1 + src/LocalSNLProvider.cpp | 60 ++++- src/SchematicView.cpp | 219 +++++++++++++++++- src/Types.cpp | 2 + src/Types.h | 65 +++++- 12 files changed, 407 insertions(+), 40 deletions(-) create mode 100644 scripts/__pycache__/najaeda_server.cpython-314.pyc create mode 100644 scripts/__pycache__/test_server.cpython-314.pyc create mode 100644 scripts/gate_shapes_example.v diff --git a/.gitignore b/.gitignore index 93915dc..25b6f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build* .DS_Store .vscode/settings.json imgui.ini +.cache/ # Private, local-only working notes (never shared/committed). notes/ diff --git a/scripts/__pycache__/najaeda_server.cpython-314.pyc b/scripts/__pycache__/najaeda_server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e35b68b30f1634e0e1f01b33c72192ace92bb7b GIT binary patch literal 30131 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/test_server.cpython-314.pyc b/scripts/__pycache__/test_server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4699302d60d768a0502fce5c1c005741d5f0f992 GIT binary patch literal 5864 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 literal 0 HcmV?d00001 diff --git a/scripts/gate_shapes_example.v b/scripts/gate_shapes_example.v new file mode 100644 index 0000000..67fdcf7 --- /dev/null +++ b/scripts/gate_shapes_example.v @@ -0,0 +1,38 @@ +// Small gate-level example exercising one instance of every PrimitiveType +// the standard shapes library (SchematicView::drawInstance()) recognizes -- +// AND/NAND/OR/NOR/XOR/XNOR at a few input arities, INV/BUF, and a DFF -- +// using cell names from the Nangate library (NangateOpenCellLibrary_typical.lib) +// in this directory. Load with: +// naja-schematic-standalone scripts/gate_shapes_example.v \ +// --liberty scripts/NangateOpenCellLibrary_typical.lib + +module top( + input a, b, c, d, + input clk, rst_n, + output y_and2, y_and3, y_and4, + output y_nand2, y_nand3, + output y_or2, y_nor2, + output y_xor2, y_xnor2, + output y_inv, y_buf, + output y_q, y_qn +); + + AND2_X1 u_and2 (.A1(a), .A2(b), .ZN(y_and2)); + AND3_X1 u_and3 (.A1(a), .A2(b), .A3(c), .ZN(y_and3)); + AND4_X1 u_and4 (.A1(a), .A2(b), .A3(c), .A4(d), .ZN(y_and4)); + + NAND2_X1 u_nand2 (.A1(a), .A2(b), .ZN(y_nand2)); + NAND3_X1 u_nand3 (.A1(a), .A2(b), .A3(c), .ZN(y_nand3)); + + OR2_X1 u_or2 (.A1(a), .A2(b), .ZN(y_or2)); + NOR2_X1 u_nor2 (.A1(a), .A2(b), .ZN(y_nor2)); + + XOR2_X1 u_xor2 (.A(a), .B(b), .Z(y_xor2)); + XNOR2_X1 u_xnor2 (.A(a), .B(b), .ZN(y_xnor2)); + + INV_X1 u_inv (.A(a), .ZN(y_inv)); + BUF_X1 u_buf (.A(a), .Z(y_buf)); + + DFFR_X1 u_dff (.D(a), .CK(clk), .RN(rst_n), .Q(y_q), .QN(y_qn)); + +endmodule diff --git a/scripts/najaeda_server.py b/scripts/najaeda_server.py index d6e34d8..feb2e27 100644 --- a/scripts/najaeda_server.py +++ b/scripts/najaeda_server.py @@ -49,6 +49,38 @@ def has_visible_primitive_instances(design): return any(not instance.getModel().isAssign() for instance in design.getPrimitiveInstances()) +# Best-effort gate/cell function classification from a design's name -- +# najaeda has no function/timing-arc API to ask directly (see PrimitiveType +# in Types.h). Checked as a case-insensitive prefix so common Liberty/ +# Verilog naming conventions ("AND2X1", "nand3_1", "DFFX2", ...) match +# regardless of the vendor-specific drive-strength/version suffix; arity +# itself is read from the instance's actual port count at render time, not +# parsed here. LocalSNLProvider.cpp's getPrimitiveType() mirrors this +# independently, same as the rest of the wire protocol (see CLAUDE.md). +_PRIMITIVE_TYPE_PREFIXES = ( + ("XNOR", "xnor"), + ("NAND", "nand"), + ("NOR", "nor"), + ("XOR", "xor"), + ("AND", "and"), + ("OR", "or"), + ("DFF", "dff"), + ("INV", "inv"), + ("NOT", "inv"), + ("BUF", "buf"), +) + +def get_primitive_type(model): + if model is None: + return "unknown" + if model.isAssign(): + return "assign" + name = model.getName().upper() + for prefix, ptype in _PRIMITIVE_TYPE_PREFIXES: + if name.startswith(prefix): + return ptype + return "unknown" + def is_anonymous_constant_net(net): # Anonymous scalar constant nets (1'b0/1'b1 tie-offs, e.g. an unconnected # input najaeda ties off implicitly) are structural noise, not @@ -86,6 +118,7 @@ def serialize_model(model, child_id, name, source_loc=None): "name": name, "child_id": child_id, "model_name": model.getName(), + "primitive_type": get_primitive_type(model), "design_ref": { "db_id": model.getDB().getID(), "library_id": model.getLibrary().getID(), @@ -303,6 +336,7 @@ async def handle_connection(websocket): "library_id": inst_model.getLibrary().getID(), "design_id": inst_model.getID(), }, + "primitive_type": get_primitive_type(inst_model), "has_instances": has_instances, "source_loc": get_source_loc(instTerm.getInstance()) }) diff --git a/src/AppLogic.cpp b/src/AppLogic.cpp index 8edb0ad..4cfcd22 100644 --- a/src/AppLogic.cpp +++ b/src/AppLogic.cpp @@ -218,6 +218,7 @@ void setupProvider(AppState& state) { ic.childId = c.value("child_id", 0u); if (c.contains("design_ref")) ic.designRef = c["design_ref"].get(); + ic.primitiveType = primitiveTypeFromString(c.value("primitive_type", std::string("unknown"))); ic.hasInstances = c.value("has_instances", false) || c.value("has_primitives", false); data.children.push_back(std::move(ic)); } diff --git a/src/EquipotentialView.cpp b/src/EquipotentialView.cpp index 6e8b93f..dd73c9d 100644 --- a/src/EquipotentialView.cpp +++ b/src/EquipotentialView.cpp @@ -40,6 +40,7 @@ struct Item { Direction direction = Direction::Inout; bool isTerm = false; DesignRef designRef{}; + PrimitiveType primitiveType = PrimitiveType::Unknown; unsigned termChildId = 0; std::optional termBit; std::vector pathIds; @@ -102,13 +103,6 @@ static float g_layoutNextY = 0.f; // Helpers // --------------------------------------------------------------------------- -// Extract the leaf segment from a slash-separated instance path. -// e.g. "top/sub/" → "" -static std::string leafSegment(const std::string& path) { - auto pos = path.rfind('/'); - return (pos == std::string::npos) ? path : path.substr(pos + 1); -} - // Strip a trailing bus-bit suffix ("Q[3]" -> "Q") so a pin label can be // matched against DiagnosisItem::terminal, which is always the base name. static std::string stripBusIndex(const std::string& label) { @@ -134,17 +128,6 @@ static std::string busLabel(const std::string& base, std::vector bits) { return base + " (*" + std::to_string(bits.size()) + ")"; } -// Derive the gate/cell model name from the leaf instance name. -// Naja SNL encodes assign statements as "". -// Additional primitives can be detected here as the library grows. -static std::string modelNameFromLeaf(const std::string& leaf) { - if (leaf.find("assign") != std::string::npos) return "assign"; - // Add more patterns here: - // if (leaf.find("DFF") != std::string::npos) return "dff"; - // if (leaf == "AND2") return "and2"; - return ""; // generic box -} - static void buildItems(const Equipotential* eq, std::vector& drivers, std::vector& receivers) { @@ -163,6 +146,7 @@ static void buildItems(const Equipotential* eq, item.label = occ.term.getString(); item.isTerm = false; item.designRef = occ.designRef; + item.primitiveType = occ.primitiveType; item.termChildId = occ.term.child_id; item.termBit = occ.term.bit; item.pathIds = occ.pathIds; @@ -295,7 +279,7 @@ static HierEmitResult emitInstanceInternals(InstanceShape& parent, int& nextInst InstanceShape cs; cs.id = nextInstId++; cs.name = parent.name.empty() ? c.name : parent.name + "/" + c.name; - cs.modelName = modelNameFromLeaf(c.name); + cs.primitiveType = c.primitiveType; cs.w = kHierChildW; cs.h = kHierChildH; cs.color = IM_COL32(90, 120, 170, 255); @@ -662,6 +646,7 @@ void EquipotentialView::renderSchematic(const std::vector& equip ImVec2 pos{}; bool initialized = false; DesignRef designRef{}; + PrimitiveType primitiveType = PrimitiveType::Unknown; bool hasInstances = false; std::optional sourceLoc; std::vector ports; @@ -693,6 +678,7 @@ void EquipotentialView::renderSchematic(const std::vector& equip if (!mi.initialized) { mi.initialized = true; mi.designRef = item.designRef; + mi.primitiveType = item.primitiveType; mi.hasInstances = item.hasInstances; mi.sourceLoc = item.sourceLoc; auto pit = g_placedPositions.find(item.key()); @@ -740,7 +726,7 @@ void EquipotentialView::renderSchematic(const std::vector& equip inst.y = mi.pos.y; inst.w = kInstW; inst.name = key; - inst.modelName = modelNameFromLeaf(leafSegment(key)); + inst.primitiveType = mi.primitiveType; inst.color = IM_COL32(100, 140, 200, 255); inst.diagOutline = DiagnosisStore::instanceColor(key); g_occInfoByShapeId[inst.id] = { key, mi.designRef, mi.sourceLoc }; diff --git a/src/EquipotentialView.h b/src/EquipotentialView.h index ccfd937..bef96b5 100644 --- a/src/EquipotentialView.h +++ b/src/EquipotentialView.h @@ -23,6 +23,7 @@ class EquipotentialView { std::string name; unsigned childId = 0; DesignRef designRef{}; + PrimitiveType primitiveType = PrimitiveType::Unknown; bool hasInstances = false; // can itself be expanded further }; // One endpoint of an internal net: a bit term on a specific sub-instance diff --git a/src/LocalSNLProvider.cpp b/src/LocalSNLProvider.cpp index f91417e..c1e6453 100644 --- a/src/LocalSNLProvider.cpp +++ b/src/LocalSNLProvider.cpp @@ -66,6 +66,43 @@ static std::string instanceName(const SNLInstance* i) { return i->getString(); } +// Best-effort gate/cell function classification from a design's name (naja +// has no function/timing-arc API to ask directly -- see PrimitiveType in +// Types.h). Checked as a case-insensitive prefix so common Liberty/Verilog +// naming conventions ("AND2X1", "nand3_1", "DFFX2", ...) match regardless of +// the vendor-specific drive-strength/version suffix; arity itself is read +// from the instance's actual port count at render time, not parsed here. +// najaeda_server.py's get_primitive_type() mirrors this independently, same +// as the rest of the wire protocol (see CLAUDE.md). +static PrimitiveType getPrimitiveType(const SNLDesign* model) { + if (!model) return PrimitiveType::Unknown; + if (NLDB0::isAssign(model)) return PrimitiveType::Assign; + + std::string name = designName(model); + for (auto& c : name) c = static_cast(std::toupper(static_cast(c))); + + static const std::pair kPrefixes[] = { + {"XNOR", PrimitiveType::Xnor}, + {"NAND", PrimitiveType::Nand}, + {"NOR", PrimitiveType::Nor}, + {"XOR", PrimitiveType::Xor}, + {"AND", PrimitiveType::And}, + {"OR", PrimitiveType::Or}, + {"DFF", PrimitiveType::Dff}, + {"INV", PrimitiveType::Inv}, + {"NOT", PrimitiveType::Inv}, + {"BUF", PrimitiveType::Buf}, + }; + for (const auto& [prefix, type] : kPrefixes) { + if (name.rfind(prefix, 0) == 0) return type; + } + return PrimitiveType::Unknown; +} + +static PrimitiveType getPrimitiveType(const SNLInstance* instance) { + return instance ? getPrimitiveType(instance->getModel()) : PrimitiveType::Unknown; +} + static bool hasVisiblePrimitiveInstances(const SNLDesign* d) { for (auto* inst : d->getPrimitiveInstances()) { auto* model = inst->getModel(); @@ -477,9 +514,10 @@ std::string LocalSNLProvider::buildInstancesResponse( continue; } children.push_back({ - {"name", instanceName(inst)}, - {"model_name", model ? designName(model) : ""}, - {"child_id", static_cast(inst->getID())}, + {"name", instanceName(inst)}, + {"model_name", model ? designName(model) : ""}, + {"primitive_type", toString(getPrimitiveType(model))}, + {"child_id", static_cast(inst->getID())}, {"design_ref", { {"db_id", model ? static_cast(model->getDB()->getID()) : 0u}, {"library_id", model ? static_cast(model->getLibrary()->getID()) : 0u}, @@ -639,6 +677,7 @@ std::string LocalSNLProvider::buildEquipotentialResponse(const json& req) const {"design_id", static_cast(model->getID())} }; } + entry["primitive_type"] = toString(getPrimitiveType(theInst)); entry["has_instances"] = hasAnySubInstances(theInst->getModel()); entry["source_loc"] = sourceLocJson(theInst); occs.push_back(std::move(entry)); @@ -685,6 +724,7 @@ std::string LocalSNLProvider::buildEquipotentialResponse(const json& req) const {"design_id", static_cast(model->getID())} }; } + entry["primitive_type"] = toString(getPrimitiveType(theInst)); entry["has_instances"] = hasAnySubInstances(theInst->getModel()); entry["source_loc"] = sourceLocJson(theInst); occs.push_back(std::move(entry)); @@ -766,9 +806,10 @@ std::string LocalSNLProvider::buildInstanceInternalsResponse(const json& req) co for (auto* inst : model->getNonPrimitiveInstances()) { auto* sub = inst->getModel(); children.push_back({ - {"name", instanceName(inst)}, - {"model_name", sub ? designName(sub) : ""}, - {"child_id", static_cast(inst->getID())}, + {"name", instanceName(inst)}, + {"model_name", sub ? designName(sub) : ""}, + {"primitive_type", toString(getPrimitiveType(sub))}, + {"child_id", static_cast(inst->getID())}, {"design_ref", { {"db_id", sub ? static_cast(sub->getDB()->getID()) : 0u}, {"library_id", sub ? static_cast(sub->getLibrary()->getID()) : 0u}, @@ -784,9 +825,10 @@ std::string LocalSNLProvider::buildInstanceInternalsResponse(const json& req) co auto* sub = inst->getModel(); if (sub && NLDB0::isAssign(sub)) continue; children.push_back({ - {"name", instanceName(inst)}, - {"model_name", sub ? designName(sub) : ""}, - {"child_id", static_cast(inst->getID())}, + {"name", instanceName(inst)}, + {"model_name", sub ? designName(sub) : ""}, + {"primitive_type", toString(getPrimitiveType(sub))}, + {"child_id", static_cast(inst->getID())}, {"design_ref", { {"db_id", sub ? static_cast(sub->getDB()->getID()) : 0u}, {"library_id", sub ? static_cast(sub->getLibrary()->getID()) : 0u}, diff --git a/src/SchematicView.cpp b/src/SchematicView.cpp index ef0a625..28ff56e 100644 --- a/src/SchematicView.cpp +++ b/src/SchematicView.cpp @@ -2,10 +2,12 @@ #include #include +#include #include #include #include #include +#include namespace { inline float clampf(float v, float lo, float hi) { @@ -82,6 +84,46 @@ std::string truncateToWidth(ImFont* font, float fontSize, const std::string& tex return text.substr(0, lo) + kEllipsis; } +// Screen-space point sampling for gate body outlines (§ standard shapes +// library, below): a plain vector of points rather than ImDrawList's own +// Path*() buffer, since a shape needs its points twice (once filled, once +// stroked as the outline) and Path*() consumes its buffer on Fill/Stroke. +void appendArcPoints(std::vector& pts, ImVec2 center, float radius, + float a0, float a1, int segments) { + for (int i = 0; i <= segments; ++i) { + float t = a0 + (a1 - a0) * (float(i) / float(segments)); + pts.push_back(ImVec2(center.x + cosf(t) * radius, center.y + sinf(t) * radius)); + } +} + +void appendQuadBezierPoints(std::vector& pts, ImVec2 p0, ImVec2 c, ImVec2 p1, int segments) { + for (int i = 0; i <= segments; ++i) { + float t = float(i) / float(segments); + float u = 1.0f - t; + pts.push_back(ImVec2(u * u * p0.x + 2.0f * u * t * c.x + t * t * p1.x, + u * u * p0.y + 2.0f * u * t * c.y + t * t * p1.y)); + } +} + +// Draws a sampled body outline both filled and stroked (plus a thicker +// diagnosis-severity stroke on top, when flagged) -- shared tail end of every +// standard gate shape below. +void fillAndStrokeBody(ImDrawList* dl, const std::vector& pts, + ImU32 fillColor, ImU32 diagOutline) { + dl->AddConvexPolyFilled(pts.data(), int(pts.size()), fillColor); + dl->AddPolyline(pts.data(), int(pts.size()), IM_COL32(0, 0, 0, 200), 1.5f, ImDrawFlags_Closed); + if (diagOutline != 0) + dl->AddPolyline(pts.data(), int(pts.size()), diagOutline, 3.0f, ImDrawFlags_Closed); +} + +// Draws a bubble (small negation circle), touching the tip of a gate body's +// output side -- shared by NAND/NOR/XNOR/INV. +void drawNegationBubble(ImDrawList* dl, ImVec2 tip, float radius, ImU32 fillColor) { + ImVec2 center(tip.x + radius, tip.y); + dl->AddCircleFilled(center, radius, fillColor); + dl->AddCircle(center, radius, IM_COL32(0, 0, 0, 200), 16, 1.5f); +} + // Draws a dashed rectangle in screen space (no corner rounding). // dashPx / gapPx are in screen pixels so they stay legible at any zoom. void addDashedRect(ImDrawList* dl, ImVec2 rmin, ImVec2 rmax, @@ -294,22 +336,179 @@ static void drawAssignInstance(ImDrawList* dl, const InstanceShape& inst, } // --------------------------------------------------------------------------- -// Gate dispatcher — add new model names here as the library grows +// Standard shapes library +// +// Traditional (non-IEC) gate symbols for PrimitiveType::{And,Nand,Or,Nor, +// Xor,Xnor,Inv,Buf,Dff}. Input/output pin positions are untouched -- they +// stay wherever EquipotentialView already placed them (left edge for +// inputs, right edge for outputs, evenly spaced by portLy()) -- these +// functions only draw a different body outline than the generic box inside +// the same inst.x/y/w/h, then call the same shared drawPorts(). Arity +// (2..N inputs) therefore needs no dedicated handling here: however many +// input ports the instance actually has already determines their spacing, +// and the body outline (D-shape / pointed shield / triangle) scales to +// whatever inst.w/inst.h the caller assigned, same as drawGenericInstance. +// --------------------------------------------------------------------------- + +// AND / NAND — flat back, semicircular (bulging right) front. NAND adds a +// negation bubble at the tip. +static void drawAndLikeInstance(ImDrawList* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize, + bool negated) { + ImVec2 rmin, rmax; + sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); + float cy = (rmin.y + rmax.y) * 0.5f; + float xm = (rmin.x + rmax.x) * 0.5f; + float r = (rmax.y - rmin.y) * 0.5f; + + std::vector pts; + pts.push_back(ImVec2(rmin.x, rmin.y)); + appendArcPoints(pts, ImVec2(xm, cy), r, -1.5707963f, 1.5707963f, 24); + pts.push_back(ImVec2(rmin.x, rmax.y)); + fillAndStrokeBody(dl, pts, inst.color, inst.diagOutline); + + if (negated) { + float bubbleR = std::max(2.5f, r * 0.22f); + drawNegationBubble(dl, ImVec2(xm + r, cy), bubbleR, inst.color); + } + + drawPorts(dl, inst, sv, canvasPos, canvasSize); +} + +// OR / NOR / XOR / XNOR — curved sides bulging outward to a point on the +// right; NOR/XNOR add a negation bubble at the tip; XOR/XNOR add the extra +// curved line just outside the input edge. +static void drawOrLikeInstance(ImDrawList* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize, + bool negated, bool exclusive) { + ImVec2 rmin, rmax; + sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); + float cy = (rmin.y + rmax.y) * 0.5f; + float sw = rmax.x - rmin.x; + float sh = rmax.y - rmin.y; + ImVec2 tip(rmax.x, cy); + + std::vector pts; + pts.push_back(ImVec2(rmin.x, rmin.y)); + appendQuadBezierPoints(pts, ImVec2(rmin.x, rmin.y), + ImVec2(rmin.x + sw * 0.5f, rmin.y - sh * 0.10f), tip, 16); + appendQuadBezierPoints(pts, tip, + ImVec2(rmin.x + sw * 0.5f, rmax.y + sh * 0.10f), + ImVec2(rmin.x, rmax.y), 16); + fillAndStrokeBody(dl, pts, inst.color, inst.diagOutline); + + if (exclusive) { + float gap = std::max(2.0f, sw * 0.08f); + dl->AddLine(ImVec2(rmin.x - gap, rmin.y), ImVec2(rmin.x - gap, rmax.y), + IM_COL32(0, 0, 0, 200), 1.5f); + } + + if (negated) { + float bubbleR = std::max(2.5f, sh * 0.11f); + drawNegationBubble(dl, tip, bubbleR, inst.color); + } + + drawPorts(dl, inst, sv, canvasPos, canvasSize); +} + +// INV / BUF — triangle pointing right, same silhouette as the "assign" +// pass-through shape; INV adds a negation bubble at the tip. +static void drawBufLikeInstance(ImDrawList* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize, + bool negated) { + ImVec2 tl = sv.worldToScreen(ImVec2(inst.x, inst.y), canvasPos, canvasSize); + ImVec2 bl = sv.worldToScreen(ImVec2(inst.x, inst.y + inst.h), canvasPos, canvasSize); + ImVec2 mr = sv.worldToScreen(ImVec2(inst.x + inst.w, inst.y + inst.h * 0.5f), canvasPos, canvasSize); + + dl->AddTriangleFilled(tl, bl, mr, inst.color); + dl->AddTriangle(tl, bl, mr, IM_COL32(0, 0, 0, 200), 1.5f); + if (inst.diagOutline != 0) + dl->AddTriangle(tl, bl, mr, inst.diagOutline, 3.0f); + + if (negated) { + float bubbleR = std::max(2.5f, (bl.y - tl.y) * 0.11f); + drawNegationBubble(dl, mr, bubbleR, inst.color); + } + + drawPorts(dl, inst, sv, canvasPos, canvasSize); +} + +// True for a pin name commonly used for a flip-flop's clock input, so +// drawDffInstance() can mark it with the usual clock-triangle notch. +static bool looksLikeClockPinName(const std::string& name) { + std::string n = name; + for (auto& c : n) c = static_cast(std::tolower(static_cast(c))); + return n == "clk" || n == "clock" || n == "ck" || n == "c"; +} + +// DFF (and other clocked sequential cells) — the generic box, plus the +// standard clock-triangle notch on the left edge at the clock pin's +// position, so a flop reads differently from a plain unclassified +// hierarchical/blackbox instance even though its outline is the same. +static void drawDffInstance(ImDrawList* dl, const InstanceShape& inst, + const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize) { + drawGenericInstance(dl, inst, sv, canvasPos, canvasSize); + + for (const auto& p : inst.ports) { + if (p.direction != Direction::Input || !looksLikeClockPinName(p.name)) continue; + ImVec2 world = sv.portWorldPos(inst, p); + float notchH = std::min(inst.h * 0.3f, 10.0f); + ImVec2 top = sv.worldToScreen(ImVec2(inst.x, world.y - notchH * 0.5f), canvasPos, canvasSize); + ImVec2 bot = sv.worldToScreen(ImVec2(inst.x, world.y + notchH * 0.5f), canvasPos, canvasSize); + ImVec2 apex = sv.worldToScreen(ImVec2(inst.x + notchH * 0.6f, world.y), canvasPos, canvasSize); + dl->AddLine(top, apex, IM_COL32(0, 0, 0, 200), 1.5f); + dl->AddLine(apex, bot, IM_COL32(0, 0, 0, 200), 1.5f); + break; // one clock pin is enough to draw the notch once + } +} + +// --------------------------------------------------------------------------- +// Gate dispatcher — add new PrimitiveType values here as the library grows // --------------------------------------------------------------------------- // To add a new gate type: // 1. Write a static drawXxxInstance() function above with the same signature -// 2. Add an else-if branch below matching its modelName string +// 2. Add a case below matching its PrimitiveType // --------------------------------------------------------------------------- void SchematicView::drawInstance(ImDrawList* dl, const InstanceShape& inst, const ImVec2& canvasPos, const ImVec2& canvasSize) const { - if (inst.modelName == "assign") { - drawAssignInstance(dl, inst, *this, canvasPos, canvasSize); - } - // else if (inst.modelName == "and2") { drawAnd2Instance (...); } - // else if (inst.modelName == "or2") { drawOr2Instance (...); } - // else if (inst.modelName == "dff") { drawDffInstance (...); } - else { - drawGenericInstance(dl, inst, *this, canvasPos, canvasSize); + switch (inst.primitiveType) { + case PrimitiveType::Assign: + drawAssignInstance(dl, inst, *this, canvasPos, canvasSize); + break; + case PrimitiveType::And: + drawAndLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false); + break; + case PrimitiveType::Nand: + drawAndLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/true); + break; + case PrimitiveType::Or: + drawOrLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false, /*exclusive=*/false); + break; + case PrimitiveType::Nor: + drawOrLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/true, /*exclusive=*/false); + break; + case PrimitiveType::Xor: + drawOrLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false, /*exclusive=*/true); + break; + case PrimitiveType::Xnor: + drawOrLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/true, /*exclusive=*/true); + break; + case PrimitiveType::Buf: + drawBufLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false); + break; + case PrimitiveType::Inv: + drawBufLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/true); + break; + case PrimitiveType::Dff: + drawDffInstance(dl, inst, *this, canvasPos, canvasSize); + break; + default: + drawGenericInstance(dl, inst, *this, canvasPos, canvasSize); + break; } } diff --git a/src/Types.cpp b/src/Types.cpp index 8b8377f..65a85a2 100644 --- a/src/Types.cpp +++ b/src/Types.cpp @@ -39,6 +39,7 @@ void from_json(const json& j, InstanceResponseJson& r) { r.name = j["name"].get(); if (j.contains("model_name") && !j["model_name"].is_null()) r.model_name = j["model_name"].get(); + r.primitive_type = primitiveTypeFromString(j.value("primitive_type", std::string("unknown"))); r.child_id = j.value("child_id", 0u); j.at("design_ref").get_to(r.design_ref); @@ -178,6 +179,7 @@ void from_json(const json& j, Equipotential& e) { if (occJson.contains("design_ref") && occJson["design_ref"].is_object()) occurrence.designRef = occJson["design_ref"].get(); + occurrence.primitiveType = primitiveTypeFromString(occJson.value("primitive_type", std::string("unknown"))); occurrence.has_instances = occJson.value("has_instances", false); occurrence.source_loc = parseSourceLoc(occJson); diff --git a/src/Types.h b/src/Types.h index 9058dac..1ec17a8 100644 --- a/src/Types.h +++ b/src/Types.h @@ -28,10 +28,68 @@ struct SourceLoc { int endColumn = 0; }; +// Coarse gate/cell function classification for an instance, used to pick a +// standard schematic symbol instead of a generic box (see +// SchematicView::drawInstance()). naja/najaeda expose no function/timing-arc +// info -- only a design/cell name string -- so both LocalSNLProvider +// (native, getPrimitiveType() in LocalSNLProvider.cpp) and najaeda_server.py +// (WASM/browser, get_primitive_type()) classify independently from that name +// via a best-effort prefix match, same spirit as their existing isAssign() +// special case (which maps here to Assign). Unknown is the default for +// anything that doesn't match -- including ordinary hierarchical modules and +// blackboxed cells with unrecognized names -- and always falls back to the +// generic box. Gate arity (2..N inputs) is NOT part of this enum: the actual +// input port count already carried on InstanceShape::ports is used instead, +// so no separate arity field needs to travel over the wire. +enum class PrimitiveType { + Unknown = 0, + And, + Nand, + Or, + Nor, + Xor, + Xnor, + Inv, + Buf, + Dff, + Assign, +}; + +inline const char* toString(PrimitiveType t) { + switch (t) { + case PrimitiveType::And: return "and"; + case PrimitiveType::Nand: return "nand"; + case PrimitiveType::Or: return "or"; + case PrimitiveType::Nor: return "nor"; + case PrimitiveType::Xor: return "xor"; + case PrimitiveType::Xnor: return "xnor"; + case PrimitiveType::Inv: return "inv"; + case PrimitiveType::Buf: return "buf"; + case PrimitiveType::Dff: return "dff"; + case PrimitiveType::Assign: return "assign"; + default: return "unknown"; + } +} + +inline PrimitiveType primitiveTypeFromString(const std::string& s) { + if (s == "and") return PrimitiveType::And; + if (s == "nand") return PrimitiveType::Nand; + if (s == "or") return PrimitiveType::Or; + if (s == "nor") return PrimitiveType::Nor; + if (s == "xor") return PrimitiveType::Xor; + if (s == "xnor") return PrimitiveType::Xnor; + if (s == "inv") return PrimitiveType::Inv; + if (s == "buf") return PrimitiveType::Buf; + if (s == "dff") return PrimitiveType::Dff; + if (s == "assign") return PrimitiveType::Assign; + return PrimitiveType::Unknown; +} + struct InstanceResponseJson { std::string name; unsigned child_id; std::string model_name; + PrimitiveType primitive_type = PrimitiveType::Unknown; DesignRef design_ref; bool has_primitives; bool has_instances; @@ -85,6 +143,7 @@ struct InstTermOccurrence { std::vector pathIds; // instance child_ids (used to send load_equipotential) BitTerm term; DesignRef designRef; // model of the tail instance — used to fetch its full interface + PrimitiveType primitiveType = PrimitiveType::Unknown; // tail instance's model, see PrimitiveType // True when the tail instance's own model has sub-instances worth showing // in a nested schematic — drives the hierarchy expand/collapse glyph. bool has_instances = false; @@ -236,7 +295,11 @@ struct Port { struct InstanceShape { int id = 0; std::string name; // instance path (display label) - std::string modelName; // gate/cell type — drives the icon dispatcher in drawInstance() + // Gate/cell function classification — drives the standard-shape dispatch + // in SchematicView::drawInstance(); PrimitiveType::Unknown draws the + // generic box. Actual input-pin count (2..N) comes from `ports` below, + // not from this enum. + PrimitiveType primitiveType = PrimitiveType::Unknown; float x = 0.0f; float y = 0.0f; // world coords (top-left) float w = 100.0f; From 24555377124c5cfa773a88cb93c1764070c2c7d8 Mon Sep 17 00:00:00 2001 From: xtof Date: Tue, 29 Sep 2026 12:00:34 +0200 Subject: [PATCH 2/3] commit shapes --- CLAUDE.md | 55 ++- python/naja_schematic/protocol.py | 58 +-- python/tests/test_protocol.py | 17 + src/AppLogic.cpp | 1 + src/EquipotentialView.cpp | 347 +++++++++------- src/EquipotentialView.h | 2 + src/LocalSNLProvider.cpp | 45 +-- src/SchematicLayout.cpp | 649 ++++++++++++++++++++++++++---- src/SchematicLayout.h | 161 +++++--- src/SchematicView.cpp | 397 ++++++++++-------- src/SchematicView.h | 14 + src/Types.cpp | 1 + src/Types.h | 35 +- tests/EquipotentialViewTest.cpp | 3 + tests/SchematicLayoutTest.cpp | 440 ++++++++++++-------- tests/TypesJsonTest.cpp | 9 +- 16 files changed, 1542 insertions(+), 692 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 41a65f4..37dafb7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -305,9 +305,8 @@ several if the net is reached more than once) -- *not* every reader on the net, unlike `equipotential_response`. Nets are returned breadth-first from the requested one and de-duplicated, capped at `kMaxTraceNets` (`MAX_TRACE_NETS` in `protocol.py`, 500) with `truncated: true` when hit. `AppLogic.cpp` adds -each to `GUIData` in order; the incremental layout in `EquipotentialView.cpp` -relies on that ordering (each net shares an already-placed instance with an -earlier one) to chain the cone right-to-left. Reachable from the tree +each to `GUIData` in order; the layered placement breaks ordering ties by +first appearance, so the cone keeps a stable shape as it grows. Reachable from the tree (right-click a term/bus-bit row -> "Trace to Driver", bus row -> "Trace Bus to Driver"; both clear the view first, like "Show Equipotential") and from the schematic (right-click a pin -> "Trace to Driver"; this one *adds* to the view @@ -321,8 +320,8 @@ the view yet are "open" (`Port::open`, drawn with a hollow-circle stub), and a while it loads; `SchematicInteraction::PendingRequests` stops repeat sends). A single click on a merged bus pin shows its bits instead. Hovering a pin highlights it and shows a tooltip naming its click action. When the requested -net arrives the view pans so the clicked box stays put on screen, even if the -hierarchy frames re-lay everything out. Pins are hit-tested nearest-first by +net arrives the view pans so the clicked box stays put on screen, even though +the whole schematic is laid out again. Pins are hit-tested nearest-first by `SchematicInteraction::pickPin`. `GUIData::addEquipotential()` rejects a net whose endpoints are all already shown (`equipotentialCovers()`), so neither a re-click nor a trace re-listing a shown net draws its wires twice. @@ -335,8 +334,9 @@ shows (**View > Show Hierarchy**, on by default, also in the canvas context menu): `layoutHierarchyGroups()` in `EquipotentialView.cpp` draws every module enclosing a displayed leaf as a nested translucent frame labelled `instance (Model)`, re-laying the leaves out inside it — each leaf keeps the -logic column the incremental layout gave it, and inside a frame its leaves -and sub-frames are bucketed into columns by that column. Frames are +logic level the layered placement gave it (inside a frame its leaves and +sub-frames are bucketed into columns by level) and, where the column allows, +the height that aligned its pins. Frames are `InstanceShape`s with `isHierGroup` set, inserted at the front of `SchematicView::instances` and drawn before the nets (`SchematicView::render()`); the leaves stay top-level shapes, so wiring and @@ -345,6 +345,17 @@ hit-testing are unchanged. Right-clicking a frame offers Show Properties and last trace (not just the last net) with a "Hierarchy" column giving each occurrence's enclosing modules. +Instance entries (`instances_response`/`primitives_response` children, +equipotential occurrences, `instance_resolved`'s `instance`) carry a +`primitive_type` (`"and"`, `"nand"`, `"or"`, `"nor"`, `"xor"`, `"xnor"`, +`"inv"`, `"buf"`, `"dff"`, `"assign"`, `"unknown"`) that picks the gate +symbol `SchematicView::drawInstance()` draws, and term entries carry +`"clock": true` on a sequential cell's clock pin (the DFF clock notch). +Both come from naja's modeling of the cell -- `SNLDesignModeling`'s +truth-table checks (`isAnd`, ...), `isSequential` and `isClock` -- and +**never from a cell or pin name**: a cell naja has no model for (e.g. +gate-level Verilog without Liberty) is `"unknown"` and draws as a box. + `diagnosis_response` is different: it's a **server push**, not a reply to a request (a diagnosis run finishes on its own schedule), and it *annotates* the already-loaded netlist rather than loading anything: @@ -388,7 +399,7 @@ the same instance-name path as diagnosis items: { "response": "instance_resolved", "path": ["u1","u2"], "found": true, "instance": { // absent for the top design ([]) "path": [["u1", 3, "Mod"], ["u2", 7, "AND2"]], // [name, child_id, model] per level - "design_ref": {...}, "has_instances": false, "source_loc": null, + "design_ref": {...}, "primitive_type": "and", "has_instances": false, "source_loc": null, "terms": [ {"name": "A", "child_id": 0, "direction": 0}, ... ] } } // expanded_instance_terms shape ``` The viewer then: @@ -481,12 +492,28 @@ appear as boxes/pins there). views. Its listener (installed in `AppLogic.cpp`) reports each change to the host. `NetlistTree` reveals selections made outside it by comparing `revision()`. Cleared on every fresh `root_response`/`root_loaded`. -- **`SchematicLayout`** — the schematic's pure placement geometry, split out - of `EquipotentialView` so it's unit-testable without ImGui frames or a - provider (`tests/SchematicLayoutTest.cpp`): `IncrementalLayout` (per-net - instance placement and column de-overlap) and `layoutHierarchyGroups()` - (module frames). Changes to how the schematic is laid out belong here, - with a test; `EquipotentialView` only turns the result into drawn shapes. +- **`SchematicLayout`** — the schematic's pure geometry, split out of + `EquipotentialView` so it's unit-testable without ImGui frames or a + provider (`tests/SchematicLayoutTest.cpp`). It follows the classic + schematic-generation pipeline and is recomputed from scratch every frame (deterministic, so + nothing moves unless the inputs change): + - `symbolGeometry()`: symbol sizes, and pins on a `kPinPitch` grid (gate + inputs centered on the output, box pins down from the top). + - `layeredPlacement()`: Sugiyama-style placement. It breaks cycles, assigns + longest-path levels, orders each column by barycenter, then places heights + by isotonic regression so connected pins line up (straight wires). + - `stackPorts()`: top-level ports on the sheet's left/right edges. + - `routeNets()`: one orthogonal tree per driving pin. Trunks go in the + box-free channels between columns, on distinct tracks that respect + channel routing's vertical constraints, plus a spine that detours around + boxes, labels and other spines. Junction dots mark real branch points. + - `layoutHierarchyGroups()`: module frames. + + Changes to how the schematic is laid out belong here, with a test; + `EquipotentialView` only turns the result into drawn shapes, and + `SchematicView` draws them. A gate symbol's body fills its box exactly, so + pin ticks always touch it. Pin names go inside boxes, none on gates, and + instance names above. - **`SchematicInteraction`** — the pure side of the schematic's pin interactions: `pickPin()` (nearest-pin hit test, radius capped in world units so a pin doesn't swallow box-body clicks at low zoom) and diff --git a/python/naja_schematic/protocol.py b/python/naja_schematic/protocol.py index a4f146a..2e7d2d9 100644 --- a/python/naja_schematic/protocol.py +++ b/python/naja_schematic/protocol.py @@ -55,25 +55,21 @@ def has_visible_primitive_instances(design): return any(not instance.getModel().isAssign() for instance in design.getPrimitiveInstances()) -# Best-effort gate/cell function classification from a design's name -- -# najaeda has no function/timing-arc API to ask directly (see PrimitiveType -# in Types.h). Checked as a case-insensitive prefix so common Liberty/ -# Verilog naming conventions ("AND2X1", "nand3_1", "DFFX2", ...) match -# regardless of the vendor-specific drive-strength/version suffix; arity -# itself is read from the instance's actual port count at render time, not -# parsed here. LocalSNLProvider.cpp's getPrimitiveType() mirrors this -# independently, same as the rest of the wire protocol (see CLAUDE.md). -_PRIMITIVE_TYPE_PREFIXES = ( - ("XNOR", "xnor"), - ("NAND", "nand"), - ("NOR", "nor"), - ("XOR", "xor"), - ("AND", "and"), - ("OR", "or"), - ("DFF", "dff"), - ("INV", "inv"), - ("NOT", "inv"), - ("BUF", "buf"), +# Gate/cell function classification, from naja's own modeling of the cell +# (the truth table a Liberty `function` or an NLDB0 gate defines, and its +# sequential model) -- never from its name. A cell naja has no model for is +# "unknown" and draws as the generic box. LocalSNLProvider.cpp's +# getPrimitiveType() mirrors this, same as the rest of the wire protocol. +_PRIMITIVE_TYPE_CHECKS = ( + ("isSequential", "dff"), + ("isInv", "inv"), + ("isBuf", "buf"), + ("isAnd", "and"), + ("isNand", "nand"), + ("isOr", "or"), + ("isNor", "nor"), + ("isXor", "xor"), + ("isXnor", "xnor"), ) def get_primitive_type(model): @@ -81,12 +77,17 @@ def get_primitive_type(model): return "unknown" if model.isAssign(): return "assign" - name = model.getName().upper() - for prefix, ptype in _PRIMITIVE_TYPE_PREFIXES: - if name.startswith(prefix): + for check, ptype in _PRIMITIVE_TYPE_CHECKS: + if getattr(model, check)(): return ptype return "unknown" +def with_clock(entry, term): + # Only sent when set: marks a flip-flop's clock pin (see drawDffInstance). + if term.is_clock(): + entry["clock"] = True + return entry + def is_anonymous_constant_net(net): # Anonymous scalar constant nets (1'b0/1'b1 tie-offs, e.g. an unconnected # input najaeda ties off implicitly) are structural noise, not @@ -256,7 +257,7 @@ def equipotential_to_json(equipotential, sinks=None): inst_model = instTerm.getInstance().getModel() has_instances = (inst_model.hasNonPrimitiveInstances() or has_visible_primitive_instances(inst_model)) - occurrences.append({ + occurrences.append(with_clock({ "path": path, "term_id": term.getID(), "name": term.getName(), @@ -276,7 +277,7 @@ def equipotential_to_json(equipotential, sinks=None): # Net on this inst term inside the instance's parent design -- # shown in the schematic's pin hover tooltip. "net": net_display_name(instTerm.getNet()) - }) + }, term)) for term in equipotential.getTerms(): # A top-level output is a receiver of the net; an input/inout drives it. if (sinks is not None @@ -498,18 +499,18 @@ def bit_terms_json(design): for b in range(lo, hi + 1): bit_term = term.getBusTermBit(b) if bit_term: - terms.append({ + terms.append(with_clock({ "name": f"{term.getName()}[{b}]", "child_id": bit_term.getID(), "direction": direction_to_int(bit_term.getDirection()), "bit": b, - }) + }, bit_term)) else: - terms.append({ + terms.append(with_clock({ "name": term.getName(), "child_id": term.getID(), "direction": direction_to_int(term.getDirection()), - }) + }, term)) return terms @@ -550,6 +551,7 @@ def _handle_resolve_instance(u, request): current = inst.getModel() reply["instance"] = { "path": levels, + "primitive_type": get_primitive_type(design), "design_ref": { "db_id": design.getDB().getID(), "library_id": design.getLibrary().getID(), diff --git a/python/tests/test_protocol.py b/python/tests/test_protocol.py index aa65901..b27f328 100644 --- a/python/tests/test_protocol.py +++ b/python/tests/test_protocol.py @@ -126,6 +126,7 @@ def test_resolve_instance(top): assert [level[0] for level in inst["path"]] == ["u_sub", "u_and"] assert [level[2] for level in inst["path"]] == ["sub", "LUT2"] assert not inst["has_instances"] + assert inst["primitive_type"] == "unknown" # LUT2 isn't a named gate assert {t["name"] for t in inst["terms"]} == {"I0", "I1", "O"} (reply,) = handle_request({"request": "resolve_instance", "path": ["u_sub"]}) @@ -150,3 +151,19 @@ def test_instance_selected_is_a_notification(top): def test_focus_instance_message(): assert protocol.focus_instance(["u_sub", "u_and"]) == { "response": "focus_instance", "path": ["u_sub", "u_and"]} + + +def test_get_primitive_type_uses_naja_modeling_not_names(): + checks = ("isAssign", "isSequential", "isInv", "isBuf", "isAnd", "isNand", + "isOr", "isNor", "isXor", "isXnor") + + def model(name, *true_checks): + return SimpleNamespace(getName=lambda: name, + **{c: (lambda c=c: c in true_checks) for c in checks}) + assert protocol.get_primitive_type(None) == "unknown" + assert protocol.get_primitive_type(model("x", "isAssign")) == "assign" + assert protocol.get_primitive_type(model("x", "isSequential")) == "dff" + assert protocol.get_primitive_type(model("ND2", "isNand")) == "nand" + assert protocol.get_primitive_type(model("OAI", "isXnor")) == "xnor" + # The name never matters: an unmodelled "AND2" is just a box. + assert protocol.get_primitive_type(model("AND2_X1")) == "unknown" diff --git a/src/AppLogic.cpp b/src/AppLogic.cpp index 6a0e99b..0ad70ee 100644 --- a/src/AppLogic.cpp +++ b/src/AppLogic.cpp @@ -238,6 +238,7 @@ void setupProvider(AppState& state) { ep.childId = t.value("child_id", 0u); if (t.contains("bit") && !t["bit"].is_null()) ep.bit = t["bit"].get(); + ep.clock = t.value("clock", false); int dirInt = t.value("direction", 0); ep.direction = dirInt == 1 ? Direction::Output : dirInt == 2 ? Direction::Inout diff --git a/src/EquipotentialView.cpp b/src/EquipotentialView.cpp index 0b1fc5a..bf1c9d1 100644 --- a/src/EquipotentialView.cpp +++ b/src/EquipotentialView.cpp @@ -24,8 +24,6 @@ using namespace SchematicLayout; // --------------------------------------------------------------------------- // Layout geometry constants (see SchematicLayout.h for the placement ones) // --------------------------------------------------------------------------- -static constexpr float kPortSpacing = 18.0f; - // Hierarchy embedding (nested boxes) geometry. static constexpr float kHierChildW = 110.0f; static constexpr float kHierChildH = 44.0f; @@ -107,8 +105,6 @@ static bool g_showHierarchy = true; // of the frame to fit the view to once this frame's layout is known. static std::optional g_pendingZoomGroup; -// Persistent layout state: where each instance has been placed so far. -static IncrementalLayout g_layout; // Instances shown on their own (showInstance), by instance path. static std::map g_startInstances; @@ -141,6 +137,23 @@ static float portLy(int i, int n) { return n > 1 ? -0.4f + 0.8f * float(i) / float(n - 1) : 0.0f; } +// Sizes `inst` as its symbol (SchematicLayout::symbolGeometry) and puts its +// pins on the pin grid: inputs down the left side, the rest down the right, +// each in their current order. `extraH` grows the box below its pins, to +// make room for nested internals. +static void applySymbolGeometry(InstanceShape& inst, float extraH) { + std::vector left, right; + for (const auto& p : inst.ports) (p.lx < 0.f ? left : right).push_back(p.name); + const SymbolGeometry g = symbolGeometry(inst.primitiveType, left, right); + inst.w = g.w; + inst.h = g.h + extraH; + size_t li = 0, ri = 0; + for (auto& p : inst.ports) { + const float y = p.lx < 0.f ? g.leftY[li++] : g.rightY[ri++]; + p.ly = y / inst.h - 0.5f; + } +} + // Screen-space mouse position converted to schematic world coordinates. static ImVec2 mouseWorldPos(const SchematicView& sv, const ImVec2& cpos) { float s = sv.transform.scale; @@ -326,9 +339,13 @@ static HierEmitResult emitInstanceInternals(InstanceShape& parent, int& nextInst // Sub-pass 4: position children top-to-bottom in a single column, // recursing into an already-expanded child *before* moving on to the // next sibling so a grown height pushes later siblings down correctly. + // Below the parent's own pin rows (applySymbolGeometry reserved the room). + float header = kHierHeaderGap; + for (const auto& p : parent.ports) + header = std::max(header, (p.ly + 0.5f) * parent.h + kPinPitch); std::vector descendants; float x = parent.x + kHierMargin; - float y = parent.y + kHierHeaderGap; + float y = parent.y + header; for (auto& cs : children) { cs.x = x; cs.y = y; @@ -340,7 +357,7 @@ static HierEmitResult emitInstanceInternals(InstanceShape& parent, int& nextInst y += cs.h + kHierGap; } - float bottom = children.empty() ? (parent.y + kHierHeaderGap) : (y - kHierGap); + float bottom = children.empty() ? (parent.y + header) : (y - kHierGap); parent.h = std::max(parent.h, (bottom - parent.y) + kHierMargin); parent.w = std::max(parent.w, kHierChildW + 2.0f * kHierMargin); @@ -359,7 +376,6 @@ void EquipotentialView::fitView() { g_pendingFit = true; } void EquipotentialView::clearNets() { g_pendingClear = true; } void EquipotentialView::resetLayout() { - g_layout.clear(); g_startInstances.clear(); g_expandedInstances.clear(); g_pendingExpansions.clear(); @@ -413,6 +429,7 @@ EquipotentialView::startInstanceFromResolved(const json& reply) { } if (start.path.empty()) return std::nullopt; if (inst.contains("design_ref")) start.designRef = inst["design_ref"].get(); + start.primitiveType = primitiveTypeFromString(inst.value("primitive_type", std::string("unknown"))); start.hasInstances = inst.value("has_instances", false); if (inst.contains("source_loc") && inst["source_loc"].is_object()) { const auto& loc = inst["source_loc"]; @@ -425,6 +442,7 @@ EquipotentialView::startInstanceFromResolved(const json& reply) { ep.name = t.value("name", std::string("")); ep.childId = t.value("child_id", 0u); if (t.contains("bit") && !t["bit"].is_null()) ep.bit = t["bit"].get(); + ep.clock = t.value("clock", false); int dirInt = t.value("direction", 0); ep.direction = dirInt == 1 ? Direction::Output : dirInt == 2 ? Direction::Inout : Direction::Input; @@ -663,14 +681,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip } } - // ----------------------------------------------------------------------- - // Layout new equips (once each, stores positions) - // ----------------------------------------------------------------------- - for (Equipotential* eq : equipotentials) - if (eq) g_layout.place(eq); - for (const auto& [path, start] : g_startInstances) - g_layout.placeAlone(path); - // ----------------------------------------------------------------------- // Rebuild geometry: merged instances + per-equip wires // ----------------------------------------------------------------------- @@ -690,10 +700,10 @@ void EquipotentialView::renderSchematic(const std::vector& equip Direction direction; unsigned termChildId; std::optional termBit; + bool clock = false; std::vector pathIds; }; struct MInst { - ImVec2 pos{}; bool initialized = false; DesignRef designRef{}; PrimitiveType primitiveType = PrimitiveType::Unknown; @@ -709,7 +719,8 @@ void EquipotentialView::renderSchematic(const std::vector& equip // Per-equip wire endpoints: an instance pin (by instance path), or a // top-level term (by name, its stub shape id filled in by pass 3). - struct WireEnd { bool isTerm; InstancePath path; std::string term; int portId; int termInstId = -1; }; + struct WireEnd { bool isTerm; InstancePath path; std::string term; int portId; int termInstId = -1; + bool drives = false; }; std::vector> equiEnds(equipotentials.size()); // Best-effort net-name label per equipotential, shown next to a merged // bus wire's slash mark in pass 4 below -- the driving pin's name is the @@ -731,7 +742,7 @@ void EquipotentialView::renderSchematic(const std::vector& equip for (const auto& item : items) { if (item.isTerm) { int pid = nextPortId++; - equiEnds[ei].push_back({ true, {}, item.label, pid }); + equiEnds[ei].push_back({ true, {}, item.label, pid, -1, pass == 0 }); g_portEquiByPortId[pid] = { item.pathIds, item.termChildId, item.termBit }; continue; } @@ -746,9 +757,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip mi.path = item.path; mi.pathModels = item.pathModels; mi.pathIds = item.pathIds; - auto pit = g_layout.positions().find(item.path); - mi.pos = pit != g_layout.positions().end() - ? pit->second : ImVec2{kLeftMargin, 0.f}; } // Find or create port slot for this port int pid = -1; @@ -764,13 +772,14 @@ void EquipotentialView::renderSchematic(const std::vector& equip ps.direction = item.direction; ps.termChildId = item.termChildId; ps.termBit = item.termBit; + ps.clock = item.clock; ps.pathIds = item.pathIds; mi.ports.push_back(std::move(ps)); g_portEquiByPortId[pid] = { item.pathIds, item.termChildId, item.termBit }; // This pin's net is in the view now: its request is done. g_pendingPinNets.resolve(pinRequestKey(g_portEquiByPortId[pid])); } - equiEnds[ei].push_back({ false, item.path, "", pid }); + equiEnds[ei].push_back({ false, item.path, "", pid, -1, pass == 0 }); } } } @@ -784,14 +793,13 @@ void EquipotentialView::renderSchematic(const std::vector& equip if (mi.initialized) continue; mi.initialized = true; mi.designRef = start.designRef; + mi.primitiveType = start.primitiveType; mi.hasInstances = start.hasInstances; mi.bitTermCount = start.ports.size(); mi.sourceLoc = start.sourceLoc; mi.path = start.path; mi.pathModels = start.pathModels; mi.pathIds = start.pathIds; - auto pit = g_layout.positions().find(path); - mi.pos = pit != g_layout.positions().end() ? pit->second : ImVec2{kLeftMargin, 0.f}; } // Pass 2: build InstanceShapes from merged data @@ -808,9 +816,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip InstanceShape inst; inst.id = nextInstId++; - inst.x = mi.pos.x; - inst.y = mi.pos.y; - inst.w = kInstW; inst.path = key; inst.name = displayPath(key); inst.primitiveType = mi.primitiveType; @@ -825,13 +830,13 @@ void EquipotentialView::renderSchematic(const std::vector& equip // finalizes positions, below) or kick off the request for them. inst.hasChildren = mi.hasInstances; inst.hierExpanded = inst.hasChildren && g_hierExpanded.count(key) > 0; + float internalsH = 0.f; if (inst.hierExpanded) { auto internalsIt = g_instanceInternals.find(key); if (internalsIt != g_instanceInternals.end()) { size_t n = internalsIt->second.children.size(); float blockH = n ? float(n) * kHierChildH + float(n - 1) * kHierGap : 0.f; - inst.h += kHierHeaderGap + blockH + kHierMargin; - inst.w = std::max(inst.w, kHierChildW + 2.0f * kHierMargin); + internalsH = blockH + kHierMargin; } else if (!g_hierPending.count(key) && g_provider) { g_hierPending.insert(key); json req; @@ -893,12 +898,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip } } - int nL = 0, nR = 0; - for (const auto& row : rows) - (row.members[0]->ep->direction == Direction::Input ? nL : nR)++; - inst.h = std::max(kInstH, float(std::max(nL, nR)) * kPortSpacing + 10.f); - - int li = 0, ri = 0; for (const auto& row : rows) { bool isIn = row.members[0]->ep->direction == Direction::Input; Port p; @@ -915,6 +914,7 @@ void EquipotentialView::renderSchematic(const std::vector& equip } else { p.id = row.members[0]->pid; p.name = row.members[0]->ep->name; + p.clock = row.members[0]->ep->clock; p.color = DiagnosisStore::netColor(key, stripBusIndex(row.members[0]->ep->name)); if (row.isExpandedBusBit) g_expandedBusGroupByPortId[p.id] = row.groupKey; p.open = row.members[0]->open; @@ -922,7 +922,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip pinRequestKey(g_portEquiByPortId[p.id]), ImGui::GetTime()); } p.lx = isIn ? -0.5f : 0.5f; - p.ly = isIn ? portLy(li++, nL) : portLy(ri++, nR); p.direction = row.members[0]->ep->direction; p.isInput = !isIn; inst.ports.push_back(p); @@ -963,12 +962,6 @@ void EquipotentialView::renderSchematic(const std::vector& equip } } - int nL = 0, nR = 0; - for (const auto& row : rows) - (row.members[0]->direction == Direction::Input ? nL : nR)++; - inst.h = std::max(kInstH, float(std::max(nL, nR)) * kPortSpacing + 10.f); - - int li = 0, ri = 0; for (const auto& row : rows) { bool isIn = row.members[0]->direction == Direction::Input; Port p; @@ -984,27 +977,72 @@ void EquipotentialView::renderSchematic(const std::vector& equip } else { p.id = row.members[0]->portId; p.name = row.members[0]->name; + p.clock = row.members[0]->clock; p.color = DiagnosisStore::netColor(key, stripBusIndex(row.members[0]->name)); if (row.isExpandedBusBit) g_expandedBusGroupByPortId[p.id] = row.groupKey; } p.lx = isIn ? -0.5f : 0.5f; - p.ly = isIn ? portLy(li++, nL) : portLy(ri++, nR); p.direction = row.members[0]->direction; p.isInput = !isIn; inst.ports.push_back(p); } } + applySymbolGeometry(inst, internalsH); + if (internalsH > 0.f) inst.w = std::max(inst.w, kHierChildW + 2.0f * kHierMargin); g_schematic.instances.push_back(std::move(inst)); } - // Either wrap the leaves in module frames (which re-lays them out), or - // keep the flat incremental layout and just resolve vertical overlaps - // from boxes drawn taller than their reserved slot (persisted, so Pass 3's - // term-column bandY and later anchoring match what's actually drawn). + auto renderedPort = [&](int pid) { + auto it = logicalToRenderedPortId.find(pid); + return it != logicalToRenderedPortId.end() ? it->second : pid; + }; + // World position of an instance end's (rendered) pin, if it's drawn. + auto pinWorld = [&](const WireEnd& we) -> std::optional { + auto kit = pathToInstId.find(we.path); + InstanceShape* s = kit != pathToInstId.end() ? g_schematic.findInstanceById(kit->second) : nullptr; + const Port* p = s ? g_schematic.findPortById(*s, renderedPort(we.portId)) : nullptr; + if (!p) return std::nullopt; + return portAnchor(*s, *p); + }; + + // Layered placement of the instance boxes (see SchematicLayout): every + // net between two instances is an edge from its driver pin to each + // receiver pin. + std::map nodeOfShape; + std::vector placeNodes; + for (const auto& inst : g_schematic.instances) { + nodeOfShape[inst.id] = int(placeNodes.size()); + placeNodes.push_back({ inst.w, inst.h }); + } + std::vector placeNets; + for (const auto& ends : equiEnds) { + PlaceNet pn; + for (const auto& we : ends) { + if (we.isTerm) continue; + auto kit = pathToInstId.find(we.path); + if (kit == pathToInstId.end()) continue; + InstanceShape* s = g_schematic.findInstanceById(kit->second); + const Port* p = s ? g_schematic.findPortById(*s, renderedPort(we.portId)) : nullptr; + if (!p) continue; + PlacePin pin{ nodeOfShape[s->id], (p->ly + 0.5f) * s->h }; + (we.drives ? pn.drivers : pn.receivers).push_back(pin); + } + if (!pn.drivers.empty() && !pn.receivers.empty()) placeNets.push_back(std::move(pn)); + } + const Placement placement = layeredPlacement(placeNodes, placeNets); + for (auto& inst : g_schematic.instances) { + const ImVec2 pos = placement.pos[nodeOfShape[inst.id]]; + inst.x = pos.x; + inst.y = pos.y; + } + + // With Show Hierarchy, wrap the leaves in module frames (which re-lays + // them out, keeping each one's logic level). bool grouped = false; if (g_showHierarchy) { std::map leafHier; - for (const auto& [path, mi] : minsts) leafHier[path] = { mi.path, mi.pathModels }; + for (const auto& [path, mi] : minsts) + leafHier[path] = { mi.path, mi.pathModels, placement.level[nodeOfShape[pathToInstId[path]]] }; auto frames = layoutHierarchyGroups(leafHier, g_schematic.instances, pathToInstId, nextInstId); grouped = !frames.empty(); std::vector frameShapes; @@ -1020,14 +1058,12 @@ void EquipotentialView::renderSchematic(const std::vector& equip std::make_move_iterator(frameShapes.begin()), std::make_move_iterator(frameShapes.end())); } - if (!grouped) g_layout.resolveColumnOverlaps(g_schematic.instances); - - // Hierarchy embedding: now that top-level positions are finalized (the - // deoverlap pass above may have shifted a box's y), lay out and append - // the nested children/internal-nets of every expanded, loaded instance. - // Collected as ids first (not pointers) since emission appends to the - // very vectors we're about to iterate, which would invalidate iterators - // held across a push_back. + + // Hierarchy embedding: now that top-level positions are final, lay out + // and append the nested children/internal-nets of every expanded, loaded + // instance. Collected as ids first (not pointers) since emission appends + // to the very vectors we're about to iterate, which would invalidate + // iterators held across a push_back. { std::vector hierExpandIds; for (const auto& inst : g_schematic.instances) @@ -1043,105 +1079,85 @@ void EquipotentialView::renderSchematic(const std::vector& equip } } - // Pass 3: build term InstanceShapes per equip (not merged) - // Port flags already placed, so two nets whose band lands on the same - // spot (e.g. two inputs feeding the same cell) stack instead of overlap. - std::vector placedTermPos; + // Pass 3: top-level ports, on the sheet's edges -- inputs + // left of everything, outputs right of it -- each level with the pin it + // connects to (a straight wire) unless that would crowd another port. + float sheetX0 = 1e30f, sheetX1 = -1e30f; + for (const auto& inst : g_schematic.instances) { + if (inst.parentShapeId >= 0 || inst.w <= 0.f) continue; + sheetX0 = std::min(sheetX0, inst.x); + sheetX1 = std::max(sheetX1, inst.x + inst.w); + } + if (sheetX0 > sheetX1) { sheetX0 = kLeftMargin; sheetX1 = kLeftMargin; } + struct PortStub { size_t ei; std::string label; Direction direction; int portId; float wantY; }; + std::vector leftStubs, rightStubs; for (size_t ei = 0; ei < equipotentials.size(); ++ei) { Equipotential* eq = equipotentials[ei]; if (!eq) continue; std::vector drivers, receivers; buildItems(eq, drivers, receivers); - - // Determine column X for this equip's terms from placed instances - float lx = kLeftMargin, rx = kLeftMargin + kInstW + kColGap; - float bandY = 0.f; int nBand = 0; - auto accInst = [&](const Item& item) { - if (item.isTerm) return; - auto kit = pathToInstId.find(item.path); - const InstanceShape* shape = - kit != pathToInstId.end() ? g_schematic.findInstanceById(kit->second) : nullptr; - if (!shape) return; - lx = std::min(lx, shape->x); - rx = std::max(rx, shape->x + shape->w); - bandY += shape->y; ++nBand; - }; - for (const auto& d : drivers) accInst(d); - for (const auto& r : receivers) accInst(r); - if (nBand) bandY /= float(nBand); - // With module frames drawn, top-level ports belong outside the - // outermost frame, not next to a leaf that sits deep inside one. - if (grouped) { - for (const auto& inst : g_schematic.instances) { - if (!inst.isHierGroup) continue; - lx = std::min(lx, inst.x); - rx = std::max(rx, inst.x + inst.w); - } - } - - float termLx = lx - 40.f; - float termRx = rx + 20.f; - for (int pass = 0; pass < 2; ++pass) { const auto& items = pass == 0 ? drivers : receivers; - float ty = bandY; for (const auto& item : items) { - if (!item.isTerm) { ty += kInstH + kRowSpacing; continue; } - - // The first end of this term still without a stub shape - // (so a term listed twice gets one stub). - int pid = -1; - for (const auto& we : equiEnds[ei]) { - if (we.isTerm && we.termInstId < 0 && we.term == item.label) { pid = we.portId; break; } - } - if (pid < 0) continue; - - bool isInput = (item.direction == Direction::Input); - const float tx = isInput ? termLx : termRx; - for (bool moved = true; moved; ) { - moved = false; - for (const auto& p : placedTermPos) - if (std::abs(p.x - tx) < 1.f && std::abs(p.y - ty) < 24.f) - { ty = p.y + 30.f; moved = true; } - } - placedTermPos.push_back(ImVec2(tx, ty)); - InstanceShape inst; - inst.id = nextInstId++; - inst.x = tx; - inst.y = ty; - inst.w = 0.f; inst.h = 0.f; - inst.partialInterface = false; - // Draws as a schematic-style boundary-port flag (see - // drawBoundaryPortInstance in SchematicView.cpp) rather than - // a generic box -- this pseudo-instance only exists to give - // the top-level design port a wire anchor point. - inst.modelName = "port"; - - Port p; - p.id = pid; p.name = item.label; - p.lx = isInput ? 1.0f : -1.0f; - p.ly = 0.f; - p.direction = item.direction; - p.isInput = isInput; - p.color = DiagnosisStore::netColor({}, stripBusIndex(item.label)); - inst.ports.push_back(p); - // Wire this term's ends to the stub. - for (auto& we : equiEnds[ei]) { - if (we.isTerm && we.termInstId < 0 && we.term == item.label) - we.termInstId = inst.id; + if (!item.isTerm) continue; + // The first end of this term not claimed by a stub yet (so a + // term listed twice gets one stub). + WireEnd* end = nullptr; + for (auto& we : equiEnds[ei]) + if (we.isTerm && we.termInstId < 0 && we.term == item.label) { end = &we; break; } + if (!end) continue; + end->termInstId = 0; // claimed; the real stub id is set below + // Level with the first instance pin across the net: a + // receiver for an input port, the driver for an output. + float wantY = 0.f; + const bool isInput = item.direction == Direction::Input; + for (int prefer = 0; prefer < 2; ++prefer) { + bool found = false; + for (const auto& we : equiEnds[ei]) { + if (we.isTerm || (prefer == 0 && we.drives == isInput)) continue; + if (auto at = pinWorld(we)) { wantY = at->y; found = true; break; } + } + if (found) break; } - g_schematic.instances.push_back(std::move(inst)); - ty += 30.f; + (isInput ? leftStubs : rightStubs).push_back({ ei, item.label, item.direction, end->portId, wantY }); } } } + for (auto* stubs : { &leftStubs, &rightStubs }) { + std::vector want; + for (const auto& st : *stubs) want.push_back(st.wantY); + const std::vector ys = stackPorts(want); + const bool isInput = stubs == &leftStubs; + for (size_t i = 0; i < stubs->size(); ++i) { + const PortStub& st = (*stubs)[i]; + InstanceShape inst; + inst.id = nextInstId++; + inst.x = isInput ? sheetX0 - kPortGap : sheetX1 + kPortGap; + inst.y = ys[i]; + inst.w = 0.f; inst.h = 0.f; + inst.partialInterface = false; + // Draws as a boundary-port flag (see + // drawBoundaryPortInstance in SchematicView.cpp) rather than + // a generic box -- this pseudo-instance only exists to give + // the top-level design port a wire anchor point. + inst.modelName = "port"; + + Port p; + p.id = st.portId; p.name = st.label; + p.lx = isInput ? 1.0f : -1.0f; + p.ly = 0.f; + p.direction = st.direction; + p.isInput = isInput; + p.color = DiagnosisStore::netColor({}, stripBusIndex(st.label)); + inst.ports.push_back(p); + for (auto& we : equiEnds[st.ei]) + if (we.isTerm && we.portId == st.portId) we.termInstId = inst.id; + g_schematic.instances.push_back(std::move(inst)); + } + } // Pass 4: wires per equip - auto renderedPort = [&](int pid) { - auto it = logicalToRenderedPortId.find(pid); - return it != logicalToRenderedPortId.end() ? it->second : pid; - }; for (size_t ei = 0; ei < equipotentials.size(); ++ei) { const auto& ends = equiEnds[ei]; if (ends.size() < 2) continue; @@ -1207,6 +1223,57 @@ void EquipotentialView::renderSchematic(const std::vector& equip g_schematic.nets = std::move(merged); } + // Route the top-level wires as one orthogonal tree per driving pin (see + // SchematicLayout::routeNets), around the boxes. Wiring nested inside an + // expanded instance keeps its simple per-wire drawing. + { + std::vector obstacles, keepOut; + for (const auto& inst : g_schematic.instances) { + if (inst.parentShapeId >= 0 || inst.w <= 0.f || inst.h <= 0.f) continue; + if (inst.isHierGroup) { // its label row + keepOut.push_back({ inst.x, inst.y, inst.x + inst.w, inst.y + kGroupHeader - kPinPitch }); + continue; + } + obstacles.push_back({ inst.x, inst.y, inst.x + inst.w, inst.y + inst.h }); + keepOut.push_back({ inst.x, inst.y - kPinPitch, inst.x + inst.w, inst.y }); // its name + } + std::map, size_t> routeOfSource; + std::vector routeNetsIn; + g_schematic.routes.clear(); + auto pinOf = [](const InstanceShape& inst, const Port& p) { + return RoutePin{ portAnchor(inst, p), p.lx >= 0.f }; + }; + for (auto& n : g_schematic.nets) { + if (n.containerShapeId >= 0) continue; + auto* srcInst = g_schematic.findInstanceById(n.srcInstance); + auto* dstInst = g_schematic.findInstanceById(n.dstInstance); + auto* srcPort = srcInst ? g_schematic.findPortById(*srcInst, n.srcPortId) : nullptr; + auto* dstPort = dstInst ? g_schematic.findPortById(*dstInst, n.dstPortId) : nullptr; + if (!srcPort || !dstPort) continue; + auto key = std::make_pair(n.srcInstance, n.srcPortId); + auto it = routeOfSource.find(key); + if (it == routeOfSource.end()) { + it = routeOfSource.emplace(key, routeNetsIn.size()).first; + routeNetsIn.push_back({ pinOf(*srcInst, *srcPort), {} }); + SchematicView::NetRoute r; + r.color = n.color; + r.netName = n.netName; + g_schematic.routes.push_back(std::move(r)); + } + routeNetsIn[it->second].receivers.push_back(pinOf(*dstInst, *dstPort)); + auto& r = g_schematic.routes[it->second]; + // A flagged wire colors the whole tree it belongs to. + if (n.color != NetWire{}.color) r.color = n.color; + r.isBus = r.isBus || n.isBus; + n.routed = true; + } + auto routed = routeNets(routeNetsIn, obstacles, keepOut); + for (size_t i = 0; i < routed.size(); ++i) { + g_schematic.routes[i].segments = std::move(routed[i].segments); + g_schematic.routes[i].junctions = std::move(routed[i].junctions); + } + } + static int lastTotal = -1; if (totalItems != lastTotal) { if (g_skipNextAutoFit) { diff --git a/src/EquipotentialView.h b/src/EquipotentialView.h index 66825be..200cddb 100644 --- a/src/EquipotentialView.h +++ b/src/EquipotentialView.h @@ -16,6 +16,7 @@ class EquipotentialView { Direction direction = Direction::Inout; unsigned childId = 0; // term child_id on the model std::optional bit; // set for bus bits + bool clock = false; // a sequential cell's clock pin }; // --- Hierarchy embedding (nested boxes) --- @@ -53,6 +54,7 @@ class EquipotentialView { std::vector pathIds; // matching child_ids std::vector pathModels; // matching model names DesignRef designRef{}; // the instance's model + PrimitiveType primitiveType = PrimitiveType::Unknown; bool hasInstances = false; std::optional sourceLoc; std::vector ports; // every pin, bus bits expanded diff --git a/src/LocalSNLProvider.cpp b/src/LocalSNLProvider.cpp index b06c4a4..29220a7 100644 --- a/src/LocalSNLProvider.cpp +++ b/src/LocalSNLProvider.cpp @@ -68,36 +68,24 @@ static std::string instanceName(const SNLInstance* i) { return i->getString(); } -// Best-effort gate/cell function classification from a design's name (naja -// has no function/timing-arc API to ask directly -- see PrimitiveType in -// Types.h). Checked as a case-insensitive prefix so common Liberty/Verilog -// naming conventions ("AND2X1", "nand3_1", "DFFX2", ...) match regardless of -// the vendor-specific drive-strength/version suffix; arity itself is read -// from the instance's actual port count at render time, not parsed here. -// protocol.py's get_primitive_type() mirrors this independently, same -// as the rest of the wire protocol (see CLAUDE.md). +// Gate/cell function classification, from naja's own modeling of the cell +// (the truth table a Liberty `function` or an NLDB0 gate defines, and its +// sequential model) -- never from its name. A cell naja has no model for +// (e.g. gate-level Verilog loaded without Liberty) is Unknown and draws as +// the generic box. protocol.py's get_primitive_type() mirrors this, same as +// the rest of the wire protocol (see CLAUDE.md). static PrimitiveType getPrimitiveType(const SNLDesign* model) { if (!model) return PrimitiveType::Unknown; if (NLDB0::isAssign(model)) return PrimitiveType::Assign; - - std::string name = designName(model); - for (auto& c : name) c = static_cast(std::toupper(static_cast(c))); - - static const std::pair kPrefixes[] = { - {"XNOR", PrimitiveType::Xnor}, - {"NAND", PrimitiveType::Nand}, - {"NOR", PrimitiveType::Nor}, - {"XOR", PrimitiveType::Xor}, - {"AND", PrimitiveType::And}, - {"OR", PrimitiveType::Or}, - {"DFF", PrimitiveType::Dff}, - {"INV", PrimitiveType::Inv}, - {"NOT", PrimitiveType::Inv}, - {"BUF", PrimitiveType::Buf}, - }; - for (const auto& [prefix, type] : kPrefixes) { - if (name.rfind(prefix, 0) == 0) return type; - } + if (SNLDesignModeling::isSequential(model)) return PrimitiveType::Dff; + if (SNLDesignModeling::isInv(model)) return PrimitiveType::Inv; + if (SNLDesignModeling::isBuf(model)) return PrimitiveType::Buf; + if (SNLDesignModeling::isAnd(model)) return PrimitiveType::And; + if (SNLDesignModeling::isNand(model)) return PrimitiveType::Nand; + if (SNLDesignModeling::isOr(model)) return PrimitiveType::Or; + if (SNLDesignModeling::isNor(model)) return PrimitiveType::Nor; + if (SNLDesignModeling::isXor(model)) return PrimitiveType::Xor; + if (SNLDesignModeling::isXnor(model)) return PrimitiveType::Xnor; return PrimitiveType::Unknown; } @@ -168,6 +156,8 @@ static json bitTermJson(const SNLBitTerm* bt) { }; if (auto* btb = dynamic_cast(bt)) t["bit"] = static_cast(btb->getBit()); + // Only sent when set: marks a flip-flop's clock pin (see drawDffInstance). + if (SNLDesignModeling::isClock(bt)) t["clock"] = true; return t; } @@ -914,6 +904,7 @@ std::string LocalSNLProvider::buildResolveInstanceResponse(const json& req) cons {"library_id", static_cast(design->getLibrary()->getID())}, {"design_id", static_cast(design->getID())} }}, + {"primitive_type", toString(getPrimitiveType(design))}, {"has_instances", hasAnySubInstances(design)}, {"source_loc", sourceLocJson(instance)}, {"terms", allBitTermsJson(design)} diff --git a/src/SchematicLayout.cpp b/src/SchematicLayout.cpp index c79b8a5..b9e7bcd 100644 --- a/src/SchematicLayout.cpp +++ b/src/SchematicLayout.cpp @@ -2,6 +2,7 @@ #include "SchematicLayout.h" #include +#include #include #include @@ -27,6 +28,7 @@ void buildItems(const Equipotential* eq, item.primitiveType = occ.primitiveType; item.termChildId = occ.term.child_id; item.termBit = occ.term.bit; + item.clock = occ.term.clock; item.pathIds = occ.pathIds; item.hasInstances = occ.has_instances; item.bitTermCount = occ.bit_term_count; @@ -39,104 +41,566 @@ void buildItems(const Equipotential* eq, } // --------------------------------------------------------------------------- -// IncrementalLayout +// Symbols // --------------------------------------------------------------------------- -void IncrementalLayout::place(const Equipotential* eq) { - if (laidOut_.count(eq)) return; - laidOut_.insert(eq); - - std::vector drivers, receivers; - buildItems(eq, drivers, receivers); - if (drivers.empty() && receivers.empty()) return; - - // Find anchor: first non-term already placed - const Item* anchor = nullptr; - bool anchorDrives = false; - - for (const auto& item : drivers) { - if (!item.isTerm && placed_.count(item.path)) - { anchor = &item; anchorDrives = true; break; } - } - if (!anchor) { - for (const auto& item : receivers) { - if (!item.isTerm && placed_.count(item.path)) - { anchor = &item; anchorDrives = false; break; } +namespace { +// Approximate width of one pin-label character at the base label size, and +// the longest label a box makes room for (drawPorts truncates past that). +constexpr float kPinCharW = 6.5f; +constexpr size_t kPinLabelMaxLen = 14; + +float snapUp(float v, float step) { return std::ceil(v / step - 1e-4f) * step; } + +float labelWidth(const std::vector& names) { + size_t n = 0; + for (const auto& s : names) n = std::max(n, std::min(s.size(), kPinLabelMaxLen)); + return float(n) * kPinCharW; +} +} // namespace + +bool isGateSymbol(PrimitiveType type) { + switch (type) { + case PrimitiveType::And: case PrimitiveType::Nand: + case PrimitiveType::Or: case PrimitiveType::Nor: + case PrimitiveType::Xor: case PrimitiveType::Xnor: + case PrimitiveType::Inv: case PrimitiveType::Buf: + case PrimitiveType::Assign: + return true; + default: + return false; + } +} + +SymbolGeometry symbolGeometry(PrimitiveType type, + const std::vector& leftNames, + const std::vector& rightNames) { + SymbolGeometry g; + const size_t nL = leftNames.size(), nR = rightNames.size(); + if (isGateSymbol(type) && nR <= 1) { + // Inputs centered on the output, kPinPitch apart: with an even + // pitch and h a multiple of it, every pin lands on the grid. + g.h = std::max(2.f * kPinPitch, kPinPitch * float(nL)); + for (size_t i = 0; i < nL; ++i) + g.leftY.push_back(0.5f * g.h + kPinPitch * (float(i) - 0.5f * float(nL - 1))); + if (nR) g.rightY.push_back(0.5f * g.h); + const bool single = type == PrimitiveType::Inv || type == PrimitiveType::Buf || + type == PrimitiveType::Assign; + const bool xorLike = type == PrimitiveType::Xor || type == PrimitiveType::Xnor; + g.w = single ? 2.f * kPinPitch : snapUp(g.h + kGrid + (xorLike ? kGrid : 0.f), kGrid); + return g; + } + // Box: pins run down from the top, names drawn inside next to them. + const size_t rows = std::max(nL, nR); + g.h = std::max(2.f * kPinPitch, kPinPitch * float(rows + 1)); + for (size_t i = 0; i < nL; ++i) g.leftY.push_back(kPinPitch * float(i + 1)); + for (size_t i = 0; i < nR; ++i) g.rightY.push_back(kPinPitch * float(i + 1)); + g.w = snapUp(std::max(6.f * kGrid, labelWidth(leftNames) + labelWidth(rightNames) + 3.f * kGrid), kGrid); + return g; +} + +// --------------------------------------------------------------------------- +// Layered placement +// --------------------------------------------------------------------------- +namespace { +struct PlaceEdge { + int u, v; // driver node -> receiver node + float du, dv; // pin offsets on each +}; + +// Weighted isotonic regression with spacing, by pool-adjacent-violators: +// places items (in this order, top to bottom) at y_i >= y_{i-1} + h_{i-1} + +// gap, as close as possible (least squares, weighted) to their targets. +std::vector packColumn(const std::vector& target, const std::vector& weight, + const std::vector& h, float gap, bool snap = true) { + const size_t n = target.size(); + std::vector offset(n, 0.f); + for (size_t i = 1; i < n; ++i) offset[i] = offset[i - 1] + h[i - 1] + gap; + struct Block { float sumW, sumWZ; size_t first, count; }; + std::vector blocks; + for (size_t i = 0; i < n; ++i) { + blocks.push_back({ weight[i], weight[i] * (target[i] - offset[i]), i, 1 }); + while (blocks.size() > 1) { + Block& b = blocks.back(); + Block& a = blocks[blocks.size() - 2]; + if (a.sumWZ / a.sumW <= b.sumWZ / b.sumW) break; + a.sumW += b.sumW; a.sumWZ += b.sumWZ; a.count += b.count; + blocks.pop_back(); } } + std::vector y(n, 0.f); + for (const auto& b : blocks) { + float z = b.sumWZ / b.sumW; + if (snap) z = std::round(z / kGrid) * kGrid; + for (size_t i = b.first; i < b.first + b.count; ++i) y[i] = z + offset[i]; + } + return y; +} - if (!anchor) { - // First/independent net: two-column layout below existing content - const float lx = kLeftMargin; - const float rx = kLeftMargin + kInstW + kColGap; - float dyl = nextY_, dyr = nextY_; - for (const auto& item : drivers) { - if (!item.isTerm) { - placed_.emplace(item.path, ImVec2{lx, dyl}); - dyl += kInstH + kRowSpacing; - } +float lowerMedian(std::vector v) { + std::nth_element(v.begin(), v.begin() + (v.size() - 1) / 2, v.end()); + return v[(v.size() - 1) / 2]; +} +} // namespace + +Placement layeredPlacement(const std::vector& nodes, const std::vector& nets) { + const int n = int(nodes.size()); + Placement out; + out.pos.assign(n, ImVec2(0.f, 0.f)); + out.level.assign(n, 0); + if (n == 0) return out; + + std::vector edges; + for (const auto& net : nets) + for (const auto& d : net.drivers) + for (const auto& r : net.receivers) + if (d.node != r.node && d.node >= 0 && r.node >= 0 && d.node < n && r.node < n) + edges.push_back({ d.node, r.node, d.dy, r.dy }); + + std::vector> outEdges(n), inEdges(n); + for (int e = 0; e < int(edges.size()); ++e) { + outEdges[edges[e].u].push_back(e); + inEdges[edges[e].v].push_back(e); + } + + // 1. Cycle breaking: an edge to a node still on the DFS stack closes a + // loop (e.g. a flop's feedback) and is left out of the layering. + std::vector isBack(edges.size(), 0), state(n, 0); + for (int root = 0; root < n; ++root) { + if (state[root]) continue; + std::vector> stack{ { root, 0 } }; + state[root] = 1; + while (!stack.empty()) { + auto& [v, next] = stack.back(); + if (next == outEdges[v].size()) { state[v] = 2; stack.pop_back(); continue; } + int e = outEdges[v][next++]; + int w = edges[e].v; + if (state[w] == 1) isBack[e] = 1; + else if (state[w] == 0) { state[w] = 1; stack.push_back({ w, 0 }); } + } + } + + // 2. Longest-path levels over the forward edges (Kahn order)... + std::vector indeg(n, 0), topo; + for (int e = 0; e < int(edges.size()); ++e) if (!isBack[e]) ++indeg[edges[e].v]; + for (int v = 0; v < n; ++v) if (!indeg[v]) topo.push_back(v); + for (size_t i = 0; i < topo.size(); ++i) { + int u = topo[i]; + for (int e : outEdges[u]) { + if (isBack[e]) continue; + int v = edges[e].v; + out.level[v] = std::max(out.level[v], out.level[u] + 1); + if (--indeg[v] == 0) topo.push_back(v); } - for (const auto& item : receivers) { - if (!item.isTerm) { - placed_.emplace(item.path, ImVec2{rx, dyr}); - dyr += kInstH + kRowSpacing; + } + // ...then pull every node with no forward in-edge right up against its + // nearest receiver, so a cone's inputs sit next to what they feed rather + // than all stacked in column 0. + for (auto it = topo.rbegin(); it != topo.rend(); ++it) { + int v = *it; + bool hasPred = false; + int minSucc = INT_MAX; + for (int e : inEdges[v]) if (!isBack[e]) hasPred = true; + for (int e : outEdges[v]) if (!isBack[e]) minSucc = std::min(minSucc, out.level[edges[e].v]); + if (!hasPred && minSucc != INT_MAX) out.level[v] = std::max(out.level[v], minSucc - 1); + } + + int maxLevel = 0; + for (int v = 0; v < n; ++v) maxLevel = std::max(maxLevel, out.level[v]); + std::vector> layers(maxLevel + 1); + for (int v = 0; v < n; ++v) layers[out.level[v]].push_back(v); + + // 3. Crossing reduction: barycenter sweeps, down then up. A neighbor + // counts at its order index plus the fraction of its height where + // the pin sits, so pin order on a tall box matters too. + std::vector order(n, 0.f); + auto renumber = [&] { + for (auto& layer : layers) + for (size_t i = 0; i < layer.size(); ++i) order[layer[i]] = float(i); + }; + renumber(); + auto frac = [&](int node, float dy) { return nodes[node].h > 0.f ? dy / nodes[node].h : 0.5f; }; + for (int sweep = 0; sweep < 4; ++sweep) { + const bool down = sweep % 2 == 0; + for (int li = 0; li <= maxLevel; ++li) { + int l = down ? li : maxLevel - li; + auto& layer = layers[l]; + std::vector> keyed; + for (int v : layer) { + float sum = 0.f; int cnt = 0; + auto visit = [&](int other, float dOther) { + int lo = out.level[other]; + if (down ? lo < l : lo > l) { sum += order[other] + frac(other, dOther); ++cnt; } + }; + for (int e : inEdges[v]) visit(edges[e].u, edges[e].du); + for (int e : outEdges[v]) visit(edges[e].v, edges[e].dv); + keyed.push_back({ cnt ? sum / float(cnt) : order[v] + 0.5f, v }); } + std::stable_sort(keyed.begin(), keyed.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + for (size_t i = 0; i < keyed.size(); ++i) layer[i] = keyed[i].second; + renumber(); } - nextY_ = std::max(dyl, dyr) + kNetVGap; - } else { - // Expansion: extend horizontally from anchor - const ImVec2 ap = placed_[anchor->path]; - const auto& items = anchorDrives ? receivers : drivers; - float newX = anchorDrives - ? ap.x + kInstW + kColGap // new receivers go right of driver - : ap.x - kInstW - kColGap; // new drivers go left of receiver - float dy = ap.y; - // Two nets can anchor to the same instance (e.g. each input of a - // gate in a driver trace) and would otherwise stack their new boxes - // at the same spot in the same column: slide down past anything - // already placed there. - auto isFree = [&](float x, float y) { - for (const auto& [key, p] : placed_) - if (std::abs(p.x - x) < kInstW && std::abs(p.y - y) < kInstH + kRowSpacing / 2) - return false; - return true; - }; - for (const auto& item : items) { - if (item.isTerm || placed_.count(item.path)) continue; - while (!isFree(newX, dy)) dy += kInstH + kRowSpacing; - placed_.emplace(item.path, ImVec2{newX, dy}); - dy += kInstH + kRowSpacing; + } + + // 4. Columns: each as wide as its widest node, then a channel sized for + // the nets crossing it. + std::vector crossing(maxLevel + 1, 0); + for (const auto& net : nets) { + int lo = INT_MAX, hi = INT_MIN; + for (const auto* pins : { &net.drivers, &net.receivers }) + for (const auto& p : *pins) { + if (p.node < 0 || p.node >= n) continue; + lo = std::min(lo, out.level[p.node]); + hi = std::max(hi, out.level[p.node]); + } + for (int l = std::max(lo, 0); l < hi; ++l) ++crossing[l]; + } + std::vector colX(maxLevel + 1, kLeftMargin); + for (int l = 0; l < maxLevel; ++l) { + float colW = 0.f; + for (int v : layers[l]) colW = std::max(colW, nodes[v].w); + colX[l + 1] = colX[l] + colW + kChannelMin + kTrackPitch * float(crossing[l]); + } + + // 5. Heights: start stacked, then alternate sweeps pulling each column + // towards pin-aligned heights with its already-placed neighbors. + std::vector y(n, 0.f); + for (auto& layer : layers) { + float cy = 0.f; + for (int v : layer) { y[v] = cy; cy += nodes[v].h + kRowSpacing; } + } + const int rounds[] = { +1, -1, +1 }; + for (int dir : rounds) { + for (int li = 0; li <= maxLevel; ++li) { + int l = dir > 0 ? li : maxLevel - li; + auto& layer = layers[l]; + std::vector target, weight, h; + for (int v : layer) { + std::vector want; + auto visit = [&](int other, float dOther, float dSelf) { + int lo = out.level[other]; + if (dir > 0 ? lo < l : lo > l) want.push_back(y[other] + dOther - dSelf); + }; + for (int e : inEdges[v]) visit(edges[e].u, edges[e].du, edges[e].dv); + for (int e : outEdges[v]) visit(edges[e].v, edges[e].dv, edges[e].du); + target.push_back(want.empty() ? y[v] : lowerMedian(want)); + weight.push_back(want.empty() ? 0.05f : float(want.size())); + h.push_back(nodes[v].h); + } + auto placed = packColumn(target, weight, h, kRowSpacing); + for (size_t i = 0; i < layer.size(); ++i) y[layer[i]] = placed[i]; } } + + float minY = 1e30f; + for (int v = 0; v < n; ++v) minY = std::min(minY, y[v]); + for (int v = 0; v < n; ++v) + out.pos[v] = ImVec2(colX[out.level[v]], y[v] - minY); + return out; } -void IncrementalLayout::placeAlone(const InstancePath& path) { - if (placed_.count(path)) return; - placed_.emplace(path, ImVec2{kLeftMargin, nextY_}); - nextY_ += kInstH + kRowSpacing + kNetVGap; +std::vector stackPorts(const std::vector& desiredY) { + std::vector idx(desiredY.size()); + for (size_t i = 0; i < idx.size(); ++i) idx[i] = i; + std::stable_sort(idx.begin(), idx.end(), + [&](size_t a, size_t b) { return desiredY[a] < desiredY[b]; }); + std::vector target, weight(idx.size(), 1.f), h(idx.size(), 0.f); + for (size_t i : idx) target.push_back(desiredY[i]); + // Not snapped: a port lines up with its pin wherever that is. + auto placed = packColumn(target, weight, h, kPinPitch, /*snap=*/false); + std::vector out(desiredY.size()); + for (size_t k = 0; k < idx.size(); ++k) out[idx[k]] = placed[k]; + return out; } -void IncrementalLayout::resolveColumnOverlaps(std::vector& instances) { - std::map> byColumn; - for (auto& inst : instances) - if (inst.w > 0.f) byColumn[std::lround(inst.x)].push_back(&inst); - for (auto& [x, col] : byColumn) { - std::sort(col.begin(), col.end(), - [](const InstanceShape* a, const InstanceShape* b) { return a->y < b->y; }); - float minY = -1e9f; - for (auto* inst : col) { - if (inst->y < minY) inst->y = minY; - minY = inst->y + inst->h + kRowSpacing; - placed_[inst->path] = ImVec2{inst->x, inst->y}; +// --------------------------------------------------------------------------- +// Routing +// --------------------------------------------------------------------------- +namespace { +struct Channel { float x0, x1; }; + +std::vector findChannels(const std::vector& obstacles, + const std::vector& nets) { + float pinMin = 1e30f, pinMax = -1e30f; + for (const auto& net : nets) { + pinMin = std::min(pinMin, net.driver.at.x); pinMax = std::max(pinMax, net.driver.at.x); + for (const auto& r : net.receivers) { + pinMin = std::min(pinMin, r.at.x); pinMax = std::max(pinMax, r.at.x); } } + std::vector> spans; + for (const auto& r : obstacles) spans.push_back({ r.x0, r.x1 }); + std::sort(spans.begin(), spans.end()); + std::vector> merged; + for (const auto& s : spans) { + if (!merged.empty() && s.first <= merged.back().second + 1.f) + merged.back().second = std::max(merged.back().second, s.second); + else + merged.push_back(s); + } + std::vector channels; + if (merged.empty()) { + channels.push_back({ pinMin - kChannelMin, pinMax + kChannelMin }); + return channels; + } + channels.push_back({ std::min(pinMin, merged.front().first - kChannelMin), merged.front().first }); + for (size_t i = 1; i < merged.size(); ++i) + channels.push_back({ merged[i - 1].second, merged[i].first }); + channels.push_back({ merged.back().second, std::max(pinMax, merged.back().second + kChannelMin) }); + return channels; +} + +// The channel a wire enters when it leaves `pin` on its side. +int channelOf(const std::vector& channels, const RoutePin& pin) { + constexpr float eps = 0.5f; + if (pin.right) { + for (int c = 0; c < int(channels.size()); ++c) + if (channels[c].x1 > pin.at.x + eps) return c; + return int(channels.size()) - 1; + } + for (int c = int(channels.size()) - 1; c >= 0; --c) + if (channels[c].x0 < pin.at.x - eps) return c; + return 0; +} + +// Something a spine must not run along: a box, a label, or another net's +// wire (net >= 0; a net's own wires don't block it). +struct Blocker { + RouteRect r; + int net = -1; +}; + +bool horizontalIsFree(const std::vector& blockers, int net, float y, float xa, float xb) { + constexpr float clearance = 4.f; + for (const auto& b : blockers) { + const auto& r = b.r; + if (b.net >= 0 && b.net == net) continue; + if (y > r.y0 - clearance && y < r.y1 + clearance && xb > r.x0 && xa < r.x1) return false; + } + return true; } -void IncrementalLayout::clear() { - placed_.clear(); - laidOut_.clear(); - nextY_ = 0.f; +// A wire's vertical run in one channel. Its horizontals attach on the +// channel's left side (from a pin or spine to the left, side -1) or right +// side (+1). +struct Trunk { + int net, channel; + float y0, y1; + std::vector> attach; // (y, side) + int track = 0; + float x = 0.f; +}; + +// Channel routing's vertical constraint: when A attaches on the left and B on +// the right at the same height, A's horizontal runs from the left edge to +// A's trunk and B's from B's trunk to the right edge -- they only stay apart +// (instead of overlapping into what looks like one wire) if A is left of B. +bool mustBeLeftOf(const Trunk& a, const Trunk& b) { + for (const auto& [ya, sa] : a.attach) + if (sa < 0) + for (const auto& [yb, sb] : b.attach) + if (sb > 0 && std::abs(ya - yb) < 0.5f * kGrid) return true; + return false; +} + +// Tracks for the trunks of one channel, left to right, respecting the +// vertical constraints (a cycle of them can't be met; its closing edge is +// dropped) and packing trunks that don't overlap vertically onto one track. +// Returns the number of tracks. +int assignTracks(std::vector& ts) { + const size_t n = ts.size(); + std::vector> before(n); // before[j]: trunks that must be left of j + std::vector> after(n); + for (size_t i = 0; i < n; ++i) + for (size_t j = 0; j < n; ++j) + if (i != j && ts[i]->net != ts[j]->net && mustBeLeftOf(*ts[i], *ts[j])) { + after[i].push_back(j); + before[j].push_back(i); + } + // Rank = longest constraint chain ending here (Kahn; leftover cycle + // members keep the rank they reached). + std::vector rank(n, 0), indeg(n, 0); + for (size_t j = 0; j < n; ++j) indeg[j] = int(before[j].size()); + std::vector queue; + for (size_t i = 0; i < n; ++i) if (!indeg[i]) queue.push_back(i); + std::vector done(n, 0); + for (size_t q = 0; q < queue.size(); ++q) { + size_t i = queue[q]; + done[i] = 1; + for (size_t j : after[i]) { + rank[j] = std::max(rank[j], rank[i] + 1); + if (--indeg[j] == 0) queue.push_back(j); + } + } + std::vector orderIdx(n); + for (size_t i = 0; i < n; ++i) orderIdx[i] = i; + std::stable_sort(orderIdx.begin(), orderIdx.end(), [&](size_t a, size_t b) { + return rank[a] != rank[b] ? rank[a] < rank[b] : ts[a]->y0 < ts[b]->y0; + }); + std::vector> tracks; + for (size_t i : orderIdx) { + size_t minTrack = 0; + for (size_t p : before[i]) + if (done[p] || rank[p] < rank[i]) minTrack = std::max(minTrack, size_t(ts[p]->track) + 1); + size_t k = minTrack; + for (; k < tracks.size(); ++k) { + bool clear = true; + for (size_t o : tracks[k]) + if (ts[o]->y0 < ts[i]->y1 + kGrid && ts[i]->y0 < ts[o]->y1 + kGrid) { clear = false; break; } + if (clear) break; + } + if (k >= tracks.size()) tracks.resize(k + 1); + tracks[k].push_back(i); + ts[i]->track = int(k); + } + return int(tracks.size()); } +} // namespace + +std::vector routeNets(const std::vector& nets, + const std::vector& obstacles, + const std::vector& keepOut) { + // A spine avoids boxes, labels, every other net's pin stubs (across the + // stub's whole channel), and the spines chosen before it. + std::vector spineBlockers; + for (const auto* rects : { &obstacles, &keepOut }) + for (const auto& r : *rects) spineBlockers.push_back({ r, -1 }); + std::vector out(nets.size()); + if (nets.empty()) return out; + const auto channels = findChannels(obstacles, nets); + for (size_t ni = 0; ni < nets.size(); ++ni) { + auto block = [&](const RoutePin& p) { + const Channel& c = channels[channelOf(channels, p)]; + spineBlockers.push_back({ { c.x0, p.at.y - 1.f, c.x1, p.at.y + 1.f }, int(ni) }); + }; + block(nets[ni].driver); + for (const auto& r : nets[ni].receivers) block(r); + } + + // Per net: driver channel, receivers grouped by channel, spine height. + struct Plan { + int driverChannel = 0; + std::map> byChannel; // channel -> receiver indices + bool hasSpine = false; + float spineY = 0.f; + std::map trunkOf; // channel -> index in trunks + }; + std::vector plans(nets.size()); + std::vector trunks; + for (size_t ni = 0; ni < nets.size(); ++ni) { + const auto& net = nets[ni]; + Plan& plan = plans[ni]; + plan.driverChannel = channelOf(channels, net.driver); + for (size_t ri = 0; ri < net.receivers.size(); ++ri) + plan.byChannel[channelOf(channels, net.receivers[ri])].push_back(ri); + + float xa = channels[plan.driverChannel].x0, xb = channels[plan.driverChannel].x1; + for (const auto& [c, _] : plan.byChannel) { + if (c == plan.driverChannel) continue; + plan.hasSpine = true; + xa = std::min(xa, channels[c].x0); + xb = std::max(xb, channels[c].x1); + } + const float dy = net.driver.at.y; + if (plan.hasSpine) { + plan.spineY = dy; + for (int k = 1; k <= 400 && !horizontalIsFree(spineBlockers, int(ni), plan.spineY, xa, xb); ++k) + plan.spineY = dy + (k % 2 ? 1.f : -1.f) * kGrid * float((k + 1) / 2); + spineBlockers.push_back({ { xa, plan.spineY - 1.f, xb, plan.spineY + 1.f }, int(ni) }); + } + + // Trunk extents: the driver's channel spans the driver, the spine + // and its own receivers; every other channel the spine and its + // receivers. + // A pin left of its channel (wire leaving rightwards) attaches on + // the channel's left side; the spine attaches on the side facing + // the channel it comes from or goes to. + std::map byCh; + auto attach = [&](int c, float y, int side) { + auto it = byCh.find(c); + if (it == byCh.end()) it = byCh.emplace(c, Trunk{ int(ni), c, y, y, {} }).first; + it->second.y0 = std::min(it->second.y0, y); + it->second.y1 = std::max(it->second.y1, y); + it->second.attach.push_back({ y, side }); + }; + auto pinSide = [](const RoutePin& p) { return p.right ? -1 : +1; }; + attach(plan.driverChannel, dy, pinSide(net.driver)); + for (const auto& [c, rs] : plan.byChannel) { + if (c != plan.driverChannel) { + attach(plan.driverChannel, plan.spineY, c > plan.driverChannel ? +1 : -1); + attach(c, plan.spineY, c > plan.driverChannel ? -1 : +1); + } + for (size_t ri : rs) attach(c, net.receivers[ri].at.y, pinSide(net.receivers[ri])); + } + for (auto& [c, t] : byCh) { + plan.trunkOf[c] = int(trunks.size()); + trunks.push_back(std::move(t)); + } + } + // Tracks, spread evenly across each channel. A straight pass-through + // (a zero-length trunk) takes part too: where its wire meets others' + // stubs still depends on its position. + std::map> byChannel; + for (auto& t : trunks) byChannel[t.channel].push_back(&t); + for (auto& [c, ts] : byChannel) { + const float n = float(assignTracks(ts)); + const float x0 = channels[c].x0, w = channels[c].x1 - channels[c].x0; + for (auto* t : ts) t->x = x0 + w * float(t->track + 1) / (n + 1.f); + } + + // Segments, then junctions where three or more directions meet. + for (size_t ni = 0; ni < nets.size(); ++ni) { + const auto& net = nets[ni]; + const Plan& plan = plans[ni]; + auto& segs = out[ni].segments; + auto add = [&](ImVec2 a, ImVec2 b) { + if (std::abs(a.x - b.x) > 1e-3f || std::abs(a.y - b.y) > 1e-3f) segs.push_back({ a, b }); + }; + const Trunk& dt = trunks[plan.trunkOf.at(plan.driverChannel)]; + add(net.driver.at, ImVec2(dt.x, net.driver.at.y)); + float spineX0 = dt.x, spineX1 = dt.x; + for (const auto& [c, ti] : plan.trunkOf) { + const Trunk& t = trunks[ti]; + add(ImVec2(t.x, t.y0), ImVec2(t.x, t.y1)); + spineX0 = std::min(spineX0, t.x); + spineX1 = std::max(spineX1, t.x); + } + if (plan.hasSpine) add(ImVec2(spineX0, plan.spineY), ImVec2(spineX1, plan.spineY)); + for (const auto& [c, rs] : plan.byChannel) { + const Trunk& t = trunks[plan.trunkOf.at(c)]; + for (size_t ri : rs) add(ImVec2(t.x, net.receivers[ri].at.y), net.receivers[ri].at); + } + + std::vector points; + for (const auto& s : segs) { points.push_back(s.a); points.push_back(s.b); } + for (const auto& p : points) { + bool left = false, right = false, up = false, down = false; + for (const auto& s : segs) { + const float tol = 0.5f; + if (std::abs(s.a.y - s.b.y) < tol && std::abs(s.a.y - p.y) < tol) { + float lo = std::min(s.a.x, s.b.x), hi = std::max(s.a.x, s.b.x); + if (p.x > lo - tol && p.x < hi + tol) { + if (p.x > lo + tol) left = true; + if (p.x < hi - tol) right = true; + } + } else if (std::abs(s.a.x - s.b.x) < tol && std::abs(s.a.x - p.x) < tol) { + float lo = std::min(s.a.y, s.b.y), hi = std::max(s.a.y, s.b.y); + if (p.y > lo - tol && p.y < hi + tol) { + if (p.y > lo + tol) up = true; + if (p.y < hi - tol) down = true; + } + } + } + if (int(left) + int(right) + int(up) + int(down) < 3) continue; + bool dup = false; + for (const auto& j : out[ni].junctions) + if (std::abs(j.x - p.x) < 0.5f && std::abs(j.y - p.y) < 0.5f) { dup = true; break; } + if (!dup) out[ni].junctions.push_back(p); + } + } + return out; +} // --------------------------------------------------------------------------- // Hierarchy grouping // --------------------------------------------------------------------------- @@ -147,7 +611,7 @@ struct GroupNode { int depth = 0; std::vector> groups; std::map groupByName; - std::vector leaves; + std::vector> leaves; // leaf, logic level // Filled by measureGroup(). float levelMin = 0.f, levelMax = 0.f, sortY = 0.f; float w = 0.f, h = 0.f; @@ -155,10 +619,6 @@ struct GroupNode { std::vector> leafRel; }; -float leafLevel(const InstanceShape& s) { - return (s.x - kLeftMargin) / (kInstW + kColGap); -} - void measureGroup(GroupNode& node, bool isRoot) { struct Elem { GroupNode* g; InstanceShape* leaf; float center, sortY, w, h; }; std::vector elems; @@ -170,8 +630,8 @@ void measureGroup(GroupNode& node, bool isRoot) { node.levelMax = std::max(node.levelMax, g->levelMax); node.sortY = std::min(node.sortY, g->sortY); } - for (auto* leaf : node.leaves) { - float lv = leafLevel(*leaf); + for (auto [leaf, level] : node.leaves) { + float lv = float(level); elems.push_back({ nullptr, leaf, lv, leaf->y, leaf->w, leaf->h }); node.levelMin = std::min(node.levelMin, lv); node.levelMax = std::max(node.levelMax, lv); @@ -185,13 +645,24 @@ void measureGroup(GroupNode& node, bool isRoot) { const float pad = isRoot ? 0.f : kGroupPad; const float top = isRoot ? 0.f : kGroupHeader; - const float colGap = isRoot ? kColGap : kGroupColGap; + const float colGap = isRoot ? kChannelMin + 2.f * kGroupPad : kGroupColGap; + // A sub-frame is raised by its header so its own leaves land at their + // heights; when that would lift it above this frame's top, everything + // here moves down together instead, keeping the alignment. + float raise = 0.f; + for (const auto& e : elems) + if (e.g) raise = std::max(raise, kGroupHeader - (e.sortY - node.sortY)); float x = pad, maxBottom = top; for (auto& [col, members] : columns) { std::stable_sort(members.begin(), members.end(), [](const Elem* a, const Elem* b) { return a->sortY < b->sortY; }); float y = top, colW = 0.f; for (auto* e : members) { + // Keep the height the layered placement gave it (relative to the + // frame's topmost content), so pins it aligned stay aligned, + // unless the column above pushes it down. + const float want = top + raise + (e->sortY - node.sortY) - (e->g ? kGroupHeader : 0.f); + y = std::max(y, want); if (e->g) e->g->rel = ImVec2(x, y); else node.leafRel.push_back({ e->leaf, ImVec2(x, y) }); y += e->h + kRowSpacing; @@ -200,7 +671,7 @@ void measureGroup(GroupNode& node, bool isRoot) { maxBottom = std::max(maxBottom, y - kRowSpacing); x += colW + colGap; } - node.w = std::max(x - colGap + pad, 2.f * pad + kInstW); + node.w = std::max(x - colGap + pad, 2.f * pad + 6.f * kGrid); node.h = maxBottom + pad; } @@ -266,7 +737,7 @@ std::vector layoutHierarchyGroups(const std::mapsecond; } - node->leaves.push_back(leaf); + node->leaves.push_back({ leaf, lh.level }); } measureGroup(root, true); diff --git a/src/SchematicLayout.h b/src/SchematicLayout.h index 12fb35e..5c7ce44 100644 --- a/src/SchematicLayout.h +++ b/src/SchematicLayout.h @@ -20,18 +20,122 @@ namespace SchematicLayout { // --------------------------------------------------------------------------- // Geometry constants (world units) +// +// Schematic sheet: every pin sits on a kGrid multiple, so a driver pin and +// the receiver pin placed level with it are joined by a straight wire. // --------------------------------------------------------------------------- -inline constexpr float kInstW = 180.0f; -inline constexpr float kInstH = 70.0f; -inline constexpr float kColGap = 120.0f; -inline constexpr float kRowSpacing = 24.0f; +inline constexpr float kGrid = 10.0f; +inline constexpr float kPinPitch = 20.0f; // distance between neighboring pins +inline constexpr float kRowSpacing = 30.0f; // vertical gap between boxes in a column inline constexpr float kLeftMargin = 20.0f; -inline constexpr float kNetVGap = 80.0f; +inline constexpr float kChannelMin = 50.0f; // narrowest routing channel between columns +inline constexpr float kTrackPitch = 10.0f; // extra channel width per vertical wire track +inline constexpr float kPortGap = 50.0f; // top-level ports -> nearest box // Hierarchy grouping (module frames around traced leaves). -inline constexpr float kGroupHeader = 30.0f; // room for the frame's label -inline constexpr float kGroupPad = 18.0f; // inner padding of a frame -inline constexpr float kGroupColGap = 90.0f; // gap between columns inside a frame +// Grid multiples, so leaves inside frames keep their pins on the grid. +inline constexpr float kGroupHeader = 40.0f; // room for the frame's label and a leaf's name +inline constexpr float kGroupPad = 20.0f; // inner padding of a frame +inline constexpr float kGroupColGap = 70.0f; // gap between columns inside a frame + +// --------------------------------------------------------------------------- +// Symbols. A gate (AND/OR/XOR families, INV/BUF, assign) is drawn as its +// standard symbol, sized by its input count; anything else is a box sized to +// fit its pin names, which are drawn inside it. Pins sit on the kPinPitch +// grid: a gate's inputs are centered on its single output, a box's pins run +// down from the top on each side. Pin offsets are from the symbol's top edge. +// --------------------------------------------------------------------------- +struct SymbolGeometry { + float w = 0.f, h = 0.f; + std::vector leftY; // one per left (input) pin, in order + std::vector rightY; // one per right (output/inout) pin, in order +}; + +// True for the types drawn as a gate symbol rather than a box. +bool isGateSymbol(PrimitiveType type); + +// A gate with more than one right-side pin can't be drawn as its symbol and +// falls back to a box. +SymbolGeometry symbolGeometry(PrimitiveType type, + const std::vector& leftNames, + const std::vector& rightNames); + +// --------------------------------------------------------------------------- +// Layered placement (Sugiyama-style): +// 1. cycles are broken (DFS back edges are ignored for layering); +// 2. each instance gets a logic level: longest path from the sources, then +// each source is pulled right next to its nearest receiver; +// 3. instances are ordered within each level by barycenter sweeps to cut +// wire crossings (first-seen order breaks ties, so adding a net mostly +// appends to the drawing); +// 4. levels become columns, with a routing channel between neighbors that +// grows with the number of nets crossing it; +// 5. each column is placed vertically by isotonic regression towards the +// heights that put an instance's pins level with the pins they connect +// to (straight wires), keeping the column order and spacing. +// Pure geometry: the result depends only on the inputs, in their order. +// --------------------------------------------------------------------------- +struct PlaceNode { + float w = 0.f, h = 0.f; +}; +struct PlacePin { + int node = 0; + float dy = 0.f; // pin offset from the node's top edge +}; +struct PlaceNet { + std::vector drivers; + std::vector receivers; +}; +struct Placement { + std::vector pos; // top-left per node + std::vector level; // logic level per node (0 = leftmost column) +}; +Placement layeredPlacement(const std::vector& nodes, const std::vector& nets); + +// Stacks the top-level port flags of one side of the drawing: each wants the +// height of the pin it connects to (`desiredY`), and no two may be closer than +// kPinPitch. Returns the heights, in input order, as close to their desired +// ones as that spacing allows. +std::vector stackPorts(const std::vector& desiredY); + +// --------------------------------------------------------------------------- +// Orthogonal net routing. Box rectangles are obstacles, and the vertical +// strips free of any box between the placed columns are routing channels. +// Each net is a tree: +// - a vertical trunk in the channel next to the driver pin, and one in the +// channel next to each group of receivers further away; +// - a horizontal spine joining those trunks, at the driver's height when +// that line is free, else at the nearest free height; +// - a horizontal branch from each pin to its trunk. +// Trunks sharing a channel are spread over distinct tracks, so two nets never +// overlap on one vertical line, ordered so that two nets' horizontals at the +// same height never overlap either (channel routing's vertical constraints); +// trunks that don't overlap vertically share a track. A +// junction dot marks every point where three or more wire directions meet. +// --------------------------------------------------------------------------- +struct RouteRect { + float x0 = 0.f, y0 = 0.f, x1 = 0.f, y1 = 0.f; +}; +struct RoutePin { + ImVec2 at; + bool right = true; // the wire leaves the pin to the right (else left) +}; +struct RouteNet { + RoutePin driver; + std::vector receivers; +}; +struct RouteSegment { + ImVec2 a, b; +}; +struct RoutedNet { + std::vector segments; + std::vector junctions; +}; +// `keepOut` areas (labels drawn outside boxes) aren't obstacles for channels +// or pin stubs, but a spine avoids running through them when it can. +std::vector routeNets(const std::vector& nets, + const std::vector& obstacles, + const std::vector& keepOut = {}); // --------------------------------------------------------------------------- // One endpoint of an equipotential: a top-level term, or a pin on an instance @@ -46,6 +150,7 @@ struct Item { PrimitiveType primitiveType = PrimitiveType::Unknown; unsigned termChildId = 0; std::optional termBit; + bool clock = false; // a sequential cell's clock pin std::vector pathIds; // Only meaningful for instance occurrences (isTerm == false): whether // this instance's model has sub-instances worth expanding into a nested @@ -64,49 +169,14 @@ struct Item { void buildItems(const Equipotential* eq, std::vector& drivers, std::vector& receivers); -// --------------------------------------------------------------------------- -// Incremental layout: each equipotential is placed once, the first time it's -// seen, and its instances keep their position from then on (so the view -// doesn't reshuffle as nets are added). A net sharing an already-placed -// instance extends horizontally from it (new receivers to the right of a -// driver, new drivers to the left of a receiver); an unrelated net gets a -// fresh two-column block below everything placed so far. -// --------------------------------------------------------------------------- -class IncrementalLayout { - public: - // No-op if `eq` was already placed. - void place(const Equipotential* eq); - // Places one instance on its own (a starting point with no net shown - // yet) in the left column below everything placed so far. No-op if - // `path` is already placed. - void placeAlone(const InstancePath& path); - - // Push boxes down within each column (same x) so none overlaps the one - // above it -- place() reserves a fixed kInstH slot per instance, but a box - // with many ports is drawn taller. Zero-width shapes (term stubs) are - // ignored. The corrected positions are persisted so later place() calls - // anchor on what's actually drawn. - void resolveColumnOverlaps(std::vector& instances); - - void clear(); - - // Instance path -> world top-left. - const std::map& positions() const { return placed_; } - - private: - std::map placed_; - std::set laidOut_; - float nextY_ = 0.f; -}; - // --------------------------------------------------------------------------- // Hierarchy grouping: nested frames for the hierarchical modules containing // the displayed leaf instances. A driver trace (or any equipotential) spans // leaf cells anywhere in the design; without this, they're shown as one flat // sea of boxes and the module structure is lost. // -// Each leaf keeps the logic column the incremental layout gave it (its x, -// i.e. its distance from the traced net), and the modules become a tree of +// Each leaf keeps the logic level the layered placement gave it, and the +// modules become a tree of // frames laid out bottom-up: inside a frame, its own leaves and sub-frames // are bucketed into columns by the (average) logic column of their contents, // left to right, and stacked by their original vertical order. That keeps the @@ -116,6 +186,7 @@ class IncrementalLayout { struct LeafHier { InstancePath path; // full instance-name path, leaf last std::vector pathModels; // matching model names ("" if unknown) + int level = 0; // logic level (layeredPlacement) }; struct HierFrame { diff --git a/src/SchematicView.cpp b/src/SchematicView.cpp index 96bfd8f..28c281d 100644 --- a/src/SchematicView.cpp +++ b/src/SchematicView.cpp @@ -44,7 +44,7 @@ constexpr float kMinPortTickLen = 2.5f; constexpr float kWireBaseThickness = 2.0f; // world-space "1x zoom" wire thickness constexpr float kMinWireThickness = 0.75f; -// Schematic-style junction dot: a filled circle marks a real electrical branch +// Junction dot: a filled circle marks a real electrical branch // (one driver pin feeding more than one receiver) -- two wires that merely // cross on screen without sharing a pin get no dot, so a dot always means // "connected here" and a bare crossing always means "not connected." @@ -58,7 +58,7 @@ constexpr float kMinJunctionDotR = 1.25f; constexpr float kBoundaryPortHalfHBase = 8.0f; constexpr float kMinBoundaryPortHalfH = 3.0f; -// Schematic-style near-monochrome palette: every instance shares the same flat +// Near-monochrome palette: every instance shares the same flat // neutral "paper" fill rather than an arbitrary per-category color, so color // stays reserved for diagnosis/selection highlighting rather than decorating // every box. The canvas itself is the same convention taken one step @@ -69,6 +69,10 @@ constexpr ImU32 kCanvasBgColor = IM_COL32(255, 255, 255, 255); constexpr ImU32 kInstanceFillColor = IM_COL32(214, 216, 220, 255); constexpr ImU32 kInstanceLineColor = IM_COL32(35, 35, 38, 235); constexpr ImU32 kPinLineColor = IM_COL32(40, 40, 40, 235); +constexpr ImU32 kPinLabelColor = IM_COL32(45, 45, 50, 235); +constexpr ImU32 kInstanceNameColor = IM_COL32(30, 40, 90, 235); +// World-space radius of a gate's negation bubble (NAND/NOR/XNOR/INV). +constexpr float kBubbleR = 4.0f; // Top-level design ports get their own tint so they can't be mistaken for an // instance pin or a small instance box. Green on purpose: diagnosis severities // use red/amber/blue (DiagnosisStore.cpp), so it doesn't read as a finding. @@ -156,9 +160,14 @@ void appendQuadBezierPoints(std::vector& pts, ImVec2 p0, ImVec2 c, ImVec // Draws a sampled body outline both filled and stroked (plus a thicker // diagnosis-severity stroke on top, when flagged) -- shared tail end of every // standard gate shape below. -void fillAndStrokeBody(ImDrawList* dl, const std::vector& pts, +void fillAndStrokeBody(ImDrawList* dl, std::vector pts, ImU32 fillColor, ImU32 diagOutline) { - dl->AddConvexPolyFilled(pts.data(), int(pts.size()), fillColor); + // Curves appended back to back repeat their joint point, which breaks + // the concave fill's triangulation. + auto same = [](ImVec2 a, ImVec2 b) { return std::abs(a.x - b.x) < 0.01f && std::abs(a.y - b.y) < 0.01f; }; + pts.erase(std::unique(pts.begin(), pts.end(), same), pts.end()); + while (pts.size() > 1 && same(pts.front(), pts.back())) pts.pop_back(); + dl->AddConcavePolyFilled(pts.data(), int(pts.size()), fillColor); dl->AddPolyline(pts.data(), int(pts.size()), kInstanceLineColor, 1.25f, ImDrawFlags_Closed); if (diagOutline != 0) dl->AddPolyline(pts.data(), int(pts.size()), diagOutline, 3.0f, ImDrawFlags_Closed); @@ -242,9 +251,13 @@ ImVec2 SchematicView::portWorldPos(const InstanceShape& inst, const Port& port) // --------------------------------------------------------------------------- // Shared port drawing (used by all gate renderers) // --------------------------------------------------------------------------- +// Pin labels: inside a box, next to the pin (the box is sized +// to hold them); a gate symbol's pins are unlabeled, as on a printed +// schematic -- except a merged bus pin, whose range is labeled outside. static void drawPorts(ImDrawList* dl, const InstanceShape& inst, const SchematicView& sv, - const ImVec2& canvasPos, const ImVec2& canvasSize) { + const ImVec2& canvasPos, const ImVec2& canvasSize, + bool namesInside) { for (const auto& p : inst.ports) { float tickLen = zoomedSizeOrHidden(kPortTickBaseLen, sv.transform.scale, kMinPortTickLen); if (tickLen <= 0.0f) continue; // too small to matter at this zoom -- same fade policy as labels @@ -270,13 +283,11 @@ static void drawPorts(ImDrawList* dl, const InstanceShape& inst, // An open pin (net not in the view yet) ends in a hollow circle, a // loading one in a filled circle: "click here to see more" without // needing color for the resting state. - ImVec2 labelAt = tickEnd; if (p.open) { float r = std::max(kMinOpenPinR, kOpenPinBaseR * sv.transform.scale); ImVec2 c(tickEnd.x + (isLeft ? -r : r), tickEnd.y); dl->AddCircleFilled(c, r, p.pending ? portColor : kCanvasBgColor); dl->AddCircle(c, r, portColor, 0, std::max(1.0f, tickThickness * 0.8f)); - labelAt.x += isLeft ? -2.0f * r : 2.0f * r; // label goes past the circle } // A merged bus pin gets the classic diagonal bus slash across its // tick instead of a plain line, in addition to its "[hi:lo]" label. @@ -288,31 +299,43 @@ static void drawPorts(ImDrawList* dl, const InstanceShape& inst, portColor, tickThickness); } + if (p.name.empty() || (!namesInside && !p.isBus)) continue; float fontSize = labelFontSize(kPortLabelBaseSize, sv.transform.scale); - if (!p.name.empty() && fontSize > 0.0f) { - ImFont* font = ImGui::GetFont(); - // Cap label width (in screen px, scaled with zoom like everything - // else) so a long bus name can't sprawl across neighboring pins. - const float kPortLabelMaxWorldWidth = 90.0f; - std::string label = truncateToWidth(font, fontSize, p.name, - kPortLabelMaxWorldWidth * sv.transform.scale); - if (!label.empty()) { - ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label.c_str()); - // Anchored past the tick's tip (not the box edge) so the - // label doesn't sit on top of the pin's own tick line. - ImVec2 lblPos = isLeft - ? ImVec2(labelAt.x - 3.0f - textSize.x, labelAt.y - textSize.y * 0.5f) - : ImVec2(labelAt.x + 3.0f, labelAt.y - textSize.y * 0.5f); - dl->AddRectFilled( - ImVec2(lblPos.x - 2.0f, lblPos.y - 1.0f), - ImVec2(lblPos.x + textSize.x + 2.0f, lblPos.y + textSize.y + 1.0f), - hovered ? kPinHoverColor : IM_COL32(30, 30, 30, 190)); - dl->AddText(font, fontSize, lblPos, IM_COL32(225, 225, 225, 235), label.c_str()); - } - } + if (fontSize <= 0.0f) continue; + ImFont* font = ImGui::GetFont(); + const float pad = 4.0f * sv.transform.scale; + // Inside: at most half the box, so left and right names never meet. + const float maxWidth = namesInside ? inst.w * 0.5f * sv.transform.scale - pad * 1.5f + : 90.0f * sv.transform.scale; + std::string label = truncateToWidth(font, fontSize, p.name, maxWidth); + if (label.empty()) continue; + ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label.c_str()); + ImVec2 lblPos; + // A clock pin's name starts past drawDffInstance()'s notch. + const float notch = p.clock ? 7.0f * sv.transform.scale : 0.0f; + if (namesInside) + lblPos = isLeft ? ImVec2(screenP.x + pad + notch, screenP.y - textSize.y * 0.5f) + : ImVec2(screenP.x - pad - textSize.x, screenP.y - textSize.y * 0.5f); + else // above the tick, outside the symbol + lblPos = isLeft ? ImVec2(tickEnd.x - textSize.x, screenP.y - textSize.y - 1.0f) + : ImVec2(tickEnd.x, screenP.y - textSize.y - 1.0f); + dl->AddText(font, fontSize, lblPos, hovered ? kPinHoverColor : kPinLabelColor, label.c_str()); } } +// Instance name above its symbol: a box's inside holds its pin +// names, and a gate symbol has no room for text. +static void drawInstanceName(ImDrawList* dl, const InstanceShape& inst, + const SchematicView& sv, ImVec2 rmin) { + float fontSize = labelFontSize(kInstanceLabelBaseSize, sv.transform.scale); + const std::string& shown = inst.label.empty() ? inst.name : inst.label; + if (shown.empty() || fontSize <= 0.0f) return; + ImFont* font = ImGui::GetFont(); + ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, shown.c_str()); + dl->AddText(font, fontSize, ImVec2(rmin.x, rmin.y - ts.y - 2.0f * sv.transform.scale), + kInstanceNameColor, shown.c_str()); +} + // --------------------------------------------------------------------------- // Generic box renderer (fallback for unknown gate types) // --------------------------------------------------------------------------- @@ -323,14 +346,14 @@ static void drawGenericInstance(ImDrawList* dl, const InstanceShape& inst, sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); if (inst.w > 0.0f && inst.h > 0.0f) { - // Schematic-style flat fill: a neutral "paper" tone shared by every + // Flat fill: a neutral "paper" tone shared by every // box, not a per-category color -- color stays reserved for // diagnosis/selection highlighting rather than decorating every // instance. Sharp corners (no rounding). dl->AddRectFilled(rmin, rmax, kInstanceFillColor, 0.0f); if (inst.partialInterface) - // Schematic-style: dashing alone (not a color) signals "only a + // Dashing alone (not a color) signals "only a // subset of the interface is shown" -- same ink as a fully // expanded box's solid border, since color stays reserved for // diagnosis/selection highlighting. @@ -344,20 +367,7 @@ static void drawGenericInstance(ImDrawList* dl, const InstanceShape& inst, dl->AddRect(rmin, rmax, inst.diagOutline, 0.0f, 0, 3.5f); if (inst.selected) addSelectionOutline(dl, rmin, rmax); - float instFontSize = labelFontSize(kInstanceLabelBaseSize, sv.transform.scale); - const std::string& shown = inst.label.empty() ? inst.name : inst.label; - if (!shown.empty() && instFontSize > 0.0f) { - ImFont* font = ImGui::GetFont(); - float padding = std::max(2.0f, 6.0f * sv.transform.scale); - float maxWidth = (rmax.x - rmin.x) - 2.0f * padding; - std::string label = truncateToWidth(font, instFontSize, shown, maxWidth); - if (!label.empty()) { - ImVec2 textSize = font->CalcTextSizeA(instFontSize, FLT_MAX, 0.0f, label.c_str()); - ImVec2 textPos = ImVec2((rmin.x + rmax.x) * 0.5f - textSize.x * 0.5f, - rmin.y + padding); - dl->AddText(font, instFontSize, textPos, contrastingTextColor(kInstanceFillColor), label.c_str()); - } - } + drawInstanceName(dl, inst, sv, rmin); if (inst.partialInterface) { float boxH = rmax.y - rmin.y; @@ -373,7 +383,7 @@ static void drawGenericInstance(ImDrawList* dl, const InstanceShape& inst, } // Hierarchy expand/collapse glyph: a small "+"/"-" square straddling - // the top-center of the box. Its world-space rect is shared with + // the top border. Its world-space rect is shared with // EquipotentialView's click hit-test via hierToggleGlyphRect(). if (canShowHierToggle(inst)) { float gx0, gy0, gx1, gy1; @@ -389,53 +399,39 @@ static void drawGenericInstance(ImDrawList* dl, const InstanceShape& inst, } } - drawPorts(dl, inst, sv, canvasPos, canvasSize); -} - -// --------------------------------------------------------------------------- -// assign / buffer — triangle pointing right (input left, output apex right) -// --------------------------------------------------------------------------- -static void drawAssignInstance(ImDrawList* dl, const InstanceShape& inst, - const SchematicView& sv, - const ImVec2& canvasPos, const ImVec2& canvasSize) { - ImVec2 tl = sv.worldToScreen(ImVec2(inst.x, inst.y), canvasPos, canvasSize); - ImVec2 bl = sv.worldToScreen(ImVec2(inst.x, inst.y + inst.h), canvasPos, canvasSize); - ImVec2 mr = sv.worldToScreen(ImVec2(inst.x + inst.w, inst.y + inst.h * 0.5f), canvasPos, canvasSize); - - // Symbol shape (not fill color) carries the gate identity, schematic-style: - // same flat neutral fill as every other box. - dl->AddTriangleFilled(tl, bl, mr, kInstanceFillColor); - dl->AddTriangle(tl, bl, mr, kInstanceLineColor, 1.25f); - if (inst.diagOutline != 0) - dl->AddTriangle(tl, bl, mr, inst.diagOutline, 3.0f); - if (inst.selected) { - ImVec2 rmin, rmax; - sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); - addSelectionOutline(dl, rmin, rmax); - } - - // Label ("assign") near top-left of the bounding box, small and subtle - ImVec2 lblPos = ImVec2(tl.x + 4.0f, tl.y + 4.0f); - dl->AddText(lblPos, contrastingTextColor(kInstanceFillColor, 180), "="); - - drawPorts(dl, inst, sv, canvasPos, canvasSize); + drawPorts(dl, inst, sv, canvasPos, canvasSize, /*namesInside=*/true); } // --------------------------------------------------------------------------- // Standard shapes library // // Traditional (non-IEC) gate symbols for PrimitiveType::{And,Nand,Or,Nor, -// Xor,Xnor,Inv,Buf,Dff}. Input/output pin positions are untouched -- they -// stay wherever EquipotentialView already placed them (left edge for -// inputs, right edge for outputs, evenly spaced by portLy()) -- these -// functions only draw a different body outline than the generic box inside -// the same inst.x/y/w/h, then call the same shared drawPorts(). Arity -// (2..N inputs) therefore needs no dedicated handling here: however many -// input ports the instance actually has already determines their spacing, -// and the body outline (D-shape / pointed shield / triangle) scales to -// whatever inst.w/inst.h the caller assigned, same as drawGenericInstance. +// Xor,Xnor,Inv,Buf,Assign}. Each body fills its instance's box exactly -- +// flat back on the left edge where the input pins attach, output tip (or +// negation bubble) on the right edge where the output pin attaches -- so the +// pin ticks drawn by drawPorts() always touch the body. Box size and pin +// positions come from SchematicLayout::symbolGeometry(). // --------------------------------------------------------------------------- +// Right end of a gate body: the box's right edge, less a negation bubble. +static float bodyRight(const ImVec2& rmax, bool negated, float scale) { + return rmax.x - (negated ? 2.0f * kBubbleR * scale : 0.0f); +} + +// Shared tail of every gate: selection, name, bubble, pins. +static void finishGate(ImDrawList* dl, const InstanceShape& inst, const SchematicView& sv, + const ImVec2& canvasPos, const ImVec2& canvasSize, + ImVec2 rmin, ImVec2 rmax, bool negated) { + if (negated) { + float r = kBubbleR * sv.transform.scale; + drawNegationBubble(dl, ImVec2(bodyRight(rmax, true, sv.transform.scale), (rmin.y + rmax.y) * 0.5f), + r, kInstanceFillColor); + } + if (inst.selected) addSelectionOutline(dl, rmin, rmax); + drawInstanceName(dl, inst, sv, rmin); + drawPorts(dl, inst, sv, canvasPos, canvasSize, /*namesInside=*/false); +} + // AND / NAND — flat back, semicircular (bulging right) front. NAND adds a // negation bubble at the tip. static void drawAndLikeInstance(ImDrawList* dl, const InstanceShape& inst, @@ -445,96 +441,87 @@ static void drawAndLikeInstance(ImDrawList* dl, const InstanceShape& inst, ImVec2 rmin, rmax; sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); float cy = (rmin.y + rmax.y) * 0.5f; - float xm = (rmin.x + rmax.x) * 0.5f; float r = (rmax.y - rmin.y) * 0.5f; + float xm = std::max(rmin.x, bodyRight(rmax, negated, sv.transform.scale) - r); std::vector pts; pts.push_back(ImVec2(rmin.x, rmin.y)); appendArcPoints(pts, ImVec2(xm, cy), r, -1.5707963f, 1.5707963f, 24); pts.push_back(ImVec2(rmin.x, rmax.y)); fillAndStrokeBody(dl, pts, kInstanceFillColor, inst.diagOutline); - if (inst.selected) addSelectionOutline(dl, rmin, rmax); - - if (negated) { - float bubbleR = std::max(2.5f, r * 0.22f); - drawNegationBubble(dl, ImVec2(xm + r, cy), bubbleR, kInstanceFillColor); - } - - drawPorts(dl, inst, sv, canvasPos, canvasSize); + finishGate(dl, inst, sv, canvasPos, canvasSize, rmin, rmax, negated); } -// OR / NOR / XOR / XNOR — curved sides bulging outward to a point on the -// right; NOR/XNOR add a negation bubble at the tip; XOR/XNOR add the extra -// curved line just outside the input edge. +// OR / NOR / XOR / XNOR — concave back, curved sides meeting in a point on +// the right; NOR/XNOR add a negation bubble at the tip; XOR/XNOR add the +// extra curved line behind the back. Input pins are extended from the box +// edge to the concave back so they visibly reach the body. static void drawOrLikeInstance(ImDrawList* dl, const InstanceShape& inst, const SchematicView& sv, const ImVec2& canvasPos, const ImVec2& canvasSize, bool negated, bool exclusive) { ImVec2 rmin, rmax; sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); - float cy = (rmin.y + rmax.y) * 0.5f; - float sw = rmax.x - rmin.x; - float sh = rmax.y - rmin.y; - ImVec2 tip(rmax.x, cy); + const float s = sv.transform.scale; + const float cy = (rmin.y + rmax.y) * 0.5f; + const float sh = rmax.y - rmin.y; + const float x0 = rmin.x + (exclusive ? 8.0f * s : 0.0f); // body back + const float x1 = bodyRight(rmax, negated, s); // body tip + const float sw = x1 - x0; + const float depth = std::min(sw * 0.25f, sh * 0.2f); // concavity of the back + ImVec2 tip(x1, cy); std::vector pts; - pts.push_back(ImVec2(rmin.x, rmin.y)); - appendQuadBezierPoints(pts, ImVec2(rmin.x, rmin.y), - ImVec2(rmin.x + sw * 0.5f, rmin.y - sh * 0.10f), tip, 16); - appendQuadBezierPoints(pts, tip, - ImVec2(rmin.x + sw * 0.5f, rmax.y + sh * 0.10f), - ImVec2(rmin.x, rmax.y), 16); + appendQuadBezierPoints(pts, ImVec2(x0, rmin.y), ImVec2(x0 + sw * 0.6f, rmin.y), tip, 16); + appendQuadBezierPoints(pts, tip, ImVec2(x0 + sw * 0.6f, rmax.y), ImVec2(x0, rmax.y), 16); + appendQuadBezierPoints(pts, ImVec2(x0, rmax.y), ImVec2(x0 + 2.0f * depth, cy), ImVec2(x0, rmin.y), 12); + pts.pop_back(); // closes back onto the first point fillAndStrokeBody(dl, pts, kInstanceFillColor, inst.diagOutline); - if (inst.selected) addSelectionOutline(dl, rmin, rmax); - if (exclusive) { - float gap = std::max(2.0f, sw * 0.08f); - dl->AddLine(ImVec2(rmin.x - gap, rmin.y), ImVec2(rmin.x - gap, rmax.y), - kInstanceLineColor, 1.25f); + // The back curve at height y (a quadratic whose control point sits at + // mid-height, so y is linear in t): x = x0 + 2t(1-t) * 2*depth. + auto backX = [&](float xBase, float y) { + float t = sh > 0.0f ? (y - rmin.y) / sh : 0.5f; + return xBase + 4.0f * depth * t * (1.0f - t); + }; + const float thickness = std::max(1.0f, 1.5f * s); + for (const auto& p : inst.ports) { + if (p.lx >= 0.0f) continue; + ImVec2 at = sv.worldToScreen(sv.portWorldPos(inst, p), canvasPos, canvasSize); + dl->AddLine(at, ImVec2(backX(x0, at.y), at.y), kPinLineColor, thickness); } - - if (negated) { - float bubbleR = std::max(2.5f, sh * 0.11f); - drawNegationBubble(dl, tip, bubbleR, kInstanceFillColor); + if (exclusive) { + std::vector arc; + appendQuadBezierPoints(arc, ImVec2(rmin.x, rmax.y), ImVec2(rmin.x + 2.0f * depth, cy), + ImVec2(rmin.x, rmin.y), 12); + dl->AddPolyline(arc.data(), int(arc.size()), kInstanceLineColor, 1.25f, 0); } - - drawPorts(dl, inst, sv, canvasPos, canvasSize); + finishGate(dl, inst, sv, canvasPos, canvasSize, rmin, rmax, negated); } -// INV / BUF — triangle pointing right, same silhouette as the "assign" -// pass-through shape; INV adds a negation bubble at the tip. +// INV / BUF / assign — triangle pointing right; INV adds a negation bubble at +// the tip, an assign is marked with "=". static void drawBufLikeInstance(ImDrawList* dl, const InstanceShape& inst, const SchematicView& sv, const ImVec2& canvasPos, const ImVec2& canvasSize, - bool negated) { - ImVec2 tl = sv.worldToScreen(ImVec2(inst.x, inst.y), canvasPos, canvasSize); - ImVec2 bl = sv.worldToScreen(ImVec2(inst.x, inst.y + inst.h), canvasPos, canvasSize); - ImVec2 mr = sv.worldToScreen(ImVec2(inst.x + inst.w, inst.y + inst.h * 0.5f), canvasPos, canvasSize); - - dl->AddTriangleFilled(tl, bl, mr, kInstanceFillColor); - dl->AddTriangle(tl, bl, mr, kInstanceLineColor, 1.25f); - if (inst.diagOutline != 0) - dl->AddTriangle(tl, bl, mr, inst.diagOutline, 3.0f); - if (inst.selected) { - ImVec2 rmin, rmax; - sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); - addSelectionOutline(dl, rmin, rmax); - } + bool negated, bool isAssign = false) { + ImVec2 rmin, rmax; + sv.worldRectToScreen(inst.x, inst.y, inst.w, inst.h, canvasPos, canvasSize, rmin, rmax); + ImVec2 tl = rmin, bl(rmin.x, rmax.y); + ImVec2 mr(bodyRight(rmax, negated, sv.transform.scale), (rmin.y + rmax.y) * 0.5f); - if (negated) { - float bubbleR = std::max(2.5f, (bl.y - tl.y) * 0.11f); - drawNegationBubble(dl, mr, bubbleR, kInstanceFillColor); + std::vector pts{ tl, mr, bl }; + fillAndStrokeBody(dl, pts, kInstanceFillColor, inst.diagOutline); + if (isAssign) { + float fs = labelFontSize(kPortLabelBaseSize, sv.transform.scale); + if (fs > 0.0f) { + ImFont* font = ImGui::GetFont(); + ImVec2 ts = font->CalcTextSizeA(fs, FLT_MAX, 0.0f, "="); + dl->AddText(font, fs, ImVec2(tl.x + (mr.x - tl.x) * 0.3f - ts.x * 0.5f, mr.y - ts.y * 0.5f), + contrastingTextColor(kInstanceFillColor, 200), "="); + } } - - drawPorts(dl, inst, sv, canvasPos, canvasSize); -} - -// True for a pin name commonly used for a flip-flop's clock input, so -// drawDffInstance() can mark it with the usual clock-triangle notch. -static bool looksLikeClockPinName(const std::string& name) { - std::string n = name; - for (auto& c : n) c = static_cast(std::tolower(static_cast(c))); - return n == "clk" || n == "clock" || n == "ck" || n == "c"; + finishGate(dl, inst, sv, canvasPos, canvasSize, rmin, rmax, negated); } // DFF (and other clocked sequential cells) — the generic box, plus the @@ -547,7 +534,7 @@ static void drawDffInstance(ImDrawList* dl, const InstanceShape& inst, drawGenericInstance(dl, inst, sv, canvasPos, canvasSize); for (const auto& p : inst.ports) { - if (p.direction != Direction::Input || !looksLikeClockPinName(p.name)) continue; + if (!p.clock) continue; ImVec2 world = sv.portWorldPos(inst, p); float notchH = std::min(inst.h * 0.3f, 10.0f); ImVec2 top = sv.worldToScreen(ImVec2(inst.x, world.y - notchH * 0.5f), canvasPos, canvasSize); @@ -560,12 +547,15 @@ static void drawDffInstance(ImDrawList* dl, const InstanceShape& inst, } // --------------------------------------------------------------------------- -// Top-level design port — schematic-style "flag": a small pentagon sized to its -// name, flat base toward the design (where the net wire attaches, at the -// pseudo-instance's own x/y -- see the zero-size term box built in -// EquipotentialView.cpp), pointed tip facing outward, off the edge of the -// design. Replaces the generic tick-line pin drawn by drawPorts() for every -// other kind of port: a boundary port isn't a pin on a box, it *is* the box. +// Top-level design port — a "flag": a small pentagon sized to its +// name, pointing the way the signal flows. The wire attaches at the +// pseudo-instance's own x/y (the zero-size term box built in +// EquipotentialView.cpp). Inputs sit on the sheet's left edge: the flag lies +// left of that point with its tip on it, pointing into the design. Outputs +// sit on the right edge: the flag's flat base is on the point and its tip +// points away, off the sheet. Replaces the generic tick-line pin drawn by +// drawPorts() for every other kind of port: a boundary port isn't a pin on a +// box, it *is* the box. // --------------------------------------------------------------------------- static void drawBoundaryPortInstance(ImDrawList* dl, const InstanceShape& inst, const SchematicView& sv, @@ -578,13 +568,6 @@ static void drawBoundaryPortInstance(ImDrawList* dl, const InstanceShape& inst, float halfH = std::max(kBoundaryPortHalfHBase * sv.transform.scale, kMinBoundaryPortHalfH); ImVec2 anchor = sv.worldToScreen(ImVec2(inst.x, inst.y), canvasPos, canvasSize); - // This layout is a fixed left-to-right flow (see EquipotentialView.cpp's - // termLx/termRx placement: primary inputs sit left of the design, - // primary outputs sit right of it). Every boundary port's tip points - // rightward -- the direction signal continues past this port, whether - // that's on into the design (an input) or on off the sheet's right edge - // (an output) -- flat base toward `anchor`, where the wire attaches. - float outDir = 1.0f; const bool hovered = p.id == sv.hoveredPortId; ImU32 outline = hovered ? kPinHoverColor : p.color != 0 ? p.color : kBoundaryPortLineColor; @@ -604,9 +587,10 @@ static void drawBoundaryPortInstance(ImDrawList* dl, const InstanceShape& inst, float bodyLen = std::max(halfH * 2.0f, textSize.x + 10.0f); float tipLen = halfH; - float baseX = anchor.x; - float bodyX = anchor.x + outDir * bodyLen; - float tipX = bodyX + outDir * tipLen; + // Input: [base .. body] tip on the anchor. Output: base on the anchor. + float baseX = p.isInput ? anchor.x - tipLen - bodyLen : anchor.x; + float bodyX = baseX + bodyLen; + float tipX = bodyX + tipLen; ImVec2 pts[5] = { { baseX, anchor.y - halfH }, @@ -674,9 +658,17 @@ void SchematicView::drawInstance(ImDrawList* dl, const InstanceShape& inst, drawBoundaryPortInstance(dl, inst, *this, canvasPos, canvasSize); return; } + // A gate with several outputs has no symbol (symbolGeometry sized it as + // a box): draw the box. + int rightPins = 0; + for (const auto& p : inst.ports) if (p.lx >= 0.0f) ++rightPins; + if (rightPins > 1 && SchematicLayout::isGateSymbol(inst.primitiveType)) { + drawGenericInstance(dl, inst, *this, canvasPos, canvasSize); + return; + } switch (inst.primitiveType) { case PrimitiveType::Assign: - drawAssignInstance(dl, inst, *this, canvasPos, canvasSize); + drawBufLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false, /*isAssign=*/true); break; case PrimitiveType::And: drawAndLikeInstance(dl, inst, *this, canvasPos, canvasSize, /*negated=*/false); @@ -768,7 +760,7 @@ void SchematicView::drawNet(ImDrawList* dl, const NetWire& net, const ImVec2& ca } } - // Bus slash mark across the horizontal run, schematic-style, plus the net + // Bus slash mark across the horizontal run, plus the net // name if we have one. if (net.isBus) { ImVec2 mid = ImVec2(midX, (p1.y + p4.y) * 0.5f); @@ -791,6 +783,59 @@ void SchematicView::drawNet(ImDrawList* dl, const NetWire& net, const ImVec2& ca } } +void SchematicView::drawRoute(ImDrawList* dl, const NetRoute& route, + const ImVec2& canvasPos, const ImVec2& canvasSize) const { + float thickness = zoomedSizeOrHidden(kWireBaseThickness, transform.scale, kMinWireThickness); + if (thickness <= 0.0f) return; + if (route.isBus) thickness *= 2.0f; + for (const auto& seg : route.segments) { + ImVec2 a = worldToScreen(seg.a, canvasPos, canvasSize); + ImVec2 b = worldToScreen(seg.b, canvasPos, canvasSize); + dl->AddLine(a, b, route.color, thickness); + } + // A square cap where a horizontal and a vertical segment end at the same + // point, so the corner isn't notched (butt-ended thick lines). + auto same = [](ImVec2 a, ImVec2 b) { return std::abs(a.x - b.x) < 0.5f && std::abs(a.y - b.y) < 0.5f; }; + for (const auto& h : route.segments) { + if (std::abs(h.a.y - h.b.y) > 0.5f) continue; + for (ImVec2 w : { h.a, h.b }) { + bool corner = false; + for (const auto& v : route.segments) + if (std::abs(v.a.x - v.b.x) < 0.5f && (same(v.a, w) || same(v.b, w))) { corner = true; break; } + if (!corner) continue; + ImVec2 p = worldToScreen(w, canvasPos, canvasSize); + float r = thickness * 0.5f; + dl->AddRectFilled(ImVec2(p.x - r, p.y - r), ImVec2(p.x + r, p.y + r), route.color); + } + } + float dotR = zoomedSizeOrHidden(kJunctionDotBaseR, transform.scale, kMinJunctionDotR); + if (dotR > 0.0f) + for (const auto& j : route.junctions) + dl->AddCircleFilled(worldToScreen(j, canvasPos, canvasSize), route.isBus ? dotR * 1.5f : dotR, route.color); + + // Bus slash mark on the longest horizontal run, plus the + // net name if we have one. + if (!route.isBus) return; + const SchematicLayout::RouteSegment* best = nullptr; + for (const auto& seg : route.segments) + if (std::abs(seg.a.y - seg.b.y) < 0.5f && + (!best || std::abs(seg.b.x - seg.a.x) > std::abs(best->b.x - best->a.x))) + best = &seg; + if (!best) return; + ImVec2 mid = worldToScreen(ImVec2((best->a.x + best->b.x) * 0.5f, best->a.y), canvasPos, canvasSize); + float slashLen = std::max(6.0f, 9.0f * transform.scale); + dl->AddLine(ImVec2(mid.x - slashLen * 0.35f, mid.y + slashLen * 0.5f), + ImVec2(mid.x + slashLen * 0.35f, mid.y - slashLen * 0.5f), + route.color, thickness * 0.5f); + float fontSize = labelFontSize(kPortLabelBaseSize, transform.scale); + if (!route.netName.empty() && fontSize > 0.0f) { + ImFont* font = ImGui::GetFont(); + ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, route.netName.c_str()); + dl->AddText(font, fontSize, ImVec2(mid.x + 6.0f, mid.y - ts.y - 3.0f), kPinLabelColor, + route.netName.c_str()); + } +} + // Main render entry bool SchematicView::computeWorldBounds(ImVec2& outMin, ImVec2& outMax) const { if (instances.empty()) return false; @@ -799,11 +844,22 @@ bool SchematicView::computeWorldBounds(ImVec2& outMin, ImVec2& outMax) const { float maxX = instances[0].x + instances[0].w; float maxY = instances[0].y + instances[0].h; for (const auto& inst : instances) { - minX = std::min(minX, inst.x); - minY = std::min(minY, inst.y); - maxX = std::max(maxX, inst.x + inst.w); + // Room for what's drawn outside the shape itself: a port's flag, an + // instance's name above it. + const bool port = inst.modelName == "port"; + const float flag = port ? 60.0f : 0.0f; + const float name = port || inst.isHierGroup ? 0.0f : 18.0f; + minX = std::min(minX, inst.x - flag); + minY = std::min(minY, inst.y - name); + maxX = std::max(maxX, inst.x + inst.w + flag); maxY = std::max(maxY, inst.y + inst.h); } + for (const auto& route : routes) + for (const auto& seg : route.segments) + for (ImVec2 p : { seg.a, seg.b }) { + minX = std::min(minX, p.x); maxX = std::max(maxX, p.x); + minY = std::min(minY, p.y); maxY = std::max(maxY, p.y); + } outMin = ImVec2(minX, minY); outMax = ImVec2(maxX, maxY); return true; @@ -938,7 +994,7 @@ void SchematicView::handleInteraction(const ImVec2& canvasPos, const ImVec2& /*c void SchematicView::render(ImDrawList* dl, const ImVec2& canvasPos, const ImVec2& canvasSize) { dl->PushClipRect(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), true); - // Schematic-style white "paper" canvas -- see kCanvasBgColor. + // White "paper" canvas -- see kCanvasBgColor. dl->AddRectFilled(canvasPos, ImVec2(canvasPos.x + canvasSize.x, canvasPos.y + canvasSize.y), kCanvasBgColor); // Hierarchy group frames go under everything else -- they only outline @@ -952,8 +1008,9 @@ void SchematicView::render(ImDrawList* dl, const ImVec2& canvasPos, const ImVec2 // before. A net nested inside an expanded instance (containerShapeId set) // is drawn later instead -- see drawSubtree below -- so that instance's // opaque box fill doesn't get painted over it afterward. + for (const auto& route : routes) drawRoute(dl, route, canvasPos, canvasSize); for (const auto& net : nets) { - if (net.containerShapeId < 0) drawNet(dl, net, canvasPos, canvasSize); + if (net.containerShapeId < 0 && !net.routed) drawNet(dl, net, canvasPos, canvasSize); } // Recursive draw: a box, then the nets internal to it, then its nested diff --git a/src/SchematicView.h b/src/SchematicView.h index ac4fa03..e706b0d 100644 --- a/src/SchematicView.h +++ b/src/SchematicView.h @@ -3,6 +3,7 @@ #include #include #include "Types.h" // ensures InstanceShape, Port, NetWire are known +#include "SchematicLayout.h" struct Transform { float scale = 1.0f; @@ -16,6 +17,17 @@ class SchematicView { std::vector instances; std::vector nets; + // A routed net: one orthogonal wire tree from a driving pin to all its + // receivers, in world coordinates (SchematicLayout::routeNets). Its + // NetWires are marked `routed` and aren't drawn on their own. + struct NetRoute { + std::vector segments; + std::vector junctions; // three or more directions meet here + ImU32 color = NetWire{}.color; + bool isBus = false; + std::string netName; + }; + std::vector routes; Transform transform; // Pin under the cursor (drawn highlighted), -1 = none. Set each frame by // the owning view, which does the hit-testing. @@ -50,6 +62,8 @@ class SchematicView { const ImVec2& canvasPos, const ImVec2& canvasSize) const; void drawNet(ImDrawList* dl, const NetWire& net, const ImVec2& canvasPos, const ImVec2& canvasSize) const; + void drawRoute(ImDrawList* dl, const NetRoute& route, + const ImVec2& canvasPos, const ImVec2& canvasSize) const; // Main render entry (parameters by const reference) void render(ImDrawList* dl, const ImVec2& canvasPos, const ImVec2& canvasSize); diff --git a/src/Types.cpp b/src/Types.cpp index 1815dfd..038bdf4 100644 --- a/src/Types.cpp +++ b/src/Types.cpp @@ -173,6 +173,7 @@ void from_json(const json& j, Equipotential& e) { if (occJson.contains("direction") && !occJson["direction"].is_null()) { term.direction = intToDirection(occJson["direction"].get()); } + term.clock = occJson.value("clock", false); if (occJson.contains("bit") && !occJson["bit"].is_null()) { term.bit = occJson["bit"].get(); } else { diff --git a/src/Types.h b/src/Types.h index ba67b12..dcffd9f 100644 --- a/src/Types.h +++ b/src/Types.h @@ -30,17 +30,16 @@ struct SourceLoc { // Coarse gate/cell function classification for an instance, used to pick a // standard schematic symbol instead of a generic box (see -// SchematicView::drawInstance()). naja/najaeda expose no function/timing-arc -// info -- only a design/cell name string -- so both LocalSNLProvider -// (native, getPrimitiveType() in LocalSNLProvider.cpp) and python/naja_schematic/protocol.py -// (WASM/browser, get_primitive_type()) classify independently from that name -// via a best-effort prefix match, same spirit as their existing isAssign() -// special case (which maps here to Assign). Unknown is the default for -// anything that doesn't match -- including ordinary hierarchical modules and -// blackboxed cells with unrecognized names -- and always falls back to the -// generic box. Gate arity (2..N inputs) is NOT part of this enum: the actual -// input port count already carried on InstanceShape::ports is used instead, -// so no separate arity field needs to travel over the wire. +// SchematicView::drawInstance()). It comes from naja's modeling of the cell +// -- SNLDesignModeling's truth-table checks (isAnd, isNor, ...) and +// isSequential -- never from its name: both LocalSNLProvider +// (getPrimitiveType()) and python/naja_schematic/protocol.py +// (get_primitive_type()) compute it the same way. Unknown is the default for +// anything naja has no such model for -- hierarchical modules, blackboxes, +// cells loaded without Liberty -- and always falls back to the generic box. +// Gate arity (2..N inputs) is NOT part of this enum: the actual input port +// count already carried on InstanceShape::ports is used instead, so no +// separate arity field needs to travel over the wire. enum class PrimitiveType { Unknown = 0, And, @@ -124,6 +123,7 @@ struct BitTerm { unsigned child_id; Direction direction; std::optional bit; + bool clock = false; // a sequential cell's clock pin (naja's isClock) std::string getString() const { return name + (bit.has_value() ? ("[" + std::to_string(bit.value()) + "]") : ""); @@ -306,6 +306,9 @@ struct Port { bool open = false; // An open pin whose net has been requested but hasn't arrived yet. bool pending = false; + // A sequential cell's clock pin, as naja models it (never guessed from + // the pin name) -- drawDffInstance() marks it with the clock notch. + bool clock = false; }; struct InstanceShape { @@ -372,7 +375,7 @@ struct NetWire { int srcPortId = 0; int dstInstance = 0; int dstPortId = 0; - // Schematic-style default: near-monochrome. Color is reserved for + // Default: near-monochrome. Color is reserved for // highlighting (diagnosis severity, selection) via an explicit override // further down the pipeline -- see EquipotentialView.cpp's srcPort/ // dstPort->color checks -- rather than being a per-net decoration. @@ -386,6 +389,9 @@ struct NetWire { // wiring nested inside it, but before that instance's children so the // children still render on top. int containerShapeId = -1; + // Drawn as part of its driver pin's routed tree (SchematicView::routes) + // rather than on its own. + bool routed = false; // Best-effort display name for the underlying net (driver pin/port name, // or the InternalNet name for hierarchy-embedded nets), shown next to @@ -401,12 +407,13 @@ inline ImVec2 portAnchor(const InstanceShape& inst, const Port& port) { } // World-space rect of an instance's hierarchy expand/collapse glyph -// (a small square straddling the top-center of the box). Shared by +// (a small square straddling the box's top border, near its right corner, +// clear of the instance name drawn above the left corner). Shared by // SchematicView's draw code and EquipotentialView's click hit-test so the // two never drift apart. inline void hierToggleGlyphRect(const InstanceShape& inst, float& x0, float& y0, float& x1, float& y1) { - x0 = inst.x + inst.w * 0.5f - 8.0f; + x0 = inst.x + inst.w - 20.0f; x1 = x0 + 16.0f; y0 = inst.y - 2.0f; y1 = y0 + 16.0f; diff --git a/tests/EquipotentialViewTest.cpp b/tests/EquipotentialViewTest.cpp index 0915d9d..b46cbf5 100644 --- a/tests/EquipotentialViewTest.cpp +++ b/tests/EquipotentialViewTest.cpp @@ -312,6 +312,7 @@ nlohmann::json resolvedU7() { "instance": { "path": [["core", 5, "Core"], ["u7", 7, "AND2"]], "design_ref": {"db_id": 1, "library_id": 2, "design_id": 3}, + "primitive_type": "and", "has_instances": false, "source_loc": null, "terms": [{"name": "A", "child_id": 0, "direction": 0}, {"name": "B", "child_id": 1, "direction": 0}, @@ -328,6 +329,7 @@ TEST(StartInstance, ParsedFromAnInstanceResolvedReply) { EXPECT_EQ(start->pathIds, (std::vector{5, 7})); EXPECT_EQ(start->pathModels, (std::vector{"Core", "AND2"})); EXPECT_EQ(start->designRef.design_id, 3u); + EXPECT_EQ(start->primitiveType, PrimitiveType::And); ASSERT_EQ(start->ports.size(), 3u); EXPECT_EQ(start->ports[2].name, "Y"); EXPECT_EQ(start->ports[2].direction, Direction::Output); @@ -345,6 +347,7 @@ TEST_F(SchematicClicks, StartInstanceIsDrawnAloneWithAllPinsOpen) { const InstanceShape* u7 = shape({"core", "u7"}); ASSERT_NE(u7, nullptr); EXPECT_FALSE(u7->partialInterface); // its whole interface is shown + EXPECT_EQ(u7->primitiveType, PrimitiveType::And); // drawn as an AND gate for (auto name : {"A", "B", "Y"}) { ASSERT_NE(pin({"core", "u7"}, name), nullptr) << name; EXPECT_TRUE(pin({"core", "u7"}, name)->open) << name; diff --git a/tests/SchematicLayoutTest.cpp b/tests/SchematicLayoutTest.cpp index 0fc1046..13e9015 100644 --- a/tests/SchematicLayoutTest.cpp +++ b/tests/SchematicLayoutTest.cpp @@ -8,8 +8,8 @@ using namespace SchematicLayout; namespace { -constexpr float kColStep = kInstW + kColGap; // x distance between logic columns -constexpr float kRowStep = kInstH + kRowSpacing; +constexpr float kBoxW = 60.f, kBoxH = 40.f; +constexpr float kRowStep = kBoxH + kRowSpacing; InstTermOccurrence occ(std::vector path, const std::string& term, Direction dir, std::vector models = {}) { @@ -22,22 +22,8 @@ InstTermOccurrence occ(std::vector path, const std::string& term, D BitTerm topTerm(const std::string& name, Direction dir) { return BitTerm{name, 0, dir, std::nullopt}; } -// driver.Q -> each receiver's A pin, all instances at the top level. -Equipotential net(const std::string& driver, std::vector receivers) { - Equipotential eq{true, {}, {}}; - eq.occurrences.push_back(occ({driver}, "Q", Direction::Output)); - for (auto& r : receivers) eq.occurrences.push_back(occ({r}, "A", Direction::Input)); - return eq; -} - -ImVec2 pos(const IncrementalLayout& l, const InstancePath& path) { - auto it = l.positions().find(path); - EXPECT_NE(it, l.positions().end()) << displayPath(path) << " was not placed"; - return it == l.positions().end() ? ImVec2(NAN, NAN) : it->second; -} - InstanceShape box(int id, const InstancePath& path, float x, float y, - float w = kInstW, float h = kInstH) { + float w = kBoxW, float h = kBoxH) { InstanceShape s; s.id = id; s.path = path; s.name = displayPath(path); s.x = x; s.y = y; s.w = w; s.h = h; return s; @@ -60,6 +46,8 @@ const InstanceShape& byPath(const std::vector& v, const InstanceP return none; } +bool onGrid(float v) { return std::abs(v / kGrid - std::round(v / kGrid)) < 1e-4f; } + } // namespace // --------------------------------------------------------------------------- @@ -84,159 +72,274 @@ TEST(SchematicLayoutItems, DriversAreTopInputsAndInstanceOutputs) { } // --------------------------------------------------------------------------- -// IncrementalLayout::place +// symbolGeometry // --------------------------------------------------------------------------- -TEST(IncrementalLayout, FirstNetPutsDriversLeftAndReceiversRight) { - IncrementalLayout layout; - auto eq = net("u1", {"u2", "u3"}); - layout.place(&eq); - - EXPECT_EQ(pos(layout, {"u1"}).x, kLeftMargin); - EXPECT_EQ(pos(layout, {"u1"}).y, 0.f); - EXPECT_EQ(pos(layout, {"u2"}).x, kLeftMargin + kColStep); - EXPECT_EQ(pos(layout, {"u2"}).y, 0.f); - EXPECT_EQ(pos(layout, {"u3"}).x, kLeftMargin + kColStep); - EXPECT_EQ(pos(layout, {"u3"}).y, kRowStep); -} - -TEST(IncrementalLayout, TopLevelTermsAreNotPlaced) { - IncrementalLayout layout; - Equipotential eq{true, {topTerm("in", Direction::Input)}, {}}; - eq.occurrences.push_back(occ({"u1"}, "A", Direction::Input)); - layout.place(&eq); - - EXPECT_EQ(layout.positions().size(), 1u); - EXPECT_EQ(layout.positions().count(InstancePath{"in"}), 0u); - // The instance is a receiver: right column, even with no instance driver. - EXPECT_EQ(pos(layout, {"u1"}).x, kLeftMargin + kColStep); -} - -TEST(IncrementalLayout, PlacingTheSameNetTwiceIsANoOp) { - IncrementalLayout layout; - auto eq = net("u1", {"u2"}); - layout.place(&eq); - auto before = layout.positions(); - layout.place(&eq); - EXPECT_EQ(layout.positions().size(), before.size()); - - // ...and it doesn't advance the "below existing content" cursor either. - auto other = net("v1", {"v2"}); - layout.place(&other); - EXPECT_EQ(pos(layout, {"v1"}).y, kRowStep + kNetVGap); -} - -TEST(IncrementalLayout, UnrelatedNetGoesBelowTheTallestColumn) { - IncrementalLayout layout; - auto a = net("u1", {"u2", "u3"}); // right column is 2 rows tall - auto b = net("v1", {"v2"}); - layout.place(&a); - layout.place(&b); - - const float expectedY = 2 * kRowStep + kNetVGap; - EXPECT_EQ(pos(layout, {"v1"}).x, kLeftMargin); - EXPECT_EQ(pos(layout, {"v1"}).y, expectedY); - EXPECT_EQ(pos(layout, {"v2"}).y, expectedY); -} - -TEST(IncrementalLayout, DriverOfAPlacedReceiverGoesOneColumnLeft) { - IncrementalLayout layout; - auto a = net("g", {"sink"}); - auto b = net("src", {"g"}); // trace one step back from g's input - layout.place(&a); - layout.place(&b); - - EXPECT_EQ(pos(layout, {"src"}).x, pos(layout, {"g"}).x - kColStep); - EXPECT_EQ(pos(layout, {"src"}).y, pos(layout, {"g"}).y); - // The anchor itself never moves. - EXPECT_EQ(pos(layout, {"g"}).x, kLeftMargin); -} - -TEST(IncrementalLayout, ReceiversOfAPlacedDriverGoOneColumnRight) { - IncrementalLayout layout; - auto a = net("g", {"sink"}); - auto b = net("sink", {"r1", "r2"}); - layout.place(&a); - layout.place(&b); - - const float x = pos(layout, {"sink"}).x + kColStep; - EXPECT_EQ(pos(layout, {"r1"}).x, x); - EXPECT_EQ(pos(layout, {"r2"}).x, x); - EXPECT_EQ(pos(layout, {"r2"}).y, pos(layout, {"r1"}).y + kRowStep); -} - -// A driver trace through a 2-input gate: each input net anchors on the gate -// and wants the same slot left of it. The second must slide down, not stack. -TEST(IncrementalLayout, FanInNetsAnchoredOnTheSameGateDoNotStack) { - IncrementalLayout layout; - auto out = net("g", {"sink"}); - auto inA = net("a", {"g"}); - auto inB = net("b", {"g"}); - layout.place(&out); - layout.place(&inA); - layout.place(&inB); - - EXPECT_EQ(pos(layout, {"a"}).x, pos(layout, {"b"}).x); - EXPECT_EQ(pos(layout, {"a"}).y, pos(layout, {"g"}).y); - EXPECT_EQ(pos(layout, {"b"}).y, pos(layout, {"a"}).y + kRowStep); -} - -TEST(IncrementalLayout, NoTwoPlacedBoxesOverlapInADeeperTrace) { - // g <- {a, b}, a <- {c, d}, b <- {e, f}: a small binary fan-in tree. - IncrementalLayout layout; - std::vector nets = { - net("g", {"sink"}), net("a", {"g"}), net("b", {"g"}), - net("c", {"a"}), net("d", {"a"}), net("e", {"b"}), net("f", {"b"})}; - for (auto& n : nets) layout.place(&n); - - std::vector boxes; - int id = 1; - for (const auto& [key, p] : layout.positions()) boxes.push_back(box(id++, key, p.x, p.y)); - for (size_t i = 0; i < boxes.size(); ++i) - for (size_t j = i + 1; j < boxes.size(); ++j) - EXPECT_FALSE(overlaps(boxes[i], boxes[j])) << boxes[i].name << " vs " << boxes[j].name; -} - -TEST(IncrementalLayout, ClearForgetsPositionsAndTheVerticalCursor) { - IncrementalLayout layout; - auto a = net("u1", {"u2"}); - layout.place(&a); - layout.clear(); - EXPECT_TRUE(layout.positions().empty()); - - layout.place(&a); // placeable again after clear, back at the top - EXPECT_EQ(pos(layout, {"u1"}).y, 0.f); +TEST(Symbols, GateInputsAreCenteredOnItsOutput) { + auto g = symbolGeometry(PrimitiveType::And, {"A1", "A2"}, {"ZN"}); + EXPECT_EQ(g.h, 2.f * kPinPitch); + EXPECT_EQ(g.leftY, (std::vector{10.f, 30.f})); + EXPECT_EQ(g.rightY, (std::vector{20.f})); + + auto g3 = symbolGeometry(PrimitiveType::Nor, {"A", "B", "C"}, {"Y"}); + EXPECT_EQ(g3.h, 3.f * kPinPitch); + EXPECT_EQ(g3.leftY, (std::vector{10.f, 30.f, 50.f})); + EXPECT_EQ(g3.rightY, (std::vector{30.f})); +} + +TEST(Symbols, EveryPinAndSizeIsOnTheGrid) { + for (auto type : {PrimitiveType::And, PrimitiveType::Xnor, PrimitiveType::Inv, + PrimitiveType::Dff, PrimitiveType::Unknown}) + for (size_t n = 1; n <= 6; ++n) { + std::vector in(n, "A"); + auto g = symbolGeometry(type, in, {"Y"}); + EXPECT_TRUE(onGrid(g.w) && onGrid(g.h)) << int(type) << " x" << n; + for (float y : g.leftY) EXPECT_TRUE(onGrid(y)) << int(type) << " x" << n; + for (float y : g.rightY) EXPECT_TRUE(onGrid(y)) << int(type) << " x" << n; + for (float y : g.leftY) EXPECT_TRUE(y > 0.f && y < g.h); + } +} + +TEST(Symbols, BufferIsASmallSquare) { + auto g = symbolGeometry(PrimitiveType::Inv, {"A"}, {"ZN"}); + EXPECT_EQ(g.w, g.h); + EXPECT_EQ(g.leftY, g.rightY); +} + +TEST(Symbols, BoxPinsRunDownFromTheTopAndItsWidthFitsTheirNames) { + auto narrow = symbolGeometry(PrimitiveType::Unknown, {"A", "B", "C"}, {"Y"}); + EXPECT_EQ(narrow.leftY, (std::vector{20.f, 40.f, 60.f})); + EXPECT_EQ(narrow.rightY, (std::vector{20.f})); + EXPECT_EQ(narrow.h, 4.f * kPinPitch); + + auto wide = symbolGeometry(PrimitiveType::Unknown, {"write_enable_n"}, {"read_data_out"}); + EXPECT_GT(wide.w, narrow.w); +} + +TEST(Symbols, GateWithSeveralOutputsIsDrawnAsABox) { + EXPECT_TRUE(isGateSymbol(PrimitiveType::Xor)); + EXPECT_FALSE(isGateSymbol(PrimitiveType::Dff)); + auto g = symbolGeometry(PrimitiveType::And, {"A", "B"}, {"Y", "YN"}); + EXPECT_EQ(g.leftY, (std::vector{20.f, 40.f})); + EXPECT_EQ(g.rightY, (std::vector{20.f, 40.f})); } // --------------------------------------------------------------------------- -// IncrementalLayout::resolveColumnOverlaps +// layeredPlacement // --------------------------------------------------------------------------- -TEST(IncrementalLayout, TallBoxPushesTheNextOneInItsColumnDown) { - IncrementalLayout layout; - std::vector shapes = { - box(1, {"tall"}, 20.f, 0.f, kInstW, 200.f), - box(2, {"below"}, 20.f, kRowStep), - box(3, {"otherColumn"}, 320.f, kRowStep), - }; - layout.resolveColumnOverlaps(shapes); +namespace { + +// A net from `from`'s output pin (at `dyOut`) to each `to` input pin (dyIn). +PlaceNet link(int from, std::vector to, float dyOut = 20.f, float dyIn = 20.f) { + PlaceNet n; + n.drivers.push_back({from, dyOut}); + for (int t : to) n.receivers.push_back({t, dyIn}); + return n; +} + +void expectNoOverlap(const std::vector& nodes, const Placement& p) { + for (size_t i = 0; i < nodes.size(); ++i) + for (size_t j = i + 1; j < nodes.size(); ++j) { + InstanceShape a = box(1, {}, p.pos[i].x, p.pos[i].y, nodes[i].w, nodes[i].h); + InstanceShape b = box(2, {}, p.pos[j].x, p.pos[j].y, nodes[j].w, nodes[j].h); + EXPECT_FALSE(overlaps(a, b)) << i << " vs " << j; + } +} + +} // namespace + +TEST(LayeredPlacement, AChainRunsLeftToRightOnOneStraightLine) { + std::vector nodes(3, {kBoxW, kBoxH}); + auto p = layeredPlacement(nodes, {link(0, {1}), link(1, {2})}); + + EXPECT_EQ(p.level, (std::vector{0, 1, 2})); + EXPECT_EQ(p.pos[0].x, kLeftMargin); + EXPECT_LT(p.pos[0].x + kBoxW, p.pos[1].x); + EXPECT_LT(p.pos[1].x + kBoxW, p.pos[2].x); + EXPECT_EQ(p.pos[0].y, p.pos[1].y); + EXPECT_EQ(p.pos[1].y, p.pos[2].y); +} + +TEST(LayeredPlacement, ReceiversPinIsLevelWithItsDriversPin) { + // Driver output at 20 from its top, receiver input at 50 from its top. + std::vector nodes = {{kBoxW, kBoxH}, {kBoxW, 80.f}}; + auto p = layeredPlacement(nodes, {link(0, {1}, 20.f, 50.f)}); + EXPECT_EQ(p.pos[0].y + 20.f, p.pos[1].y + 50.f); +} + +TEST(LayeredPlacement, AFanInsDriversStackWithoutOverlapInPinOrder) { + // a -> g.A (top pin), b -> g.B (bottom pin): a ends up above b. + std::vector nodes(3, {kBoxW, kBoxH}); + PlaceNet toA = link(0, {2}, 20.f, 10.f), toB = link(1, {2}, 20.f, 30.f); + auto p = layeredPlacement(nodes, {toB, toA}); // net order must not matter + EXPECT_EQ(p.level[0], p.level[1]); + EXPECT_LT(p.pos[0].y, p.pos[1].y); + expectNoOverlap(nodes, p); +} + +TEST(LayeredPlacement, ASourceSitsNextToTheNodeItFeeds) { + // 0 -> 1 -> 2 -> 3, and a side input 4 -> 3. + std::vector nodes(5, {kBoxW, kBoxH}); + auto p = layeredPlacement(nodes, {link(0, {1}), link(1, {2}), link(2, {3}), link(4, {3})}); + EXPECT_EQ(p.level[3], 3); + EXPECT_EQ(p.level[4], 2); +} + +TEST(LayeredPlacement, AFeedbackLoopStillFlowsLeftToRight) { + // A flop and a gate feeding each other. + std::vector nodes(2, {kBoxW, kBoxH}); + auto p = layeredPlacement(nodes, {link(0, {1}), link(1, {0})}); + EXPECT_NE(p.level[0], p.level[1]); + expectNoOverlap(nodes, p); +} + +TEST(LayeredPlacement, UnconnectedNodesArePlacedApart) { + std::vector nodes(3, {kBoxW, kBoxH}); + auto p = layeredPlacement(nodes, {}); + EXPECT_EQ(p.level, (std::vector{0, 0, 0})); + expectNoOverlap(nodes, p); +} + +TEST(LayeredPlacement, AChannelWidensWithTheNetsCrossingIt) { + std::vector nodes(6, {kBoxW, kBoxH}); + auto one = layeredPlacement(nodes, {link(0, {3})}); + auto many = layeredPlacement(nodes, {link(0, {3}), link(1, {4}), link(2, {5})}); + EXPECT_GT(many.pos[3].x, one.pos[3].x); +} + +TEST(LayeredPlacement, ALargerRandomGraphHasNoOverlapsAndIsDeterministic) { + std::vector nodes; + std::vector nets; + unsigned seed = 7; + auto rnd = [&](unsigned n) { seed = seed * 1103515245u + 12345u; return int((seed >> 16) % n); }; + for (int i = 0; i < 60; ++i) { + nodes.push_back({kBoxW + 10.f * float(rnd(4)), kBoxH + kPinPitch * float(rnd(3))}); + if (i > 0) nets.push_back(link(rnd(unsigned(i)), {i}, 20.f, 20.f)); + if (i > 2) nets.push_back(link(rnd(unsigned(i)), {i}, 20.f, 40.f)); + } + auto p = layeredPlacement(nodes, nets); + expectNoOverlap(nodes, p); + for (const auto& net : nets) // every edge points right + EXPECT_LT(p.pos[net.drivers[0].node].x, p.pos[net.receivers[0].node].x); + auto again = layeredPlacement(nodes, nets); + for (size_t i = 0; i < nodes.size(); ++i) { + EXPECT_EQ(p.pos[i].x, again.pos[i].x); + EXPECT_EQ(p.pos[i].y, again.pos[i].y); + } +} + +TEST(StackPorts, PortsKeepTheirHeightUnlessCrowded) { + EXPECT_EQ(stackPorts({0.f, 100.f}), (std::vector{0.f, 100.f})); + auto ys = stackPorts({50.f, 50.f, 7.f}); + EXPECT_EQ(ys[2], 7.f); // not snapped: level with its pin + EXPECT_GE(ys[0] - ys[2], kPinPitch - 1e-3f); + EXPECT_GE(ys[1] - ys[0], kPinPitch - 1e-3f); +} + +// --------------------------------------------------------------------------- +// routeNets +// --------------------------------------------------------------------------- - EXPECT_EQ(shapes[0].y, 0.f); - EXPECT_EQ(shapes[1].y, 200.f + kRowSpacing); - EXPECT_EQ(shapes[2].y, kRowStep); // different column: untouched - // Persisted, so the next place() anchors on the drawn position. - EXPECT_EQ(pos(layout, {"below"}).y, 200.f + kRowSpacing); +namespace { + +bool isOrthogonal(const RouteSegment& s) { + return std::abs(s.a.x - s.b.x) < 1e-3f || std::abs(s.a.y - s.b.y) < 1e-3f; +} + +bool crosses(const RouteSegment& s, const RouteRect& r) { + float x0 = std::min(s.a.x, s.b.x), x1 = std::max(s.a.x, s.b.x); + float y0 = std::min(s.a.y, s.b.y), y1 = std::max(s.a.y, s.b.y); + return x1 > r.x0 + 0.5f && x0 < r.x1 - 0.5f && y1 > r.y0 + 0.5f && y0 < r.y1 - 0.5f; +} + +// The x of the vertical segment of `r` in (x0, x1), if any. +float trunkX(const RoutedNet& r, float x0, float x1) { + for (const auto& s : r.segments) + if (std::abs(s.a.x - s.b.x) < 1e-3f && std::abs(s.a.y - s.b.y) > 1e-3f && s.a.x > x0 && s.a.x < x1) + return s.a.x; + return NAN; } -TEST(IncrementalLayout, ColumnOverlapsIgnoreZeroWidthTermStubs) { - IncrementalLayout layout; - std::vector shapes = { - box(1, {"u1"}, 20.f, 0.f), - box(2, {}, 20.f, 10.f, 0.f, 0.f), +// Two columns of boxes: [0,40] and [100,140]. +const std::vector kTwoColumns = {{0.f, 0.f, 40.f, 200.f}, {100.f, 0.f, 140.f, 200.f}}; + +} // namespace + +TEST(RouteNets, AlignedPinsGetAStraightWire) { + auto r = routeNets({{{{40.f, 50.f}, true}, {{{100.f, 50.f}, false}}}}, kTwoColumns); + ASSERT_EQ(r.size(), 1u); + for (const auto& s : r[0].segments) { + EXPECT_FLOAT_EQ(s.a.y, 50.f); + EXPECT_FLOAT_EQ(s.b.y, 50.f); + } + EXPECT_TRUE(r[0].junctions.empty()); +} + +TEST(RouteNets, AFanOutIsOneTrunkWithAJunctionAtEachBranch) { + RouteNet net{{{40.f, 50.f}, true}, + {{{100.f, 10.f}, false}, {{100.f, 50.f}, false}, {{100.f, 90.f}, false}, {{100.f, 130.f}, false}}}; + auto r = routeNets({net}, kTwoColumns); + int verticals = 0; + for (const auto& s : r[0].segments) { + EXPECT_TRUE(isOrthogonal(s)); + if (std::abs(s.a.x - s.b.x) < 1e-3f) ++verticals; + } + EXPECT_EQ(verticals, 1); + // Branches at 50 (where the driver joins too) and 90; 10 and 130 are corners. + ASSERT_EQ(r[0].junctions.size(), 2u); + EXPECT_FLOAT_EQ(r[0].junctions[0].y + r[0].junctions[1].y, 50.f + 90.f); +} + +TEST(RouteNets, NetsSharingAChannelGetTheirOwnTrack) { + RouteNet a{{{40.f, 20.f}, true}, {{{100.f, 120.f}, false}}}; + RouteNet b{{{40.f, 60.f}, true}, {{{100.f, 160.f}, false}}}; + auto r = routeNets({a, b}, kTwoColumns); + float xa = trunkX(r[0], 40.f, 100.f), xb = trunkX(r[1], 40.f, 100.f); + ASSERT_FALSE(std::isnan(xa) || std::isnan(xb)); + EXPECT_NE(xa, xb); +} + +TEST(RouteNets, AStubNeverOverlapsAnotherNetsBranchAtTheSameHeight) { + // Net a leaves its driver at y=50; net b enters a receiver at y=50 too. a's + // stub runs from the left edge to a's trunk, b's branch from b's trunk to + // the right edge: they only stay apart if a's trunk is left of b's. + RouteNet a{{{40.f, 50.f}, true}, {{{100.f, 90.f}, false}}}; + RouteNet b{{{40.f, 10.f}, true}, {{{100.f, 50.f}, false}}}; + for (bool swapped : {false, true}) { + auto r = swapped ? routeNets({b, a}, kTwoColumns) : routeNets({a, b}, kTwoColumns); + float xa = trunkX(r[swapped ? 1 : 0], 40.f, 100.f), xb = trunkX(r[swapped ? 0 : 1], 40.f, 100.f); + EXPECT_LT(xa, xb) << "swapped=" << swapped; + } +} + +TEST(RouteNets, ASpineDetoursAroundABoxInTheWay) { + // Driver in column 0, receiver in column 2, a box in column 1 at the + // driver's height. + std::vector boxes = {{0.f, 0.f, 40.f, 100.f}, {100.f, 30.f, 140.f, 70.f}, + {200.f, 0.f, 240.f, 100.f}}; + auto r = routeNets({{{{40.f, 50.f}, true}, {{{200.f, 50.f}, false}}}}, boxes); + for (const auto& s : r[0].segments) { + EXPECT_TRUE(isOrthogonal(s)); + for (const auto& b : boxes) EXPECT_FALSE(crosses(s, b)); + } +} + +TEST(RouteNets, TwoSpinesDoNotShareAHeight) { + // Both nets have to detour around the middle box, the same way. + std::vector boxes = {{0.f, 0.f, 40.f, 100.f}, {100.f, 0.f, 140.f, 100.f}, + {200.f, 0.f, 240.f, 100.f}}; + RouteNet a{{{40.f, 20.f}, true}, {{{200.f, 20.f}, false}}}; + RouteNet b{{{40.f, 40.f}, true}, {{{200.f, 40.f}, false}}}; + auto r = routeNets({a, b}, boxes); + auto spineY = [](const RoutedNet& n) { + for (const auto& s : n.segments) + if (std::abs(s.a.y - s.b.y) < 1e-3f && std::min(s.a.x, s.b.x) < 100.f && std::max(s.a.x, s.b.x) > 140.f) + return s.a.y; + return NAN; }; - layout.resolveColumnOverlaps(shapes); - EXPECT_EQ(shapes[1].y, 10.f); - EXPECT_EQ(layout.positions().count(InstancePath{}), 0u); + ASSERT_FALSE(std::isnan(spineY(r[0])) || std::isnan(spineY(r[1]))); + EXPECT_NE(spineY(r[0]), spineY(r[1])); } // --------------------------------------------------------------------------- @@ -245,7 +348,7 @@ TEST(IncrementalLayout, ColumnOverlapsIgnoreZeroWidthTermStubs) { namespace { -// Leaves at logic column `col` (as IncrementalLayout would have placed them). +// Leaves at logic level `col` (as layeredPlacement would have placed them). struct LeafSpec { InstancePath path; std::vector models; int col; float y; }; struct HierFixture { @@ -257,8 +360,8 @@ struct HierFixture { explicit HierFixture(const std::vector& leaves) { for (const auto& l : leaves) { int id = nextInstId++; - shapes.push_back(box(id, l.path, kLeftMargin + float(l.col) * kColStep, l.y)); - leafHier[l.path] = {l.path, l.models}; + shapes.push_back(box(id, l.path, kLeftMargin + float(l.col) * 200.f, l.y)); + leafHier[l.path] = {l.path, l.models, l.col}; pathToInstId[l.path] = id; } } @@ -411,3 +514,16 @@ TEST(HierarchyGroups, LeavesWithoutAShapeAreIgnored) { ASSERT_EQ(frames.size(), 1u); EXPECT_EQ(frames[0].shape.path, (InstancePath{"core"})); } + +TEST(HierarchyGroups, LeavesKeepTheHeightsTheirPinsWereAlignedAt) { + // g2's pin was aligned 20 below g1's by the layered placement. + HierFixture fx({{{"core", "g1"}, {}, 0, 100.f}, {{"core", "g2"}, {}, 1, 120.f}}); + fx.run(); + EXPECT_EQ(byPath(fx.shapes, {"core", "g2"}).y - byPath(fx.shapes, {"core", "g1"}).y, 20.f); +} + +TEST(HierarchyGroups, ANestedFrameKeepsItsLeafLevelWithAShallowerOne) { + HierFixture fx({{{"m", "sub", "g"}, {}, 0, 100.f}, {{"m", "h"}, {}, 1, 100.f}}); + fx.run(); + EXPECT_EQ(byPath(fx.shapes, {"m", "sub", "g"}).y, byPath(fx.shapes, {"m", "h"}).y); +} diff --git a/tests/TypesJsonTest.cpp b/tests/TypesJsonTest.cpp index 3558fea..9a0bf57 100644 --- a/tests/TypesJsonTest.cpp +++ b/tests/TypesJsonTest.cpp @@ -240,7 +240,7 @@ TEST(DiagnosisItemJson, ParsesNetKindAndUnknownSeverityDefaultsInfo) { EXPECT_TRUE(d.path.empty()); } -TEST(GeometryHelpers, HierToggleGlyphRectIsCenteredOnTop) { +TEST(GeometryHelpers, HierToggleGlyphRectStraddlesTheTopRightCorner) { InstanceShape inst; inst.x = 100.0f; inst.y = 50.0f; @@ -252,8 +252,11 @@ TEST(GeometryHelpers, HierToggleGlyphRectIsCenteredOnTop) { EXPECT_FLOAT_EQ(x1 - x0, 16.0f); EXPECT_FLOAT_EQ(y1 - y0, 16.0f); - // Centered on inst's horizontal midpoint. - EXPECT_FLOAT_EQ((x0 + x1) / 2.0f, inst.x + inst.w * 0.5f); + // Straddling the top border near the right corner, inside the box's width. + EXPECT_LT(y0, inst.y); + EXPECT_GT(y1, inst.y); + EXPECT_GT(x0, inst.x + inst.w * 0.5f); + EXPECT_LE(x1, inst.x + inst.w); } TEST(GeometryHelpers, CanShowHierToggleRequiresChildrenAndMinSize) { From a231f3eafd5581bc325d3363cd277e635fb0644e Mon Sep 17 00:00:00 2001 From: xtof Date: Tue, 29 Sep 2026 12:08:12 +0200 Subject: [PATCH 3/3] REUSE: annotate scripts/gate_shapes_example.v Co-Authored-By: Claude Opus 5.5 --- REUSE.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/REUSE.toml b/REUSE.toml index 40b0eda..550b3cf 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -38,6 +38,7 @@ path = [ "python/tests/test_transports.py", "python/tests/test_widget.py", "scripts/fakeram45_64x32.lib", + "scripts/gate_shapes_example.v", "scripts/najaeda_server.py", "scripts/test_server.py", "scripts/tinyrocket.v",