Compare commits

..

2 Commits

Author SHA1 Message Date
4567274cf3 Merge pull request 'Added Additonal scripts' (#2) from master into main
Reviewed-on: #2
2026-01-31 21:30:39 +00:00
4bc442fd75 Added Additonal scripts 2026-02-01 01:26:09 +04:00
24 changed files with 431815 additions and 0 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,316 @@
"""
Scan each PDF in this folder and find duplicate connection tags (inside the SAME PDF)
based on user-provided wildcard patterns (using '*' as "anything").
Output: one Excel file per PDF: <PDF_NAME>_duplicates.xlsx
"""
from __future__ import annotations
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Set, Tuple
# -----------------------------
# USER PATTERNS (wildcards)
# -----------------------------
# '*' means "any characters". Everything else is treated literally.
WILDCARD_PATTERNS: List[str] = [
"*_*_TPE*",
"*_*_S*_PB",
"*_*_S*_PB_LT",
"*_*_JR*_PB",
"*_*_JR*_PB_LT",
"*_*_SS*_SPB",
"*_*_SS*_STPB",
"*_*_SS*_SPB_LT",
"*_*_EN*_PB",
"*_*_EN*_PB_LT",
"*_*_PE*",
"*_*_LPE*",
"*_*_FPE*",
"*_*_BCN*_R",
"*_*_BCN*_B",
"*_*_BCN*_A",
"*_*_BCN*_G",
"*_*_BCN*_H",
"*_*_EPC*_1",
"*_*_EPC*_2",
"*_*_VFD1_DISC",
"*_*_*_STO1",
"*_*_*_ESTOP1",
"*_*_LS*",
"*_*_ENC*",
"*_*_ENW*",
"*_*_ENS*",
"*_*_PX*",
"*_*_SOL*",
"*_*_DIV*",
"*_*_PS*",
"*_*_BDS*",
"*_*_TS*",
]
# -----------------------------
# CABLE PATTERNS (separate check)
# -----------------------------
# Rule: if a cable label appears more than 2 times in the SAME PDF => duplicated/overused.
CABLE_WILDCARD_PATTERNS: List[str] = [
"*_*_VFD*_I0",
"*_*_VFD*_I1",
"*_*_VFD*_I2",
"*_*_VFD*_I3",
"*_*_VFD*_IO0",
"*_*_VFD*_IO1",
"*_*_VFD*_SI0",
"*_*_VFD*_SI1",
"*_*_VFD*_SI2",
"*_*_VFD*_SI3",
"*_*_VFD*_SO0",
"*_FIO*_P0_C0",
"*_FIO*_P0_C1",
"*_FIO*_P1_C2",
"*_FIO*_P1_C3",
"*_FIO*_P2_C4",
"*_FIO*_P2_C5",
"*_FIO*_P3_C6",
"*_FIO*_P3_C7",
"*_FIO*_P4_C8",
"*_FIO*_P4_C9",
"*_FIO*_P5_C10",
"*_FIO*_P5_C11",
"*_FIO*_P6_C12",
"*_FIO*_P6_C13",
"*_FIO*_P7_C14",
"*_FIO*_P7_C15",
"*_FIOH*_C7_A",
"*_FIOH*_C7_B",
"*_FIOH*_C5_A",
"*_FIOH*_C5_B",
"*_FIOH*_C3_A",
"*_FIOH*_C3_B",
"*_FIOH*_C1_A",
"*_FIOH*_C1_B",
"*_FIOH*_C8_A",
"*_FIOH*_C8_B",
"*_FIOH*_C6_A",
"*_FIOH*_C6_B",
"*_FIOH*_C4_A",
"*_FIOH*_C4_B",
"*_FIOH*_C2_A",
"*_FIOH*_C2_B",
]
# Candidate token: something like "PS3_2_VFD1_DISC" (>= 2 underscore-separated parts)
TOKEN_RE = re.compile(r"\b[A-Z0-9]+(?:_[A-Z0-9]+)+\b", re.IGNORECASE)
def _compile_wildcard_patterns(patterns: Iterable[str]) -> List[re.Pattern]:
compiled: List[re.Pattern] = []
for p in patterns:
# Treat everything literally except '*' which becomes '.*'
parts = [re.escape(x) for x in p.split("*")]
regex = ".*".join(parts)
# Match full token
compiled.append(re.compile(rf"^{regex}$", re.IGNORECASE))
return compiled
def _tokenize(text: str) -> List[str]:
# Normalize common oddities
text = text.replace("\u00ad", "") # soft hyphen
# PDFs sometimes insert whitespace/newlines around underscores; normalize that.
# Example: "PS3_2_VFD1_\nDISC" -> "PS3_2_VFD1_DISC"
text = re.sub(r"\s*_\s*", "_", text)
return [m.group(0).upper() for m in TOKEN_RE.finditer(text)]
def _ensure_deps() -> Tuple[object, object]:
"""
Returns (fitz_module, pandas_module). Exits with helpful message if missing.
"""
try:
import fitz # PyMuPDF
except Exception:
print(
"Missing dependency: PyMuPDF\n"
"Install with:\n"
" python -m pip install --upgrade pip\n"
" python -m pip install pymupdf\n",
file=sys.stderr,
)
raise
try:
import pandas as pd
except Exception:
print(
"Missing dependency: pandas (and openpyxl for Excel)\n"
"Install with:\n"
" python -m pip install --upgrade pip\n"
" python -m pip install pandas openpyxl\n",
file=sys.stderr,
)
raise
return fitz, pd
@dataclass
class Occurrence:
token: str
pages: Set[int] # 1-based page numbers
count: int
def find_duplicates_in_pdf(pdf_path: Path, compiled_patterns: List[re.Pattern]) -> List[Occurrence]:
fitz, _ = _ensure_deps()
occurrences: Dict[str, Occurrence] = {}
with fitz.open(pdf_path) as doc:
for page_index in range(doc.page_count):
page = doc.load_page(page_index)
text = page.get_text("text") or ""
tokens = _tokenize(text)
if not tokens:
continue
for t in tokens:
if not any(r.match(t) for r in compiled_patterns):
continue
if t not in occurrences:
occurrences[t] = Occurrence(token=t, pages=set(), count=0)
occurrences[t].pages.add(page_index + 1)
occurrences[t].count += 1
# "Duplicate" = appears on more than one page (what you asked for)
dups = [o for o in occurrences.values() if len(o.pages) > 1]
dups.sort(key=lambda o: (-len(o.pages), -o.count, o.token))
return dups
def find_cable_overuse_in_pdf(
pdf_path: Path,
compiled_patterns: List[re.Pattern],
*,
allowed_occurrences: int = 2,
) -> List[Occurrence]:
"""
Separate check from page-duplicate logic:
- Cable labels are often printed twice (both ends) => OK up to allowed_occurrences (default 2).
- If total occurrences > allowed_occurrences, flag it.
"""
fitz, _ = _ensure_deps()
occurrences: Dict[str, Occurrence] = {}
with fitz.open(pdf_path) as doc:
for page_index in range(doc.page_count):
page = doc.load_page(page_index)
text = page.get_text("text") or ""
tokens = _tokenize(text)
if not tokens:
continue
for t in tokens:
if not any(r.match(t) for r in compiled_patterns):
continue
if t not in occurrences:
occurrences[t] = Occurrence(token=t, pages=set(), count=0)
occurrences[t].pages.add(page_index + 1)
occurrences[t].count += 1
overused = [o for o in occurrences.values() if o.count > allowed_occurrences]
overused.sort(key=lambda o: (-o.count, -len(o.pages), o.token))
return overused
def write_excel_for_pdf(
pdf_path: Path,
duplicates: List[Occurrence],
cable_overuse: List[Occurrence],
) -> Path:
_, pd = _ensure_deps()
out_path = pdf_path.with_name(pdf_path.stem + "_duplicates.xlsx")
rows = []
for d in duplicates:
rows.append(
{
"Token": d.token,
"Pages": ", ".join(map(str, sorted(d.pages))),
"UniquePagesCount": len(d.pages),
"TotalOccurrences": d.count,
}
)
df = pd.DataFrame(rows, columns=["Token", "Pages", "UniquePagesCount", "TotalOccurrences"])
cable_rows = []
for c in cable_overuse:
cable_rows.append(
{
"CableLabel": c.token,
"Pages": ", ".join(map(str, sorted(c.pages))),
"UniquePagesCount": len(c.pages),
"TotalOccurrences": c.count,
}
)
cable_df = pd.DataFrame(
cable_rows, columns=["CableLabel", "Pages", "UniquePagesCount", "TotalOccurrences"]
)
with pd.ExcelWriter(out_path, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Duplicates")
cable_df.to_excel(writer, index=False, sheet_name="CableOveruse")
summary = pd.DataFrame(
[
{
"PDF": pdf_path.name,
"DuplicateTokens": len(duplicates),
"CableOverusedLabels": len(cable_overuse),
},
]
)
summary.to_excel(writer, index=False, sheet_name="Summary")
return out_path
def main() -> int:
base_dir = Path(__file__).resolve().parent
pdfs = sorted(base_dir.glob("*.pdf"))
if not pdfs:
print(f"No PDFs found in: {base_dir}")
return 1
compiled_patterns = _compile_wildcard_patterns(WILDCARD_PATTERNS)
compiled_cable_patterns = _compile_wildcard_patterns(CABLE_WILDCARD_PATTERNS)
print(f"Found {len(pdfs)} PDF(s). Checking duplicates INSIDE each PDF only...")
for pdf in pdfs:
print(f"\n--- {pdf.name} ---")
try:
dups = find_duplicates_in_pdf(pdf, compiled_patterns)
cable_overuse = find_cable_overuse_in_pdf(pdf, compiled_cable_patterns, allowed_occurrences=2)
out_xlsx = write_excel_for_pdf(pdf, dups, cable_overuse)
print(f"Duplicate tokens (appear on >1 page): {len(dups)}")
print(f"Cable labels overused (total occurrences > 2): {len(cable_overuse)}")
print(f"Excel written: {out_xlsx.name}")
except Exception as e:
print(f"ERROR processing {pdf.name}: {e}", file=sys.stderr)
return 2
print("\nDone.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,336 @@
;; AS_AttrBatch.lsp
;; Batch-edit a single attribute tag for all matching AS* blocks inside a user selection.
;; Command: ASBATCHATTR
;;
;; Command-line version (no dialog/DCL).
(defun asba:_unique (lst / out)
(foreach x lst
(if (not (member x out))
(setq out (cons x out))
)
)
(reverse out)
)
(defun asba:_ss->list (ss / i out)
(setq i 0 out '())
(if ss
(while (< i (sslength ss))
(if (= (type (ssname ss i)) 'ENAME)
(setq out (cons (ssname ss i) out))
)
(setq i (1+ i))
)
)
(reverse out)
)
(defun asba:_effname (ename / ed nm r obj)
;; Try to get the "EffectiveName" (handles dynamic blocks).
;; Fully guarded: if anything fails, fall back to DXF group 2.
(setq nm nil)
(if (= (type ename) 'ENAME)
(progn
;; COM path (safe) -> try to set nm
(setq r (vl-catch-all-apply 'vl-load-com '()))
(setq r (vl-catch-all-apply 'vlax-ename->vla-object (list ename)))
(if (not (vl-catch-all-error-p r))
(progn
(setq obj r)
(setq r (vl-catch-all-apply 'vlax-property-available-p (list obj 'EffectiveName)))
(if (and (not (vl-catch-all-error-p r)) r)
(progn
(setq r (vl-catch-all-apply 'vla-get-EffectiveName (list obj)))
(if (and (not (vl-catch-all-error-p r)) (= (type r) 'STR))
(setq nm r)
)
)
)
)
)
;; DXF fallback if nm not found
(if (not nm)
(progn
(setq ed (entget ename))
(setq r (cdr (assoc 2 ed)))
(if (= (type r) 'STR) (setq nm r))
)
)
)
)
nm
)
(defun asba:_attrib-tags-of-insert (ins / e ed tags tagTmp)
;; Read attribute TAGs using DXF traversal (avoids VLA objects/variants).
(setq tags '())
(setq e (entnext ins))
(while (and e (setq ed (entget e)) (= (cdr (assoc 0 ed)) "ATTRIB"))
(setq tagTmp (cdr (assoc 2 ed)))
(if (= (type tagTmp) 'STR)
(setq tags (cons (strcase tagTmp) tags))
)
(setq e (entnext e))
)
tags
)
(defun asba:_find-first-insert-by-name (ss blockName / enames e nm)
(setq enames (asba:_ss->list ss))
(setq e nil)
(foreach x enames
(if (and (null e)
(= (type x) 'ENAME)
(= (cdr (assoc 0 (entget x))) "INSERT"))
(progn
(setq nm (asba:_effname x))
(if (and (= (type nm) 'STR) (= (strcase nm) (strcase blockName)))
(setq e x)
)
)
)
)
e
)
(defun asba:_set-attrib-value-on-insert (ins tag newValue / e ed curTag)
;; Update only the chosen TAG on a single INSERT.
;; Returns T if changed, NIL otherwise.
(setq e (entnext ins))
(while (and e (setq ed (entget e)) (= (cdr (assoc 0 ed)) "ATTRIB"))
(setq curTag (cdr (assoc 2 ed)))
(if (and (= (type curTag) 'STR) (= (strcase curTag) (strcase tag)))
(progn
(if (assoc 1 ed)
(setq ed (subst (cons 1 newValue) (assoc 1 ed) ed))
(setq ed (append ed (list (cons 1 newValue))))
)
(entmod ed)
(entupd ins)
(setq e nil) ;; stop after first match
(setq ed T) ;; reuse as "changed" flag
)
(setq e (entnext e))
)
)
(if (= ed T) T nil)
)
(defun asba:_blocks-in-selection (ss / enames names nm)
;; Kept for backward compatibility (returns unique names only).
(mapcar 'car (asba:_block-counts-in-selection ss))
)
(defun asba:_block-counts-in-selection (ss / enames counts nm cell)
;; Returns alist: (("AS_VFD" . 50) ("AS_PMM" . 1) ...)
(setq enames (asba:_ss->list ss))
(setq counts '())
(foreach e enames
(if (= (cdr (assoc 0 (entget e))) "INSERT")
(progn
(setq nm (asba:_effname e))
(if (and (= (type nm) 'STR) (wcmatch (strcase nm) "AS*"))
(progn
(setq nm (strcase nm))
(setq cell (assoc nm counts))
(if cell
(setq counts (subst (cons nm (1+ (cdr cell))) cell counts))
(setq counts (cons (cons nm 1) counts))
)
)
)
)
)
)
;; sort by name
(vl-sort counts '(lambda (a b) (< (car a) (car b))))
)
(defun asba:_counts->display (alist / out)
(setq out '())
(foreach p alist
(setq out (cons (strcat (car p) " (" (itoa (cdr p)) ")") out))
)
(reverse out)
)
(defun asba:_attrs-for-block (ss blockName / ins tags)
;; Attributes are normally consistent across block references.
;; To avoid scanning every insert (and any weird/proxy entities), read tags from the first valid insert.
(setq ins (asba:_find-first-insert-by-name ss blockName))
(if ins
(progn
(setq tags (asba:_attrib-tags-of-insert ins))
(setq tags (asba:_unique tags))
(vl-sort tags '<)
)
'()
)
)
(defun asba:_apply-attr (ss blockName tag newValue / enames changed nm attrefs att)
(setq enames (asba:_ss->list ss))
(setq changed 0)
(foreach e enames
(if (= (cdr (assoc 0 (entget e))) "INSERT")
(progn
(setq nm (asba:_effname e))
(if (and (= (type nm) 'STR) (= (strcase nm) (strcase blockName)))
(progn
(if (asba:_set-attrib-value-on-insert e tag newValue)
(setq changed (1+ changed))
)
)
)
)
)
)
changed
)
(defun asba:_print-numbered (title items / i)
(prompt (strcat "\n" title))
(setq i 1)
(foreach it items
(prompt (strcat "\n " (itoa i) ") " it))
(setq i (1+ i))
)
)
(defun asba:_choose-index (promptText maxN / n)
(setq n nil)
(while (not n)
(setq n (getint (strcat "\n" promptText " (1-" (itoa maxN) ", Enter to cancel): ")))
(cond
((null n) (setq n 0))
((or (< n 1) (> n maxN))
(prompt "\nInvalid number.")
(setq n nil)
)
)
)
n
)
(defun asba:_ss-has (ss ename)
;; returns T if ename is in selection set ss
(if (and ss ename)
(if (ssmemb ename ss) T nil)
nil
)
)
(defun asba:_pick-block-from-ss (ss / sel ename nm)
(setq nm nil)
(while (not nm)
(setq sel (entsel "\nClick a block (AS*) inside the selected zone (Enter to cancel): "))
(cond
((null sel) (setq nm "")) ;; cancelled
(t
(setq ename (car sel))
(if (and ename (= (cdr (assoc 0 (entget ename))) "INSERT") (asba:_ss-has ss ename))
(progn
(setq nm (asba:_effname ename))
(if (not (and (= (type nm) 'STR) (wcmatch (strcase nm) "AS*")))
(progn (prompt "\nThat block name does not start with AS*.") (setq nm nil))
)
)
(progn (prompt "\nPlease click a block INSERT that is inside your selected zone.") (setq nm nil))
)
)
)
)
(if (= nm "") nil nm)
)
(defun asba:_getstring-safe (msg / r)
;; Some AutoCAD builds do not support (getstring T ...). Use the simplest form.
(setq r (getstring msg))
r
)
(defun c:ASBATCHATTR (/ ss blockCounts blockNames blockDisplay mode block bIdx attrs tag aIdx newVal changed blkTotal)
(vl-load-com)
(prompt "\nSelect a zone/area (window/crossing allowed). Press Enter when done...")
(setq ss (ssget))
(if (not ss)
(progn (prompt "\nNothing selected.") (princ))
(progn
(setq blockCounts (asba:_block-counts-in-selection ss))
(setq blockNames (mapcar 'car blockCounts))
(setq blockDisplay (asba:_counts->display blockCounts))
(if (not blockNames)
(progn (prompt "\nNo blocks found with name starting with AS* in the selection.") (princ))
(progn
;; Choose block: Pick (mouse) or List (numbered)
(initget "Pick List")
(setq mode (getkword "\nChoose block by [Pick/List] <Pick>: "))
(if (null mode) (setq mode "Pick"))
(cond
((= mode "Pick")
(setq block (asba:_pick-block-from-ss ss))
)
(t
(asba:_print-numbered "Unique blocks (AS*) with counts:" blockDisplay)
(setq bIdx (asba:_choose-index "Choose block number" (length blockNames)))
(setq block (if (= bIdx 0) nil (nth (1- bIdx) blockNames)))
)
)
(if (not block)
(progn (prompt "\nCancelled.") (princ))
(progn
(setq blkTotal (cdr (assoc (strcase block) blockCounts)))
(setq attrs (asba:_attrs-for-block ss block))
(if (not attrs)
(progn (prompt (strcat "\nBlock " block " has no attributes in the selection.")) (princ))
(progn
;; Choose attribute: numbered list only (reliable + matches requested workflow)
(asba:_print-numbered (strcat "Unique attribute tags for " block ":") attrs)
(setq aIdx (asba:_choose-index "Choose attribute number" (length attrs)))
(setq tag (if (= aIdx 0) nil (nth (1- aIdx) attrs)))
(if (not tag)
(progn (prompt "\nCancelled.") (princ))
(progn
(setq newVal (asba:_getstring-safe (strcat "\nNew value for tag " tag " (Enter to cancel): ")))
;; If user presses Enter immediately, AutoCAD may return "".
(if (or (null newVal) (= newVal ""))
(progn (prompt "\nCancelled.") (princ))
(progn
(setq changed (asba:_apply-attr ss block tag newVal))
(command "_.REGEN")
(prompt
(strcat
"\nDone. Changed "
(itoa changed)
" block(s) out of "
(itoa (if blkTotal blkTotal 0))
" selected "
block
" block(s)."
)
)
(princ)
)
)
)
)
)
)
)
)
)
)
)
)
)
(princ "\nLoaded AS_AttrBatch. Run command: ASBATCHATTR")
(princ)

View File

@ -0,0 +1,25 @@
# AS Batch Attribute Editor (AutoLISP)
This tool lets you select an area in your drawing, list **unique block names starting with `AS*`**, list **unique attribute tags** for a chosen block, and then **change one attribute value for all matching blocks** inside the selected area.
## Files
- `AS_AttrBatch.lsp` — AutoLISP logic (**this is the only required file**)
No `.dcl` file is used — this is a command-line tool.
## Install / Run
1. Copy `AS_AttrBatch.lsp` into any folder.
2. In AutoCAD run `APPLOAD` and load `AS_AttrBatch.lsp`.
3. Run command: `ASBATCHATTR`
## How to use
1. Select a zone/part of the drawing (window/crossing selection is fine), then press **Enter**.
2. Choose the block:
- Option **Pick**: click a block in the selected zone
- Option **List**: choose from a numbered list
- The list shows **unique block names** with a **count** like `AS_VFD (50)`
3. Choose the attribute tag from the **numbered list**.
4. Type the new value and press **Enter**.
Only the chosen attribute tag is updated. All other attributes remain unchanged.