@sealab/mcp-server 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -1
- package/dist/tools/ai-drawing.js +563 -1
- package/dist/tools/cart-cross-check.js +182 -0
- package/dist/tools/elevation-run-segmentation.js +99 -0
- package/dist/tools/orders.js +2 -0
- package/dist/tools/placement-projection.js +119 -0
- package/dist/tools/projects.js +368 -0
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/tools/ai-drawing.test.ts +464 -44
- package/src/tools/ai-drawing.ts +750 -36
- package/src/tools/cart-cross-check.test.ts +84 -0
- package/src/tools/cart-cross-check.ts +267 -0
- package/src/tools/elevation-run-segmentation.test.ts +64 -0
- package/src/tools/elevation-run-segmentation.ts +175 -0
- package/src/tools/orders.ts +7 -3
- package/src/tools/placement-projection.test.ts +185 -0
- package/src/tools/placement-projection.ts +247 -0
- package/src/tools/projects.test.ts +135 -0
- package/src/tools/projects.ts +370 -0
package/dist/index.js
CHANGED
|
@@ -12,10 +12,11 @@ const configuration_info_1 = require("./tools/configuration-info");
|
|
|
12
12
|
const saved_settings_1 = require("./tools/saved-settings");
|
|
13
13
|
const canvas_1 = require("./tools/canvas");
|
|
14
14
|
const permissions_1 = require("./tools/permissions");
|
|
15
|
+
const projects_1 = require("./tools/projects");
|
|
15
16
|
const ai_drawing_1 = require("./tools/ai-drawing");
|
|
16
17
|
const ai_drawing_overlay_1 = require("./tools/ai-drawing-overlay");
|
|
17
18
|
const schema_normalizer_1 = require("./schema-normalizer");
|
|
18
|
-
const allTools = [...catalog_1.catalogTools, ...orders_1.orderTools, ...configuration_1.configurationTools, ...configuration_info_1.configurationInfoTools, ...saved_settings_1.savedSettingsTools, ...canvas_1.canvasTools, ...permissions_1.permissionTools, ...ai_drawing_1.aiDrawingTools, ...ai_drawing_overlay_1.aiDrawingOverlayTools];
|
|
19
|
+
const allTools = [...catalog_1.catalogTools, ...orders_1.orderTools, ...configuration_1.configurationTools, ...configuration_info_1.configurationInfoTools, ...saved_settings_1.savedSettingsTools, ...canvas_1.canvasTools, ...permissions_1.permissionTools, ...projects_1.projectTools, ...ai_drawing_1.aiDrawingTools, ...ai_drawing_overlay_1.aiDrawingOverlayTools];
|
|
19
20
|
const server = new index_js_1.Server({ name: 'sealab', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
20
21
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
21
22
|
tools: allTools.map((t) => ({
|
package/dist/tools/ai-drawing.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.aiDrawingTools = exports.AiCabinetCoordinatesDeleteInputSchema = exports.AiCabinetCoordinatesPatchInputListSchema = exports.AiCabinetCoordinatesReplaceInputSchema = exports.AiCabinetCoordinatePatchInputSchema = exports.AiCabinetCoordinateInputSchema = exports.AiCanvasCabinetPlacementsDeleteInputSchema = exports.AiCanvasCabinetPlacementsPatchInputSchema = exports.AiCanvasCabinetPlacementsByCanvasInputSchema = exports.AiCanvasCabinetPlacementsUpsertInputSchema = exports.AiCanvasCabinetPlacementsReplaceInputSchema = exports.AiCanvasCabinetPlacementPatchSchema = exports.AiCanvasCabinetPlacementInputSchema = exports.AiCanvasImageDeleteInputSchema = exports.AiCanvasImageUploadInputSchema = exports.AiCanvasImageInputSchema = exports.AiCanvasMetadataInputSchema = exports.LinkAiCartToDrawingSessionInputSchema = exports.AiDrawingSessionReviewRenderInputSchema = exports.AiReduxCartImportInputSchema = exports.AiOrderReadyPayloadClearInputSchema = exports.AiOrderReadyPayloadSetInputSchema = exports.AiOrderReadyPayloadReadInputSchema = exports.DeleteAiCabinetInputSchema = exports.ConfigureAiCabinetInputSchema = exports.DuplicateAiCabinetInputSchema = exports.AiCartCreateInputSchema = exports.AiCartSnapshotInputSchema = exports.AiCartItemPatchSchema = exports.AiCartItemInputSchema = exports.AiCartItemIdParamSchema = exports.AiCartIdParamSchema = exports.AiDrawingSessionCreateInputSchema = exports.AiCanvasMetaSchema = exports.AiCanvasProjectionCalibrationSchema = void 0;
|
|
39
|
+
exports.aiDrawingTools = exports.AiCabinetCoordinatesDeleteInputSchema = exports.AiCabinetCoordinatesPatchInputListSchema = exports.AiCabinetCoordinatesReplaceInputSchema = exports.AiCabinetCoordinatePatchInputSchema = exports.AiCabinetCoordinateInputSchema = exports.ProjectAiCanvasElevationRunInputSchema = exports.ProjectAiCanvasCabinetPlacementsInputSchema = exports.AiCanvasCabinetPlacementsDeleteInputSchema = exports.AiCanvasCabinetPlacementsPatchInputSchema = exports.AiCanvasCabinetPlacementsByCanvasInputSchema = exports.AiCanvasCabinetPlacementsUpsertInputSchema = exports.AiCanvasCabinetPlacementsReplaceInputSchema = exports.AiCanvasCabinetPlacementPatchSchema = exports.AiCanvasCabinetPlacementInputSchema = exports.AiCanvasImageDeleteInputSchema = exports.AiCanvasImageUploadInputSchema = exports.AiCanvasImageInputSchema = exports.AiCanvasMetadataInputSchema = exports.LinkAiCartToDrawingSessionInputSchema = exports.AiDrawingSessionReviewRenderInputSchema = exports.AiReduxCartImportInputSchema = exports.AiOrderReadyPayloadClearInputSchema = exports.AiOrderReadyPayloadSetInputSchema = exports.AiOrderReadyPayloadReadInputSchema = exports.DeleteAiCabinetInputSchema = exports.ConfigureAiCabinetInputSchema = exports.DuplicateAiCabinetInputSchema = exports.AiCartCreateInputSchema = exports.AiCartSnapshotInputSchema = exports.AiCartItemPatchSchema = exports.AiCartItemInputSchema = exports.AiCartItemIdParamSchema = exports.AiCartIdParamSchema = exports.AiDrawingSessionCreateInputSchema = exports.AiCanvasMetaSchema = exports.AiCanvasProjectionCalibrationSchema = void 0;
|
|
40
40
|
exports.getAiDrawingState = getAiDrawingState;
|
|
41
41
|
exports.renderAiDrawingSessionReviewTool = renderAiDrawingSessionReviewTool;
|
|
42
42
|
exports.createAiDrawingSession = createAiDrawingSession;
|
|
@@ -51,6 +51,8 @@ exports.replaceAiCanvasCabinetPlacements = replaceAiCanvasCabinetPlacements;
|
|
|
51
51
|
exports.upsertAiCanvasCabinetPlacements = upsertAiCanvasCabinetPlacements;
|
|
52
52
|
exports.patchAiCanvasCabinetPlacements = patchAiCanvasCabinetPlacements;
|
|
53
53
|
exports.deleteAiCanvasCabinetPlacements = deleteAiCanvasCabinetPlacements;
|
|
54
|
+
exports.projectAiCanvasCabinetPlacements = projectAiCanvasCabinetPlacements;
|
|
55
|
+
exports.projectAiCanvasElevationRun = projectAiCanvasElevationRun;
|
|
54
56
|
exports.replaceAiCabinetCoordinates = replaceAiCabinetCoordinates;
|
|
55
57
|
exports.patchAiCabinetCoordinates = patchAiCabinetCoordinates;
|
|
56
58
|
exports.deleteAiCabinetCoordinates = deleteAiCabinetCoordinates;
|
|
@@ -74,6 +76,9 @@ const fs = __importStar(require("fs"));
|
|
|
74
76
|
const path = __importStar(require("path"));
|
|
75
77
|
const form_data_1 = __importDefault(require("form-data"));
|
|
76
78
|
const ai_drawing_session_review_renderer_1 = require("./ai-drawing-session-review-renderer");
|
|
79
|
+
const placement_projection_1 = require("./placement-projection");
|
|
80
|
+
const elevation_run_segmentation_1 = require("./elevation-run-segmentation");
|
|
81
|
+
const cart_cross_check_1 = require("./cart-cross-check");
|
|
77
82
|
const LEGACY_DRAWING_TOOL_NAME = 'the legacy frontend drawing route';
|
|
78
83
|
const STORED_CARTS_TERM = 'stored carts';
|
|
79
84
|
const AI_CART_TOOL_BOUNDARY = 'AIDrawingTool is backend-owned and isolated. These tools do not mutate the Redux cart, ' +
|
|
@@ -723,6 +728,208 @@ exports.AiCanvasCabinetPlacementsDeleteInputSchema = AiDrawingSessionIdParamSche
|
|
|
723
728
|
canvasType: CanvasTypeSchema,
|
|
724
729
|
positionNames: zod_1.z.array(zod_1.z.string().min(1)).min(1).describe('Nonempty list of per-canvas placement positionName values to delete from exactly this canvas.'),
|
|
725
730
|
}).strict().describe('Delete selected placements from one backend-owned AIDrawingTool canvas. ' + AI_CANVAS_PLACEMENT_GUIDANCE);
|
|
731
|
+
// ---------------------------------------------------------------------------
|
|
732
|
+
// Deterministic placement projection (project_ai_canvas_cabinet_placements)
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
const AI_PLACEMENT_PROJECTION_GUIDANCE = 'Compute exact per-canvas placement rectangles from one pixels-per-inch calibration plus each cabinet\'s real ' +
|
|
735
|
+
'inch dimensions, instead of eyeballing raw pixel boxes. This is the preferred way to create placements: a ' +
|
|
736
|
+
'vision model cannot reliably guess precise pixel rectangles, and per-box guesses drift, overlap, and double up ' +
|
|
737
|
+
'across rows of similar cabinets. Workflow: (1) read a visible dimension string and the finished-floor/Z0 line on ' +
|
|
738
|
+
'the actual source image to establish horizontalPixelsPerInch, verticalPixelsPerInch, floorZ0Px, and one horizontal ' +
|
|
739
|
+
'origin reference; (2) supply each cabinet face by its real along-wall start, width, body height, and ' +
|
|
740
|
+
'finished-floor-to-bottom height in inches (read width/height from the catalog/order-ready payload and the ' +
|
|
741
|
+
'drawing dimension strings, never from guessed pixels); (3) this tool projects every rectangle from the same scale ' +
|
|
742
|
+
'so the whole run stays aligned and gap-correct. imageSizePx, basisImageWidthPx, and basisImageHeightPx must all ' +
|
|
743
|
+
'equal the true pixel size of the source image the calibration was measured on, so the review renderer rescales ' +
|
|
744
|
+
'consistently. Elevation faces project along-wall position and height-above-floor; plan footprints project ' +
|
|
745
|
+
'room/global center X/Y with width/depth. After projecting, render_ai_drawing_session_review and verify against the ' +
|
|
746
|
+
'source pixels. ' +
|
|
747
|
+
AI_CANVAS_PLACEMENT_GUIDANCE;
|
|
748
|
+
const ProjectionImageSizePxSchema = zod_1.z.object({
|
|
749
|
+
width: zod_1.z.number().finite().positive().describe('True source image width in pixels.'),
|
|
750
|
+
height: zod_1.z.number().finite().positive().describe('True source image height in pixels.'),
|
|
751
|
+
}).strict();
|
|
752
|
+
const ProjectionPixelPointSchema = zod_1.z.object({
|
|
753
|
+
x: zod_1.z.number().finite().describe('Image pixel X.'),
|
|
754
|
+
y: zod_1.z.number().finite().describe('Image pixel Y.'),
|
|
755
|
+
}).strict();
|
|
756
|
+
const ProjectorElevationCalibrationSchema = zod_1.z.object({
|
|
757
|
+
canvasType: ElevationCanvasTypeSchema.describe('Elevation canvas this calibration applies to.'),
|
|
758
|
+
imageSizePx: ProjectionImageSizePxSchema.describe('True pixel size of the elevation source image the calibration was measured on.'),
|
|
759
|
+
horizontalPixelsPerInch: zod_1.z.number().finite().positive().describe('Pixels per inch along the wall, derived from a visible horizontal dimension string.'),
|
|
760
|
+
verticalPixelsPerInch: zod_1.z.number().finite().positive().describe('Pixels per inch vertically, derived from a visible vertical dimension (e.g. ceiling height A.F.F.).'),
|
|
761
|
+
floorZ0Px: zod_1.z.number().finite().describe('Pixel Y of the finished-floor / Z0 reference line.'),
|
|
762
|
+
horizontalOriginPx: ProjectionPixelPointSchema.describe('A pixel point whose along-wall inch coordinate is known (e.g. the left wall corner).'),
|
|
763
|
+
horizontalOriginCoordinateInches: zod_1.z.number().finite().default(0).describe('The along-wall inch coordinate that horizontalOriginPx maps to. Usually 0 at the chosen origin.'),
|
|
764
|
+
horizontalAxisPositiveDirection: AxisHorizontalDirectionSchema.describe('Screen direction in which the along-wall inch coordinate increases.'),
|
|
765
|
+
verticalAxisPositiveDirection: AxisVerticalDirectionSchema.default('up').describe('Screen direction in which height-above-floor increases. Normally "up".'),
|
|
766
|
+
}).strict();
|
|
767
|
+
const ProjectorPlanCalibrationSchema = zod_1.z.object({
|
|
768
|
+
canvasType: zod_1.z.literal('plan'),
|
|
769
|
+
imageSizePx: ProjectionImageSizePxSchema.describe('True pixel size of the plan source image the calibration was measured on.'),
|
|
770
|
+
pixelsPerInch: zod_1.z.number().finite().positive().describe('Pixels per room inch on the plan.'),
|
|
771
|
+
roomOriginPx: ProjectionPixelPointSchema.describe('Pixel point that maps to room/global origin (0, 0).'),
|
|
772
|
+
xPositiveDirection: AxisHorizontalDirectionSchema.describe('Screen direction in which room X increases.'),
|
|
773
|
+
yPositiveDirection: AxisVerticalDirectionSchema.describe('Screen direction in which room Y increases.'),
|
|
774
|
+
}).strict();
|
|
775
|
+
const ProjectorPlacementSpecSchema = zod_1.z.object({
|
|
776
|
+
aiCartItemId: zod_1.z.string().uuid().optional().describe('Optional backend AI cart item UUID. Required unless positionName exactly matches an active AI cart item.'),
|
|
777
|
+
positionName: zod_1.z.string().min(1).describe('Physical AI cart item positionName and per-canvas placement identity.'),
|
|
778
|
+
// Elevation fields
|
|
779
|
+
horizontalStartInches: zod_1.z.number().finite().optional().describe('Elevation only: left edge of the face along the wall, in the wall\'s horizontal inch coordinate.'),
|
|
780
|
+
heightInches: zod_1.z.number().finite().positive().optional().describe('Elevation only: cabinet face height in inches (base cabinet body height excludes toekick and countertop).'),
|
|
781
|
+
bottomFromFloorInches: zod_1.z.number().finite().nonnegative().default(0).describe('Elevation only: finished-floor-to-bottom of this face in inches. 0 for a base cabinet on the floor.'),
|
|
782
|
+
// Plan fields (center mode)
|
|
783
|
+
centerXInches: zod_1.z.number().finite().optional().describe('Plan center mode: cabinet center X in room/global inches.'),
|
|
784
|
+
centerYInches: zod_1.z.number().finite().optional().describe('Plan center mode: cabinet center Y in room/global inches.'),
|
|
785
|
+
depthInches: zod_1.z.number().finite().positive().optional().describe('Plan only: cabinet front-to-back depth in inches.'),
|
|
786
|
+
rotation: CoordinateRotationSchema.optional().describe('Plan center mode: wall rotation 0/90/180/270; swaps width/depth axes.'),
|
|
787
|
+
// Plan fields (wall-anchored mode — preferred: pins the back edge to the wall so the box cannot bleed into the wall)
|
|
788
|
+
wallBackEdgeInches: zod_1.z.number().finite().optional().describe('Plan wall-anchored mode: room-inch coordinate of the cabinet BACK edge on the depth axis — i.e. the wall inner face. The footprint grows into the room from here.'),
|
|
789
|
+
depthAxis: zod_1.z.enum(['x', 'y']).optional().describe('Plan wall-anchored mode: room axis perpendicular to the wall (the depth direction).'),
|
|
790
|
+
depthDirection: zod_1.z.enum(['positive', 'negative']).optional().describe('Plan wall-anchored mode: direction in room coordinates that depth grows from the wall into the room.'),
|
|
791
|
+
alongWallStartInches: zod_1.z.number().finite().optional().describe('Plan wall-anchored mode: start of the run along the wall, in room inches on the non-depth axis.'),
|
|
792
|
+
// Common
|
|
793
|
+
widthInches: zod_1.z.number().finite().positive().describe('Cabinet width in inches (along the face / along the wall).'),
|
|
794
|
+
}).strict();
|
|
795
|
+
exports.ProjectAiCanvasCabinetPlacementsInputSchema = AiDrawingSessionIdParamSchema.extend({
|
|
796
|
+
canvasType: CanvasTypeSchema,
|
|
797
|
+
elevationCalibration: ProjectorElevationCalibrationSchema.optional().describe('Required when canvasType is an elevation (north/south/east/west).'),
|
|
798
|
+
planCalibration: ProjectorPlanCalibrationSchema.optional().describe('Required when canvasType is plan.'),
|
|
799
|
+
placements: zod_1.z.array(ProjectorPlacementSpecSchema).min(1).describe('Cabinet specs in real inches. Elevation specs need horizontalStartInches + widthInches + heightInches ' +
|
|
800
|
+
'(+ optional bottomFromFloorInches). Plan specs need centerXInches + centerYInches + widthInches + depthInches ' +
|
|
801
|
+
'(+ optional rotation).'),
|
|
802
|
+
writeMode: zod_1.z.enum(['replace', 'upsert', 'return_only']).default('upsert').describe('replace: overwrite all placements on this canvas. upsert: create/update by positionName without deleting others. ' +
|
|
803
|
+
'return_only: compute and return rectangles without writing to the backend (useful for a dry run before committing).'),
|
|
804
|
+
crossCheckCartItems: zod_1.z.boolean().default(true).describe('When true (default), verify each placement against the active AI cart manifest before writing: every placement ' +
|
|
805
|
+
'must match a real cart item and its width/height must match that cabinet (within tolerance). Blocks the write on ' +
|
|
806
|
+
'mismatch so a box cannot be stretched to cover multiple cabinets. Set false only for a calibration dry run.'),
|
|
807
|
+
widthToleranceInches: zod_1.z.number().finite().positive().optional().describe('Allowed width difference vs the cart item (default 1.0").'),
|
|
808
|
+
heightToleranceInches: zod_1.z.number().finite().positive().optional().describe('Allowed height/depth difference vs the cart item (default 1.5").'),
|
|
809
|
+
source: PlacementSourceSchema.optional(),
|
|
810
|
+
}).strict().superRefine((input, ctx) => {
|
|
811
|
+
const isPlan = input.canvasType === 'plan';
|
|
812
|
+
if (isPlan && !input.planCalibration) {
|
|
813
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['planCalibration'], message: 'planCalibration is required when canvasType is plan.' });
|
|
814
|
+
}
|
|
815
|
+
if (isPlan && input.elevationCalibration) {
|
|
816
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['elevationCalibration'], message: 'Do not provide elevationCalibration for a plan canvas.' });
|
|
817
|
+
}
|
|
818
|
+
if (!isPlan && !input.elevationCalibration) {
|
|
819
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['elevationCalibration'], message: 'elevationCalibration is required when canvasType is an elevation.' });
|
|
820
|
+
}
|
|
821
|
+
if (!isPlan && input.planCalibration) {
|
|
822
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['planCalibration'], message: 'Do not provide planCalibration for an elevation canvas.' });
|
|
823
|
+
}
|
|
824
|
+
if (!isPlan && input.elevationCalibration && input.elevationCalibration.canvasType !== input.canvasType) {
|
|
825
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['elevationCalibration', 'canvasType'], message: 'elevationCalibration.canvasType must match canvasType.' });
|
|
826
|
+
}
|
|
827
|
+
input.placements.forEach((placement, index) => {
|
|
828
|
+
if (isPlan) {
|
|
829
|
+
if (placement.depthInches === undefined) {
|
|
830
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'depthInches'], message: 'depthInches is required for plan placements.' });
|
|
831
|
+
}
|
|
832
|
+
const hasWallAnchor = placement.wallBackEdgeInches !== undefined ||
|
|
833
|
+
placement.depthAxis !== undefined ||
|
|
834
|
+
placement.depthDirection !== undefined ||
|
|
835
|
+
placement.alongWallStartInches !== undefined;
|
|
836
|
+
const hasCenter = placement.centerXInches !== undefined || placement.centerYInches !== undefined;
|
|
837
|
+
if (hasWallAnchor) {
|
|
838
|
+
// Wall-anchored mode (preferred): all four anchor fields required, and no center fields.
|
|
839
|
+
for (const field of ['wallBackEdgeInches', 'depthAxis', 'depthDirection', 'alongWallStartInches']) {
|
|
840
|
+
if (placement[field] === undefined) {
|
|
841
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, field], message: `${field} is required for wall-anchored plan placements.` });
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (hasCenter) {
|
|
845
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'centerXInches'], message: 'Use either wall-anchored fields or centerXInches/centerYInches, not both.' });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
// Center mode (fallback).
|
|
850
|
+
if (placement.centerXInches === undefined) {
|
|
851
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'centerXInches'], message: 'Provide wall-anchored fields (preferred) or centerXInches for plan placements.' });
|
|
852
|
+
}
|
|
853
|
+
if (placement.centerYInches === undefined) {
|
|
854
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'centerYInches'], message: 'Provide wall-anchored fields (preferred) or centerYInches for plan placements.' });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
if (placement.horizontalStartInches === undefined) {
|
|
860
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'horizontalStartInches'], message: 'horizontalStartInches is required for elevation placements.' });
|
|
861
|
+
}
|
|
862
|
+
if (placement.heightInches === undefined) {
|
|
863
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'heightInches'], message: 'heightInches is required for elevation placements.' });
|
|
864
|
+
}
|
|
865
|
+
if (placement.rotation !== undefined && placement.rotation !== 0) {
|
|
866
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['placements', index, 'rotation'], message: AI_CANVAS_PLACEMENT_ROTATION_GUIDANCE });
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
validateUniquePositionNamesForCanvas(input.placements, ctx);
|
|
871
|
+
}).describe('Project exact AIDrawingTool per-canvas cabinet placement rectangles from a pixels-per-inch calibration and real ' +
|
|
872
|
+
'cabinet inch dimensions, then optionally write them. ' + AI_PLACEMENT_PROJECTION_GUIDANCE);
|
|
873
|
+
const AI_ELEVATION_RUN_SEGMENTATION_GUIDANCE = 'Decompose each dimensioned elevation run into its individual physical cabinets BEFORE placing boxes, so each ' +
|
|
874
|
+
'cabinet gets its own rectangle instead of one box spanning a whole run. This fixes under-segmentation, where a ' +
|
|
875
|
+
'multi-cabinet run (for example a 3\'-0" "EQ A | EQ A" upper that is actually two double-door cabinets) is wrongly ' +
|
|
876
|
+
'treated as a single cabinet. Workflow per run: (1) read the primary dimension string for the run total and the ' +
|
|
877
|
+
'secondary chain (EQ / EQ A / EQ B subdivisions) plus the "2" TYP." reveals and the visible vertical seams between ' +
|
|
878
|
+
'cabinets and doors; (2) decide how many physical cabinets the run contains — a vertical seam with its own stiles ' +
|
|
879
|
+
'and a separate cart item is a separate cabinet, whereas two doors sharing one box with no center stile gap are one ' +
|
|
880
|
+
'double-door cabinet; (3) enumerate the modules left to right, each with its real width and an explicit frontType ' +
|
|
881
|
+
'(single_door, double_door, drawer_stack, etc.) and its own positionName/aiCartItemId; (4) this tool tiles them ' +
|
|
882
|
+
'contiguously, projects one rectangle per module from the shared calibration, and returns segmentationWarnings when ' +
|
|
883
|
+
'a module looks too wide for one cabinet, a door type is implausible for its width, the module count is fewer than ' +
|
|
884
|
+
'the equal subdivisions you read, or the widths do not match the run dimension. Resolve every segmentationWarning ' +
|
|
885
|
+
'by re-reading the source drawing and creating the correct number of cabinets (each with its own AI cart item) ' +
|
|
886
|
+
'before reporting completion. Each module must correspond to a real AI cart item; do not invent modules to satisfy ' +
|
|
887
|
+
'the dimension total. ' +
|
|
888
|
+
AI_PLACEMENT_PROJECTION_GUIDANCE;
|
|
889
|
+
const ProjectorElevationRunModuleSchema = zod_1.z.object({
|
|
890
|
+
positionName: zod_1.z.string().min(1).describe('Physical AI cart item positionName / placement identity for this single cabinet.'),
|
|
891
|
+
aiCartItemId: zod_1.z.string().uuid().optional().describe('Backend AI cart item UUID for this cabinet. Required unless positionName matches an active AI cart item.'),
|
|
892
|
+
widthInches: zod_1.z.number().finite().positive().describe('Real cabinet width in inches for this one module.'),
|
|
893
|
+
heightInches: zod_1.z.number().finite().positive().describe('Real cabinet face height in inches (base body height excludes toekick/countertop).'),
|
|
894
|
+
bottomFromFloorInches: zod_1.z.number().finite().nonnegative().default(0).describe('Finished-floor-to-bottom of this face in inches.'),
|
|
895
|
+
frontType: zod_1.z.enum([
|
|
896
|
+
'single_door',
|
|
897
|
+
'double_door',
|
|
898
|
+
'pair_of_doors',
|
|
899
|
+
'drawer_stack',
|
|
900
|
+
'door_over_drawer',
|
|
901
|
+
'open_shelf',
|
|
902
|
+
'appliance_panel',
|
|
903
|
+
'filler',
|
|
904
|
+
]).describe('Front type for this module. Declaring it forces the single-vs-double-door determination per cabinet.'),
|
|
905
|
+
}).strict();
|
|
906
|
+
const ProjectorElevationRunSchema = zod_1.z.object({
|
|
907
|
+
runStartInches: zod_1.z.number().finite().describe('Along-wall inch coordinate of this run\'s left edge, in the calibration horizontal coordinate.'),
|
|
908
|
+
modules: zod_1.z.array(ProjectorElevationRunModuleSchema).min(1).describe('Ordered left-to-right cabinets that make up this run.'),
|
|
909
|
+
expectedRunTotalInches: zod_1.z.number().finite().positive().optional().describe('Run total from the primary dimension string (e.g. 3\'-0" => 36). Used to warn when module widths do not add up.'),
|
|
910
|
+
declaredSubdivisionCount: zod_1.z.number().int().positive().optional().describe('Number of equal subdivisions you read in the secondary chain (e.g. "EQ A | EQ A" => 2). Warns if you provide fewer modules.'),
|
|
911
|
+
revealBetweenModulesInches: zod_1.z.number().finite().nonnegative().optional().describe('Reveal/gap between adjacent cabinets in inches (e.g. "2" TYP.").'),
|
|
912
|
+
maxSingleCabinetWidthInches: zod_1.z.number().finite().positive().optional().describe('Width above which a module is flagged as suspected multiple cabinets. Default 48.'),
|
|
913
|
+
}).strict();
|
|
914
|
+
exports.ProjectAiCanvasElevationRunInputSchema = AiDrawingSessionIdParamSchema.extend({
|
|
915
|
+
canvasType: ElevationCanvasTypeSchema.describe('Elevation canvas (north/south/east/west). Run segmentation is elevation-only.'),
|
|
916
|
+
elevationCalibration: ProjectorElevationCalibrationSchema,
|
|
917
|
+
runs: zod_1.z.array(ProjectorElevationRunSchema).min(1).describe('One or more dimensioned runs to decompose and place.'),
|
|
918
|
+
writeMode: zod_1.z.enum(['replace', 'upsert', 'return_only']).default('upsert').describe('replace: overwrite all placements on this canvas. upsert: create/update by positionName. return_only: dry run.'),
|
|
919
|
+
crossCheckCartItems: zod_1.z.boolean().default(true).describe('When true (default), verify each module against the active AI cart manifest before writing: every module must ' +
|
|
920
|
+
'match a real cart item of matching width/height. Blocks the write on mismatch, so a run can only hold as many ' +
|
|
921
|
+
'cabinets as there are real items of the right size. Set false only for a calibration dry run.'),
|
|
922
|
+
widthToleranceInches: zod_1.z.number().finite().positive().optional().describe('Allowed width difference vs the cart item (default 1.0").'),
|
|
923
|
+
heightToleranceInches: zod_1.z.number().finite().positive().optional().describe('Allowed height difference vs the cart item (default 1.5").'),
|
|
924
|
+
source: PlacementSourceSchema.optional(),
|
|
925
|
+
}).strict().superRefine((input, ctx) => {
|
|
926
|
+
if (input.elevationCalibration.canvasType !== input.canvasType) {
|
|
927
|
+
ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['elevationCalibration', 'canvasType'], message: 'elevationCalibration.canvasType must match canvasType.' });
|
|
928
|
+
}
|
|
929
|
+
const allPositionNames = input.runs.flatMap((run) => run.modules.map((m) => m.positionName));
|
|
930
|
+
validateUniquePositionNamesForCanvas(allPositionNames.map((positionName) => ({ positionName })), ctx);
|
|
931
|
+
}).describe('Decompose dimensioned elevation runs into individual cabinets and project one placement rectangle per cabinet. ' +
|
|
932
|
+
AI_ELEVATION_RUN_SEGMENTATION_GUIDANCE);
|
|
726
933
|
const AiCoordinateDerivationEvidenceSchema = zod_1.z.object({
|
|
727
934
|
basis: zod_1.z.enum(['validated_plan_placement', 'user_confirmed']).describe('How this canonical x/y/z coordinate was derived. MCP must not write canonical coordinates from guessed elevation rectangles.'),
|
|
728
935
|
sourceCanvasType: zod_1.z.literal('plan').describe('Canonical x/y coordinates must be derived from validated plan placement geometry, not elevation-local pixels.'),
|
|
@@ -773,8 +980,31 @@ function validatePlacementRotationsForCanvas(canvasType, placements, ctx) {
|
|
|
773
980
|
}
|
|
774
981
|
});
|
|
775
982
|
}
|
|
983
|
+
function validateUniquePositionNamesForCanvas(placements, ctx) {
|
|
984
|
+
const seenIndexByPositionName = new Map();
|
|
985
|
+
placements.forEach((placement, index) => {
|
|
986
|
+
const positionName = placement.positionName;
|
|
987
|
+
if (positionName === undefined) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const firstIndex = seenIndexByPositionName.get(positionName);
|
|
991
|
+
if (firstIndex !== undefined) {
|
|
992
|
+
ctx.addIssue({
|
|
993
|
+
code: zod_1.z.ZodIssueCode.custom,
|
|
994
|
+
path: ['placements', index, 'positionName'],
|
|
995
|
+
message: `Duplicate positionName "${positionName}" in one canvas write. Each physical cabinet must have exactly ` +
|
|
996
|
+
'one placement per canvas; duplicates render as stacked/doubled boxes. Use one placement per positionName ' +
|
|
997
|
+
'on this canvas and reuse the same positionName on other canvases instead.',
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
seenIndexByPositionName.set(positionName, index);
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
776
1005
|
function validatePlacementOperationForCanvas(canvasType, placements, ctx) {
|
|
777
1006
|
validatePlacementRotationsForCanvas(canvasType, placements, ctx);
|
|
1007
|
+
validateUniquePositionNamesForCanvas(placements, ctx);
|
|
778
1008
|
}
|
|
779
1009
|
exports.AiCabinetCoordinateInputSchema = zod_1.z.object({
|
|
780
1010
|
positionName: zod_1.z.string().min(1).describe('Required AI drawing coordinate identity. It is unique within an AI drawing session.'),
|
|
@@ -1200,6 +1430,319 @@ async function deleteAiCanvasCabinetPlacements(input) {
|
|
|
1200
1430
|
return 'Unexpected error deleting AI canvas cabinet placements.';
|
|
1201
1431
|
}
|
|
1202
1432
|
}
|
|
1433
|
+
async function fetchActiveAiCartItems(sessionId) {
|
|
1434
|
+
try {
|
|
1435
|
+
const { data } = await api_client_1.aiDrawingClient.get(`/sessions/${sessionId}/state`);
|
|
1436
|
+
const items = data?.activeCart?.items;
|
|
1437
|
+
if (!Array.isArray(items)) {
|
|
1438
|
+
return {
|
|
1439
|
+
error: 'Could not read an active AI cart for this session, so placements cannot be cross-checked against the ' +
|
|
1440
|
+
'manifest. Create the AI cart, link it to the session, and sync items first (create_ai_cart / ' +
|
|
1441
|
+
'link_ai_cart_to_drawing_session / sync_ai_cart_snapshot), or rerun with crossCheckCartItems false for a dry run.',
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
return { items: items };
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
try {
|
|
1448
|
+
(0, api_client_1.handleAxiosError)(error);
|
|
1449
|
+
}
|
|
1450
|
+
catch (e) {
|
|
1451
|
+
return { error: e.message };
|
|
1452
|
+
}
|
|
1453
|
+
return { error: 'Unexpected error reading AI drawing session state for cart cross-check.' };
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function buildCrossCheckBlockedResult(sessionId, canvasType, result, extra) {
|
|
1457
|
+
return JSON.stringify({
|
|
1458
|
+
sessionId,
|
|
1459
|
+
canvasType,
|
|
1460
|
+
wrote: false,
|
|
1461
|
+
crossCheckBlocked: true,
|
|
1462
|
+
matchedCartItemCount: result.matchedCount,
|
|
1463
|
+
cartCrossCheckBlocking: result.blocking,
|
|
1464
|
+
cartCrossCheckWarnings: result.warnings,
|
|
1465
|
+
...extra,
|
|
1466
|
+
note: 'Write blocked: placements did not reconcile with the active AI cart manifest. Each placement must match a ' +
|
|
1467
|
+
'real cart item and its real-world dimensions. Fix the cabinet count/widths (or the cart items) and rerun. ' +
|
|
1468
|
+
'This guarantees a run cannot hold more cabinets than there are real items of the right size.',
|
|
1469
|
+
}, null, 2);
|
|
1470
|
+
}
|
|
1471
|
+
async function projectAiCanvasCabinetPlacements(input) {
|
|
1472
|
+
const isPlan = input.canvasType === 'plan';
|
|
1473
|
+
const imageSizePx = isPlan
|
|
1474
|
+
? input.planCalibration.imageSizePx
|
|
1475
|
+
: input.elevationCalibration.imageSizePx;
|
|
1476
|
+
const elevationCalibration = isPlan
|
|
1477
|
+
? undefined
|
|
1478
|
+
: {
|
|
1479
|
+
canvasType: input.elevationCalibration.canvasType,
|
|
1480
|
+
imageSizePx: input.elevationCalibration.imageSizePx,
|
|
1481
|
+
horizontalPixelsPerInch: input.elevationCalibration.horizontalPixelsPerInch,
|
|
1482
|
+
verticalPixelsPerInch: input.elevationCalibration.verticalPixelsPerInch,
|
|
1483
|
+
floorZ0Px: input.elevationCalibration.floorZ0Px,
|
|
1484
|
+
horizontalOriginPx: input.elevationCalibration.horizontalOriginPx,
|
|
1485
|
+
horizontalOriginCoordinateInches: input.elevationCalibration.horizontalOriginCoordinateInches,
|
|
1486
|
+
horizontalAxisPositiveDirection: input.elevationCalibration.horizontalAxisPositiveDirection,
|
|
1487
|
+
verticalAxisPositiveDirection: input.elevationCalibration.verticalAxisPositiveDirection,
|
|
1488
|
+
};
|
|
1489
|
+
const planCalibration = isPlan
|
|
1490
|
+
? {
|
|
1491
|
+
canvasType: 'plan',
|
|
1492
|
+
imageSizePx: input.planCalibration.imageSizePx,
|
|
1493
|
+
pixelsPerInch: input.planCalibration.pixelsPerInch,
|
|
1494
|
+
roomOriginPx: input.planCalibration.roomOriginPx,
|
|
1495
|
+
xPositiveDirection: input.planCalibration.xPositiveDirection,
|
|
1496
|
+
yPositiveDirection: input.planCalibration.yPositiveDirection,
|
|
1497
|
+
}
|
|
1498
|
+
: undefined;
|
|
1499
|
+
const outOfBounds = [];
|
|
1500
|
+
const projectedPlacements = input.placements.map((spec) => {
|
|
1501
|
+
const isWallAnchored = isPlan && spec.wallBackEdgeInches !== undefined;
|
|
1502
|
+
const projected = isWallAnchored
|
|
1503
|
+
? (0, placement_projection_1.projectPlanWallAnchoredPlacement)({
|
|
1504
|
+
wallBackEdgeInches: spec.wallBackEdgeInches,
|
|
1505
|
+
depthAxis: spec.depthAxis,
|
|
1506
|
+
depthDirection: spec.depthDirection,
|
|
1507
|
+
alongWallStartInches: spec.alongWallStartInches,
|
|
1508
|
+
widthInches: spec.widthInches,
|
|
1509
|
+
depthInches: spec.depthInches,
|
|
1510
|
+
}, planCalibration)
|
|
1511
|
+
: isPlan
|
|
1512
|
+
? (0, placement_projection_1.projectPlanPlacement)({
|
|
1513
|
+
centerXInches: spec.centerXInches,
|
|
1514
|
+
centerYInches: spec.centerYInches,
|
|
1515
|
+
widthInches: spec.widthInches,
|
|
1516
|
+
depthInches: spec.depthInches,
|
|
1517
|
+
rotation: spec.rotation,
|
|
1518
|
+
}, planCalibration)
|
|
1519
|
+
: (0, placement_projection_1.projectElevationPlacement)({
|
|
1520
|
+
horizontalStartInches: spec.horizontalStartInches,
|
|
1521
|
+
widthInches: spec.widthInches,
|
|
1522
|
+
heightInches: spec.heightInches,
|
|
1523
|
+
bottomFromFloorInches: spec.bottomFromFloorInches,
|
|
1524
|
+
}, elevationCalibration);
|
|
1525
|
+
if ((0, placement_projection_1.isProjectedPlacementOutsideImage)(projected, imageSizePx)) {
|
|
1526
|
+
outOfBounds.push({
|
|
1527
|
+
positionName: spec.positionName,
|
|
1528
|
+
reason: `Projected rectangle (centerX ${projected.centerX}, centerY ${projected.centerY}, width ${projected.width}, ` +
|
|
1529
|
+
`height ${projected.height}) extends outside the ${imageSizePx.width}x${imageSizePx.height} calibration image. ` +
|
|
1530
|
+
'Re-check the calibration (pixels-per-inch, origin, floor line) or the cabinet inch dimensions against the source drawing.',
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return {
|
|
1534
|
+
aiCartItemId: spec.aiCartItemId,
|
|
1535
|
+
positionName: spec.positionName,
|
|
1536
|
+
centerX: projected.centerX,
|
|
1537
|
+
centerY: projected.centerY,
|
|
1538
|
+
width: projected.width,
|
|
1539
|
+
height: projected.height,
|
|
1540
|
+
rotation: projected.rotation,
|
|
1541
|
+
placementSpace: 'IMAGE_PIXELS',
|
|
1542
|
+
basisImageWidthPx: imageSizePx.width,
|
|
1543
|
+
basisImageHeightPx: imageSizePx.height,
|
|
1544
|
+
source: input.source,
|
|
1545
|
+
};
|
|
1546
|
+
});
|
|
1547
|
+
let crossCheckExtra;
|
|
1548
|
+
if (input.crossCheckCartItems) {
|
|
1549
|
+
const cart = await fetchActiveAiCartItems(input.sessionId);
|
|
1550
|
+
if ('error' in cart) {
|
|
1551
|
+
return cart.error;
|
|
1552
|
+
}
|
|
1553
|
+
const checkSpecs = input.placements.map((spec) => ({
|
|
1554
|
+
positionName: spec.positionName,
|
|
1555
|
+
aiCartItemId: spec.aiCartItemId,
|
|
1556
|
+
widthInches: spec.widthInches,
|
|
1557
|
+
heightInches: isPlan ? undefined : spec.heightInches,
|
|
1558
|
+
depthInches: isPlan ? spec.depthInches : undefined,
|
|
1559
|
+
}));
|
|
1560
|
+
const result = (0, cart_cross_check_1.crossCheckPlacementsAgainstCart)(checkSpecs, cart.items, {
|
|
1561
|
+
widthToleranceInches: input.widthToleranceInches,
|
|
1562
|
+
heightToleranceInches: input.heightToleranceInches,
|
|
1563
|
+
depthToleranceInches: input.heightToleranceInches,
|
|
1564
|
+
reportCoverage: input.writeMode === 'replace',
|
|
1565
|
+
});
|
|
1566
|
+
if (result.blocking.length > 0) {
|
|
1567
|
+
return buildCrossCheckBlockedResult(input.sessionId, input.canvasType, result);
|
|
1568
|
+
}
|
|
1569
|
+
crossCheckExtra = {
|
|
1570
|
+
matchedCartItemCount: result.matchedCount,
|
|
1571
|
+
cartCrossCheckWarnings: result.warnings,
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
return commitProjectedPlacements({
|
|
1575
|
+
sessionId: input.sessionId,
|
|
1576
|
+
canvasType: input.canvasType,
|
|
1577
|
+
writeMode: input.writeMode,
|
|
1578
|
+
imageSizePx,
|
|
1579
|
+
projectedPlacements,
|
|
1580
|
+
outOfBounds,
|
|
1581
|
+
extra: crossCheckExtra,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
async function commitProjectedPlacements(params) {
|
|
1585
|
+
const { sessionId, canvasType, writeMode, imageSizePx, projectedPlacements, outOfBounds, extra } = params;
|
|
1586
|
+
if (writeMode === 'return_only') {
|
|
1587
|
+
return JSON.stringify({
|
|
1588
|
+
sessionId,
|
|
1589
|
+
canvasType,
|
|
1590
|
+
writeMode,
|
|
1591
|
+
wrote: false,
|
|
1592
|
+
basisImageWidthPx: imageSizePx.width,
|
|
1593
|
+
basisImageHeightPx: imageSizePx.height,
|
|
1594
|
+
projectedPlacements: projectedPlacements.map(toBackendPlacementPayload),
|
|
1595
|
+
outOfBoundsWarnings: outOfBounds,
|
|
1596
|
+
...extra,
|
|
1597
|
+
note: 'return_only dry run: nothing was written. Inspect projectedPlacements, outOfBoundsWarnings, and any ' +
|
|
1598
|
+
'segmentationWarnings, then rerun with writeMode upsert or replace to persist. After writing, call ' +
|
|
1599
|
+
'render_ai_drawing_session_review and verify against the source image.',
|
|
1600
|
+
}, null, 2);
|
|
1601
|
+
}
|
|
1602
|
+
try {
|
|
1603
|
+
const path = `/sessions/${sessionId}/placements/${canvasType}`;
|
|
1604
|
+
const body = { placements: projectedPlacements.map(toBackendPlacementPayload) };
|
|
1605
|
+
const { data } = writeMode === 'replace' ? await api_client_1.aiDrawingClient.put(path, body) : await api_client_1.aiDrawingClient.post(path, body);
|
|
1606
|
+
return JSON.stringify({
|
|
1607
|
+
sessionId,
|
|
1608
|
+
canvasType,
|
|
1609
|
+
writeMode,
|
|
1610
|
+
wrote: true,
|
|
1611
|
+
basisImageWidthPx: imageSizePx.width,
|
|
1612
|
+
basisImageHeightPx: imageSizePx.height,
|
|
1613
|
+
projectedPlacementCount: projectedPlacements.length,
|
|
1614
|
+
outOfBoundsWarnings: outOfBounds,
|
|
1615
|
+
...extra,
|
|
1616
|
+
backendResponse: data,
|
|
1617
|
+
note: 'Placements were projected from calibration and written. Resolve any segmentationWarnings, then call ' +
|
|
1618
|
+
'render_ai_drawing_session_review and verify each rectangle against the visible source linework before reporting completion.',
|
|
1619
|
+
}, null, 2);
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
try {
|
|
1623
|
+
(0, api_client_1.handleAxiosError)(error);
|
|
1624
|
+
}
|
|
1625
|
+
catch (e) {
|
|
1626
|
+
return e.message;
|
|
1627
|
+
}
|
|
1628
|
+
return 'Unexpected error projecting and writing AI canvas cabinet placements.';
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
async function projectAiCanvasElevationRun(input) {
|
|
1632
|
+
const cal = input.elevationCalibration;
|
|
1633
|
+
const imageSizePx = cal.imageSizePx;
|
|
1634
|
+
const elevationCalibration = {
|
|
1635
|
+
canvasType: cal.canvasType,
|
|
1636
|
+
imageSizePx: cal.imageSizePx,
|
|
1637
|
+
horizontalPixelsPerInch: cal.horizontalPixelsPerInch,
|
|
1638
|
+
verticalPixelsPerInch: cal.verticalPixelsPerInch,
|
|
1639
|
+
floorZ0Px: cal.floorZ0Px,
|
|
1640
|
+
horizontalOriginPx: cal.horizontalOriginPx,
|
|
1641
|
+
horizontalOriginCoordinateInches: cal.horizontalOriginCoordinateInches,
|
|
1642
|
+
horizontalAxisPositiveDirection: cal.horizontalAxisPositiveDirection,
|
|
1643
|
+
verticalAxisPositiveDirection: cal.verticalAxisPositiveDirection,
|
|
1644
|
+
};
|
|
1645
|
+
// Decompose each run into tiled modules — one placement per physical cabinet.
|
|
1646
|
+
const segmentationWarnings = [];
|
|
1647
|
+
const segmentedModules = [];
|
|
1648
|
+
input.runs.forEach((run, runIndex) => {
|
|
1649
|
+
const result = (0, elevation_run_segmentation_1.segmentElevationRun)({
|
|
1650
|
+
runStartInches: run.runStartInches,
|
|
1651
|
+
modules: run.modules,
|
|
1652
|
+
expectedRunTotalInches: run.expectedRunTotalInches,
|
|
1653
|
+
declaredSubdivisionCount: run.declaredSubdivisionCount,
|
|
1654
|
+
revealBetweenModulesInches: run.revealBetweenModulesInches,
|
|
1655
|
+
maxSingleCabinetWidthInches: run.maxSingleCabinetWidthInches,
|
|
1656
|
+
});
|
|
1657
|
+
for (const warning of result.warnings) {
|
|
1658
|
+
segmentationWarnings.push(`run[${runIndex}]: ${warning}`);
|
|
1659
|
+
}
|
|
1660
|
+
for (const m of result.modules) {
|
|
1661
|
+
segmentedModules.push({
|
|
1662
|
+
positionName: m.positionName,
|
|
1663
|
+
aiCartItemId: m.aiCartItemId,
|
|
1664
|
+
horizontalStartInches: m.horizontalStartInches,
|
|
1665
|
+
widthInches: m.widthInches,
|
|
1666
|
+
heightInches: m.heightInches,
|
|
1667
|
+
bottomFromFloorInches: m.bottomFromFloorInches,
|
|
1668
|
+
frontType: m.frontType,
|
|
1669
|
+
runIndex,
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
const outOfBounds = [];
|
|
1674
|
+
const projectedPlacements = segmentedModules.map((m) => {
|
|
1675
|
+
const projected = (0, placement_projection_1.projectElevationPlacement)({
|
|
1676
|
+
horizontalStartInches: m.horizontalStartInches,
|
|
1677
|
+
widthInches: m.widthInches,
|
|
1678
|
+
heightInches: m.heightInches,
|
|
1679
|
+
bottomFromFloorInches: m.bottomFromFloorInches,
|
|
1680
|
+
}, elevationCalibration);
|
|
1681
|
+
if ((0, placement_projection_1.isProjectedPlacementOutsideImage)(projected, imageSizePx)) {
|
|
1682
|
+
outOfBounds.push({
|
|
1683
|
+
positionName: m.positionName,
|
|
1684
|
+
reason: `Projected rectangle for ${m.positionName} extends outside the ${imageSizePx.width}x${imageSizePx.height} ` +
|
|
1685
|
+
'calibration image. Re-check the calibration or this module\'s width/height/start against the source drawing.',
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
return {
|
|
1689
|
+
aiCartItemId: m.aiCartItemId,
|
|
1690
|
+
positionName: m.positionName,
|
|
1691
|
+
centerX: projected.centerX,
|
|
1692
|
+
centerY: projected.centerY,
|
|
1693
|
+
width: projected.width,
|
|
1694
|
+
height: projected.height,
|
|
1695
|
+
rotation: 0,
|
|
1696
|
+
placementSpace: 'IMAGE_PIXELS',
|
|
1697
|
+
basisImageWidthPx: imageSizePx.width,
|
|
1698
|
+
basisImageHeightPx: imageSizePx.height,
|
|
1699
|
+
source: input.source,
|
|
1700
|
+
};
|
|
1701
|
+
});
|
|
1702
|
+
const segmentationExtra = {
|
|
1703
|
+
moduleCount: segmentedModules.length,
|
|
1704
|
+
runCount: input.runs.length,
|
|
1705
|
+
segmentationWarnings,
|
|
1706
|
+
moduleSummary: segmentedModules.map((m) => ({
|
|
1707
|
+
positionName: m.positionName,
|
|
1708
|
+
runIndex: m.runIndex,
|
|
1709
|
+
frontType: m.frontType,
|
|
1710
|
+
widthInches: m.widthInches,
|
|
1711
|
+
horizontalStartInches: m.horizontalStartInches,
|
|
1712
|
+
})),
|
|
1713
|
+
};
|
|
1714
|
+
if (input.crossCheckCartItems) {
|
|
1715
|
+
const cart = await fetchActiveAiCartItems(input.sessionId);
|
|
1716
|
+
if ('error' in cart) {
|
|
1717
|
+
return cart.error;
|
|
1718
|
+
}
|
|
1719
|
+
const checkSpecs = segmentedModules.map((m) => ({
|
|
1720
|
+
positionName: m.positionName,
|
|
1721
|
+
aiCartItemId: m.aiCartItemId,
|
|
1722
|
+
widthInches: m.widthInches,
|
|
1723
|
+
heightInches: m.heightInches,
|
|
1724
|
+
}));
|
|
1725
|
+
const result = (0, cart_cross_check_1.crossCheckPlacementsAgainstCart)(checkSpecs, cart.items, {
|
|
1726
|
+
widthToleranceInches: input.widthToleranceInches,
|
|
1727
|
+
heightToleranceInches: input.heightToleranceInches,
|
|
1728
|
+
reportCoverage: input.writeMode === 'replace',
|
|
1729
|
+
});
|
|
1730
|
+
if (result.blocking.length > 0) {
|
|
1731
|
+
return buildCrossCheckBlockedResult(input.sessionId, input.canvasType, result, segmentationExtra);
|
|
1732
|
+
}
|
|
1733
|
+
segmentationExtra.matchedCartItemCount = result.matchedCount;
|
|
1734
|
+
segmentationExtra.cartCrossCheckWarnings = result.warnings;
|
|
1735
|
+
}
|
|
1736
|
+
return commitProjectedPlacements({
|
|
1737
|
+
sessionId: input.sessionId,
|
|
1738
|
+
canvasType: input.canvasType,
|
|
1739
|
+
writeMode: input.writeMode,
|
|
1740
|
+
imageSizePx,
|
|
1741
|
+
projectedPlacements,
|
|
1742
|
+
outOfBounds,
|
|
1743
|
+
extra: segmentationExtra,
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1203
1746
|
async function replaceAiCabinetCoordinates(input) {
|
|
1204
1747
|
try {
|
|
1205
1748
|
const { data } = await api_client_1.aiDrawingClient.put(`/sessions/${input.sessionId}/coordinates`, {
|
|
@@ -1942,6 +2485,25 @@ exports.aiDrawingTools = [
|
|
|
1942
2485
|
inputSchema: exports.AiCanvasCabinetPlacementsDeleteInputSchema,
|
|
1943
2486
|
handler: deleteAiCanvasCabinetPlacements,
|
|
1944
2487
|
},
|
|
2488
|
+
{
|
|
2489
|
+
name: 'project_ai_canvas_cabinet_placements',
|
|
2490
|
+
description: 'Compute and write exact backend-owned AIDrawingTool per-canvas cabinet placement rectangles from a ' +
|
|
2491
|
+
'pixels-per-inch calibration plus real cabinet inch dimensions. Prefer this over hand-writing centerX/centerY/' +
|
|
2492
|
+
'width/height placement boxes: it removes pixel-guessing error and keeps a whole run of cabinets aligned and ' +
|
|
2493
|
+
'gap-correct from one shared scale. ' +
|
|
2494
|
+
AI_PLACEMENT_PROJECTION_GUIDANCE,
|
|
2495
|
+
inputSchema: exports.ProjectAiCanvasCabinetPlacementsInputSchema,
|
|
2496
|
+
handler: projectAiCanvasCabinetPlacements,
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
name: 'project_ai_canvas_elevation_run',
|
|
2500
|
+
description: 'Decompose dimensioned elevation runs into individual cabinets and write one placement rectangle per cabinet. ' +
|
|
2501
|
+
'Use this whenever an elevation run could contain more than one cabinet (almost always): it forces you to ' +
|
|
2502
|
+
'enumerate each cabinet with its width and front type so a multi-cabinet run is never collapsed into one box. ' +
|
|
2503
|
+
AI_ELEVATION_RUN_SEGMENTATION_GUIDANCE,
|
|
2504
|
+
inputSchema: exports.ProjectAiCanvasElevationRunInputSchema,
|
|
2505
|
+
handler: projectAiCanvasElevationRun,
|
|
2506
|
+
},
|
|
1945
2507
|
{
|
|
1946
2508
|
name: 'replace_ai_cabinet_coordinates',
|
|
1947
2509
|
description: 'Replace all backend-owned AIDrawingTool cabinet coordinates for a session. ' +
|