app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
"""2D drawings with Fusion's automatic drawing API (adsk.drawing.DrawingManager, a July 2026 preview).
drawing_create makes a drawing of the active, saved cloud design in one journaled call: Fusion lays
out the sheets itself, with a base view and its projected views, an optional isometric view, a
filled title block and automatic dimensions. The API cannot place a view or set a view's scale; the
layout picks the scale for the chosen sheet size. The drawing is a new cloud file (.f2d) in the
design's folder, or in folderId.
drawing_export writes PDF, DWG and DXF of a drawing, and drawing_status says whether one is ready.
Measured on Fusion 2705 (2026-11-03): createDrawing took 24.7 to 33.6 s for a small part (three
runs); it works out of process (a drawing engine, AcCoreConsole.exe, lays out, dimensions and saves,
then quits) and does not open the drawing. Moving the new file to another folder took 0.6 s and
kept its file id.
HARD RULES (both found live, each one blocked Fusion's main thread for every caller):
1. Never export in the call that opens a drawing. A drawing a script opens only finishes loading
after that script returns, and opening an automatically created drawing starts the interactive
Edit Auto Dimensions command (AUTODIMENSIONEDIT) in the drawing engine. An export sent while that
command waits for input is fed to its prompt and rejected ("Invalid selection"), and
DrawingExportManager.execute holds the main thread until its 600 s timeout ("3 : export timed
out"); PDF, DWG and DXF each waited the full 600 s.
2. Never open a drawing invisibly. Documents.open(file, False) opened the drawing in 0.8 s by
Fusion's log, but no drawing engine started for it and the call did not return.
So drawing_export opens visibly and answers drawing_not_ready (retryable); a later call exports only
once the sheets read, Fusion runs no command, the drawing engine that loaded the drawing has no
unfinished command and its log has been quiet for a few seconds. (View.type is no readiness signal:
it raised "pAsset" for every view of a fully opened, idle drawing.)
"""
import math
import os
from pathlib import Path
import re
import tempfile
import threading
import time
import adsk.core
import adsk.fusion
try:
import adsk.drawing # always present inside Fusion 2705+; offline tests install a stand-in
except ImportError:
pass
from .core import MechanicalError, require, target
from .exporting import publish_staged
STANDARDS = {"iso": "ISODrawingStandardType", "asme": "ASMEDrawingStandardType"}
UNITS = {"mm": "MillimeterDrawingUnitType", "in": "InchDrawingUnitType"}
ISO_SIZES = {"A4": "A4ISOSheetSize", "A3": "A3ISOSheetSize", "A2": "A2ISOSheetSize", "A1": "A1ISOSheetSize", "A0": "A0ISOSheetSize"}
ASME_SIZES = {"A": "AASMESheetSize", "B": "BASMESheetSize", "C": "CASMESheetSize", "D": "DASMESheetSize", "E": "EASMESheetSize"}
DEFAULTS = {"iso": {"units": "mm", "sheetSize": "A3"}, "asme": {"units": "in", "sheetSize": "B"}}
ORIENTATIONS = {"landscape": "LandscapeSheetOrientationType", "portrait": "PortraitSheetOrientationType"}
DIMENSIONS = {"overall": "OverallDimensionStrategyType", "automatic": "AutomaticDimensionStrategyType",
"baseline": "BaselineDimensionStrategyType", "chain": "ChainDimensionStrategyType",
"ordinate": "OrdinateDimensionStrategyType", "symmetric": "SymmetricDimensionStrategyType",
"symmetric_baseline": "SymmetricWithBaselineDimensionStrategyType",
"symmetric_ordinate": "SymmetricWithOrdinateDimensionStrategyType"}
HOLE_NOTES = {"hole_and_thread": "HoleAndThreadNoteHolePreferencesType", "hole": "HoleNoteOnlyHolePreferencesType",
"thread": "ThreadNoteOnlyHolePreferencesType", "none": "NoHoleAnnotationsHolePreferencesType"}
DATUMS = {"model_origin": "ModelOriginDefaultOriginType", "bottom_left": "BottomLeftDefaultOriginType",
"bottom_right": "BottomRightDefaultOriginType", "top_left": "TopLeftDefaultOriginType",
"top_right": "TopRightDefaultOriginType"}
STYLES = {"visible": "VisibleEdgesDrawingViewStyleType", "visible_and_hidden": "VisibleAndHiddenEdgesDrawingViewStyleType",
"shaded_and_hidden": "ShadedAndHiddenEdgesDrawingViewStyleType", "shaded": "ShadedVisibleEdgesDrawingViewStyleType"}
TANGENT_EDGES = {"off": "OffTangentEdgeDisplayType", "full": "FullLengthTangentEdgeDisplayType",
"shortened": "ShortenedTangentEdgeDisplayType"}
CONTENT = {"full": "FullAssemblyDrawingContentType", "visible": "VisibleOnlyDrawingContentType"}
LEVELS = {"first": "FirstLevelOnlySheetCreationType", "all": "AllLevelsSheetCreationType"}
DWG_FORMATS = {"autocad": "AutoCADDWGFormat", "simplified": "SimplifiedDWGFormat"}
VIEW_TYPES = {0: "base", 1: "projected", 2: "section", 3: "detail", 4: "auxiliary", 5: "flat_pattern", 6: "unknown"}
EXTENSIONS = {"pdf": ".pdf", "dwg": ".dwg", "dxf": ".dxf"}
IDLE_COMMANDS = ("", "SelectCommand")
AUTO_DIMENSION_EDIT = "AUTODIMENSIONEDIT"
# the same command as Fusion's UI names it; only this command is ever ended, never one a person started
AUTO_DIMENSION_UI_COMMAND = "FusionDrawingAutoDimensionEditCommand"
# file id -> {"at": time.time() when a call here opened that drawing (its engine log is read from then on),
# "previous": the document that was active then, active again when the drawing closes,
# "before": creationIds of the documents open then: opening a drawing also opens the design it
# references, and close closes that design only if it was not open before}
_opened = {}
def _enum(cls_name, member):
return getattr(getattr(adsk.drawing, cls_name), member)
def _main_thread():
require(threading.current_thread() is threading.main_thread(), "wrong_thread", "Drawings require Fusion's main thread.")
def _drawing_api():
manager = getattr(getattr(adsk, "drawing", None), "DrawingManager", None)
require(manager is not None, "unsupported_capability",
"This Fusion has no automatic drawing API (adsk.drawing.DrawingManager arrived in July 2026); update Fusion.")
dm = manager.get()
require(dm is not None, "unsupported_capability", "Fusion returned no DrawingManager.")
return dm
# ---------------------------------------------------------------- drawing_create
def resolve_options(args):
"""The caller's options with defaults filled in; refuses combinations Fusion would silently ignore."""
standard = args.get("standard", "iso")
defaults = DEFAULTS[standard]
size = args.get("sheetSize", defaults["sheetSize"])
sizes = ISO_SIZES if standard == "iso" else ASME_SIZES
require(size in sizes, "sheet_size_mismatch",
"Sheet %s is not a%s size; use %s." % (size, "n ISO" if standard == "iso" else "n ASME", ", ".join(sizes)),
standard=standard, sheetSize=size)
orientation = args.get("orientation", "landscape")
require(not (orientation == "portrait" and size in ("A0", "E")), "invalid_input",
"Fusion has no portrait %s sheet." % size)
views = args.get("views") or {}
sheets = args.get("sheets") or {}
omit = args.get("omit") or {}
return {"standard": standard, "units": args.get("units", defaults["units"]), "sheetSize": size, "orientation": orientation,
"orthographic": views.get("orthographic", True), "isometric": views.get("isometric", True),
"style": args.get("style", "visible"), "tangentEdges": args.get("tangentEdges", "off"),
"dimensions": args.get("dimensions", "overall"), "holeNotes": args.get("holeNotes", "hole_and_thread"),
"datum": args.get("datum", "model_origin"), "content": args.get("content", "full"),
"levels": sheets.get("levels", "all"), "assembly": sheets.get("assembly", True),
"subassemblies": sheets.get("subassemblies", True), "components": sheets.get("components", True),
"sheetMetal": sheets.get("sheetMetal", True), "partsList": sheets.get("partsList", True),
"omitKeywords": omit.get("keywords", "Bolt,Screw,Nut,Washer"), "omitFasteners": omit.get("fasteners", False),
"configuration": args.get("configuration", "")}
def apply_options(inp, o):
"""Copy resolved options onto a CreateDrawingInput (automatic mode)."""
inp.standard = _enum("DrawingStandardTypes", STANDARDS[o["standard"]])
inp.units = _enum("DrawingUnitTypes", UNITS[o["units"]])
inp.sheetSize = _enum("SheetSizes", (ISO_SIZES if o["standard"] == "iso" else ASME_SIZES)[o["sheetSize"]])
inp.orientationType = _enum("SheetOrientationTypes", ORIENTATIONS[o["orientation"]])
inp.content = _enum("DrawingContentTypes", CONTENT[o["content"]])
inp.sheetCreationType = _enum("SheetCreationTypes", LEVELS[o["levels"]])
if o["configuration"]:
inp.configuration = o["configuration"]
prefs = inp.automationPreferences
g = prefs.globalPreferences
g.isMainAssemblySheetGenerated = bool(o["assembly"])
g.isSubAssemblySheetGenerated = bool(o["subassemblies"])
g.isComponentSheetGenerated = bool(o["components"])
g.isFoldedModelSheetGenerated = bool(o["sheetMetal"])
g.isFlatPatternSheetGenerated = bool(o["sheetMetal"])
g.isAnimationSheetGenerated = False
g.isAutoDimensionEnabled = o["dimensions"] != "none"
g.omitComponentsWithKeywords = o["omitKeywords"]
g.isDetectAndOmitFasteners = bool(o["omitFasteners"])
for p in (prefs.componentPreferences, prefs.foldedModelPreferences):
p.sheetViewPreferences.isOrthogonalViewAdded = bool(o["orthographic"])
p.sheetViewPreferences.isIsometricViewAdded = bool(o["isometric"] and o["orthographic"])
for asm in (prefs.mainAssemblyPreferences, prefs.subAssemblyPreferences):
# the assembly's isometric sheet carries the parts list, so views.isometric leaves it alone
asm.isoViewSheetPreferences.isSheetCreated = True
asm.isoViewSheetPreferences.isPartsListIncluded = bool(o["partsList"])
asm.orthogonalViewSheetPreferences.isSheetCreated = bool(o["orthographic"])
for p in (prefs.componentPreferences, prefs.foldedModelPreferences, prefs.flatPatternPreferences,
prefs.mainAssemblyPreferences, prefs.subAssemblyPreferences):
dims = p.autoDimensionPreferences
dims.isAutoDimensionEnabled = o["dimensions"] != "none"
if o["dimensions"] != "none":
dims.dimensionStrategyType = _enum("DimensionStrategyTypes", DIMENSIONS[o["dimensions"]])
dims.holePreferencesType = _enum("HolePreferencesTypes", HOLE_NOTES[o["holeNotes"]])
if hasattr(dims, "defaultOriginType"):
dims.defaultOriginType = _enum("DefaultOriginTypes", DATUMS[o["datum"]])
view = p.drawingViewPreferences
view.style = _enum("DrawingViewStyleTypes", STYLES[o["style"]])
view.tangentEdgesType = _enum("TangentEdgeDisplayTypes", TANGENT_EDGES[o["tangentEdges"]])
def drawing_create(app, args):
_main_thread()
doc, design, value = target(app, args, True, model_write=False)
options = resolve_options(args)
source = doc.dataFile
require(source is not None, "design_not_saved",
"Fusion draws a design from its cloud file, and this design was never saved. Save it into an "
"'Adom AI Workspace' subfolder (fusion_save_to_cloud), then call drawing_create again.")
require(not doc.isModified or args.get("useSavedVersion", False), "unsaved_changes",
"The drawing would show the last saved version, not the changes made since. Save a version first, "
"or pass useSavedVersion:true to draw the saved one.", savedVersion=source.versionNumber)
require(source.isComplete, "design_upload_pending", "The design's last save is still uploading; call again in a few seconds.",
retryable=True)
destination = None
if args.get("folderId"):
destination = app.data.findFolderById(args["folderId"])
require(destination is not None, "folder_not_found", "No cloud folder with that folderId.")
require(not destination.isRoot, "refused_root_upload", "Put drawings in a subfolder, never a project's root folder.")
else:
require(not source.parentFolder.isRoot, "refused_root_upload",
"The design sits in its project's root folder, where Fusion would also put the drawing. Pass folderId "
"of a subfolder (an 'Adom AI Workspace' task folder) for the drawing.")
dm = _drawing_api()
inp = dm.createDrawingInput(source, _enum("DrawingCreationModes", "AutomaticDrawingCreationMode"))
require(inp is not None, "drawing_failed", "Fusion did not create a drawing input for this design.")
apply_options(inp, options)
started = time.perf_counter()
drawing = dm.createDrawing(inp)
seconds = round(time.perf_counter() - started, 1)
require(drawing is not None, "drawing_failed", "Fusion's automatic drawing failed for this design.", seconds=seconds)
moved = None
if destination is not None:
moved = bool(drawing.move(destination))
if not moved:
# a failed call changes nothing: the drawing it made is deleted, not left beside the design
removed = bool(drawing.deleteMe())
raise MechanicalError("drawing_move_failed", "Fusion made the drawing but could not move it to folderId.",
drawingDeleted=removed, seconds=seconds)
if args.get("name"):
drawing.name = args["name"]
return {"target": value, "drawing": file_data(drawing), "source": file_data(source), "options": options,
"seconds": seconds, "movedToFolder": moved,
"next": "drawing_export with drawing.fileId; it opens the drawing in one call and exports in the next."}
def file_data(f):
folder = f.parentFolder
return {"fileId": f.id, "name": f.name, "version": f.versionNumber, "extension": f.fileExtension,
"folder": folder.name if folder else None, "folderId": folder.id if folder else None,
"project": f.parentProject.name if f.parentProject else None}
# ---------------------------------------------------------------- readiness
def find_open(app, file_id):
for i in range(app.documents.count):
d = app.documents.item(i)
try:
if d.dataFile is not None and d.dataFile.id == file_id:
return d
except Exception:
continue
return None
def engine_log_path():
"""Fusion's drawing-engine log: %LOCALAPPDATA%/Autodesk/Autodesk Fusion 360/<user>/logs/FusionDrawing.log."""
root = Path(os.environ.get("LOCALAPPDATA", "")) / "Autodesk" / "Autodesk Fusion 360"
logs = sorted(root.glob("*/logs/FusionDrawing.log"), key=lambda p: p.stat().st_mtime, reverse=True) if root.exists() else []
return logs[0] if logs else None
LOG_LINE = re.compile(r"^(\d{8}T\d{6}) \w \.(\d+) (.*)$")
QUIET_SECONDS = 5
def engine_state(since, path=None, now=None):
"""What Fusion's drawing engines logged at or after `since` (epoch seconds), from FusionDrawing.log:
per engine process, its unfinished commands, whether it loaded a document (opendoc) and whether it
closed; plus how long the log has been quiet. None when the log cannot be found."""
path = path or engine_log_path()
if path is None:
return None
stamp = time.strftime("%Y%m%dT%H%M%S", time.localtime(since))
engines, last = {}, None
with open(path, encoding="utf-8", errors="replace") as f:
for raw in f:
m = LOG_LINE.match(raw.rstrip("\n"))
if not m or m.group(1) < stamp:
continue
last = m.group(1)
e = engines.setdefault(m.group(2), {"open": [], "loaded": False, "closed": False})
text = m.group(3)
if text.startswith("cmd will Start: "):
e["open"].append(text[16:].strip())
elif text.startswith("cmd Finished: "):
name = text[14:].split(" Reason")[0].strip()
if name in e["open"]:
e["open"].remove(name)
elif '"action":"opendoc"' in text:
e["loaded"] = True
elif text.startswith("closing dwg") or text.startswith("unloading AcFusionService"):
e["closed"] = True
quiet = None
if last is not None:
quiet = round((now if now is not None else time.time()) - time.mktime(time.strptime(last, "%Y%m%dT%H%M%S")), 1)
live = {pid: e for pid, e in engines.items() if not e["closed"]}
return {"loaded": any(e["loaded"] for e in live.values()),
"unfinished": {pid: e["open"] for pid, e in live.items() if e["open"]}, "quietSeconds": quiet}
PAPER = {v: k for k, v in {**ISO_SIZES, **ASME_SIZES, "custom": "CustomSizeSheetSize"}.items()}
def paper_name(sheet):
for member, name in PAPER.items():
if getattr(adsk.drawing.SheetSizes, member, None) == sheet.sheetSize:
return name
return str(sheet.sheetSize)
def view_data(view):
"""A view's type and curve count. Fusion 2705's View.type raises "InternalValidationError: pAsset"
on drawings it opened (its log: no base model asset in the drawing document), so a view that
cannot be read is reported as such and is not taken as a sign that the drawing is still loading."""
try:
return {"type": VIEW_TYPES.get(view.type, str(view.type)), "curves": view.viewCurves.count}
except Exception as error:
return {"type": None, "unreadable": str(error)[:80]}
def sheet_data(drawing):
"""Sheets and their views. Width and height are Fusion's own numbers (mm on an mm drawing: 420 x 297
for A3 landscape, read live); units is the drawing's unit setting."""
units = "mm" if drawing.documentSettings.units == adsk.drawing.DrawingUnitTypes.MillimeterDrawingUnitType else "in"
out = []
for i in range(drawing.sheets.count):
sheet = drawing.sheets.item(i)
views = [view_data(sheet.views.item(j)) for j in range(sheet.views.count)]
out.append({"index": i + 1, "name": sheet.name, "paper": paper_name(sheet), "width": sheet.width, "height": sheet.height,
"units": units, "views": views})
return out
def readiness(app, doc, file_id):
"""(ready, detail) for a drawing this session opened, without changing anything. Ready means its
sheets read, Fusion runs no command, a drawing engine loaded the drawing since it was opened
(opendoc in the engine's log) and has no unfinished command, and the engine log has been quiet for
QUIET_SECONDS: in the live run Edit Auto Dimensions started in the same second the open finished,
so a single instant is not enough."""
detail = {"open": True, "active": doc.isActive, "visible": doc.isVisible,
"activeCommand": app.userInterface.activeCommand}
try:
drawing = adsk.drawing.DrawingDocument.cast(doc).drawing
detail["sheets"] = sheet_data(drawing)
detail["loaded"] = True
except Exception as error:
detail["loaded"], detail["loadError"] = False, str(error)[:160]
engine = engine_state(_opened[file_id]["at"])
detail["engine"] = engine
detail["ready"] = bool(detail["loaded"] and detail["activeCommand"] in IDLE_COMMANDS and engine is not None
and engine["loaded"] and not engine["unfinished"]
and engine["quietSeconds"] is not None and engine["quietSeconds"] >= QUIET_SECONDS)
return detail["ready"], detail
def drawing_status(app, args):
_main_thread()
file_id = args["drawing"]["fileId"]
doc = find_open(app, file_id)
if doc is None:
return {"drawing": {"fileId": file_id}, "open": False, "ready": False,
"next": "drawing_export opens it; the export follows in a later call."}
if file_id not in _opened:
return {"drawing": {"fileId": file_id, "name": doc.name}, "open": True, "ready": False, "openedHere": False,
"next": "Open elsewhere (by a person or before the add-in restarted); drawing_export only exports drawings it opened."}
ready, detail = readiness(app, doc, file_id)
return {"drawing": {"fileId": file_id, "name": doc.name}, "openedHere": True, **detail}
# ---------------------------------------------------------------- drawing_export
def not_ready(message, **details):
raise MechanicalError("drawing_not_ready", message, retryable=True, retryAfterSeconds=details.pop("retryAfterSeconds", 3), **details)
def drawing_export(app, args):
_main_thread()
file_id = args["drawing"]["fileId"]
outputs = {k: args[k] for k in EXTENSIONS if args.get(k)}
require(outputs, "invalid_input", "Name at least one of pdf, dwg or dxf (absolute paths on the PC).")
overwrite = bool(args.get("overwrite", False))
for kind, raw in outputs.items():
path = Path(raw)
require(path.is_absolute(), "invalid_path", "%s must be an absolute path on the PC." % kind)
require(path.suffix.lower() == EXTENSIONS[kind], "extension_mismatch", "%s must end in %s." % (kind, EXTENSIONS[kind]))
require(overwrite or not path.exists(), "destination_exists", "%s exists; pass overwrite:true to replace it." % path)
doc = find_open(app, file_id)
if doc is None:
data_file = app.data.findFileById(file_id)
require(data_file is not None, "drawing_not_found", "No cloud file with that fileId.")
require(data_file.fileExtension == "f2d", "not_a_drawing", "That file is a %s, not a drawing (.f2d)." % data_file.fileExtension)
_opened[file_id] = {"at": time.time(), "previous": app.activeDocument,
"before": {app.documents.item(i).creationId for i in range(app.documents.count)}}
# visible: an invisible open never returned (hard rule 2)
opened = app.documents.open(data_file, True)
require(opened is not None, "drawing_open_failed", "Fusion could not open the drawing.")
not_ready("Opened the drawing; Fusion finishes loading it after this call returns. Call drawing_export again in a "
"few seconds (a new operationId): exporting in the call that opens a drawing blocks Fusion for 10 minutes.",
opened=True, retryAfterSeconds=5)
require(file_id in _opened, "drawing_open_elsewhere",
"This drawing is open in Fusion but was not opened by drawing_export in this add-in session, so its "
"drawing engine's state is unknown and an export could block Fusion. Close it in Fusion, then call again.")
ready, detail = readiness(app, doc, file_id)
if not ready:
unfinished = [c for cmds in ((detail.get("engine") or {}).get("unfinished") or {}).values() for c in cmds]
if detail.get("activeCommand") == AUTO_DIMENSION_UI_COMMAND and doc.isActive and AUTO_DIMENSION_EDIT in unfinished:
# Fusion starts Edit Auto Dimensions when an automatic drawing opens; end it like Esc would
ended = app.userInterface.terminateActiveCommand()
not_ready("Ended Fusion's Edit Auto Dimensions command on the drawing; export in the next call.",
endedCommand=detail.get("activeCommand"), terminated=bool(ended), detail=detail)
not_ready("The drawing is still loading or Fusion is busy with a command; call again shortly.", detail=detail)
drawing = adsk.drawing.DrawingDocument.cast(doc).drawing
manager = drawing.exportManager
results, started = {}, time.perf_counter()
for kind, raw in outputs.items():
if results:
# each export runs a command in the drawing engine; the next one goes only once that finished
engine = engine_state(_opened[file_id]["at"])
if engine is None or engine["unfinished"]:
not_ready("The drawing engine is still busy after the %s export; call again for the rest." % ", ".join(results),
outputs=results, engine=engine)
results[kind] = export_one(manager, kind, Path(raw), overwrite, args)
out = {"drawing": {"fileId": file_id, "name": doc.name}, "sheets": detail.get("sheets"), "outputs": results,
"seconds": round(time.perf_counter() - started, 1)}
if "dxf" in results:
out["scale"] = dxf_scale(Path(outputs["dxf"]))
if args.get("close", False):
out["closed"] = close_drawing(app, doc, file_id)
return out
def close_drawing(app, doc, file_id):
"""Close the drawing without saving, then the designs its open brought in (only those that were not
open before and are unmodified), and put the previously active document back in front."""
record = _opened.pop(file_id, {})
references = set()
try:
refs = doc.documentReferences
references = {refs.item(i).dataFile.id for i in range(refs.count) if refs.item(i).dataFile is not None}
except Exception:
pass
# the drawing reads as modified once its Edit Auto Dimensions command ends (live, 2026-11-05), so
# isModified cannot tell a person's edits from that; it is closed unsaved as Drew wrote it
doc.close(False)
closed = [file_id]
before = record.get("before") or set()
for i in reversed(range(app.documents.count)):
d = app.documents.item(i)
try:
fid = d.dataFile.id if d.dataFile is not None else None
except Exception:
continue
if fid in references and d.creationId not in before and not d.isModified:
d.close(False)
closed.append(fid)
previous = record.get("previous")
if previous is not None and previous.isValid:
previous.activate()
return closed
def dxf_dimensions(path):
"""(measured value, length on the sheet) of every linear DIMENSION in an ASCII DXF: group 42 is the
measured value, 13/23 and 14/24 the two definition points, 50 the angle of a rotated dimension."""
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
pairs = [(lines[i].strip(), lines[i + 1].strip()) for i in range(0, len(lines) - 1, 2)]
found, current = [], None
for code, value in pairs:
if code == "0":
if current is not None:
found.append(current)
current = {} if value == "DIMENSION" else None
elif current is not None and code in ("13", "23", "14", "24", "42", "50", "70"):
try:
current[code] = float(value)
except ValueError:
pass
if current is not None:
found.append(current)
out = []
for d in found:
if not {"13", "23", "14", "24", "42"} <= set(d) or int(d.get("70", 0)) & 15 not in (0, 1) or d["42"] <= 0:
continue # linear (rotated, horizontal, vertical) and aligned dimensions only
dx, dy = d["14"] - d["13"], d["24"] - d["23"]
if int(d.get("70", 0)) & 15 == 0:
a = math.radians(d.get("50", 0.0))
length = abs(dx * math.cos(a) + dy * math.sin(a))
else:
length = math.hypot(dx, dy)
out.append((d["42"], length))
return out
def dxf_scale(path):
"""The scale the layout chose, read from the exported DXF's dimensions (sheet length / measured
value, median over the dimensions); None when the drawing has no linear dimensions."""
try:
ratios = sorted(length / value for value, length in dxf_dimensions(path) if value > 0 and length > 0)
except OSError:
return None
if not ratios:
return None
r = ratios[len(ratios) // 2]
text = ("%g:1" % round(r, 3)) if r >= 1 else ("1:%g" % round(1 / r, 3))
return {"ratio": round(r, 4), "text": text, "dimensions": len(ratios), "source": "dxf dimensions"}
def export_one(manager, kind, destination, overwrite, args):
destination.parent.mkdir(parents=True, exist_ok=True)
started = time.perf_counter()
with tempfile.TemporaryDirectory(prefix=".adom-drawing-", dir=str(destination.parent)) as folder:
staged = Path(folder) / ("drawing" + EXTENSIONS[kind])
if kind == "pdf":
options = manager.createPDFExportOptions(str(staged))
options.openPDF = False
options.useLineWeights = bool(args.get("pdfLineWeights", True))
if args.get("pdfSheets"):
options.sheetRange = args["pdfSheets"]
elif kind == "dwg":
options = manager.createDWGExportOptions(str(staged))
options.format = _enum("DWGFormats", DWG_FORMATS[args.get("dwgFormat", "autocad")])
else:
options = manager.createDXFExportOptions(str(staged))
options.exportSplinesAsSplines = bool(args.get("dxfSplines", True))
require(options is not None, "export_preflight_failed", "Fusion did not create %s export options." % kind)
require(bool(manager.execute(options)), "export_failed", "Fusion's %s export returned false." % kind)
result = publish_staged(staged, destination, overwrite)
result["seconds"] = round(time.perf_counter() - started, 1)
return result
OPERATIONS = {"drawing_create": drawing_create, "drawing_export": drawing_export, "drawing_status": drawing_status}
WRITES = {"drawing_create", "drawing_export"}