Skip to main content
Technical ManualLooking for step-by-step instructions? In the User Manual: Takeoff ViewerTakeoff Items

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 IDNameCategoryDependencies
CrestApps.OrchardCore.Estimator.TakeoffEstimator TakeoffEstimatorCrestApps.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.

DocumentOne perHolds
TakeoffItemThing taken offProjectId, Name, Kind (Count, Linear or Area), Color (#rrggbb), Notes, CreatedUtc, ModifiedUtc
SheetTakeoffSheet with takeoffProjectId, 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 tableOne row perColumns
TakeoffItemIndexTakeoff itemProjectId, Name, Kind
SheetTakeoffIndexSheet takeoffProjectId, 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:

SourceWhenRatio
CalibratedThe sheet takeoff has a CalibratedRatioThe calibrated ratio
SheetThe sheet has a printed scale that DrawingScale can readThe printed scale, such as 96 for 1/8" = 1'-0"
NotToScaleThe scale says NTSNone
NoneNo scaleNone

SheetScale.Calibrate takes two points and a real length in feet, and uses DrawingScale.FromCalibration. TakeoffMeasure.Quantity gives the quantity of one mark:

KindUnitQuantity
CountEA1, on any sheet
LinearLFThe length of the polyline in feet, plus the sum of DropFeet
AreaSFThe 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​

ServiceLifetimeWhat it does
ITakeoffItemStore (TakeoffItemStore)ScopedThe items of a project, in name order
ISheetTakeoffStore (SheetTakeoffStore)ScopedThe takeoff of a sheet or a project; DeleteBySheetsAsync deletes the takeoff of sheets
TakeoffEditorScopedAddMarkAsync, 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
ProjectTakeoffSummaryScopedThe 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:

HandlerImplementsRuns when
TakeoffSheetEventHandlerIDrawingSheetEventHandler.DeletingAsync(sheets)DrawingSheetRemover deletes sheets: a sheet delete, a document delete, a document type change, and the pages left over after a split again
TakeoffProjectEventHandlerIProjectEventHandler.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​

RouteScreen
/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})BodyResponse
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​

FileBuilt fromWhat it does
scripts/takeoff-viewer.jsAssets/js/takeoff-viewer.jsThe tools, the SVG overlay, the side panel and the keyboard keys
styles/takeoff-viewer.cssAssets/scss/takeoff-viewer.scssThe 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​

ProjectPurpose
src/Core/CrestApps.OrchardCore.Estimator.CoreThe takeoff models, indexes, stores, TakeoffGeometry, SheetScale, TakeoffMeasure, TakeoffRules, TakeoffSummary and TakeoffEditor
src/Modules/CrestApps.OrchardCore.Estimator.TakeoffThe module: manifest, startup, migration, controllers, display drivers, clean-up handlers, views and the viewer assets