@@ -3227,6 +3227,170 @@ test.describe("Workspace Manager V2 bootstrap", () => {
32273227 }
32283228 });
32293229
3230+ test("aligns Object Vector Studio V2 selection bounds to transformed preview geometry", async ({ page }) => {
3231+ const server = await startRepoServer();
3232+ const pageErrors = [];
3233+ const consoleErrors = [];
3234+
3235+ page.on("pageerror", (error) => {
3236+ pageErrors.push(error.message);
3237+ });
3238+ page.on("console", (message) => {
3239+ if (message.type() === "error") {
3240+ consoleErrors.push(message.text());
3241+ }
3242+ });
3243+
3244+ await coverageReporter.start(page);
3245+ try {
3246+ await page.setViewportSize({ width: 1366, height: 1000 });
3247+ await page.goto(`${server.baseUrl}/tools/object-vector-studio-v2/index.html`, { waitUntil: "networkidle" });
3248+ await page.evaluate(() => {
3249+ sessionStorage.setItem("object-vector-studio-v2.runtimePalette", JSON.stringify({
3250+ id: "transform-bounds-palette",
3251+ swatches: [
3252+ { id: "white", value: "#ffffff" },
3253+ { id: "blue", value: "#60a5fa" }
3254+ ]
3255+ }));
3256+ });
3257+ await page.locator("#objectVectorStudioV2ImportJsonInput").setInputFiles({
3258+ buffer: Buffer.from(JSON.stringify({
3259+ name: "Transform Bounds Object Set",
3260+ objects: [
3261+ {
3262+ id: "object.bounds.transformed-rectangle",
3263+ name: "Transformed Rectangle",
3264+ shapes: [
3265+ {
3266+ geometry: { height: 40, width: 80, x: -40, y: -20 },
3267+ locked: false,
3268+ order: 1,
3269+ style: { fill: "#ffffff", fillOpacity: 1, stroke: "#60a5fa", strokeOpacity: 1, strokeWidth: 1 },
3270+ tool: "rectangle",
3271+ transform: { origin: { x: -10, y: 5 }, rotation: 30, scaleX: 0.5, scaleY: 0.5, x: 60, y: 10 },
3272+ visible: true
3273+ }
3274+ ],
3275+ tags: []
3276+ }
3277+ ],
3278+ toolId: "object-vector-studio-v2",
3279+ version: 1
3280+ }, null, 2)),
3281+ mimeType: "application/json",
3282+ name: "object-vector-transform-bounds.json"
3283+ });
3284+ await expect(page.locator("#objectVectorStudioV2RenderSurface [data-shape-index='0']")).toHaveClass(/is-selected/);
3285+
3286+ const metrics = await page.locator("#objectVectorStudioV2RenderSurface").evaluate((surface) => {
3287+ const drawingScale = 10;
3288+ const app = window.__objectVectorStudioV2App;
3289+ const shape = app.selectedShape();
3290+ const transform = shape.transform;
3291+ const geometry = shape.geometry;
3292+ const transformPoint = (point) => {
3293+ const radians = transform.rotation * Math.PI / 180;
3294+ const relativeX = (point.x - transform.origin.x) * transform.scaleX;
3295+ const relativeY = (point.y - transform.origin.y) * transform.scaleY;
3296+ const rotatedX = relativeX * Math.cos(radians) - relativeY * Math.sin(radians);
3297+ const rotatedY = relativeX * Math.sin(radians) + relativeY * Math.cos(radians);
3298+ return {
3299+ x: (transform.x + transform.origin.x + rotatedX) * drawingScale,
3300+ y: (transform.y + transform.origin.y + rotatedY) * drawingScale
3301+ };
3302+ };
3303+ const corners = [
3304+ { x: geometry.x, y: geometry.y },
3305+ { x: geometry.x + geometry.width, y: geometry.y },
3306+ { x: geometry.x + geometry.width, y: geometry.y + geometry.height },
3307+ { x: geometry.x, y: geometry.y + geometry.height }
3308+ ].map(transformPoint);
3309+ const xValues = corners.map((point) => point.x);
3310+ const yValues = corners.map((point) => point.y);
3311+ const expected = {
3312+ center: transformPoint({ x: geometry.x + geometry.width / 2, y: geometry.y + geometry.height / 2 }),
3313+ height: Math.max(...yValues) - Math.min(...yValues),
3314+ width: Math.max(...xValues) - Math.min(...xValues),
3315+ x: Math.min(...xValues),
3316+ y: Math.min(...yValues)
3317+ };
3318+ const selectionBox = surface.querySelector("[data-selection-bounds='0']");
3319+ const handles = Object.fromEntries(Array.from(surface.querySelectorAll("[data-resize-handle]")).map((handle) => [
3320+ handle.dataset.resizeHandle,
3321+ {
3322+ cx: Number(handle.getAttribute("x")) + Number(handle.getAttribute("width")) / 2,
3323+ cy: Number(handle.getAttribute("y")) + Number(handle.getAttribute("height")) / 2
3324+ }
3325+ ]));
3326+ const raw = {
3327+ height: geometry.height * drawingScale,
3328+ width: geometry.width * drawingScale,
3329+ x: geometry.x * drawingScale,
3330+ y: geometry.y * drawingScale
3331+ };
3332+ const svgPoint = surface.createSVGPoint();
3333+ svgPoint.x = expected.center.x;
3334+ svgPoint.y = expected.center.y;
3335+ const screenCenter = svgPoint.matrixTransform(surface.getScreenCTM());
3336+ const hitShape = document.elementFromPoint(screenCenter.x, screenCenter.y)?.closest("[data-shape-index]");
3337+
3338+ return {
3339+ expected,
3340+ handles,
3341+ hitShapeIndex: hitShape?.dataset.shapeIndex || null,
3342+ raw,
3343+ rawContainsTransformedCenter: expected.center.x >= raw.x
3344+ && expected.center.x <= raw.x + raw.width
3345+ && expected.center.y >= raw.y
3346+ && expected.center.y <= raw.y + raw.height,
3347+ screenCenter: { x: screenCenter.x, y: screenCenter.y },
3348+ selection: {
3349+ height: Number(selectionBox.getAttribute("height")),
3350+ width: Number(selectionBox.getAttribute("width")),
3351+ x: Number(selectionBox.getAttribute("x")),
3352+ y: Number(selectionBox.getAttribute("y"))
3353+ }
3354+ };
3355+ });
3356+
3357+ expect(metrics.rawContainsTransformedCenter).toBe(false);
3358+ expect(metrics.hitShapeIndex).toBe("0");
3359+ expect(metrics.selection.x).toBeCloseTo(metrics.expected.x - 4, 3);
3360+ expect(metrics.selection.y).toBeCloseTo(metrics.expected.y - 4, 3);
3361+ expect(metrics.selection.width).toBeCloseTo(metrics.expected.width + 8, 3);
3362+ expect(metrics.selection.height).toBeCloseTo(metrics.expected.height + 8, 3);
3363+ expect(metrics.selection.width).toBeLessThan(metrics.raw.width);
3364+
3365+ const expectedHandleCenters = {
3366+ ne: { x: metrics.expected.x + metrics.expected.width + 4, y: metrics.expected.y - 4 },
3367+ nw: { x: metrics.expected.x - 4, y: metrics.expected.y - 4 },
3368+ se: { x: metrics.expected.x + metrics.expected.width + 4, y: metrics.expected.y + metrics.expected.height + 4 },
3369+ sw: { x: metrics.expected.x - 4, y: metrics.expected.y + metrics.expected.height + 4 }
3370+ };
3371+ for (const [handle, expectedCenter] of Object.entries(expectedHandleCenters)) {
3372+ expect(metrics.handles[handle].cx).toBeCloseTo(expectedCenter.x, 3);
3373+ expect(metrics.handles[handle].cy).toBeCloseTo(expectedCenter.y, 3);
3374+ }
3375+
3376+ const transformBeforeDrag = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().transform }));
3377+ await page.mouse.move(metrics.screenCenter.x, metrics.screenCenter.y);
3378+ await page.mouse.down();
3379+ await page.mouse.move(metrics.screenCenter.x + 40, metrics.screenCenter.y + 24, { steps: 4 });
3380+ await page.mouse.up();
3381+ const transformAfterDrag = await page.evaluate(() => ({ ...window.__objectVectorStudioV2App.selectedShape().transform }));
3382+ expect(transformAfterDrag.x).not.toBe(transformBeforeDrag.x);
3383+ expect(transformAfterDrag.y).not.toBe(transformBeforeDrag.y);
3384+ await expect(page.locator("#statusLog")).toHaveValue(/OK Dragged shape row 0 by/);
3385+
3386+ expect(pageErrors).toEqual([]);
3387+ expect(consoleErrors).toEqual([]);
3388+ } finally {
3389+ await coverageReporter.stop(page);
3390+ await server.close();
3391+ }
3392+ });
3393+
32303394 test("expands Object Vector Studio V2 asset authoring controls", async ({ page }, testInfo) => {
32313395 const server = await startRepoServer();
32323396 const pageErrors = [];
0 commit comments