Skip to content

Commit d074899

Browse files
author
DavidQ
committed
Fix Object Vector V2 zoom anchoring and fullscreen tool layout behavior - PR_26133_101-object-vector-zoom-and-layout-fixes
1 parent c02bb6d commit d074899

5 files changed

Lines changed: 157 additions & 16 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# PR_26133_101 Object Vector Zoom And Layout Fixes
2+
3+
## Scope
4+
- Read docs/dev/PROJECT_INSTRUCTIONS.md before changes.
5+
- Preserved the Workspace manifest/schema contract; no schema or workspace manifest structures were changed.
6+
- Used the integrated PR_26133_100 code state as the prior reference. The PR_100 delta ZIP was requested but was not present under tmp/ at PR_101 start.
7+
- Limited implementation to Object Vector Studio V2 zoom, Tools/fullscreen layout, object-scale anchoring, and matching workspace-v2 coverage.
8+
9+
## Changes
10+
- Raised Object Preview MAX_ZOOM from 0.5 to 1.0.
11+
- Made the Tools accordion compact so its container ends under the Snap/Grid/Words button row instead of stretching.
12+
- Restored fullscreen right-column vertical scrolling while keeping the column inside the viewport.
13+
- Fixed Object Transform scale so each new scale value is applied as a relative ratio from the last object-scale preview value. This prevents child shapes such as flame/inner line strokes from drifting away from hull anchor lines during repeated object scale/zoom adjustments.
14+
- Kept PR_100 object/shape rotation, center marker, and multi-selected shape order behavior intact.
15+
16+
## Playwright Impact
17+
- Playwright impacted: Yes.
18+
- Validates Object Vector V2 max zoom clamping/display, compact Tools layout, fullscreen right-column scroll behavior, and non-compounding object-scale origin offsets.
19+
- Expected pass behavior: zoom clamps to 1.0, Tools content reaches accordion bottom, fullscreen right panel scrolls within the viewport, and repeated object scale changes preserve child shape origin offsets at the requested scale.
20+
- Expected fail behavior: stale max zoom assertions, stretched Tools accordions, non-scrollable fullscreen right column, or compounded object-scale origin drift fail the workspace-v2 tests.
21+
22+
## Validation
23+
- PASS: node --check tools/object-vector-studio-v2/js/ToolStarterApp.js
24+
- PASS: node --check tests/playwright/tools/WorkspaceManagerV2.spec.mjs
25+
- PASS: git diff --check (CRLF advisory warnings only)
26+
- PASS: npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list -g "shows Object Vector Studio V2 layout shell|expands Object Vector Studio V2 asset authoring controls"
27+
- PASS: npm run test:workspace-v2 (56 passed)
28+
29+
## Manual Validation
30+
1. Open Object Vector Studio V2, select an object with multiple child shapes such as the Asteroids ship, and use Object Transform scale/zoom controls repeatedly.
31+
- Expected: inner/flame line shapes stay anchored to the hull lines instead of drifting upward.
32+
2. Use Object Preview zoom controls up to the maximum.
33+
- Expected: internal zoom reaches 1.0 and the UI remains responsive.
34+
3. Open fullscreen mode and scroll the right column.
35+
- Expected: the right column remains inside the viewport and scrolls vertically.
36+
4. Inspect Tools accordion height.
37+
- Expected: its bottom sits under the last row of Snap/Grid/Words buttons.
38+
39+
## Out Of Scope
40+
- Full samples smoke test was skipped per PR_26133_101 instructions.
41+
- No workspace schema cleanup, manifest restructuring, or sample JSON updates were performed.

tests/playwright/tools/WorkspaceManagerV2.spec.mjs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,6 +1824,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18241824
const labelButton = document.querySelector("#objectVectorStudioV2ToolLabelModeButton");
18251825
const toolsContent = document.querySelector("#objectVectorStudioV2ShapeToolsContent");
18261826
const shapesContent = document.querySelector("#objectVectorStudioV2ShapesContent");
1827+
const toolsContentRect = toolsContent.getBoundingClientRect();
1828+
const toolsAccordion = toolsContent.closest(".accordion-v2").getBoundingClientRect();
18271829
const shapesContentRect = shapesContent.getBoundingClientRect();
18281830
const shapesAccordion = shapesContent.closest(".accordion-v2").getBoundingClientRect();
18291831
const leftPanel = document.querySelector(".tool-starter__panel--left");
@@ -1843,6 +1845,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18431845
textButtonWider: Math.round(rect.width) > Math.round(rect.height),
18441846
toolsButtonLabels,
18451847
toolsContainsShapeButtons: Boolean(toolsContent.querySelector(".object-vector-studio-v2__tool-toggle")),
1848+
toolsReachesBottom: Math.abs(toolsContentRect.bottom - toolsAccordion.bottom) <= 1,
18461849
visibleIconTopOffsetRange: Math.max(...iconTopOffsets) - Math.min(...iconTopOffsets),
18471850
shapesReachesBottom: Math.abs(shapesContentRect.bottom - shapesAccordion.bottom) <= 1,
18481851
zOrderAbsentBeforeObjectSelection: !document.querySelector(".object-vector-studio-v2__z-order-actions"),
@@ -1857,6 +1860,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
18571860
textButtonWider: true,
18581861
toolsButtonLabels: ["Snap Grid", "Snap Angle", "Grid", "Icons"],
18591862
toolsContainsShapeButtons: false,
1863+
toolsReachesBottom: true,
18601864
visibleIconTopOffsetRange: 0,
18611865
shapesReachesBottom: true,
18621866
zOrderAbsentBeforeObjectSelection: true,
@@ -3431,7 +3435,7 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34313435

