Skip to content

Commit e21042f

Browse files
author
DavidQ
committed
Fix Asteroids bezel visibility and apply Object Vector rounding in runtime rendering - PR_26133_106-asteroids-bezel-rounding-fixes
1 parent 2f6407c commit e21042f

5 files changed

Lines changed: 356 additions & 9 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# PR_26133_105-object-vector-snapline-scale-controls
2+
3+
## Summary
4+
- Verified Object Vector Studio V2 no longer renders the selected-shape center/origin X marker.
5+
- Verified snap-enabled drawing renders a visible snap line using the same generated SVG geometry as the preview, including rounded curve path data when point rounding is enabled.
6+
- Verified Object Transform Scale updates the preview in realtime without persisting object scale state until Resize is used.
7+
- Verified Object Transform and Shape Transform Scale rows include right-side `X` reset buttons that restore scale to `1.0`.
8+
- Preserved the single object-origin model and did not reintroduce shape-origin state or controls.
9+
10+
## Scope Notes
11+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before continuing.
12+
- Used the integrated PR_26133_104 state as the prior reference.
13+
- Workspace manifest/schema structures were preserved.
14+
- Scope was limited to Object Vector V2 snapline and scale-control behavior plus targeted Playwright coverage stabilization.
15+
- Full samples smoke test skipped as requested.
16+
17+
## Targeted Verification
18+
- Shape center/origin X marker is absent from the Object Preview selection chrome.
19+
- Object-origin marker remains a `+`.
20+
- Snap line appears while drawing when snap mode is enabled.
21+
- Rounded polygon/polyline snap line uses curve path data when rounding is enabled.
22+
- Object Transform scale step/input changes update rendered bounds immediately.
23+
- Object Transform scale reset returns the preview scale to `1.0`.
24+
- Shape Transform scale reset returns selected shape scale to `1.0`.
25+
- Object scale remains a transient preview value unless geometry is rewritten through Resize.
26+
27+
## Validation Results
28+
- PASS `node --check tools/object-vector-studio-v2/js/ToolStarterApp.js`
29+
- PASS `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --grep "expands Object Vector Studio V2 asset authoring controls" --workers=1 --reporter=list`
30+
- PASS `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --grep "Object Vector Studio V2" --workers=1 --reporter=list` (16 passed)
31+
- PASS `npm run test:workspace-v2` (56 passed)
32+
- PASS `git diff --check`
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PR_26133_106-asteroids-bezel-rounding-fixes
2+
3+
## Summary
4+
- Fixed Asteroids bezel visibility so the bezel layer renders in normal play view as well as fullscreen.
5+
- Preserved existing fullscreen transparent-window canvas fitting behavior.
6+
- Updated Object Vector runtime canvas rendering to honor authored point rounding for polygon, polyline, and rectangle/square geometry.
7+
- Updated Object Vector runtime SVG previews to emit rounded path geometry when authored rounding is present.
8+
9+
## Scope Notes
10+
- Read `docs/dev/PROJECT_INSTRUCTIONS.md` before implementation.
11+
- Used the PR_26133_105 delta as the prior reference and preserved its staged/local changes.
12+
- Workspace manifest/schema structures were not changed.
13+
- Full samples smoke test skipped as requested; this PR is limited to Asteroids bezel visibility and Object Vector runtime rounding.
14+
15+
## Playwright Impact
16+
- Playwright impacted: Yes.
17+
- Validated Asteroids normal-mode bezel visibility, fullscreen bezel fit preservation, and runtime Object Vector rounded path rendering.
18+
- Expected pass behavior: bezel state is visible before fullscreen, fullscreen still uses `transparent-window-fit`, and rounded Asteroids ship hull rendering emits canvas quadratic curves plus SVG rounded path output.
19+
- Expected fail behavior: tests fail if bezel remains hidden outside fullscreen or if runtime rounded geometry renders as straight-only polygon output.
20+
21+
## Manual Validation
22+
- Open `/games/Asteroids/index.html`.
23+
- Confirm the bezel is visible around the canvas before entering fullscreen.
24+
- Click the canvas to enter fullscreen and confirm the canvas still fits inside the bezel window.
25+
- Confirm the Asteroids ship hull shows the authored rounded interior point rather than a sharp straight join.
26+
27+
## Validation Results
28+
- PASS `node --check src/engine/runtime/fullscreenBezel.js`
29+
- PASS `node --check src/engine/rendering/ObjectVectorRuntimeAssetService.js`
30+
- PASS `node --check games/Asteroids/index.js`
31+
- PASS `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --grep "fits the game canvas inside the fullscreen play area|loads Object Vector Studio V2 runtime assets into Asteroids gameplay rendering" --workers=1 --reporter=list` (2 passed)
32+
- PASS `npm run test:workspace-v2` (56 passed)
33+
- PASS `git diff --check`
34+
- PASS Playwright V8 coverage report generated; changed runtime files were covered:
35+
- `src/engine/runtime/fullscreenBezel.js`
36+
- `src/engine/rendering/ObjectVectorRuntimeAssetService.js`

