Estimator Takeoff Module
The CrestApps.OrchardCore.Estimator.Takeoff module provides the drawing viewer and manual takeoff. An estimator opens a sheet of a project, counts devices, measures runs with drops and areas, and calibrates the scale. The marks of each takeoff item are added over every sheet and per sheet location.
| Feature ID | Name | Category | Dependencies |
|---|---|---|---|
CrestApps.OrchardCore.Estimator.Takeoff | Estimator Takeoff | Estimator | CrestApps.OrchardCore.Estimator |
The Estimator setup recipe enables the feature. The module adds no permissions: viewing the takeoff of a project needs CanViewAsync on the project, and changing it needs CanManageAsync, the same project access as the Estimator module.
Data model
All documents are in the Estimator YesSql collection.
| Document | One per | Holds |
|---|---|---|
TakeoffItem | Thing taken off | ProjectId, Name, Kind (Count, Linear or Area), Color (#rrggbb), Notes, CreatedUtc, ModifiedUtc |
SheetTakeoff | Sheet with takeoff | ProjectId, SheetId, Location, CalibratedRatio, Marks, ModifiedUtc |
A TakeoffMark (in SheetTakeoff.Marks) has an Id, the ItemId, its Points and DropFeet, CreatedByUserId and CreatedUtc. All the marks of a sheet are in one document, so the viewer loads a sheet with one query and an edit rewrites one document.
| Index table | One row per | Columns |
|---|---|---|
TakeoffItemIndex | Takeoff item | ProjectId, Name, Kind |
SheetTakeoffIndex | Sheet takeoff | ProjectId, SheetId, MarkCount |
The TakeoffMigrations data migration creates them. The design is recorded in docs/engineering/adr/0003-takeoff-viewer.md in the repository.
Coordinates and quantities
Points are in PDF points (1/72 inch) from the top-left corner of the page as it is shown, which is the PDF.js viewport at scale 1. The sheet keeps its width and height in the same unit. A point is refused when it is off the page; the check uses the longer side for both axes, because a rotated page swaps its sides.
The scale ratio is the real length divided by the paper length. One point on paper is ratio / 864 feet (72 points per inch, 12 inches per foot). SheetScale.Resolve picks the ratio:
| Source | When | Ratio |
|---|---|---|
Calibrated | The sheet takeoff has a CalibratedRatio | The calibrated ratio |
Sheet | The sheet has a printed scale that DrawingScale can read | The printed scale, such as 96 for 1/8" = 1'-0" |
NotToScale | The scale says NTS | None |
None | No scale | None |
SheetScale.Calibrate takes two points and a real length in feet, and uses DrawingScale.FromCalibration. TakeoffMeasure.Quantity gives the quantity of one mark:
| Kind | Unit | Quantity |
|---|---|---|
Count | EA | 1, on any sheet |
Linear | LF | The length of the polyline in feet, plus the sum of DropFeet |
Area | SF | The polygon area (shoelace formula) in square feet |
A length or an area on a sheet without a ratio has no quantity. TakeoffSummary.Summarize adds the quantities per item and per location, rounded to hundredths, and counts the marks without a quantity (UnmeasuredCount). The viewer uses the same formulas in Assets/js/takeoff-geometry.js, tested in tests/js/takeoff-geometry.test.mjs.
TakeoffRules.Validate checks a mark before it is kept: a count has one point, a run at least two and an area at least three, at most 2,000 points, and drops of 0 feet or more on runs only.
Services
| Service | Lifetime | What it does |
|---|---|---|
ITakeoffItemStore (TakeoffItemStore) | Scoped | The items of a project, in name order |
ISheetTakeoffStore (SheetTakeoffStore) | Scoped | The takeoff of a sheet or a project; DeleteBySheetsAsync deletes the takeoff of sheets |
TakeoffEditor | Scoped | AddMarkAsync, DeleteMarkAsync, CalibrateAsync, ClearCalibrationAsync, SetLocationAsync and DeleteItemAsync (which removes the item's marks on every sheet). Returns a TakeoffEditResult with a TakeoffEditProblem when the change is refused |
ProjectTakeoffSummary | Scoped | The summary lines of a project, worked out once per request |
Estimate lines
Estimate lines take their quantities from takeoff items, over every sheet or per location, through EstimateTakeoff of the Estimator Estimates module.
Clean-up handlers
The module deletes the takeoff that a deleted sheet or project leaves behind through two handlers of the base module:
| Handler | Implements | Runs when |
|---|---|---|
TakeoffSheetEventHandler | IDrawingSheetEventHandler.DeletingAsync(sheets) | DrawingSheetRemover deletes sheets: a sheet delete, a document delete, a document type change, and the pages left over after a split again |
TakeoffProjectEventHandler | IProjectEventHandler.DeletingAsync(project) | A project is deleted |
Split again keeps the sheet identifier of each page that is still there, so the marks, the location and the calibration of the sheet stay.
Admin routes
| Route | Screen |
|---|---|
/Admin/estimator/takeoff/Viewer/{sheetId} | The viewer of a sheet |
/Admin/estimator/takeoff-items/Index?projectId={id} | The takeoff items of a project with their quantities, as an AdminList |
/Admin/estimator/takeoff-items/Create?projectId={id} | Adds an item |
/Admin/estimator/takeoff-items/Edit/{itemId} | Changes an item. The kind is locked when the item has marks |
/Admin/estimator/takeoff-items/Delete/{itemId} (POST) | Deletes an item and its marks |
The module adds a Takeoff button to the rows of the sheet index (a DrawingSheet display driver at Actions:3) and a Takeoff card to the project page. The project page builds the Project shape in the DetailAdmin display type, so any feature can add a card with a Project display driver.
Viewer API
The viewer posts JSON with the antiforgery token in the RequestVerificationToken header. Each route needs CanManageAsync on the project of the sheet, and returns 400 { "error" } when the change is refused.
Request (POST /Admin/estimator/takeoff/{action}/{sheetId}) | Body | Response |
|---|---|---|
AddMark | { "itemId", "points": [{ "x", "y" }], "dropFeet": [] } | The mark: { "id", "itemId", "points", "dropFeet" } |
DeleteMark | { "value": markId } | { "deleted": true }, or 404 |
Calibrate | { "from": { "x", "y" }, "to": { "x", "y" }, "realFeet" } | { "ratio", "scaleSource": "Calibrated" } |
ClearCalibration | { "ratio", "scaleSource" } | |
Location | { "value": location } | { "location" } |
AddItem | { "name", "kind": "Count" | "Linear" | "Area" } | { "id", "name", "kind", "color" } |
The viewer page writes its start data as JSON in a script element: the PDF address, the page number, the page size, the ratio and its source, the location, the items, the marks, the color palette, the API addresses and the localized texts.
Viewer assets
| File | Built from | What it does |
|---|---|---|
scripts/takeoff-viewer.js | Assets/js/takeoff-viewer.js | The tools, the SVG overlay, the side panel and the keyboard keys |
styles/takeoff-viewer.css | Assets/scss/takeoff-viewer.scss | The viewer layout |
vendor/pdfjs/ | pdfjs-dist (copy group takeoff-pdfjs) | PDF.js 6.4.299 (Apache-2.0), which draws the page on a canvas |
The script loads PDF.js with a dynamic import() only on the viewer page, and sets GlobalWorkerOptions.workerSrc to the vendored worker. The document download route supports HTTP ranges, so PDF.js reads a large PDF in parts. When the zoom changes, the script cancels the render in progress and waits for it before it draws again, because PDF.js does not draw twice on one canvas at the same time.
Package layout
| Project | Purpose |
|---|---|
src/Core/CrestApps.OrchardCore.Estimator.Core | The takeoff models, indexes, stores, TakeoffGeometry, SheetScale, TakeoffMeasure, TakeoffRules, TakeoffSummary and TakeoffEditor |
src/Modules/CrestApps.OrchardCore.Estimator.Takeoff | The module: manifest, startup, migration, controllers, display drivers, clean-up handlers, views and the viewer assets |