stdraw 0.7.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stdraw/__init__.py +32 -0
- stdraw/__main__.py +350 -0
- stdraw/_interactive_js.py +2030 -0
- stdraw/anchors.py +103 -0
- stdraw/bowtielayout.py +350 -0
- stdraw/check.py +651 -0
- stdraw/columnlayout.py +531 -0
- stdraw/comblayout.py +533 -0
- stdraw/crossing_exact.py +1004 -0
- stdraw/delivery.py +81 -0
- stdraw/dsl.py +99 -0
- stdraw/layout3d.py +167 -0
- stdraw/layout_auto.py +1827 -0
- stdraw/layoutdiff.py +424 -0
- stdraw/loops.py +145 -0
- stdraw/metrics.py +1225 -0
- stdraw/model.py +134 -0
- stdraw/placement.py +220 -0
- stdraw/planar.py +625 -0
- stdraw/recipes.py +68 -0
- stdraw/regularity.py +176 -0
- stdraw/render.py +361 -0
- stdraw/render3d.py +165 -0
- stdraw/rendercheck.py +90 -0
- stdraw/ringlayout.py +475 -0
- stdraw/routing.py +2630 -0
- stdraw/spinewinglayout.py +329 -0
- stdraw/symlayout.py +245 -0
- stdraw/textmetrics.py +97 -0
- stdraw/visibility.py +382 -0
- stdraw-0.7.13.dist-info/METADATA +383 -0
- stdraw-0.7.13.dist-info/RECORD +36 -0
- stdraw-0.7.13.dist-info/WHEEL +5 -0
- stdraw-0.7.13.dist-info/entry_points.txt +2 -0
- stdraw-0.7.13.dist-info/licenses/LICENSE +21 -0
- stdraw-0.7.13.dist-info/top_level.txt +1 -0
stdraw/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""stdraw systems-thinking diagram engine.
|
|
3
|
+
|
|
4
|
+
Three-layer architecture:
|
|
5
|
+
Layout layer (planar / layout3d, replaceable implementations)
|
|
6
|
+
Quality layer (metrics five-metric certificate + check geometry self-inspection + loops loop semantic audit) -- this project's moat
|
|
7
|
+
Viewer layer (viewer/)
|
|
8
|
+
|
|
9
|
+
Honest tiering promise: if a certificate cannot be obtained we say so; we never claim 0 crossings for arbitrary graphs.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.7.13"
|
|
13
|
+
|
|
14
|
+
from .model import Graph, Node, Edge, Loop, Group
|
|
15
|
+
from .dsl import parse, dumps
|
|
16
|
+
from .planar import PlanarLayout
|
|
17
|
+
from . import recipes
|
|
18
|
+
from .metrics import certificate, certificate_auto
|
|
19
|
+
from .check import geometry_report
|
|
20
|
+
from .loops import loops_audit
|
|
21
|
+
from . import crossing_exact # WS2: exact crossing-number layer (optional ortools)
|
|
22
|
+
from . import regularity # WS2: structure detection + computable definitions of regularity
|
|
23
|
+
from . import placement # WS2: slot-discretized placement (optional ortools)
|
|
24
|
+
from . import ringlayout # t_af83815d: loop-driven ring placement (trigger decision + slots)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Graph", "Node", "Edge", "Loop", "Group",
|
|
28
|
+
"parse", "dumps",
|
|
29
|
+
"PlanarLayout", "certificate", "certificate_auto", "geometry_report",
|
|
30
|
+
"loops_audit", "recipes",
|
|
31
|
+
"crossing_exact", "regularity", "placement",
|
|
32
|
+
]
|
stdraw/__main__.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""CLI: python -m stdraw layout <input.cld|input.json> [options]
|
|
3
|
+
|
|
4
|
+
Outputs (--outdir, default ./stdraw-out):
|
|
5
|
+
graph.json graph model (DSL->JSON normalization)
|
|
6
|
+
layout.json 2D layout (delivered coordinates, all integers)
|
|
7
|
+
geometry_report.json 11-check geometry self-report (CI gate form)
|
|
8
|
+
certificate.json five-metric + placement metrics + honest grading certificate
|
|
9
|
+
loops_audit.json loop semantics audit (lint)
|
|
10
|
+
demo-2d.html standalone rendering page (certificate embedded)
|
|
11
|
+
positions3d.json 3D per-solid assignment coordinates
|
|
12
|
+
demo-3d.html 3D interactive page (needs ../assets/three.min.js relative path)
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from . import __version__
|
|
20
|
+
from .model import Graph
|
|
21
|
+
from .dsl import parse, dumps
|
|
22
|
+
from .planar import PlanarLayout, NotPlanarError, LayoutConfig
|
|
23
|
+
from .recipes import run as recipes_run
|
|
24
|
+
from .check import geometry_report_to_file
|
|
25
|
+
from .metrics import certificate, scene_from_planar
|
|
26
|
+
from .loops import loops_audit
|
|
27
|
+
from .render import render_html
|
|
28
|
+
from .layout3d import SOLIDS, layout3d
|
|
29
|
+
from .render3d import render_3d_html
|
|
30
|
+
from .delivery import strip_delivery_metadata, stamp_cli_deliverable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_graph(path: str) -> Graph:
|
|
34
|
+
if path.endswith(".json"):
|
|
35
|
+
with open(path, encoding="utf-8") as f:
|
|
36
|
+
return Graph.from_dict(json.load(f))
|
|
37
|
+
with open(path, encoding="utf-8") as f:
|
|
38
|
+
text = f.read()
|
|
39
|
+
return parse(text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_manual_layout(path: str) -> dict:
|
|
43
|
+
"""Manual layout JSON -> the manual argument of layout().
|
|
44
|
+
|
|
45
|
+
Accepts a full layout.json or the demo-2d.html "Export layout" subset
|
|
46
|
+
({nodes: {id: {x,y}|[x,y]}, viewBox}). source records the file name for
|
|
47
|
+
meta provenance.
|
|
48
|
+
Optionally edges: [{a, b, waypoints: [[x,y],...]}] — waypoints manually
|
|
49
|
+
anchored by the user on demo-2d bend handles; --from-layout recomputes
|
|
50
|
+
them under the same caliber.
|
|
51
|
+
"""
|
|
52
|
+
with open(path, encoding="utf-8") as f:
|
|
53
|
+
raw = json.load(f)
|
|
54
|
+
if "nodes" not in raw:
|
|
55
|
+
raise ValueError(f"{path}: manual layout is missing nodes (node id -> x,y)")
|
|
56
|
+
return {"nodes": raw["nodes"], "viewBox": raw.get("viewBox"),
|
|
57
|
+
"edges": raw.get("edges"),
|
|
58
|
+
"source": os.path.basename(path)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv=None):
|
|
62
|
+
ap = argparse.ArgumentParser(
|
|
63
|
+
prog="stdraw",
|
|
64
|
+
description="stdraw — planarity-aware systems-thinking diagram engine "
|
|
65
|
+
"(text DSL in, certified diagram out)",
|
|
66
|
+
epilog=(
|
|
67
|
+
"Examples:\n"
|
|
68
|
+
" stdraw layout examples/limits-to-growth.cld --outdir out\n"
|
|
69
|
+
" python -m stdraw layout examples/limits-to-growth.cld --outdir out --pitch 62\n"
|
|
70
|
+
"Outputs (--outdir, default ./stdraw-out): graph.json / layout.json /\n"
|
|
71
|
+
"geometry_report.json / certificate.json / loops_audit.json /\n"
|
|
72
|
+
"demo-2d.html / positions3d.json / demo-3d.html"
|
|
73
|
+
),
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
75
|
+
ap.add_argument("command", choices=["layout", "layout-diff"],
|
|
76
|
+
help="subcommand: layout = full layout pipeline; layout-diff = auto vs manual three-layer diff")
|
|
77
|
+
ap.add_argument("input", help="input .cld (DSL) or .json (graph model)")
|
|
78
|
+
ap.add_argument("input2", nargs="?",
|
|
79
|
+
help="layout-diff second argument: manual layout layout.json")
|
|
80
|
+
ap.add_argument("--outdir", default="stdraw-out")
|
|
81
|
+
ap.add_argument("--recipe", default="auto", choices=["auto", "ortho"],
|
|
82
|
+
help="layout recipe: auto = auto-v2 (default, straight/arc lines + new hard gates); "
|
|
83
|
+
"ortho = ortho-v1 (orthogonal vertical-line constructive, behavior frozen, pin-safe)")
|
|
84
|
+
ap.add_argument("--linestyle", default="straight", choices=["straight", "arc"],
|
|
85
|
+
help="auto recipe line style (default straight; arc = all-arc preference)")
|
|
86
|
+
ap.add_argument("--skip-3d", action="store_true", help="run the 2D layer only")
|
|
87
|
+
ap.add_argument("--solids", default="sphere,cube,tetra,cone",
|
|
88
|
+
help="3D solid list (comma separated)")
|
|
89
|
+
ap.add_argument("--pitch", type=int, default=None,
|
|
90
|
+
help="row pitch px (default 56; raise it for small graphs to pass the clearance gate, parameter is transparent)")
|
|
91
|
+
ap.add_argument("--value-pad", type=float, default=None, metavar="PX",
|
|
92
|
+
help="extra per-side padding px for the value (second) text line "
|
|
93
|
+
"(default: historical baseline; raise it when long ticker/price "
|
|
94
|
+
"value strings overflow the planned card width)")
|
|
95
|
+
ap.add_argument("--column-order", default=None, metavar="V1,V2,...",
|
|
96
|
+
help="v0.7.11 deterministic constraint (comb layouts): explicit "
|
|
97
|
+
"source-column internal order, top->bottom, as a hard "
|
|
98
|
+
"constraint (components keep their internal DFS order)")
|
|
99
|
+
ap.add_argument("--hub-side", default=None, metavar="NODE=left|right,...",
|
|
100
|
+
help="v0.7.11 deterministic constraint (comb layouts): explicit "
|
|
101
|
+
"hub side assignment, e.g. wdc=left,stx=right")
|
|
102
|
+
ap.add_argument("--name", default="", help="graph name (default: the DSL graph name)")
|
|
103
|
+
ap.add_argument("--exact", action="store_true",
|
|
104
|
+
help="WS2 exact crossing-number layer: B&B squeeze computes cr(G) and writes the "
|
|
105
|
+
"certificate exactness block (proven_optimal/gap; requires the optional "
|
|
106
|
+
"ortools dependency; honestly degraded when not installed)")
|
|
107
|
+
ap.add_argument("--from-layout", default=None, metavar="MANUAL_JSON",
|
|
108
|
+
help="manual layout JSON (the demo-2d.html \"Export layout\" product or a "
|
|
109
|
+
"layout.json subset: node id->x,y + viewBox). Runs the same routing + "
|
|
110
|
+
"certificate pipeline on manual coordinates; hard-gate caliber unchanged, "
|
|
111
|
+
"graded honestly")
|
|
112
|
+
ap.add_argument("--compare-auto", action="store_true",
|
|
113
|
+
help="with --from-layout: also runs the automatic layout of the same graph into <outdir>-compare-auto/, "
|
|
114
|
+
"writing the dual-certificate comparison compare-auto.json (the aesthetic distance between engine and human)")
|
|
115
|
+
ap.add_argument("--cld", default=None,
|
|
116
|
+
help="layout-diff: graph source (.cld/.json), enables the semantic layer (loop membership / cohesion)")
|
|
117
|
+
ap.add_argument("--diff-out", default="layout-diff.json",
|
|
118
|
+
help="layout-diff: machine-readable JSON output path")
|
|
119
|
+
a = ap.parse_args(argv)
|
|
120
|
+
|
|
121
|
+
if a.command == "layout-diff":
|
|
122
|
+
if not a.input2:
|
|
123
|
+
print("layout-diff requires two arguments: <auto-layout.json> <manual-layout.json>",
|
|
124
|
+
file=sys.stderr)
|
|
125
|
+
sys.exit(2)
|
|
126
|
+
from .layoutdiff import main as diff_main
|
|
127
|
+
sys.exit(diff_main([a.input, a.input2] +
|
|
128
|
+
(["--cld", a.cld] if a.cld else []) +
|
|
129
|
+
["--out", a.diff_out]))
|
|
130
|
+
|
|
131
|
+
g = load_graph(a.input)
|
|
132
|
+
os.makedirs(a.outdir, exist_ok=True)
|
|
133
|
+
deferred_exit = 0 # --compare-auto: emit the comparison report first, exit later on geometry fail
|
|
134
|
+
name = a.name or g.name or os.path.splitext(os.path.basename(a.input))[0]
|
|
135
|
+
problems = g.validate()
|
|
136
|
+
if problems:
|
|
137
|
+
print("Structural validation failed:", *problems, sep="\n ", file=sys.stderr)
|
|
138
|
+
sys.exit(2)
|
|
139
|
+
|
|
140
|
+
def w(fn, obj):
|
|
141
|
+
p = os.path.join(a.outdir, fn)
|
|
142
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
143
|
+
json.dump(obj, f, ensure_ascii=False, indent=1)
|
|
144
|
+
return p
|
|
145
|
+
|
|
146
|
+
# Graph model + normalized DSL text
|
|
147
|
+
p = w("graph.json", g.to_dict())
|
|
148
|
+
with open(os.path.join(a.outdir, "graph.cld"), "w", encoding="utf-8") as f:
|
|
149
|
+
f.write(dumps(g))
|
|
150
|
+
print(f"[1/6] graph model OK: {len(g.nodes)} nodes / {len(g.edges)} edges / "
|
|
151
|
+
f"{len(g.loops)} declared loops -> {p}")
|
|
152
|
+
|
|
153
|
+
# Loop semantics audit (lint)
|
|
154
|
+
lr = loops_audit(g)
|
|
155
|
+
p = w("loops_audit.json", lr)
|
|
156
|
+
print(f"[2/6] loop audit: declared {lr['summary']['declared']}, closed "
|
|
157
|
+
f"{lr['summary']['closed']}, independently enumerated {lr['summary']['enumerated']}, "
|
|
158
|
+
f"problems {lr['summary']['problems']} -> {p}")
|
|
159
|
+
for pr in lr["problems"]:
|
|
160
|
+
print(" lint:", pr)
|
|
161
|
+
|
|
162
|
+
# 2D layout
|
|
163
|
+
if a.recipe == "ortho":
|
|
164
|
+
cfg = LayoutConfig()
|
|
165
|
+
if a.pitch:
|
|
166
|
+
cfg.pitch = a.pitch
|
|
167
|
+
eng = PlanarLayout(g, cfg)
|
|
168
|
+
try:
|
|
169
|
+
v4 = eng.run()
|
|
170
|
+
except NotPlanarError as e:
|
|
171
|
+
print(f"[3/6] 2D planar layer inapplicable: {e}")
|
|
172
|
+
print(" honest degradation: skipping the 2D certificate (a replaceable layout-layer implementation is M1+)")
|
|
173
|
+
v4 = None
|
|
174
|
+
else:
|
|
175
|
+
# ortho-v1 freeze-compat: the pipeline stamps delivery-contract
|
|
176
|
+
# metadata (stdraw/delivery.py), but the CLI ortho layout.json is a
|
|
177
|
+
# byte-frozen production pin (tests/baselines/, ci_checks gate A),
|
|
178
|
+
# so the metadata is stripped here; the ortho delivery contract
|
|
179
|
+
# travels on certificate.json (interactive / drag_layer) instead.
|
|
180
|
+
v4 = strip_delivery_metadata(v4)
|
|
181
|
+
labels = {n.id: n.label for n in g.nodes}
|
|
182
|
+
values = {n.id: n.value for n in g.nodes if n.value}
|
|
183
|
+
p = w("layout.json", v4)
|
|
184
|
+
print(f"[3/6] 2D layout (ortho-v1 frozen recipe): {v4['viewBox'][2]}x"
|
|
185
|
+
f"{v4['viewBox'][3]}, {v4['meta']['rows']} rows -> {p}")
|
|
186
|
+
# 11-check geometry self-report (runs on delivered coordinates)
|
|
187
|
+
rc = geometry_report_to_file(
|
|
188
|
+
v4, os.path.join(a.outdir, "geometry_report.json"))
|
|
189
|
+
# Five-metric certificate (legacy caliber, byte-compatible)
|
|
190
|
+
cert = certificate(scene_from_planar(v4), graph_name=name,
|
|
191
|
+
layer="2D",
|
|
192
|
+
notes=["unrelated lines piercing cards occlusion-style is allowed by design (cards are opaque)"],
|
|
193
|
+
interactive="yes", drag_layer="yes")
|
|
194
|
+
p = w("certificate.json", cert)
|
|
195
|
+
m = cert["metrics"]
|
|
196
|
+
print(f"[4/6] certificate: crossings {m['crossings']} / true card piercings {m['occlusions_true_0px']} / "
|
|
197
|
+
f"gap {m['min_card_gap']} / alignment {m['row_align_rate']} / CV "
|
|
198
|
+
f"{m['spacing_cv']} -> grade [{cert['grade']}] -> {p}")
|
|
199
|
+
hp = os.path.join(a.outdir, "demo-2d.html")
|
|
200
|
+
with open(hp, "w", encoding="utf-8") as f:
|
|
201
|
+
f.write(render_html(v4, name, cert, lr, labels, values))
|
|
202
|
+
print(f" demo -> {hp}")
|
|
203
|
+
if rc:
|
|
204
|
+
sys.exit(1)
|
|
205
|
+
else:
|
|
206
|
+
from .layout_auto import AutoConfig
|
|
207
|
+
from .metrics import certificate_auto, scene_from_auto
|
|
208
|
+
cfg = AutoConfig()
|
|
209
|
+
if a.pitch:
|
|
210
|
+
cfg.pitch = a.pitch
|
|
211
|
+
if a.value_pad is not None:
|
|
212
|
+
cfg.value_pad_extra = a.value_pad
|
|
213
|
+
if a.column_order:
|
|
214
|
+
cfg.column_order = [v.strip() for v in a.column_order.split(",") if v.strip()]
|
|
215
|
+
if a.hub_side:
|
|
216
|
+
cfg.hub_side = {}
|
|
217
|
+
for _part in a.hub_side.split(","):
|
|
218
|
+
if "=" in _part:
|
|
219
|
+
_k, _v = _part.split("=", 1)
|
|
220
|
+
_v = _v.strip().lower()
|
|
221
|
+
if _v in ("left", "right"):
|
|
222
|
+
cfg.hub_side[_k.strip()] = _v
|
|
223
|
+
if a.from_layout:
|
|
224
|
+
# Manual layout: same routing + certificate pipeline; hard-gate
|
|
225
|
+
# caliber unchanged, graded honestly
|
|
226
|
+
from .layout_auto import layout as _auto_layout_fn
|
|
227
|
+
try:
|
|
228
|
+
_manual = load_manual_layout(a.from_layout)
|
|
229
|
+
v4 = _auto_layout_fn(g, cfg, a.linestyle, manual=_manual)
|
|
230
|
+
except ValueError as e:
|
|
231
|
+
print(f"manual layout unusable: {e}", file=sys.stderr)
|
|
232
|
+
sys.exit(2)
|
|
233
|
+
else:
|
|
234
|
+
v4 = recipes_run("auto-v2", g, cfg, a.linestyle)
|
|
235
|
+
labels = {n.id: n.label for n in g.nodes}
|
|
236
|
+
values = {n.id: n.value for n in g.nodes if n.value}
|
|
237
|
+
# Full CLI deliverable (demo-2d.html + certificate.json) is produced
|
|
238
|
+
# below: the layout.json that ships with it is re-stamped interactive
|
|
239
|
+
# (delivery="stdraw-cli", no warning) per the delivery contract.
|
|
240
|
+
v4 = stamp_cli_deliverable(v4)
|
|
241
|
+
p = w("layout.json", v4)
|
|
242
|
+
meta = v4["meta"]
|
|
243
|
+
print(f"[3/6] 2D layout (auto-v2, engine={meta['position_engine']}, "
|
|
244
|
+
f"line style={meta['line_style']}): {v4['viewBox'][2]}x{v4['viewBox'][3]}, "
|
|
245
|
+
f"{meta['rows']} rows -> {p}")
|
|
246
|
+
rc = geometry_report_to_file(
|
|
247
|
+
v4, os.path.join(a.outdir, "geometry_report.json"))
|
|
248
|
+
exactness = None
|
|
249
|
+
if a.exact:
|
|
250
|
+
from .crossing_exact import crossing_exact
|
|
251
|
+
exactness = crossing_exact(g)
|
|
252
|
+
print(f" [exact] cr={exactness['crossing_number']} "
|
|
253
|
+
f"proven_optimal={exactness['proven_optimal']} "
|
|
254
|
+
f"lb={exactness['lb']} gap={exactness['gap']} "
|
|
255
|
+
f"coverage={exactness['coverage']}")
|
|
256
|
+
cert = certificate_auto(scene_from_auto(v4), graph_name=name, layer="2D",
|
|
257
|
+
graph=g, exactness=exactness,
|
|
258
|
+
interactive="yes", drag_layer="yes")
|
|
259
|
+
p = w("certificate.json", cert)
|
|
260
|
+
m = cert["metrics"]
|
|
261
|
+
print(f"[4/6] certificate: crossings {m['crossings']} / box press {m['box_press_1px']} / "
|
|
262
|
+
f"edge-box clearance {m['edge_box_min_clear']} / anchor violations {m['anchor_violations']} / "
|
|
263
|
+
f"gap {m['min_card_gap']} / CV {m['spacing_cv']} -> "
|
|
264
|
+
f"grade [{cert['grade']}] -> {p}")
|
|
265
|
+
hp = os.path.join(a.outdir, "demo-2d.html")
|
|
266
|
+
with open(hp, "w", encoding="utf-8") as f:
|
|
267
|
+
f.write(render_html(v4, name, cert, lr, labels, values))
|
|
268
|
+
print(f" demo -> {hp}")
|
|
269
|
+
if rc:
|
|
270
|
+
if a.from_layout and a.compare_auto:
|
|
271
|
+
deferred_exit = 1 # emit the comparison report first, exit code deferred
|
|
272
|
+
else:
|
|
273
|
+
sys.exit(1)
|
|
274
|
+
|
|
275
|
+
# 3D per-solid assignment
|
|
276
|
+
if not a.skip_3d:
|
|
277
|
+
gd = {"nodes": [{"id": n.id, "label": n.label} for n in g.nodes],
|
|
278
|
+
"edges": [{"a": e.a, "b": e.b, "p": e.polarity} for e in g.edges]}
|
|
279
|
+
solids = [s.strip() for s in a.solids.split(",") if s.strip()]
|
|
280
|
+
unknown = [s for s in solids if s not in SOLIDS]
|
|
281
|
+
if unknown:
|
|
282
|
+
print(f"unknown solids {unknown}, choose from {list(SOLIDS)}", file=sys.stderr)
|
|
283
|
+
sys.exit(2)
|
|
284
|
+
layouts = {s: layout3d(gd, s) for s in solids}
|
|
285
|
+
p = w("positions3d.json",
|
|
286
|
+
{s: layouts[s]["positions"] for s in solids})
|
|
287
|
+
print(f"[5/6] 3D assignment: {solids} -> {p}")
|
|
288
|
+
hp = os.path.join(a.outdir, "demo-3d.html")
|
|
289
|
+
note = ("deterministic placement: slots + minimum-total-edge-length assignment; "
|
|
290
|
+
"no crossing promise in 3D, projection crossings are solved by the camera")
|
|
291
|
+
with open(hp, "w", encoding="utf-8") as f:
|
|
292
|
+
f.write(render_3d_html(gd, layouts, name, note))
|
|
293
|
+
print(f" demo -> {hp} (must live in the same tree as assets/three.min.js relative path)")
|
|
294
|
+
# ── compare-auto: automatic-layout comparison for the same graph (dual certificates) ──
|
|
295
|
+
if a.from_layout and a.compare_auto:
|
|
296
|
+
cmp_dir = a.outdir.rstrip("/\\") + "-compare-auto"
|
|
297
|
+
print("\n-- compare-auto: automatic layout comparison for the same graph --")
|
|
298
|
+
try:
|
|
299
|
+
rc_cmp = main(["layout", a.input, "--outdir", cmp_dir, "--skip-3d"]
|
|
300
|
+
+ (["--name", name] if name else []))
|
|
301
|
+
except SystemExit as _e: # geometry fail -> exit 1, recorded honestly
|
|
302
|
+
rc_cmp = _e.code
|
|
303
|
+
|
|
304
|
+
def _cert(d):
|
|
305
|
+
with open(os.path.join(d, "certificate.json"), encoding="utf-8") as f:
|
|
306
|
+
return json.load(f)
|
|
307
|
+
|
|
308
|
+
cert_m, cert_a = _cert(a.outdir), _cert(cmp_dir)
|
|
309
|
+
keys = ("crossings", "box_press_1px", "edge_box_min_clear",
|
|
310
|
+
"anchor_violations", "avg_edge_length", "row_align_rate",
|
|
311
|
+
"min_card_gap", "spacing_cv", "column_snap24_rate")
|
|
312
|
+
table = {}
|
|
313
|
+
for k in keys:
|
|
314
|
+
mv, av = cert_m["metrics"].get(k), cert_a["metrics"].get(k)
|
|
315
|
+
delta = (mv - av) if isinstance(mv, (int, float)) and \
|
|
316
|
+
isinstance(av, (int, float)) else None
|
|
317
|
+
table[k] = {"manual": mv, "auto": av,
|
|
318
|
+
"delta_manual_minus_auto": delta}
|
|
319
|
+
cmp_obj = {
|
|
320
|
+
"tool": "stdraw --compare-auto", "graph": name,
|
|
321
|
+
"manual_outdir": a.outdir, "auto_outdir": cmp_dir,
|
|
322
|
+
"auto_cli_exit": rc_cmp,
|
|
323
|
+
"metrics": table,
|
|
324
|
+
"grade": {"manual": cert_m["grade"], "auto": cert_a["grade"]},
|
|
325
|
+
"gates_passed": {"manual": cert_m["gates_passed"],
|
|
326
|
+
"auto": cert_a["gates_passed"]},
|
|
327
|
+
"note": ("the gap is the aesthetic distance between engine and human "
|
|
328
|
+
"(the basis for future objective-function tuning); the hard-gate "
|
|
329
|
+
"caliber applies equally to manual layouts and grades honestly "
|
|
330
|
+
"without manual favors; the comparison automatic layout did not "
|
|
331
|
+
"run the --exact layer"),
|
|
332
|
+
}
|
|
333
|
+
p = os.path.join(a.outdir, "compare-auto.json")
|
|
334
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
335
|
+
json.dump(cmp_obj, f, ensure_ascii=False, indent=1)
|
|
336
|
+
print(" metric manual auto")
|
|
337
|
+
for k, v in table.items():
|
|
338
|
+
print(f" {k:<26} {str(v['manual']):>8} {str(v['auto']):>10}"
|
|
339
|
+
f" D={v['delta_manual_minus_auto']}")
|
|
340
|
+
print(f" grade: manual={cert_m['grade']} / auto={cert_a['grade']}; "
|
|
341
|
+
f"gates manual {cert_m['gates_passed']} / auto {cert_a['gates_passed']}"
|
|
342
|
+
f" (auto_cli_exit={rc_cmp})")
|
|
343
|
+
print(f" -> {p}")
|
|
344
|
+
|
|
345
|
+
print("[6/6] done.")
|
|
346
|
+
return deferred_exit
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
sys.exit(main())
|