OpenTakeoff

io.github.Kentucky-ai由 GitHub 帳號 Kentucky-ai 發布v0.9.105更新於 Nov 7, 2026

Construction takeoff for AI agents: load plans, set scale, measure, count, export with provenance.

概覽

AI 產生的概覽

本機 MCP 伺服器,讓助理在施工圖 PDF 上量取面積、長度與數量,並匯出標註圖說集、報表與估價。

可以做什麼
  • 開啟圖說 PDF,檢視圖面資訊、讀取文字與向量線條,並渲染局部影像供核對
  • 依標註、校正或辨識到的比例設定圖面比例,再量取多邊形、線段、牆面面積與計數
  • 批次尋找重複符號與明細表列,推導踢腳線與交接線,並套用修正規則
  • 列出、編輯、刪除已提交圖形,處理重疊條件,並復原操作
  • 為條件計價產生估價,匯出 Excel、投標書 PDF、DXF、報表 JSON 與標註圖說集
  • 匯入既有量取檔案,並讀取內建 wiki 資源了解作業流程
適用情境
適合需要助理對施工圖做工程量量取(地坪、飾面、屋面等工種)並交付標註圖說集與數字的場合。也適合希望由代理起草測量、再由人在瀏覽器畫布中審核的估算人員。
執行需求
以本機 stdio 程序執行,可透過 npx(需 Node 20 或更新版本)或從複製的儲存庫建置的 Docker 啟動;未宣告需要帳號、API 金鑰或環境變數。圖說 PDF 需位於本機,匯出檔案會寫入磁碟。
安裝前請注意
助理可以寫入、編輯與刪除圖形,並將檔案匯出到磁碟;不過代理的成果在畫布中由人接受前僅是虛線提案。價格與估價設定由使用者提供。可選的資料貢獻據稱需主動開啟且僅含衍生資料,啟用前請確認送出內容。

安裝

在 SourceWeft 中

  1. 開啟 儀表板中的 OpenTakeoff,將其新增到工作區。
  2. 為需要使用其工具的對話啟用該服務。

Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。

其他 MCP 客戶端

參照 儲存庫 中的啟動說明。

README

[OpenTakeoff: the scan star beside the wordmark, and an agent tracing two rooms of a floor plan on their wall faces]

Measure construction plans in your browser, or hand the same engine to your AI agent.
Free, open source and local. Nothing uploads, and nobody has to sign up.

[GitHub stars] [npm] [npm downloads] [MCP registry] [CI] [License: AGPL-3.0] [Sponsor]

▶ Try it now  ·  🔌 Plug in your agent  ·  🛠 Send your first PR

日本語 · 한국어 · 简体中文


A takeoff is the measuring that every construction bid starts with: how much floor, how much wall, how many fixtures, at what scale and on which sheet. OpenTakeoff is an open-source engine for that job. It has two front ends:

  • 🧑‍🔧 A browser canvas for people. Drag in a plan set, set the scale, trace rooms, and export a report and a marked-up planset.
  • 🤖 An MCP server for agents. It offers 62 tools over stdio. An agent drives the same engine with the same math and the same scale gate as the canvas, and its work stays a dashed proposal until a person accepts it.

Both front ends share one takeoff format, so you can open an agent's takeoff in the canvas, check it and fix it. Every shape records the scale it was measured at, the method that drew it, and whether a person or an agent drew it.

[!NOTE] One-Click Area is temporarily gated while the flood engine is re-validated on a wider plan corpus. Trace rooms with Area (A) in the canvas and measure_polygon over MCP. Every other tool works as normal. See docs/design/ONE_CLICK_GATE.md.

Pick your door

You are…Start hereTime
🧑‍🔧An estimator with a bid dueOpen the canvas, click Guided first takeoff, then read the user manual5 min
🤖An AI agent, or the person wiring one upRun npx -y opentakeoff-mcp (see Plug it into your agent), then read the agent manual1 min
🛠A developerRead AGENTS.md for the repo map, FEATURES.md for where each feature lives in the code, then Contribute10 min
🏗A crew that wants its own copyFork it. Any static host will run it, and nothing phones home5 min

Plug it into your agent

The server needs Node 20 or newer. You don't need to clone or build anything. Pick your client:

ClientInstall
Claude Codeclaude mcp add opentakeoff -- npx -y opentakeoff-mcp
Codex CLIcodex mcp add opentakeoff -- npx -y opentakeoff-mcp
Cursor[Install in Cursor]
VS Code[Install in VS Code]
Claude DesktopDouble-click opentakeoff-mcp.mcpb
Docker (from a clone)docker build -f mcp/Dockerfile -t opentakeoff-mcp . && docker run --rm -i opentakeoff-mcp
Any other stdio clientUse the JSON below
json
{  "mcpServers": {    "opentakeoff": { "command": "npx", "args": ["-y", "opentakeoff-mcp"] }  }}

If you've cloned the repo, the root .mcp.json, .cursor/mcp.json and .vscode/mcp.json register the server when you open the folder, so there's nothing else to set up.

Your first prompt. Paste this once the server is connected:

Load demo/sample-finish-plan.pdf from the OpenTakeoff repo, read the scale note on the floor plan, set the scale, trace room 134 on its wall faces under its finish tag from the schedule, check your ring with view_sheet, and export the marked-up PDF.

The step-by-step walkthrough for each client is in Connect OpenTakeoff to your AI. The server can also describe itself: read the takeoff://wiki resource before you load a plan.

All 62 tools, grouped by stage
StageTools
Open and orientload_plan · sheet_info · sheet_context · get_sheet_vectors · read_sheet_text · find_text · view_sheet
Scaleset_scale
Measuremeasure_polygon · cut_out · measure_line · measure_surface · place_count · count_marks
Repeat and derivesymbol_sweep · sweep_schedule_row · derive_base · derive_transitions · apply_rules
Read the drawing setsheet_graph · resolve_tag · find_schedule
Edit and auditlist_shapes · edit_shape · edit_condition · edit_materials · duplicate_condition · split_condition · delete_shape · undo_last · scope_duplicates · scope_merge
Propose for reviewpropose_takeoff · revise_proposal · withdraw_proposal · propose_condition_edit · withdraw_condition_edit
Mark and signannotate · list_annotations · edit_annotation · link_annotation · mark_verdict · delete_verdict
Ask the architectcreate_rfi · list_rfis · resolve_rfi · delete_rfi
Priceset_pricing · set_estimate_settings · estimate_summary · export_estimate · export_proposal
Hand offtakeoff_summary · export_takeoff · export_report · export_marked_pdf · export_dxf · import_takeoff

The generated reference, with the required arguments for each tool, is docs/MCP_TOOL_INDEX.md. The full tool reference is mcp/README.md.

How a takeoff runs

[Open the plan, set the scale, trace rooms on the wall faces, derive base and transitions, deliver a marked-up planset PDF] [A real run at 3× speed: an AI agent in a terminal takes off three patient rooms on a VA medical center finish plan over MCP. Each export lands in the web app as dashed pencil proposals, and the operator accepts them, 743.64 SF in all.]

A real run at 3× speed. The agent takes off patient rooms 161–163 on a federal finish plan and exports after each commit. Each shape lands in the canvas as a dashed proposal and becomes ink only when the operator clicks Accept. Watch it live and uncut: autonomous agent run (2:47) · finish plan to report in about a minute (1:14) · canvas walkthrough (1:10)

The rules that make it safe to hand to a model

  1. Scale is a gate, not a default. The engine reads the scale note but never applies it on its own. Measuring an unscaled sheet is refused.
  2. The ring sits on the wall faces. Rooms are traced on the innermost wall-face strokes. Doors are crossed on the wall centerline, and the agent checks each ring on a rendered overlay before it moves on.
  3. Agent work stays pencil until a person inks it. An agent can sign its own work with an AGENT diamond. Only a person at the canvas can apply the green APPROVED seal.
  4. Withheld is an answer. When a sweep or a derivation can't be sure, it says why and hands back coordinates. It doesn't return a plausible number.
  5. The deliverable is a marked-up planset, not JSON. export_marked_pdf burns the work into the drawings behind a legend cover with totals.

The full contract, with the reasoning behind each rule, is in the tour.

What's new