34323436
const zoomSource = await readFile("tools/object-vector-studio-v2/js/ToolStarterApp.js", "utf8");
34333437
expect(zoomSource).toContain("const DEFAULT_VIEWPORT = Object.freeze({ height: 220, width: 320, x: 0, y: 0, zoom: 0.1 });");
3434-
expect(zoomSource).toContain("const MAX_ZOOM = 0.5;");
3438+
expect(zoomSource).toContain("const MAX_ZOOM = 1.0;");
34353439
expect(zoomSource).toContain("const MIN_ZOOM = 0.01;");
34363440
expect(zoomSource).toContain("const ZOOM_STEP = 0.01;");
34373441
expect(zoomSource).toMatch(/formatZoomPercentage\(\) \{\s+return Math\.round\(this\.viewport\.zoom \* 100\);\s+\}/);
@@ -3456,8 +3460,8 @@ test.describe("Workspace Manager V2 bootstrap", () => {
34563460
await page.evaluate(() => {
34573461
window.__objectVectorStudioV2App.zoomViewport(2.5);
34583462
});
3459-
await expect(page.locator("#objectVectorStudioV2CoordinateDisplay")).toHaveText("Origin: 0, 0 | Canvas origin 0,0 centered | Zoom 500%");
3460-
await expect(page.locator("#objectVectorStudioV2RenderSurface")).toHaveAttribute("viewBox", "-320 -220 640 440");
3463+
await expect(page.locator("#objectVectorStudioV2CoordinateDisplay")).toHaveText("Origin: 0, 0 | Canvas origin 0,0 centered | Zoom 1000%");
3464+
await expect(page.locator("#objectVectorStudioV2RenderSurface")).toHaveAttribute("viewBox", "-160 -110 320 220");
34613465
await page.evaluate(() => {
34623466
window.__objectVectorStudioV2App.zoomViewport(0);
34633467
});
@@ -3708,6 +3712,17 @@ test.describe("Workspace Manager V2 bootstrap", () => {
37083712
expect(remainingOpenSections.every((entry) => entry.height >= 44)).toBe(true);
37093713
expect(collapsedLayout.find((entry) => entry.title.startsWith("Palette"))?.height).toBeLessThanOrEqual(320);
37103714
await expect(page.locator(".tool-starter__panel--right")).toHaveCSS("overflow-y", "auto");
3715+
const maxZoomState = await page.evaluate(() => {
3716+
const app = window.__objectVectorStudioV2App;
3717+
app.zoomViewport(2);
3718+
return {
3719+
display: document.querySelector("#objectVectorStudioV2CoordinateDisplay").textContent,
3720+
zoom: app.viewport.zoom
3721+
};
3722+
});
3723+
expect(maxZoomState.zoom).toBe(1);
3724+
expect(maxZoomState.display).toContain("Zoom 1000%");
3725+
await page.locator("#objectVectorStudioV2ResetViewButton").click();
37113726

37123727
const summary = page.locator("[data-tool-starter-summary]");
37133728
await summary.click();
@@ -3724,15 +3739,22 @@ test.describe("Workspace Manager V2 bootstrap", () => {
37243739
const left = document.querySelector(".tool-starter__panel--left").getBoundingClientRect();
37253740
const center = document.querySelector(".tool-starter__panel--center").getBoundingClientRect();
37263741
const right = document.querySelector(".tool-starter__panel--right").getBoundingClientRect();
3742+
const rightPanel = document.querySelector(".tool-starter__panel--right");
37273743
return {
37283744
centerHeight: Math.round(center.height),
37293745
centerWidth: Math.round(center.width),
37303746
leftBeforeCenter: left.right <= center.left,
3731-
rightAfterCenter: right.left >= center.right
3747+
rightAfterCenter: right.left >= center.right,
3748+
rightBottomWithinViewport: right.bottom <= window.innerHeight + 1,
3749+
rightOverflowY: getComputedStyle(rightPanel).overflowY,
3750+
rightScrollable: rightPanel.scrollHeight >= rightPanel.clientHeight
37323751
};
37333752
});
37343753
expect(fullscreenLayout.leftBeforeCenter).toBe(true);
37353754
expect(fullscreenLayout.rightAfterCenter).toBe(true);
3755+
expect(fullscreenLayout.rightBottomWithinViewport).toBe(true);
3756+
expect(fullscreenLayout.rightOverflowY).toBe("auto");
3757+
expect(fullscreenLayout.rightScrollable).toBe(true);
37363758
expect(fullscreenLayout.centerWidth).toBeGreaterThan(300);
37373759
expect(fullscreenLayout.centerHeight).toBeGreaterThan(300);
37383760

@@ -5765,6 +5787,21 @@ test.describe("Workspace Manager V2 bootstrap", () => {
57655787
});
57665788
expect(groupedRotateBackTransforms).toEqual([{ rotation: 15 }, { rotation: 15 }]);
57675789

5790+
const objectScaleOriginOffsetsBefore = await page.evaluate(() => {
5791+
const app = window.__objectVectorStudioV2App;
5792+
const object = app.selectedObject();
5793+
const origin = app.objectTransformOrigin(object);
5794+
const frame = app.activeFrame();
5795+
return object.shapes.map((shape, shapeIndex) => {
5796+
const effective = app.effectiveShapeForFrame(shape, frame, shapeIndex);
5797+
const transform = app.ensureShapeTransform(effective);
5798+
const originWorld = app.transformedPoint(transform.shapeOrigin, transform);
5799+
return {
5800+
x: Number((originWorld.x - origin.x).toFixed(3)),
5801+
y: Number((originWorld.y - origin.y).toFixed(3))
5802+
};
5803+
});
5804+
});
57685805
await page.locator("#objectVectorStudioV2ObjectScaleUpLargeButton").click();
57695806
await expect(page.locator("#statusLog")).toHaveValue(/OK Object scale preview set to 1\.1 for UFO Template\./);
57705807
const objectScaleAfterLargeStep = await page.evaluate(() => {
@@ -5787,6 +5824,25 @@ test.describe("Workspace Manager V2 bootstrap", () => {
57875824
});
57885825
});
57895826
expect(objectScaleAfterSmallStep).toEqual([{ scaleX: 1.09, scaleY: 1.09 }, { scaleX: 1.09, scaleY: 1.09 }]);
5827+
const objectScaleOriginOffsetsAfterSmallStep = await page.evaluate(() => {
5828+
const app = window.__objectVectorStudioV2App;
5829+
const object = app.selectedObject();
5830+
const origin = app.objectTransformOrigin(object);
5831+
const frame = app.activeFrame();
5832+
return object.shapes.map((shape, shapeIndex) => {
5833+
const effective = app.effectiveShapeForFrame(shape, frame, shapeIndex);
5834+
const transform = app.ensureShapeTransform(effective);
5835+
const originWorld = app.transformedPoint(transform.shapeOrigin, transform);
5836+
return {
5837+
x: Number((originWorld.x - origin.x).toFixed(3)),
5838+
y: Number((originWorld.y - origin.y).toFixed(3))
5839+
};
5840+
});
5841+
});
5842+
objectScaleOriginOffsetsAfterSmallStep.forEach((offset, index) => {
5843+
expect(offset.x).toBeCloseTo(Number((objectScaleOriginOffsetsBefore[index].x * 1.09).toFixed(3)), 2);
5844+
expect(offset.y).toBeCloseTo(Number((objectScaleOriginOffsetsBefore[index].y * 1.09).toFixed(3)), 2);
5845+
});
57905846

57915847
await page.evaluate(() => window.__objectVectorStudioV2App.selectShape(0, "shape transform single-shape verification"));
57925848
await expect(page.locator("#objectVectorStudioV2ShapeTransform #objectVectorStudioV2ScaleDownSmallButton")).toBeEnabled();

tools/object-vector-studio-v2/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
213213
</div>
214214
</section>
215215

216-
<section class="accordion-v2 tool-starter__accordion tool-starter__accordion--fill is-open" data-accordion-v2-open="true">
216+
<section class="accordion-v2 tool-starter__accordion tool-starter__accordion--compact is-open" data-accordion-v2-open="true">
217217
<button class="accordion-v2__header" type="button" aria-expanded="true" aria-controls="objectVectorStudioV2ShapeToolsContent">
218218
<span>Tools</span>
219219
<span class="accordion-v2__icon" aria-hidden="true">+</span>

tools/object-vector-studio-v2/js/ToolStarterApp.js

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const SVG_NS = "http://www.w3.org/2000/svg";
1313
const DEFAULT_VIEWPORT = Object.freeze({ height: 220, width: 320, x: 0, y: 0, zoom: 0.1 });
1414
const GRID_STEP = 10;
1515
const OBJECT_PREVIEW_DRAWING_SCALE = GRID_STEP;
16-
const MAX_ZOOM = 0.5;
16+
const MAX_ZOOM = 1.0;
1717
const MIN_ZOOM = 0.01;
1818
const ZOOM_STEP = 0.01;
1919
const TRANSPARENT_STYLE_COLOR = "#00000000";
@@ -525,6 +525,7 @@ export class ToolStarterApp {
525525
this.previewRedoStack = [];
526526
this.previewPointerEdit = null;
527527
this.transformInputValues = new Map();
528+
this.objectScalePreviewValues = new Map();
528529
this.stateControlStateId = "";
529530
this.pendingAddObjectClick = false;
530531
this.hiddenObjectIds = new Set();
@@ -2805,7 +2806,7 @@ export class ToolStarterApp {
28052806
const section = document.createElement("section");
28062807
section.className = "object-vector-studio-v2__edit-panel object-vector-studio-v2__edit-panel--transform object-vector-studio-v2__edit-panel--object-transform";
28072808
const origin = this.objectTransformOrigin(object);
2808-
const objectScaleInput = Number(this.transformInputValue("objectVectorStudioV2ObjectScaleInput", "1"));
2809+
const objectScaleInput = Number(this.objectScalePreviewValues.get(object.id) ?? 1);
28092810
const objectScale = Number.isFinite(objectScaleInput) && objectScaleInput > 0 ? objectScaleInput : 1;
28102811
section.append(
28112812
this.createObjectOriginControlRow(origin),
@@ -4100,17 +4101,17 @@ export class ToolStarterApp {
41004101
};
41014102
}
41024103

4103-
transformWithScaledOriginAroundPivot(transform, pivot, scale) {
4104+
transformWithRelativeScaleAroundPivot(transform, pivot, scaleRatio) {
41044105
const normalized = this.ensureShapeTransform({ transform });
41054106
const originWorld = this.transformedPoint(normalized.shapeOrigin, normalized);
41064107
const nextOriginWorld = {
4107-
x: this.formatViewportNumber(pivot.x + (originWorld.x - pivot.x) * scale),
4108-
y: this.formatViewportNumber(pivot.y + (originWorld.y - pivot.y) * scale)
4108+
x: this.formatViewportNumber(pivot.x + (originWorld.x - pivot.x) * scaleRatio),
4109+
y: this.formatViewportNumber(pivot.y + (originWorld.y - pivot.y) * scaleRatio)
41094110
};
41104111
return {
41114112
...normalized,
4112-
scaleX: Number(scale.toFixed(3)),
4113-
scaleY: Number(scale.toFixed(3)),
4113+
scaleX: this.formatViewportNumber(normalized.scaleX * scaleRatio),
4114+
scaleY: this.formatViewportNumber(normalized.scaleY * scaleRatio),
41144115
x: this.formatViewportNumber(nextOriginWorld.x - normalized.shapeOrigin.x),
41154116
y: this.formatViewportNumber(nextOriginWorld.y - normalized.shapeOrigin.y)
41164117
};
@@ -7384,20 +7385,54 @@ export class ToolStarterApp {
73847385
this.transformInputValues.set(inputElement.id, inputElement.value);
73857386
}
73867387
this.applySelectedObjectScaleValue(nextScale, {
7388+
previousScale: input.value,
73877389
okMessage: `OK Object scale preview set to ${this.formatScaleInputValue(nextScale)} for ${this.selectedObject()?.name || "selected object"}.`
73887390
});
73897391
}
73907392

7391-
applySelectedObjectScaleValue(scale, { okMessage } = {}) {
7393+
currentObjectScalePreviewValue(object = this.selectedObject()) {
7394+
const storedScale = this.objectScalePreviewValues.get(object?.id);
7395+
return Number.isFinite(storedScale) && storedScale > 0 ? storedScale : 1;
7396+
}
7397+
7398+
applySelectedObjectScaleValue(scale, { okMessage, previousScale } = {}) {
73927399
const origin = this.readObjectOriginInputs();
73937400
if (!origin.ok) {
73947401
this.statusLog.write(`FAIL Invalid object transform rejected: ${origin.error}`);
73957402
return false;
73967403
}
73977404
const object = this.selectedObject();
7398-
return this.updateSelectedObjectTransforms("scale", (shape) => {
7399-
shape.transform = this.transformWithScaledOriginAroundPivot(this.ensureShapeTransform(shape), origin.value, scale);
7405+
const priorScale = Number.isFinite(previousScale) && previousScale > 0
7406+
? previousScale
7407+
: this.currentObjectScalePreviewValue(object);
7408+
const scaleRatio = scale / priorScale;
7409+
if (!Number.isFinite(scaleRatio) || scaleRatio <= 0) {
7410+
this.statusLog.write("FAIL Invalid object transform rejected: scale ratio must be greater than 0.");
7411+
return false;
7412+
}
7413+
const priorStoredScale = object?.id ? this.objectScalePreviewValues.get(object.id) : undefined;
7414+
const priorInputScale = this.transformInputValues.get("objectVectorStudioV2ObjectScaleInput");
7415+
const nextScale = this.formatViewportNumber(scale);
7416+
if (object?.id) {
7417+
this.objectScalePreviewValues.set(object.id, nextScale);
7418+
this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", this.formatScaleInputValue(nextScale));
7419+
}
7420+
const committed = this.updateSelectedObjectTransforms("scale", (shape) => {
7421+
shape.transform = this.transformWithRelativeScaleAroundPivot(this.ensureShapeTransform(shape), origin.value, scaleRatio);
74007422
}, okMessage || `OK Object scale preview set to ${this.formatScaleInputValue(scale)} for ${object?.name || "selected object"}.`, "Object Transform scale failed schema validation");
7423+
if (!committed && object?.id) {
7424+
if (priorStoredScale === undefined) {
7425+
this.objectScalePreviewValues.delete(object.id);
7426+
} else {
7427+
this.objectScalePreviewValues.set(object.id, priorStoredScale);
7428+
}
7429+
if (priorInputScale === undefined) {
7430+
this.transformInputValues.delete("objectVectorStudioV2ObjectScaleInput");
7431+
} else {
7432+
this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", priorInputScale);
7433+
}
7434+
}
7435+
return committed;
74017436
}
74027437

74037438
resizeSelectedObject() {
@@ -7428,12 +7463,18 @@ export class ToolStarterApp {
74287463
}
74297464

74307465
const nextPayload = this.cloneCurrentPayload();
7466+
const currentScale = this.currentObjectScalePreviewValue(object);
7467+
const scaleRatio = input.value / currentScale;
7468+
if (!Number.isFinite(scaleRatio) || scaleRatio <= 0) {
7469+
this.statusLog.write("FAIL Resize Geometry rejected for selected object: scale ratio must be greater than 0.");
7470+
return;
7471+
}
74317472
try {
74327473
targetIndexes.forEach((shapeIndex) => {
74337474
const baseShape = this.findShapeInPayload(nextPayload, shapeIndex);
74347475
const override = this.frameOverrideInPayload(nextPayload, shapeIndex, { create: false });
74357476
const transformTarget = override?.transform ? { transform: override.transform, geometry: baseShape.geometry } : baseShape;
7436-
const transform = this.transformWithScaledOriginAroundPivot(this.ensureShapeTransform(transformTarget), origin.value, input.value);
7477+
const transform = this.transformWithRelativeScaleAroundPivot(this.ensureShapeTransform(transformTarget), origin.value, scaleRatio);
74377478
this.resizeShapeGeometryByTransformScale(baseShape, transform);
74387479
transform.scaleX = 1;
74397480
transform.scaleY = 1;
@@ -7453,6 +7494,7 @@ export class ToolStarterApp {
74537494
}
74547495

74557496
this.transformInputValues.set("objectVectorStudioV2ObjectScaleInput", "1");
7497+
this.objectScalePreviewValues.set(object.id, 1);
74567498
this.commitPayloadUpdate(
74577499
nextPayload,
74587500
object.id,

tools/object-vector-studio-v2/styles/toolStarter.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,6 +2292,8 @@ html:fullscreen body.tools-platform-surface[data-tool-id="object-vector-studio-v
22922292
justify-self: end;
22932293
width: 360px;
22942294
max-width: 360px;
2295+
overflow-x: hidden;
2296+
overflow-y: auto;
22952297
}
22962298

22972299
body.tools-platform-surface.tools-platform-fullscreen-active[data-tool-id="object-vector-studio-v2"] .tool-starter__panel--center,

0 commit comments

Comments
 (0)