src/engine/rendering/ObjectVectorRuntimeAssetService.js

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,160 @@ function objectTransformOrigin(object) {
143143
return { x: 0, y: 0 };
144144
}
145145

146+
function pointStyleValue(value) {
147+
return value === "round" ? "round" : "square";
148+
}
149+
150+
function strokeLineJoinValue(value) {
151+
return pointStyleValue(value) === "round" ? "round" : "miter";
152+
}
153+
154+
function shapeRoundingRadius(shape) {
155+
const value = Number(shape?.style?.roundingRadius);
156+
return Number.isFinite(value) && value >= 0 ? value : 4;
157+
}
158+
159+
function shapeGeometryPoints(shape) {
160+
const geometryTool = shapeGeometryTool(shape);
161+
if (geometryTool === "rectangle") {
162+
const x = Number(shape.geometry.x) || 0;
163+
const y = Number(shape.geometry.y) || 0;
164+
const width = Number(shape.geometry.width) || 0;
165+
const height = Number(shape.geometry.height) || 0;
166+
return [
167+
{ x, y },
168+
{ x: x + width, y },
169+
{ x: x + width, y: y + height },
170+
{ x, y: y + height }
171+
];
172+
}
173+
if (geometryTool === "polygon" || geometryTool === "polyline") {
174+
return Array.isArray(shape?.geometry?.points) ? shape.geometry.points : [];
175+
}
176+
return [];
177+
}
178+
179+
function shapePointRoundingValues(shape) {
180+
const points = shapeGeometryPoints(shape);
181+
if (!points.length) {
182+
return [];
183+
}
184+
const explicit = Array.isArray(shape?.style?.pointRounding) ? shape.style.pointRounding : null;
185+
if (explicit) {
186+
return Array.from({ length: points.length }, (_, index) => explicit[index] === true);
187+
}
188+
return Array.from({ length: points.length }, () => false);
189+
}
190+
191+
function roundedPointPathCommands(shape, { closed } = {}) {
192+
const geometryTool = shapeGeometryTool(shape);
193+
if (!["polygon", "polyline", "rectangle"].includes(geometryTool)) {
194+
return [];
195+
}
196+
const sourcePoints = shapeGeometryPoints(shape);
197+
const pointCount = sourcePoints.length;
198+
if (pointCount < 3) {
199+
return [];
200+
}
201+
const pointRounding = shapePointRoundingValues(shape);
202+
const hasRoundedPoint = pointRounding.some((isRounded, index) => isRounded === true && (closed || (index > 0 && index < pointCount - 1)));
203+
const radius = shapeRoundingRadius(shape);
204+
if (!hasRoundedPoint || radius <= 0) {
205+
return [];
206+
}
207+
const points = sourcePoints.map((point) => ({
208+
x: Number(point.x) || 0,
209+
y: Number(point.y) || 0
210+
}));
211+
const roundedVertex = (index) => {
212+
if (pointRounding[index] !== true || (!closed && (index === 0 || index === pointCount - 1))) {
213+
return null;
214+
}
215+
const previous = points[index === 0 ? pointCount - 1 : index - 1];
216+
const current = points[index];
217+
const next = points[index === pointCount - 1 ? 0 : index + 1];
218+
const previousDistance = Math.hypot(previous.x - current.x, previous.y - current.y);
219+
const nextDistance = Math.hypot(next.x - current.x, next.y - current.y);
220+
if (previousDistance <= 0 || nextDistance <= 0) {
221+
return null;
222+
}
223+
const vertexRadius = Math.min(radius, previousDistance / 2, nextDistance / 2);
224+
if (vertexRadius <= 0) {
225+
return null;
226+
}
227+
return {
228+
after: {
229+
x: current.x + ((next.x - current.x) / nextDistance) * vertexRadius,
230+
y: current.y + ((next.y - current.y) / nextDistance) * vertexRadius
231+
},
232+
before: {
233+
x: current.x + ((previous.x - current.x) / previousDistance) * vertexRadius,
234+
y: current.y + ((previous.y - current.y) / previousDistance) * vertexRadius
235+
},
236+
current
237+
};
238+
};
239+
const commands = [];
240+
const appendVertex = (index) => {
241+
const rounded = roundedVertex(index);
242+
if (!rounded) {
243+
commands.push({ point: points[index], type: "line" });
244+
return;
245+
}
246+
commands.push({ point: rounded.before, type: "line" });
247+
commands.push({ control: rounded.current, point: rounded.after, type: "quadratic" });
248+
};
249+
if (closed) {
250+
commands.push({ point: roundedVertex(0)?.after || points[0], type: "move" });
251+
for (let index = 1; index < pointCount; index += 1) {
252+
appendVertex(index);
253+
}
254+
appendVertex(0);
255+
commands.push({ type: "close" });
256+
return commands;
257+
}
258+
commands.push({ point: points[0], type: "move" });
259+
for (let index = 1; index < pointCount - 1; index += 1) {
260+
appendVertex(index);
261+
}
262+
commands.push({ point: points[pointCount - 1], type: "line" });
263+
return commands;
264+
}
265+
266+
function applyRuntimePathCommands(context, commands) {
267+
commands.forEach((command) => {
268+
if (command.type === "move") {
269+
context.moveTo(command.point.x, command.point.y);
270+
} else if (command.type === "line") {
271+
context.lineTo(command.point.x, command.point.y);
272+
} else if (command.type === "quadratic") {
273+
context.quadraticCurveTo(command.control.x, command.control.y, command.point.x, command.point.y);
274+
} else if (command.type === "close") {
275+
context.closePath();
276+
}
277+
});
278+
}
279+
280+
function formatSvgNumber(value) {
281+
const parsed = Number(value);
282+
return Number.isFinite(parsed) ? String(Math.round(parsed * 1000) / 1000) : "0";
283+
}
284+
285+
function runtimePathCommandsToSvgPath(commands) {
286+
return commands.map((command) => {
287+
if (command.type === "move") {
288+
return `M ${formatSvgNumber(command.point.x)} ${formatSvgNumber(command.point.y)}`;
289+
}
290+
if (command.type === "line") {
291+
return `L ${formatSvgNumber(command.point.x)} ${formatSvgNumber(command.point.y)}`;
292+
}
293+
if (command.type === "quadratic") {
294+
return `Q ${formatSvgNumber(command.control.x)} ${formatSvgNumber(command.control.y)} ${formatSvgNumber(command.point.x)} ${formatSvgNumber(command.point.y)}`;
295+
}
296+
return "Z";
297+
}).join(" ");
298+
}
299+
146300
function normalizeFill(value) {
147301
return value && value !== "transparent" ? value : null;
148302
}
@@ -745,6 +899,8 @@ export class ObjectVectorRuntimeAssetService {
745899
context.scale(transform.scaleX, transform.scaleY);
746900
context.translate(-transformOrigin.x, -transformOrigin.y);
747901
context.lineWidth = shape.style.strokeWidth;
902+
context.lineCap = pointStyleValue(shape.style.strokeLinecap ?? shape.style.startPointStyle ?? shape.style.pointStyle);
903+
context.lineJoin = strokeLineJoinValue(shape.style.pointStyle ?? shape.style.strokeLinecap);
748904
context.fillStyle = normalizeFill(shape.style.fill) || "transparent";
749905
context.strokeStyle = normalizeStroke(shape.style.stroke) || "transparent";
750906
this.buildPath(context, shape);
@@ -767,6 +923,11 @@ export class ObjectVectorRuntimeAssetService {
767923
context.beginPath();
768924
const geometryTool = shapeGeometryTool(shape);
769925
if (geometryTool === "rectangle") {
926+
const roundedPath = roundedPointPathCommands(shape, { closed: true });
927+
if (roundedPath.length) {
928+
applyRuntimePathCommands(context, roundedPath);
929+
return;
930+
}
770931
context.rect(shape.geometry.x, shape.geometry.y, shape.geometry.width, shape.geometry.height);
771932
return;
772933
}
@@ -784,6 +945,11 @@ export class ObjectVectorRuntimeAssetService {
784945
return;
785946
}
786947
if (geometryTool === "polygon" || geometryTool === "polyline") {
948+
const roundedPath = roundedPointPathCommands(shape, { closed: geometryTool === "polygon" });
949+
if (roundedPath.length) {
950+
applyRuntimePathCommands(context, roundedPath);
951+
return;
952+
}
787953
shape.geometry.points.forEach((point, index) => {
788954
if (index === 0) {
789955
context.moveTo(point.x, point.y);
@@ -842,9 +1008,17 @@ export class ObjectVectorRuntimeAssetService {
8421008
}
8431009

8441010
shapeToSvg(shape, transformOrigin = { x: 0, y: 0 }) {
845-
const style = ` fill="${escapeXml(shape.style.fill)}" fill-opacity="${shape.style.fillOpacity}" stroke="${escapeXml(shape.style.stroke)}" stroke-opacity="${shape.style.strokeOpacity}" stroke-width="${shape.style.strokeWidth}" transform="${svgTransformAttribute(shapeTransform(shape), transformOrigin)}"`;
1011+
const lineCap = pointStyleValue(shape.style.strokeLinecap ?? shape.style.startPointStyle ?? shape.style.pointStyle);
1012+
const lineJoin = strokeLineJoinValue(shape.style.pointStyle ?? shape.style.strokeLinecap);
1013+
const style = ` fill="${escapeXml(shape.style.fill)}" fill-opacity="${shape.style.fillOpacity}" stroke="${escapeXml(shape.style.stroke)}" stroke-opacity="${shape.style.strokeOpacity}" stroke-width="${shape.style.strokeWidth}" stroke-linecap="${lineCap}" stroke-linejoin="${lineJoin}" transform="${svgTransformAttribute(shapeTransform(shape), transformOrigin)}"`;
8461014
const geometryTool = shapeGeometryTool(shape);
8471015
if (geometryTool === "rectangle") {
1016+
const roundedPath = roundedPointPathCommands(shape, { closed: true });
1017+
if (roundedPath.length) {
1018+
const d = runtimePathCommandsToSvgPath(roundedPath);
1019+
const points = shapeGeometryPoints(shape).map((point) => `${point.x},${point.y}`).join(" ");
1020+
return `<path d="${d}" points="${points}" data-runtime-rounded-point-render="path"${style}/>`;
1021+
}
8481022
return `<rect x="${shape.geometry.x}" y="${shape.geometry.y}" width="${shape.geometry.width}" height="${shape.geometry.height}"${style}/>`;
8491023
}
8501024
if (geometryTool === "circle") {
@@ -858,6 +1032,11 @@ export class ObjectVectorRuntimeAssetService {
8581032
}
8591033
if (geometryTool === "polygon" || geometryTool === "polyline") {
8601034
const points = shape.geometry.points.map((point) => `${point.x},${point.y}`).join(" ");
1035+
const roundedPath = roundedPointPathCommands(shape, { closed: geometryTool === "polygon" });
1036+
if (roundedPath.length) {
1037+
const d = runtimePathCommandsToSvgPath(roundedPath);
1038+
return `<path d="${d}" points="${points}" data-runtime-rounded-point-render="path"${style}/>`;
1039+
}
8611040
return geometryTool === "polygon"
8621041
? `<polygon points="${points}"${style}/>`
8631042
: `<polyline points="${points}"${style}/>`;

src/engine/runtime/fullscreenBezel.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,11 +1150,11 @@ export default class fullscreenBezel {
11501150
};
11511151
}
11521152

1153-
const shouldShow = fullscreenActive && this.ready && !this.missing;
1153+
const shouldShow = this.ready && !this.missing;
11541154
this.element.style.display = toDisplayValue(shouldShow);
11551155
this.element.style.visibility = toVisibilityValue(shouldShow);
11561156
this.element.style.opacity = toOpacityValue(shouldShow);
1157-
if (shouldShow) {
1157+
if (shouldShow && fullscreenActive) {
11581158
const fitted = this.applyCanvasWindowFitLayout();
11591159
if (!fitted) {
11601160
this.applyCanvasFullscreenFitLayout() || this.applyCanvasFallbackLayout();

0 commit comments

Comments
 (0)