October 2026

  • ✨ The scan star. OpenTakeoff has a new mark for its AI. The star ripples while the agent works and shows up in the Work panel, the Agent tab and the watermark on exported PDFs (#571, #576).
  • 💲 Estimate and proposal. You can now price a takeoff by hand (unit, material and labor, tax, overhead and profit), export it to Excel and download a letter-size bid proposal PDF. Agents get the same features through five new verbs: set_pricing, set_estimate_settings, estimate_summary, export_estimate and export_proposal. Prices always come from the estimator, and nothing fills one in for you (#558–#562, MCP 0.9.99).
  • 🪟 Reference split-screen. You can now keep a read-only second sheet open beside the one you're tracing (#570, by @knmurphy).
  • 🗂 Sheets as a tree. Multi-page PDFs show up as folders, and each row is labeled with the sheet number from its title block (#523).
  • ⚡ A faster room engine. Exact grid indexes replace quadratic scans. On a 24-sheet replay, all 548 room probes came back identical, and buildNet time dropped by 2.4× to 2.5× on the slowest sheets (#540, #547, #550).
  • 🔐 Security refresh. The MCP SDK and Zod 4 were upgraded without changing the tool set or the takeoff payloads (#566, MCP 0.9.100).

September 2026: on-device OCR for scanned sheets, plan-set text search, Copy text, schedule import from raster pages, the annotation toolbar, Pin references and a guided first takeoff. Everything is in the CHANGELOG, and the older highlights are in the tour.

What's in the box

AreaWhat you get
IngestPDFs, images and whole .zip plan sets, unpacked in the browser, with up to 4 sheets side by side
ScaleDetects the drawn scale note, calibrates from a known dimension, and grades a dimension check, all per sheet
MeasureArea, Rectangle, Linear, Curved Line, Surface Area (walls), Count, Cut Out, ⟂ Transitions and Zone check, in imperial or metric
ConditionsA color and CAD hatch for each finish, waste %, ×N multiplier, height, thickness, and import from the finish schedule
MaterialsCoverage rate × basis gives rounded order quantities. Includes a grout calculator and the figured seam LF
Roll goodsLanes, seams, multi-roll splits and to-scale cuts with drag-to-reorder nesting
Multi-sheetA sheet tree with title-block numbers, a gallery, levels, stitching across a match line, PDF layer roles and a reference split
EstimateUnit prices, tax, overhead and profit, an Excel export and a bid proposal PDF
Report and exportSF, SY, LF and EA per condition, with and without waste. Exports CSV, JSON, Excel, DXF and a Marked Set PDF
ReviewRevisions with quantity deltas, markups, approval seals, an RFI register, and agent proposals you accept or reject
MCP server62 tools, sheet resources, a packaged wiki and multi-document sessions (mcp/)
PrivacyRuns only in the client. Work is stored in IndexedDB and localStorage and is never uploaded

You'll find every feature in depth in the tour, every feature mapped to its code in FEATURES.md, and every click in the user manual.

Contribute

OpenTakeoff merges evidence, not promises. It is used on real commercial bids, so a pull request has to show that it works. It doesn't need to be big. People and agents are held to the same bar, and a PR that clears it merges fast.

1 · Find something. Pick an open issue, an RFC, a flagship challenge, a good first issue, or a bug you hit on your own plans. If it's still an idea, start in Discussions.

2 · Build it.

bash
git clone https://github.com/<you>/opentakeoff && cd opentakeoff/webnpm ci && npm run dev    # http://localhost:5173npm run check            # the exact CI gate

3 · Prove it.

Your changeThe evidence it carries
Visible (a panel, a tool, an export)A screenshot of each surface you changed, and a short recording for a flow
Engine (geometry, totals, MCP, readers)An exact-output gate, a before/after table with its conditions, and the command that reproduces it
Any PRNotes for the reviewer: coordinate spaces, limits you chose on purpose, and anything you didn't verify

Two merged PRs show the bar well. #346 covers a UI feature with a screenshot of each surface, a recording and the reviewer's notes. #547 covers an engine change with 548/548 identical room probes and a before/after retime table. Both come from @knmurphy. The PR template walks you through the same structure, and CONTRIBUTING.md has the ground rules, including the one-comment contributor agreement.

🤖 Agents are welcome contributors. Point yours at AGENTS.md. PRs from forks run the full CI with a read-only token, and an agent's PR merges under the same evidence bar as anyone else's. Engine work that lands ships to every agent that pulls opentakeoff-mcp.

[OpenTakeoff contributors]

Fork it

OpenTakeoff is AGPL-3.0. You can fork it, change it, and ship it to your own crew or as the base of your own product. If you run a modified copy for other people, including as a hosted service, publish your changes under the same license.

  1. Fork on GitHub, then clone your fork.
  2. Run it: cd web && npm ci && npm run dev. npm run check is the exact CI gate (typecheck, lint, test and build).
  3. Put it on your own URL. The repo includes a netlify.toml, so importing your fork into Netlify deploys it with no settings. Any static host works, and docs/SELF_HOSTING.md covers the one nginx gotcha.

[Deploy to Netlify]

The geometry libraries have no React or DOM code, so you can lift them straight out: oneclick.ts (flood fill, face extraction, corner snap), sheets.ts (scale, polygon area), totals.js (waste, SY, order quantities) and rollgoods.js (lanes and seams).

The data layer — why this engine exists

Every finished takeoff is a set of expert decisions: this region gets this finish, at this waste. If you record those decisions, you get a labeled dataset (plan geometry paired with the finish an expert assigned to it) that doesn't exist today. The thesis, stated so it can be attacked, is that markup is label.

OpenTakeoff is the instrument that produces that corpus. Collection is opt-in and auditable. The Contribute button builds a payload that contains only derived data. It never includes the PDF, file names, client names or scale values, and a whitelist in contribute.js enforces that. The contract is docs/CONTRIBUTION_SPEC.md, and the bundled capture server banks the rows on your own machine. If you don't run it, nothing is captured. The engine is free because a corpus is only as good as the number of real takeoffs that flow through it. The research program behind this, including the model work and the evaluation method, is in the tour. OpenTakeoff Academy scores agents on operating this engine against geometry they don't control.

Star history

[OpenTakeoff star history]

If OpenTakeoff saved you an afternoon, a ⭐ helps the next estimator find it.

Who's building this

I run estimating for a commercial flooring company, and the takeoff tool I bid with is one I wrote. OpenTakeoff is the open half of that work: a measuring engine for anyone, person or agent, who needs to read a building. The models trained on our own estimating archive stay ours, and the boundary is drawn in public so it can be held to account.

— Michael · Kentucky AI

[NVIDIA Inception Program member badge]

Kentucky AI is a member of the NVIDIA Inception program.

Contact: for research collaborations, data questions, press, or anything that isn't a bug, write to [email protected]. Bugs and feature requests go in issues. Security reports follow SECURITY.md, through private vulnerability reporting.

Contributor credit

Thanks to @knmurphy (Kevin Murphy) for the reference split-screen, the room-engine speedups, the sheet tree, image captures, drawing styles and sharper overlays. The Pin reference tool builds on his image-capture work. The roll-goods layout engine was contributed by Michael Hartman.

License

GNU Affero General Public License v3.0 (AGPL-3.0-only). You can use it, fork it, ship it and build on it, as long as you share your changes when you run a modified copy for others. Through commit 84c7f48e (tag apache-2.0-final) OpenTakeoff was Apache-2.0, and that code stays Apache-2.0 for everyone who already has it. See NOTICE for attribution, the relicensing note and the section 7(b) attribution term. Contributions are accepted under the CLA. For commercial terms outside the AGPL, contact Kentucky AI.

Privacy Policy · Terms of Service · Shared wiki for people and agents

© 2026 NVIDIA, the NVIDIA logo, and NVIDIA Inception are trademarks and/or registered trademarks of NVIDIA Corporation in the U.S. and other countries.

來源:README.md,提交 b15bf95

工具

62

assistant_open

Open the OpenTakeoff Assistant window: the person can then talk to YOU from inside the OpenTakeoff canvas (opentakeoff.kentucky-ai.com) instead of only here. Starts a private link on 127.0.0.1 and returns a one-time pairing `code` and a `link`. Give the person the link (or the 6-letter code to type into the canvas's Assistant window), then call assistant_listen and keep calling it: each message they type arrives as its result; do the work with the other tools, answer with assistant_say, then listen again. Call it when the person asks for the OpenTakeoff assistant/window, or says "listen" after a pause.

無需輸入參數。

assistant_listen

Wait for the person's next message in the OpenTakeoff Assistant window (after assistant_open). Returns status "message" with their text — do what they ask (with the other tools), reply with assistant_say, then call assistant_listen again. Status "timeout" = nothing typed yet: call assistant_listen again right away, saying nothing to the person. Status "sleep" = 10 quiet minutes: stop listening and tell the person in one line to say "listen" when they want you back (then call assistant_open, which reconnects without a new code).

輸入參數wait_s

assistant_say

Show your reply in the OpenTakeoff Assistant window (Markdown). Use it for every answer to a message that came through assistant_listen — the person is reading the window, not this chat. Then call assistant_listen again.

輸入參數text必填

load_plan

Open a plan PDF from disk. Default: replace the whole session (previous documents, scales, conditions, and shapes are cleared). merge: true ADDS the document to the working set instead (#152) — a bid set is plans + schedule + addenda, not one PDF — keeping every scale, condition, and shape; sheet keys carry file names so documents never collide, the sheet graph spans the whole set (resolve_tag can chain a plan tag on one file to a schedule row in another), and the marked set covers every worked sheet. Re-loading an already-merged file is refused — reload = replace, deliberately. Returns file, files, page_count, and one entry per sheet. The loaded sheets also become browsable resources (takeoff://sheets). Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數path必填merge

sheet_info

Sheet detail: dims (px and pt), vector segment count, whether the sheet has vector linework (measure_polygon runs on it; get_sheet_vectors returns the strokes), scale status, the detected scale suggestion, and this sheet's committed shape count. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheet必填

set_scale

Set a sheet's scale — exactly ONE of: label (a standard scale, e.g. '1/4" = 1'-0"'), upp (real feet per image px), calibrate (two points along a known dimension plus its real feet), or use_detected (adopt the drawn scale note read off the sheet). The detected scale is never applied automatically — setting it is always this explicit call. Changing an existing scale recomputes measurements and cutout restore quantities from geometry and records one undo_last step. Counts are unchanged. Human-reviewed dimensional work must be recalibrated in the canvas; scales must be finite and positive. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數upplabelsheet必填calibrateuse_detected

propose_takeoff

Open a PROPOSAL — a named batch of the shapes you are about to commit, with one identity (#365). Every shape you commit from here on (measure_polygon, measure_line, measure_surface, place_count, the sweeps, the derives, cut_out) attaches to it until you open another proposal or withdraw this one; the estimator then sees ONE Accept pill for the whole batch instead of one per shape — a forty-room pass becomes one decision, not forty. Use it BEFORE the work, the way an estimator titles a takeoff before tracing: "Level 2 rooms per finish schedule A-601", "Base derived from CPT-1 rooms". label is what the estimator reads on the pill; rationale is what decided the batch (the schedule row, the sheet, the rule) — both required, neither is a comment. Nothing here commits geometry or changes a total: an empty proposal is just a heading. The batch is what revise_proposal replaces and withdraw_proposal removes; shapes the estimator has already accepted leave the batch and no agent verb reaches them. takeoff_summary carries the ledger (pending / accepted / withdrawn per batch).

輸入參數label必填rationale必填

measure_polygon

Measure a closed polygon you supply (min 3 vertices, image px): area_sf and perimeter_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it; role "deduct" subtracts. A room ring belongs on the innermost wall-face strokes from get_sheet_vectors, crossing each door opening on the wall centerline and wrapping columns and stubs; never on a hatch edge, casework or a door leaf. Check it with view_sheet overlay:true on a tight crop and fix it with edit_shape. A CURVED wall is a circle: do not chord it and do not hand-tessellate it — give the bow one point on the wall and list its index in arc_through. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數rolesheet必填verts必填conditionarc_through

cut_out

Cut a REAL hole in a committed floor_area shape (#206) — the way the canvas cuts one (#137): the same lib/cutout.js boolean subtract, so the two surfaces can never disagree about what a hole holds. The parent keeps its outer ring plus the reconciled hole(s) (verts_norm_holes), its computed nets for real — N cuts compose, overlap between cuts never double-deducts (set subtraction), a hole ADDS perimeter — and the deduct commits carrying cuts_shape_id so the report and legend read the reconciled number, never a second arithmetic pass. This is the verb for a column, a floor drain, an island of casework INSIDE a room; an independent measure_polygon role:"deduct" stays the tool for a deduction that isn't a hole in one parent. Refusal over guessing: the ring must sit FULLY inside the parent's outer ring (an edge-crossing cut is a boundary correction — edit_shape the parent instead), and a cut that would erase the parent or split it in two refuses whole (trace the pieces as rooms). One journal entry — undo_last restores parent and hole together; delete_shape on the deduct later reverts the cut too (a multi-cut parent rebuilds from the chain's pristine snapshot minus the survivors). AN OPEN RUN IS CLIPPED, NOT SUBTRACTED: wall tile (surface_area) and base/transitions (linear) are polylines traced in plan, so the ring removes the stretch it covers, the run keeps its id and takes what survives, and a cut through the MIDDLE leaves the far side as its own shape (same condition, same height) — quantities ride the surviving length, which is exact, since wall SF is LF × height and a border's SF is LF × thickness. No deduct is minted for a run: there is no area for one to sit on, and a deduct's SF counts against the FLOOR total a run never fills. A ring that misses the run, one that swallows it whole (delete_shape it), and a curved run (its verts are control points) all refuse. A derived base with numeric openings also refuses: those deductions have no stored location; use measure_

輸入參數verts必填parent_shape_id必填

measure_line

Measure an open polyline (min 2 points, image px): length_lf at the sheet's scale. Requires the scale to be set. Pass condition to commit it as a linear shape (base, transitions, feature strips, conduit and home runs). A curved run (base along a radius wall, a curved feature strip) takes arc_through: one point on the bow, marked. DROP AND RISE (#441): a plan trace is the flat X–Y path; the material also travels VERTICALLY — a home run drops from the ceiling to a panel, rises to a box. length_lf is the TOTAL: plan + rise + drop. The condition's rise_ft / drop_ft (edit_condition) are the defaults for every run under it; pass rise_ft / drop_ft here to give THIS run its own legs (0 included — "no drop on this one" is a statement), and the reply splits plan_lf / vertical_lf beside the total when a leg exists. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數pts必填sheet必填drop_ftrise_ftconditionarc_through

measure_surface

Surface Area — wall SF (#146): trace an OPEN run along the wall in plan view (min 2 points, image px) and the quantity is traced LF × height. This is how wall tile, wainscot, and wall systems are taken off — the quantity family measure_polygon cannot produce. Height lives on the CONDITION (the canvas's H knob): pass height_ft to set it on this call (journals as its own undo step, like typing H before tracing), or set it once with edit_condition; with neither, this refuses and mints nothing. The shape snapshots the height it was quantified at. Requires the sheet's scale. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數pts必填sheet必填condition必填height_ftarc_through

place_count

Count markers — EA (#146): one point, one each. Thresholds, stair nosings, floor boxes, entrance mats — the scale-free quantity family. Commits one count shape per point (computed {count: 1}, exactly the canvas's Count tool), NO scale required, and the whole call is ONE undo step. takeoff_summary reports them as ea; the marked set draws each marker. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheet必填points必填condition必填

symbol_sweep

Find EVERY instance of a repeated plan symbol from ONE example — drains, thresholds, fixtures, transition markers: marquee a tight seed_rect around a single instance and the vector linework is searched for every other placement of that same segment cluster. Deterministic geometry, not vision: each placement scores as the length-weighted fraction of the seed's segments reproduced within tolerance_px, under translation plus 0/90/180/270 rotation and mirroring (symbols rotate on plans — both ON by default; turn them off to pin orientation). Score ≥ 0.92 is a match; the 0.75–0.92 band comes back in `withheld` with a reason — a near-match is a question you answer by LOOKING (view_sheet at its `at`), never a silent commit and never a silent drop. RICHER VARIANTS are named, never silent: a placement that reproduces the whole seed but carries >30% extra linework fully inside its footprint (a register against a grille seed — the same outline plus louvers) comes back with its measured `extra` fraction on the row — LOOK at those first, they are the classic mislabel; background lines CROSSING the symbol and coincident duplicate ink never trip this. By default such placements still COUNT, because the contained-seed workflow below depends on supersets matching (seed a bare sub-shape, count the richer symbols that contain it, exclude what you don't mean). Pass `variant_guard: true` when your seed is the WHOLE symbol — grilles, drains, fixtures marqueed complete — and extra-ink placements demote to `withheld` as questions instead of counting; the guard stands down automatically when `exclude` counter-examples are in play, since supplying negatives is manual variant discrimination. The seed's own location is reported in `seed` and never double-committed. Every proposed placement is scored up to a hard work ceiling sized for pathological sheets, and the reply says which it was: complete true means the count is a total; complete false (with candidates.dropped > 0) means the count is a

輸入參數scopesheet必填commitmirrorexcludeconditionrotationsseed_rect必填commit_seedtolerance_pxvariant_guardluminance_tolerance

sweep_schedule_row

Take off a schedule row's mark from the row itself — the estimator's own gesture: a transition type sometimes exists only as a schedule row plus tag markers scattered across the plan sheets, and this tool mints the condition FROM the row and finds every occurrence. Pass the row's key (e.g. 'T1') and the tool (1) reads the row from the set's schedule tables (the sheet_graph/find_schedule machinery — the row is the condition's cited source), (2) anchors a geometric fingerprint on the marker the tag is DRAWN as on a plan sheet (a deterministic pad ladder around the tag text; where the tag occurs more than once the fingerprint must recur at a second occurrence before it is trusted — `anchor.corroborated`), and (3) sweeps every PLAN-role sheet for it. The count is geometry AND text agreeing: drafting reuses one bubble shape across many marks, so a match counts ONLY when the row's own tag sits within the marker footprint (its bbox rides the match as `tag_at` evidence); a match labeled with a SIBLING row's tag is excluded and says whose it is, an unlabeled match is withheld as a question, and a tag drawn with no matching marker is disclosed as text_only. REFUSAL over guessing, with the reason and the fix: no such row; the same key in two tables (ambiguous); a tag drawn on no plan sheet; no repeatable marker linework around the tag — a fingerprint is never guessed from text alone (the fallback is always: marquee one instance with symbol_sweep). commit: true commits the counted matches as EA markers under the row's own key — one undo step for the whole set-wide sweep, every marker carrying origin.assignment {source: "schedule"} plus the anchor and row citation on origin.symbol.seed. The COUNT is scale-free (EA), but matching is not: where the anchor sheet and a target sheet both carry a scale, the marker is resized by their exact ratio before matching (`scaled` per sheet), and where one does not, the sweep runs at 1:1 and discloses it (`scale_assumed`) rather than reporting

輸入參數tag必填commitmirrorrotationstolerance_px

count_marks

The COUNT TAKEOFF in one deterministic call — no seeds, no model, seconds: census every VALUE-ANNOTATED mark tag on the plan-role sheets, counted per schedule mark, committed as EA markers when asked. The identity rule is the annotated-device drafting pattern: a device is drawn as its mark tag with a value under it ("S1" over "200" — CFM on air devices, GPM on fixtures, a rating on equipment), so a tag WITH a paired value counts, a tag inside a schedule table's own region is a row label (excluded, tallied), and every other occurrence is WITHHELD with a reason and coordinates — a tag amid linework but unvalued may be a real device (view_sheet it), a bare tag is probably a note mention. Marks default to the set's schedule row keys (a compound row "R1 / E1" answers for R1 AND E1; each mark cites its row), or state them: {marks: ["S1","R1"]}. The complement to sweep_schedule_row: THAT tool is for marks drawn ON their marker with no value (finish tags in bubbles) and matches geometry; this one is for annotated devices and needs no fingerprint at all. Refusal-honest: scans refuse (no text layer), a set with no mark-shaped rows refuses unless marks are stated, non-plan sheets are skipped with the role that excused them. commit: true commits every counted occurrence under its mark's own tag — ONE undo step for the whole census, schedule citation on origin. Counts are scale-free (EA) — no set_scale needed. Then AUDIT: view_sheet {overlay: true} where the markers landed, and read every withheld entry — a withheld item you ignore is a hole in the bid. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt. EQUIPMENT marks (a row in an equipment/device schedule — fans, pumps, heaters, fixtures, panels) follow the leader-tag convention instead: a scheduled mark drawn amid linework with no value under it is counted BY LABEL (occurrence by: "label", counted_by_label on the m

輸入參數markscommit

derive_base

Mint the wall base from committed rooms (#148) — the estimator's most mechanical derivation: base LF = room perimeter − stated door openings. For every floor_area shape of source_condition, commits ONE linear shape under condition (e.g. 'RB-1') tracing that room's boundary, quantified NET of the openings you state per room. The openings are YOUR claim to make — look at the doors with view_sheet, state {shape_id, lf} per room (repeat a shape_id to stack openings); the tool never guesses, and your claim is recorded on origin.derived (from_shape_id, gross_lf, openings_lf). The output geometry remains the whole perimeter: deducted openings are numerical, not visible gaps. For a drawing of the actual installed base, use measure_line on the physical runs after checking door jambs, alcoves and open finish splits. All-or-nothing: an unknown shape_id, a negative lf, or openings meeting a room's whole perimeter refuses the call before anything commits. The whole derivation is ONE undo step. Deriving onto the source condition is refused — base lands on its own tag.

輸入參數openingscondition必填source_condition必填

derive_transitions

Mint the transition where two finishes MEET (#202) — the derivation that follows derive_base, and the line an estimator draws by hand on every job. Pass the two finish tags and the tag the transition commits under (e.g. condition_a 'CPT-1', condition_b 'PT-1', condition 'T-1'), and every committed room of each is compared against every committed room of the other. WHAT THE GEOMETRY ACTUALLY IS, because it decides what you get back: flood-traced rooms DO NOT SHARE EDGES. A trace fills to the wall linework, so two rooms across a partition are separated by four to eight inches of nothing — testing for a shared edge finds zero transitions on a real planset. What is there is proximity, in two flavours that mean completely different things: • BUTT JOINT — the two rings run together inside ONE open space (a lobby that changes from carpet to tile with no wall between). The transition IS that run, and it commits as a linear shape under your tag, origin.derived naming both parent shapes and the measured gap. • WALL-SEPARATED — the rings run parallel across a partition. The rooms are adjacent, but the transition is NOT the shared wall: it is a threshold, in the doorway, and NOTHING in the trace record says where the doorway is (the flood engine seals openings and reports how MUCH boundary it synthesised, never where). Committing 34 LF of threshold because two rooms share 34 LF of wall would be a wrong bid with a machine's confidence behind it. These come back in `withheld` — measured, with their length, their gap in inches, and an `at` point — as questions you answer by LOOKING (view_sheet at `at`, then measure_line or place_count the threshold yourself). The symbol_sweep doctrine: a near-match is never a silent commit and never a silent drop. Tuning: max_gap_in (default 12) is how far apart two rings can be and still count as adjacent at all — raise it for thick walls, and every extra inch turns more of the plan into wall_separated questions, never into committed LF. min_

輸入參數condition必填max_gap_inmin_run_incondition_a必填condition_b必填

takeoff_summary

Per-condition totals (floor/wall/border SF, LF, EA, SY, with and without waste) plus grand totals — the Report's numbers, computed by the same rules. Numbers only: the deliverable that SHOWS the work on the drawings is export_marked_pdf. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

無需輸入參數。

export_takeoff

The full "opentakeoff.takeoff_canvas.v1" annotations payload — exactly what the app autosaves, importable by it. Returned inline; pass path to also write it to disk as JSON. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數pathoverwrite

export_dxf

The takeoff as a CAD drawing — a DXF (R2000) AutoCAD, BricsCAD, LibreCAD and Revit import as native geometry, not a picture. ONE sheet per file, like a DWG: every committed shape on that sheet becomes an LWPOLYLINE (floor rings CLOSED, walls and linear runs open, count marks a 1-ft circle), on a layer named for its finish — OT-<TAG>, with -DEDUCT / -HOLE / -WALL / -LINEAR / -COUNT suffix layers so a CAD user isolates any bucket with one layer filter, and room labels as TEXT on OT-LABELS. Coordinates are real units in the sheet's own frame: origin at the sheet's BOTTOM-left, Y up (CAD convention), feet by default ($INSUNITS 2) or metres with units:"m"; a ring's area in CAD equals its area in export_report to rounding, so the drawing IS the audit. Requires the sheet's scale (refuses otherwise — pixels in a DXF are worse than nothing); with several sheets carrying shapes, pass sheet to choose the drawing (the refusal lists them). The reply names every shape left out and why — a reconciled deduct ships as its parent's -HOLE ring, never twice. Writes to path (required — a DXF lives on disk, next to the DWG it aligns to); pair with export_marked_pdf for the reviewed planset.

輸入參數path必填sheetunitsoverwrite

export_report

The computed Report document — "opentakeoff.report.v1", the same schema the canvas Report's JSON export writes. Everything a pricing consumer needs without re-implementing the app's math: per-condition quantities with waste and multiplier applied (gross and *_net), the computed materials BUY LIST per condition (order quantity = basis ÷ coverage rate, rounded up to whole purchase units) plus the project-wide roll-up summed by (name, unit), per-sheet BASE subtotals, scale provenance per sheet, and annotations. Contrast: export_takeoff is the raw canvas payload (materials as CONFIG rows, no computed quantities) and takeoff_summary strips materials for a compact reply — when the numbers are leaving for pricing, consume this. A report alone is HALF the deliverable: pair it with export_marked_pdf, because a takeoff is reviewed on marked drawings, not on numbers. Returned inline; pass path to also write it to disk as JSON.

輸入參數pathoverwriteproject_name

set_pricing

Price one condition for the estimate — the canvas Estimate panel's rows. A condition has one estimate line per quantity kind it carries (its area, LF and EA — see estimate_summary); the first is its MAIN line. unit is what the main line is bought in (SF, SY, LF, EA). line picks WHICH line material and labor price: leave it out (or pass the main line's unit) for the main line; pass "LF", "EA" or "SF" (a second area line prices in SF) to price that other line — one call per line. material and labor are $ per that line's unit: material is priced on the ORDER quantity (waste applied — SY is the waste-adjusted SY), labor on the MEASURED quantity (no waste); EA carries no waste on either. materials prices the condition's supporting-material rows (from edit_materials) per THEIR unit (bag, pail, roll) — quantities are already whole packages. null clears a price back to "not entered"; 0 is an explicit $0 (owner-furnished material, labor-only scope). A bordered linear run's derived second line (its LF when the border SF is the main line, or the reverse) is OPTIONAL: it shows and can be priced, but left empty it is carried in the main line, never missing. Every price comes from the estimator or a document they gave you — never invent, estimate or 'typical' one; a price nobody entered stays unentered and estimate_summary names it. The reply carries the line this call priced (line) and all of the condition's lines (lines), with a note naming any line still unpriced. Rides export_takeoff into the app; undo_last reverses the call.

輸入參數lineunitlabormaterialcondition必填materials

set_estimate_settings

The project's markups, proposal text and client — the canvas's estimate_settings, bid_proposal and client_info. Markups: tax_pct (sales tax on material), overhead_pct (on material + labor + tax), profit_pct with profit_mode markup (× (1 + p)) or margin (÷ (1 − p); 33.33% margin is ×1.5). proposal: itemized (a sell price on each scope line, the base bid allocated to the cent) or lump sum, scope_note, inclusions / exclusions (one per line), terms, valid_days. client: who the proposal is for. Only what you pass changes; an empty client string clears that field. Until markups are set the estimate prices at 0% — say so rather than assume a number. Every price comes from the estimator or a document they gave you — never invent, estimate or 'typical' one; a price nobody entered stays unentered and estimate_summary names it. Rides export_takeoff; undo_last reverses the call.

輸入參數clienttax_pctproposalprofit_pctprofit_modeoverhead_pct

estimate_summary

The priced estimate — the canvas Estimate panel as data: one line per quantity kind each measured condition carries — its area, LF and EA are separate lines (unit, order and measured quantities, rates, material / labor / supply cost, main marks each condition's first line; status priced | partial | unpriced with what is missing, or optional — a bordered linear run's derived line nobody priced, carried in its main line and never missing; set_pricing prices any line: the main one by default, the others with line "LF" / "EA" / "SF"), then material, labor, sales tax, cost, overhead, profit and the base bid. complete is false while anything is unentered: the total then counts the missing pieces at $0 and is "priced so far", and unpriced lists every hole — report it to the estimator, never fill it. Read-only.

無需輸入參數。

export_estimate

Write the priced estimate as an Excel workbook — the canvas Estimate panel's Excel button: an Estimate tab (every line, then material, labor, tax, overhead, profit, base bid), a Materials tab (each supply's packages, price, cost) and, when anything is unentered, an Unpriced tab. Values only; an unentered rate is an empty cell, never 0. Every tab ends with "Prepared with OpenTakeoff". Default path: next to the loaded plan as "<project or plan> - estimate.xlsx". The reply repeats complete and unpriced — tell the user when the total is only priced so far.

輸入參數pathoverwritecompany_nameproject_name

export_proposal

Write the bid proposal PDF — the canvas Proposal dialog's download: letterhead from company, prepared-for (set_estimate_settings client), the scope (each condition, its spec description, measured quantity), the base bid (lump sum, or itemized when the project's proposal says so), inclusions / exclusions / terms and signature blocks. Every page carries a light diagonal "OpenTakeoff" and the line "Prepared with OpenTakeoff · opentakeoff.kentucky-ai.com". Pass company exactly as the estimator gave it — this server has no Company setup of its own. It never refuses an incomplete estimate; the reply's complete and unpriced say what the base bid is missing, and the user must hear it before sending. Default path: next to the loaded plan as "<project or plan> - proposal.pdf".

輸入參數pathcompanyoverwriteproject_name

import_takeoff

The way BACK IN (#151): load an "opentakeoff.takeoff_canvas.v1" file — a prior export_takeoff, or the app's own save — into this session, through the SAME tested merge rules as the app's Sheet-menu import: finish-tag identity joins imported conditions onto this session's own (their knobs win), new ids append, duplicate ids skip (re-import is idempotent), and THIS session's calibration wins per sheet. An uncalibrated empty session adopts the file wholesale. New dimensional shapes refuse atomically if their source scale differs from the session calibration or is missing; align scales and re-export, or use a fresh session. Counts and duplicate IDs are exempt. Legacy agent traces without review flags arrive reviewed:false. Resume yesterday's work, extend a takeoff a human already reviewed (their ink stays ink — reviewed shapes arrive untouchable by agent verbs), or audit someone else's export with list_shapes/takeoff_summary. Requires a loaded plan; shapes referencing OTHER files ride along and count in totals but can't be viewed against this document — the reply's unknown_files names them. Approval marks ride the file too — transport, not minting: an estimator seal arriving by import stays estimator ink, listable but untouchable here. undo_last removes the imported SHAPES as one step; adopted conditions, scales, annotations, and approval marks stay.

輸入參數path必填

apply_rules

Re-run the correction rules the takeoff arrived with (#207) — the lessons an estimator TAUGHT the canvas (#88): "every room like this loses the mechanical chase." A rule is a deterministic predicate (enclosed linework islands under a size cap, inside the rule's condition's rooms), never a re-prompt. Evaluation is the same pure rules.ts engine the canvas Preview runs; the commit is the one batch the canvas's Apply makes — ONE journal entry, undo_last takes the whole batch back. Everything lands reviewed: false (this server has no review gate), and the reply's per-rule disclosure — what each rule produced, what was skipped, with ids — IS your preview: read it, then view_sheet overlay:true. Idempotent by construction: any candidate an existing deduct already covers is dropped by the engine, so re-running after new rooms commit is the intended workflow and never double-deducts. Rules arrive ONLY via import_takeoff (minting a new rule is an estimator's correction and stays behind the canvas's human Preview→Apply gate); with none imported this refuses. Pass sheet to scan one sheet; omit it to scan every sheet holding the rules' rooms. Uncalibrated and scanned-raster sheets come back in skipped_sheets, named.

輸入參數sheet

export_marked_pdf

The MARKED-UP PLANSET — the deliverable of every takeoff. Writes a distribution-ready PDF to disk: a legend cover (per-condition totals, swatches, a by-sheet breakdown) followed by every sheet that carries takeoff shapes or annotations, vector-copied from the source plan with the work burned in as drawn — condition colors and hatches, a quantity chip on every shape, annotation clouds/callouts/highlights, and approval marks (the estimator's APPROVED rings, the agent's AGENT diamonds — the cover tallies the split). Built by the same module as the canvas's MARKED SET button, so agent output and app output are one implementation. Every page carries a light diagonal "OpenTakeoff" and the line "Prepared with OpenTakeoff · opentakeoff.kentucky-ai.com". A construction takeoff is no good without markup: finish EVERY takeoff by writing this file and giving the user its path (export_report carries the numbers for pricing; this carries the evidence). When the shapes were machine-traced and unreviewed, the document says so on its last page — the review path is importing the export_takeoff payload into the app, where agent shapes arrive as pencil proposals. Default path: next to the loaded plan as "<plan> - marked set.pdf". Needs no native canvas — pure vector copy, so it works even where view_sheet cannot render. The one source it refuses: an ENCRYPTED plan PDF (owner password, empty user password — it opens everywhere, but its pages cannot be vector-copied and there is no canvas here to render them); the refusal names the sheet — export the marked set from the app, or supply an unencrypted PDF.

輸入參數pathoverwriteproject_name

list_shapes

The mid-session shape inventory (#149): every committed shape's id, sheet, condition tag, role, quantities, room label, vertex count, and review state in one compact read — the ids edit_shape and delete_shape assume you have, without pulling the whole export_takeoff payload to find one shape. Filter by sheet, by condition, or both; filters narrow, an empty list is a result, not an error.

輸入參數sheetcondition

delete_shape

Remove a committed shape by the id returned when it was committed. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數shape_id必填

scope_duplicates

Two conditions claiming the same floor, as a list (#366). Every pair of committed floor_area shapes on one sheet whose EXACT polygon intersection exceeds min_fraction of the smaller shape — with the shared SF, which condition each belongs to, whether the estimator already affirmed either, and a look region to pass to view_sheet {overlay: true}. Pairs on DIFFERENT conditions are collisions: every total downstream counts that floor twice. Pairs on the SAME condition are a double trace (a different bug) and come back in duplicates. shared_floor_sf is the whole compared set's Σ areas − union, counted once per cell no matter how many shapes pile on it — the number takeoff_summary carries and the one that has to read 0 before any total means anything. Machine-precision edge remnants are ignored; a real overlap below 0.01 SF stays listed with an explanatory note. Supporting materials belong in edit_materials coverage rows, not duplicate floor polygons. Deducts and runs are not claims. Read-only; a shape on an unscaled sheet or with a degenerate ring is listed in unmeasured, never counted as zero. Same rule as the room eval's shared-floor gate (iou ≥ 0.5 = the same space claimed twice). Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheetmin_fraction

scope_merge

Resolve ONE collision (#366): given a pair of floor shapes and the winner, the loser gives up the shared floor — TRIMMED to its remainder by an exact boolean difference (the cut_out module's own arithmetic; its quantities re-measured from the result), or DELETED outright when the overlap is near-total (≥ 98% of the loser: the same space claimed twice, not a room with a sliver left). One journal step either way; undo_last restores the loser verbatim. Who wins: state winner; with it omitted the reviewed shape wins over a pending one, and the verb refuses when neither is reviewed (it does not guess which condition the floor belongs to) or when BOTH are (that is the estimator's call — the collision shows on both condition rows in the canvas). The ink rule is absolute: a loser the estimator affirmed is refused whoever you name. A trim that would split the loser into disjoint pieces refuses — that is a re-trace decision, not a merge — and a loser carrying reconciled cutouts refuses (delete the cuts first).

輸入參數winnershape_a必填shape_b必填

revise_proposal

Replace EVERY still-pending shape in a proposal with a new set, as ONE journal step (#365) — the move for "I re-measured and got a better batch". The old pending shapes go, the replacements commit under the same proposal, and undo_last puts the previous batch back exactly. All-or-nothing: the whole replacement is validated (sheet, scale, vertex count, a height for surface_area) before the first pending shape is removed, so a malformed last shape leaves the batch untouched and the error says which entry and why. Shapes the estimator already accepted are ink — they stay, and they are not part of what this replaces. verts are image px like every other tool; roles and minimums match the measure tools (floor_area/deduct ≥3, linear/surface_area ≥2, count 1). An empty shapes list is refused — withdraw_proposal is the verb for that. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數shapes必填proposal_id必填

withdraw_proposal

Take a proposal back (#365): every still-pending shape in the batch is removed in ONE journal step, the record stays marked withdrawn (its label is history the estimator may still read), and new commits stop attaching to it. Shapes the estimator already accepted are ink and stay — the reply counts them. This is the honest exit for "that batch was wrong" — one call instead of N delete_shape calls, and undo_last restores the whole batch.

輸入參數proposal_id必填

sheet_context

The sheet's STRUCTURE in one call and one frame: the classified vector segments, the positioned text spans, and the hatch-family instances of a region — everything the engine itself floods against, exposed as data instead of pixels. Use it when you need to REASON about a region rather than look at it: which lines bound this space and at what pen weight, what the region says, and which periodic fill pattern covers it. The join is the point — all three arrive in image px with no reconciliation left to do, and the reply echoes the post-clamp region so passing that same rect to view_sheet gives you the matching render by construction. Hatch families carry a content-derived id (same pattern spec ⇒ same id, anywhere on the sheet), so matching a plan region to a legend swatch is comparing two ids, not guessing from a render — read the legend region, read the room region, match ids, and cite both bboxes as evidence. Decimation is declared, ordered, and counted on every reply: segments shorter than min_len_px drop first (invisible ink), then a max_segments cap applies LONGEST-FIRST so walls survive and hatch strokes go; kept + dropped always reconciles to total_in_region, and whole segments drop with their meta intact — nothing is ever simplified or merged, because these are classified segments and a merge would rewrite the classification. A scan returns has_vector_linework: false with empty vectors — absence of linework, never a claim the region is blank. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheet必填regionmin_len_pxmax_segments

get_sheet_vectors

The STROKES — the sheet's vector layer exactly as the engine is fed it, so you can run your own geometry against what the app sees (#367). view_sheet lets you look, read_sheet_text lets you read, sheet_context classifies a region; this returns the raw extractor output that all of them and every shape verb (the wall network's pen weights, symbol_sweep's matching) work from: flat points [x1, y1, x2, y2, …] in image px, one meta byte per segment (low nibble flags: 1 curve chord, 2 clip-only, 4 fill-only, 8 polyline arc; pen width = meta >> 4), per-segment stroke luminance, the drawn figure each segment belongs to (subpath ordinal), the sheet's placed-image area, and its PDF layer table with a per-segment layer index (sheet_info.layers names and classifies the same ids). Nothing is classified, decimated, or merged here — segments arrive whole, in extraction order, undecimated, which is the point: a reader can build its own room finder, symbol matcher, or wall classifier on the same array and commit through the existing verbs with provenance intact. Paged, never clipped silently: a dense sheet runs to hundreds of thousands of segments, so the reply carries limit (default 20000 segments, ceiling 100000) and the ledger offset + returned + dropped === total on every page; dropped is exactly what passing next_cursor as cursor recovers. region keeps every segment that intersects the rect (endpoints untouched — the same keep test sheet_context uses, so total here equals sheet_context's total_in_region) and echoes it post-clamp. Read-only and stateless — no shape, condition, or scale is touched. A scan has no strokes: the verb refuses and names view_sheet as the path. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數limitsheet必填cursorregion

edit_shape

REVISE a shape you already committed, instead of deleting it and starting over: pass new verts to move the geometry, condition to reassign it to a different finish tag, role to switch between floor_area / deduct / linear, label to name the room it belongs to, or any combination. Quantities are recomputed from the result — a role flip alone re-measures (closed area vs open length). The loop this is for: measure_polygon to commit, view_sheet with overlay:true to LOOK at what landed, then edit_shape to fix the two vertices that overshot into the corridor. label is the per-room reporting seam: this is how a shape traced by hand — or one whose room number the sweep read wrong — joins the same per-room breakdown the Report and the workbook's floor × room tab group by. Shapes a human affirmed (origin.reviewed) are ink and are refused — an agent revises its own pencil and nothing else. Agent self-revision is tallied on origin.agent_edits, kept deliberately separate from the human-correction fields. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數rolelabelvertsdrop_ftrise_ftshape_id必填condition

edit_materials

Add, remove, or patch supporting-materials rows on a condition — the coverage-rate lines that turn a measured area/length/count into an order quantity (adhesive at N sf/gal, grout at N lf/bag, …), matching the canvas's per-condition Supporting Materials panel. Each row is {name, per, basis, unit, round, note}: quantity = the condition's basis total (area/linear/count/seam_lf) ÷ per, rounded up to whole purchase units unless round:false. basis "seam_lf" is the one basis that is FIGURED rather than measured: it is the length where two cuts meet on the floor, read off the condition's roll layout (set roll_setup with edit_condition), which is what a heat-weld rod or a carpet seam tape is bought by. A 20-ft-wide room off a 12-ft roll seams once down its length; the same square footage as two 10-ft rooms seams not at all, and no percentage of the area or the perimeter can tell those two jobs apart. Without a roll_setup — or with no committed floor shapes to lay out — a seam_lf row reads 0, which is the honest state rather than a guess. condition names an existing OR NEW finish tag (minted on first touch, same as measure_polygon) — add alone is enough to seed materials on a condition before you've traced anything. remove/patch target existing row ids from this reply or export_takeoff (takeoff_summary strips materials for a compact quantities-only reply); a bad id 404s the WHOLE call before anything is written, and referencing an id on a tag with no condition yet errors rather than silently minting an empty one. No review gate here — materials rows are quantity config, not traced geometry, so this edits directly; undo_last reverses a call in one step (the condition's whole materials array, snapshotted before the write, restored verbatim).

輸入參數addpatchremovecondition必填

edit_condition

Set a condition's quantity knobs — waste %, multiplier, height_ft (the H knob measure_surface quantifies against), and/or roll_setup (the roll-goods opt-in: seams and order footage figured from the committed rooms, #147). takeoff_summary emits waste-adjusted *_net order quantities and a per-condition multiplier, and every export carries both, but conditions minted through the measure tools start at waste 0 / multiplier 1 — without this tool an agent's takeoff always ships net === gross (#131). waste_pct is the estimator's cut-waste percentage (carpet commonly 5–10); multiplier scales every quantity on the condition (×N identical floors — takeoff_summary applies it before waste). condition must resolve to an EXISTING finish tag — a typo'd tag errors rather than minting an empty condition (the edit_materials remove/patch rule, not its add rule: these knobs mean nothing on a condition that doesn't exist yet). No review gate — quantity config, not traced geometry; undo_last reverses a call in one step (both knobs snapshotted together, restored verbatim).

輸入參數drop_ftrise_ftcondition必填height_ftwaste_pctmultiplierroll_setup

propose_condition_edit

PROPOSE a change to a condition instead of making it (#365): a diff — a new finish tag (rename), waste %, ×N multiplier, height_ft, roll_setup — held PENDING until the estimator accepts it from the panel. edit_condition is the wrong power for "I think this condition is wrong": a tag rename or a knob change should be a decision the estimator makes, not one they discover. Until acceptance NOTHING changes — takeoff_summary and export_report keep computing from the current values and carry the diff beside them (proposed_condition_edits), and once accepted the report is byte-for-byte what a direct edit_condition would have produced (the same write path). Only fields that differ from the current value are recorded; a proposal that changes nothing is refused, and a rename onto a tag another condition already carries is refused (two conditions on one tag would make one unreachable). One pending diff per condition — proposing again replaces the earlier one (undo_last restores it). rationale is required: the estimator accepts a reason.

輸入參數drop_ftrise_ftcondition必填height_ftrationale必填waste_pctfinish_tagmultiplierroll_setup

withdraw_condition_edit

Drop a pending condition-edit proposal (#365) without touching the condition. undo_last re-seats it.

輸入參數proposal_id必填

duplicate_condition

Twin a condition — the same finish measured somewhere else, with its own supporting materials. One finish in two areas is not two conditions and it is not one either: the same sheet goods over a slab and over a raised deck take the same field material and different preparation underneath (one wants a moisture barrier, the other a primer and a different adhesive). The twin arrives carrying the original's whole materials list and keeps FOLLOWING it — change a coverage rate on the original and every twin that has not touched that row gets it; edit a row on the twin and only THAT row stops following. `label` is REQUIRED and becomes the tag suffix ('CPT-1' + 'Level 2' → 'CPT-1 – Level 2'), because every tool in this server resolves a condition by finish tag and takes the FIRST match: two conditions sharing a tag would make one permanently unreachable, and a takeoff re-import collapses them last-wins. A label already in use is refused rather than de-collided. No takeoffs come along — measure the new area against the returned condition_id. Reversible with undo_last; use split_condition to end the inheritance permanently.

輸入參數label必填condition必填

split_condition

Cut a twin loose from its family: every following material row freezes at its current values and edits to the original stop reaching it. It keeps its finish tag and still groups with its siblings — only the inheritance ends. Use when two variants have diverged far enough that following one another is wrong. A condition that already owns its materials returns split:false rather than erroring. Reversible with undo_last.

輸入參數condition必填

undo_last

Step back over your OWN last n mutations, newest first — a committed measure_polygon, an edit_shape, a delete_shape, an edit_materials call, an edit_condition call, or an RFI verb (create_rfi / resolve_rfi / delete_rfi). Each step is reversed exactly (a commit is removed, an edit is restored verbatim, a delete is re-inserted where it was, a materials edit's whole array is restored, a condition edit's waste/multiplier pair is restored), so this restores state rather than approximating it. Reads are never journaled, so n counts gestures that changed something, not tool calls you made. Use it when a sweep committed against the wrong condition or a batch went in on the wrong sheet — one call instead of N deletes. Scope: this session's own history only. It is not the browser canvas's undo stack, and load_plan clears it along with the shapes it refers to.

輸入參數n

sheet_graph

The plan-set INDEX (#87): every sheet's role (plan / schedule / legend / …, with confidence and the title evidence), the schedule tables found (kind, row count, region — a schedule CONTINUED across sheets ("… SCHEDULE — CONT'D") reads as ONE table, the continuation fragment naming its base in "continues"; rotated column headers are read at their quarter-turn and flagged), every number CORROBORATED as a room (with the stacked room NAME when one exists, the room's BUILDING on multi-building sets, and "corroboration" saying why it counts as a room) plus "unmatched_tags" — the numbers that are NOT rooms (keynote hexagons, detail markers, dimension fragments, legend rows), each with a reason, listed and never dropped; READ those reasons, one of them may be a room the schedule left out, the detail callouts (3/A-601 → sheet edges), the set's building designators, every REVISION marker the set carries (text markers "Δ2"/"REV 2" AND drawn deltas — a bare digit inside a triangle of linework, proven from vector geometry and flagged drawn — in "revisions", and attached to the schedule row / room tag they sit on), and named indexing gaps in "notes". Built once per document from the text layer and cached. This is how an agent decides WHAT to measure without a human enumerating the rooms: list the rooms here, resolve each with resolve_tag, then measure with measure_polygon on its wall faces. A scanned set (no text layer) returns available: false — unavailable, never half-populated. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

無需輸入參數。

resolve_tag

Resolve ONE room tag across the set (#87): the plan tag → its room-finish schedule row → each finish code's definition in the finish/material schedule, EVERY edge carrying an evidence pointer (sheet + literal text + bbox — pass a bbox to view_sheet to look at the source). Rows carried by a continuation sheet ("… SCHEDULE — CONT'D") resolve exactly like base-sheet rows, citing the sheet the ink is on. The doctrine is refusal over guessing: a room that appears on the plan with no schedule row returns status "unresolved" with the reason (and still cites the plan tag); reused room numbers return "ambiguous" rather than picking one — on a multi-building set the refusal LISTS the candidate rows per building, and a building-qualified tag ("A-134") picks the building the set names. A delta triangle or REV tag on the answering row (or the plan bubble) rides the result as "revisions": the codes returned are the POST-revision answer, but the ink changed under that delta — view_sheet the marker's bbox and check the addendum before pricing. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數tag必填

find_schedule

Locate a schedule table in the set (#87): pass a kind ("room finish", "material"/"finish") and get every matching table's sheet, title, headers, TOTAL row count, and REGION — sized for a view_sheet look or a read_sheet_text pull of exactly the table. A schedule continued across sheets is ONE match whose "parts" list every fragment (base first) with its own viewable region; tables read through rotated headers say so; a table answering for one building carries "building"; a table with delta/REV-marked rows says how many in "revised_rows". Errors with what WAS found when the asked-for kind isn't in the set. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數kind必填

read_sheet_text

The sheet's text with positions — items [{str, x, y}] in image px plus the joined text. Optionally restrict to a region {x0, y0, x1, y1}. Use it to read title blocks, room labels, finish schedules, and scale notes. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheet必填region

find_text

LOCATE a known string on a sheet — the complement to read_sheet_text (which returns what a region SAYS; this finds WHERE a string you already know sits). Case-insensitive substring match against each pdf.js text run, so a room label split across runs ("OFFICE" then "134" as separate items) needs a find_text call per fragment, or read_sheet_text over a region to see the whole thing joined. Every hit tells you which room a polygon belongs to — the locate-then-trace workflow: find_text the room number, then measure_polygon (One-Click is temporarily gated on this server) on that room's wall faces. Optionally restrict to a region {x0, y0, x1, y1}; results cap at limit (default 200), with count/truncated telling you exactly how much a tighter region or higher limit would recover. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數q必填limitsheet必填region

view_sheet

SEE the sheet — render the page (or a crop of it) to a PNG image. This is your eyes on the plan, so CROP, DON'T SQUINT: the render downsamples to the px budget (≤2000 long side), which on an E-size sheet is ~4 sheet pixels per returned pixel — a full-sheet render finds WHERE things are, and only a tight region crop can tell you what the linework and labels actually say. Never audit a trace or read a dimension off a full-sheet render. region is in image px — the same space as every other tool — so a feature at pixel (ix, iy) of the returned image sits at x = region_x0 + ix × (region_x1 − region_x0) / img_w (same for y), and those coordinates go straight into measure_polygon, or read_sheet_text. overlay:true burns the session's committed shapes into the render (human-affirmed ink solid red, unreviewed machine shapes dashed blue) — render again after committing to verify your geometry landed where you intended, and sanity-check what you see: a fixture-sized ring where a room should be means the seed landed inside a stall or casework; an outsized ring means the flood escaped through an opening. To MEASURE rather than guess, pass grid: a calibrated measuring grid is burned in — thin lines every 1 ft, heavy blue every 5 ft, foot labels along the crop edges, feet counted from the crop's top-left corner. Count grid cells between walls exactly like an estimator scaling a plan; never derive a dimension by eye when the grid can give it to you. grid "auto" uses the sheet's set scale; before set_scale, pass the drawing scale read off the title block as inches-per-foot — "1/4" for a 1/4" = 1'-0" plan, "3/16", "0.25". marks (#297) burns DISCLOSURE layers into the render, so what a reply names, the picture shows: pass the coordinate lists a tool disclosed — question: withheld placements (orange ?-circles), struck: rejections a counter-example or luminance gate refused (magenta struck ×), ring: reference points like the sweep's own seed (violet double ring). The colors sit deliberat

輸入參數pxgridmarkssheet必填regionoverlay

annotate

Place an annotation on a sheet — a note ABOUT the work, never a measurement of it. Types: cloud and highlight take rect:[[x0,y0],[x1,y1]] (a revision cloud around an area, a highlight box over it), text takes at:[x,y], callout takes at:[x,y] plus target:[x,y] (the point its leader aims at), arrow takes from:[x,y] and to:[x,y] (tail and head — plank/seam direction, the markup flooring drawings use most; #150), bubble takes at:[x,y] plus optional r (a keynote/detail circle carrying centered text), dimension takes from:[x,y] and to:[x,y] (its two measured endpoints) and labels itself with the length between them at the sheet's scale — drawn as a dimension line with end ticks and the measurement centered. A dimension states a REAL length, so it is the one annotation the scale gate applies to: on an unscaled sheet it refuses exactly like the measure tools (set_scale first) rather than dressing a px figure up as feet. It still touches no quantity — a dimension is a note about a distance, not a takeoff line item. Pass condition to attach the note to a finish tag, which is what makes it part of that SCOPE rather than a floating remark: it then wears the condition's colour on the canvas and in the marked-set PDF, and travels with it into the report. The tag is minted on first touch like measure_polygon, so you can annotate CPT-1 before anything is traced for it. Omit condition for a note about the sheet itself. Write {{qty}} in the text to quote the linked condition's live measured quantity (multiplier applied, no waste): "x{{qty}} fixtures" reads "x6 EA fixtures" on the sheet and follows every later count; the stored text keeps the field, and list_annotations reports text_resolved. No review gate: the pencil-not-ink rule exists to stop an agent inventing geometry, and a cloud reading "verify substrate" is not geometry. It touches no quantity. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloa

輸入參數rattofromrecttexttype必填sheet必填targetcondition

list_annotations

Every annotation on the takeoff, with condition_id RESOLVED to its finish tag so you can act on the reply without joining against conditions[]. Filter by sheet, by condition, or both. Coordinates come back in image px (the same frame you passed in), not the normalized form they're stored as. `unattached` counts the notes carrying no condition — the candidates for link_annotation. `verdicts` is the approval family's inventory (mark_verdict/delete_verdict): every mark with its actor stated — the estimator's APPROVED ring or the agent's AGENT diamond — under the same filters, a condition filter reaching a verdict through its target shape. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數sheetcondition

edit_annotation

Shorten, replace or clear the text of an existing annotation. Get annotation_id from list_annotations (annotations, not verdicts). Changes only text: position, shape, dimension length, condition links, quantities and review records stay unchanged. Empty text clears the note; a dimension still prints its measured length. Refuses an RFI-linked note: review that question's context in the browser RFI register. One undo_last step restores the previous text. Does not create a verdict or human approval.

輸入參數text必填annotation_id必填

mark_verdict

Mark the agent's VERDICT on work — the pencil half of the approval family, and the only half an agent can mint. Two actors exist on the record: the estimator's APPROVED ring is ink, minted solely by a human's click at the canvas's Approve tool; this tool mints the AGENT diamond and structurally nothing else — it takes no actor input to misuse. Target the work either way: shape_id anchors the mark ON a committed shape (a room at its area centroid, a run at its on-path midpoint, a count marker at its point) and records WHAT was marked — the shape_id stays on the record as provenance, and the glyph keeps its own anchor even if the shape is later deleted; or sheet + at drops the mark at a sheet point (image px). Exactly one target. Optional text rides the record through every export; the glyph itself always reads AGENT. A verdict touches no quantity and gates nothing: it is the agent's signed claim that it checked this work — pencil beside the estimator's ink, never in its place. The mark renders as the graphite AGENT diamond on the canvas and in the marked set, the marked-set cover tallies the split ("Approval stamps: N estimator-approved · M agent-marked"), and the record rides the annotations payload through export_takeoff / import_takeoff and the app's own saves. One mark per shape (re-mark = delete_verdict, then mark again); list_annotations returns the inventory in verdicts[]; undo_last steps over a mark exactly like any other mutation. Coordinates are image px at render scale 2.0: PDF pt × 2, origin top-left, y down (the browser canvas's native space). Sheet payloads carry dims in both px and pt.

輸入參數attextsheetshape_id

delete_verdict

Lift an agent verdict mark by id (mark_verdict's reply, or list_annotations verdicts[]). Agent marks only: the estimator's APPROVED seal is human ink and is refused — the same line edit_shape holds on reviewed shapes. Journaled like every mutation, so undo_last re-seats a lifted mark exactly where it was.

輸入參數verdict_id必填

create_rfi

Raise an RFI — a Request For Information — when the drawing set contradicts itself or cannot answer a question you need answered to take the work off: a room-finish schedule row that names a tag the plan never draws, a room label the schedule has no row for, a finish called out two ways, a scale that disagrees with a stated dimension. It lands in the estimator's RFI register (the canvas's RFI panel) with the next number in that register's own sequence (RFI-001, RFI-002, …), status open, dated today, on the sheet you name. You raise it as the agent: the record carries origin {actor: "agent", reviewed: false} and is PENDING — pencil — until the estimator accepts it in the register, because an RFI goes to the architect and nothing sends without a human. It still prints in the marked set's RFI schedule like any other RFI, so the question is on the deliverable. Pass markup_ids to pin it to annotations already on the sheet (annotate a cloud or callout at the conflict first, then link it here) — a linked markup carries the RFI number on the canvas and in the marked set, and list_rfis reports which finish tags the question touches through those links. Prefer this to describing the conflict in prose: a question in the register is tracked, numbered, and answered; a sentence in a reply is lost. Journaled; undo_last takes it back.

輸入參數sheet必填title必填question必填markup_ids

list_rfis

Every RFI in the register with its status, sheet, who raised it (actor) and whether an agent-raised one is still pending the estimator's acceptance, its linked markup ids, and the finish tags those markups are attached to — the scopes the question touches. withdrawn[] lists the numbers delete_rfi tombstoned, so a gap in the sequence is explained rather than silent. Read this before raising a question the register already holds.

無需輸入參數。

resolve_rfi

Answer an OPEN RFI: the answer lands as its response, status becomes answered (the register's own state for "response in"), and the response date stamps exactly as the panel's would, plus an ISO timestamp of the resolve. Only an open RFI resolves — an answered, closed, or void one is refused rather than re-answered or quietly revived (undo_last reverses your own resolve if the answer was wrong). Record the answer the drawings or the architect actually gave; an RFI is not resolved by guessing.

輸入參數answer必填rfi_id必填

delete_rfi

Withdraw an RFI. A TOMBSTONE, never a renumber: the record stays with its number reserved, so the register and the marked set keep printing a gap where it was and the next RFI takes the next number — an RFI number that went out and then meant something else would be a lie. Every markup linked to it keeps its note and loses the link (the canvas's own delete rule). Withdraw a question you raised in error; a question the architect answered is closed in the register, not deleted. Journaled; undo_last puts the record and its links back.

輸入參數rfi_id必填

open_suite

Open a trade suite: enables that trade's tools (they are hidden until opened) and returns its method and condition names. Open only the trade you are taking off; opening is idempotent and never closes anything. No arguments = the catalog. Suites: - finishes (Div 09): flooring, base, transitions, wall finishes: core tools, no extra tools - roofing (Div 07): sloped roof area and squares, true rake/hip/valley LF, drip edge, starter, ridge cap, underlayment - siding (Div 07): net wall area and squares from elevations, opening trim, starter, corners, lap pieces - soffit_fascia (Div 07): soffit SF from eave/rake LF × depth, fascia LF, wall receiver, attic intake vent area - glazing (Div 08): storefront / curtain wall: frame SF, lites, glass SF, mullion LF, perimeter sealant - doors (Div 08): doors per type and size from the door schedule, matched to tags on the plans: core tools - plumbing (Div 22): fixture counts checked against the schedule QTY, pipe LF by system/size, hangers, fittings, insulation - electrical (Div 26/27/28): legend devices and fixtures counted, conduit LF with drops, wire LF by conductors, boxes, whips division opens every suite of a CSI division ("07" = roofing + siding + soffit_fascia).

輸入參數suitedivision

版本歷史

9
  1. v0.9.105最新Nov 7, 2026
  2. v0.9.100Oct 25, 2026
  3. v0.9.99Oct 25, 2026
  4. v0.9.98Oct 11, 2026
  5. v0.9.96Oct 3, 2026
  6. v0.9.94Oct 1, 2026
  7. v0.9.93Oct 1, 2026
  8. v0.9.90Sep 25, 2026
  9. v0.9.87Sep 16, 2026

評分

已滿足適用檢查項的 90%

該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。

  • 近 30 天內可連通25 分
  • 已宣告認證方式10 分

    請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 圖示10 分
  • 文件15 分
  • 授權條款10 分