Files
FreeCADWasmDev/tools/part-circle-vertex-picking-business-cases.mjs

374 lines
28 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { playwright } from './part-primitive-business-cases.mjs';
import { checkEdgeLocation } from './part-edge-location-business-cases.mjs';
import { checkPrimitiveExpressions } from './part-primitive-expression-business-cases.mjs';
export const runPrimitiveExpressionCases = ids => runCircleVertexPickingCases(ids, true);
export async function runCircleVertexPickingCases(ids, expressionsOnly = false) {
const capabilityId = 'Part:desktop:Part_Primitives';
if (!ids.includes(capabilityId)) return [];
const directory = path.resolve('docs/current/part-circle-vertex-picking-business', new Date().toISOString().replace(/[:.]/g, '-'));
fs.mkdirSync(directory, { recursive: true });
const result = { capabilityId, status: 'failed', cases: [], stages: [], evidenceFiles: {},
remaining: ['Location normal parity for standalone edges/vertices and ambiguous multi-face vertices',
'Expression binding and live task lifecycle'],
deferredCases: ['runCircleThreePointCases', 'runCylinderSkewCases', 'runLegacyPrimitiveTaskCases', 'runAdditionalPrimitiveCases',
'runWedgePrimitiveCases', 'runHelixSpiralCases', 'runPrimitivePlacementCases', 'runPrimitiveContainerCases'] };
const stage = name => { result.stages.push({ name, at: new Date().toISOString() }); process.stderr.write(`Circle vertex picking: ${name}\n`); };
const record = (input, actual, name) => result.cases.push({ input, actual, assertions: [{ name, passed: true }] });
const near = (actual, expected) => assert.ok(Number.isFinite(actual) && Math.abs(actual - expected) < 1e-6, `${actual} != ${expected}`);
const points = [[-10, 0, 0], [0, 0, 10], [10, 0, 0]];
let browser;
try {
for (const file of ['tools/part-circle-vertex-picking-business-cases.mjs', 'tools/part-primitive-business-cases.mjs',
'tools/part-edge-location-business-cases.mjs',
'tools/part-primitive-expression-business-cases.mjs',
'web/src/part-primitives-task.tsx', 'web/src/part-desktop-adapter.ts', 'web/src/part-document-facade.ts',
'web/src/openscad-desktop-view.tsx', 'web/src/workbench-shell.tsx',
'freecad-source/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp',
...['js', 'wasm', 'worker.js'].map(ext => `web/public/kernel/FreeCADWebDocumentProbe.${ext}`)])
result.evidenceFiles[file] = createHash('sha256').update(fs.readFileSync(file)).digest('hex');
stage('browser-native-load-enter');
browser = await playwright().chromium.launch({ headless: true, executablePath: process.env.FREECAD_CHROME ?? '/usr/bin/google-chrome',
args: ['--no-sandbox', '--disable-dev-shm-usage'] });
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
const errors = []; page.on('pageerror', error => errors.push(String(error)));
await page.goto(`${process.env.FREECAD_BROWSER_ORIGIN ?? 'http://127.0.0.1:5180'}/workbench.html?module=Part&command=Part_Primitives&document=CirclePick`);
const panel = page.locator('section[aria-label="Part primitives"]');
const click = async name => {
await page.evaluate(() => { window.circlePickBefore = window.__freecadWorkbenchState?.response; });
await panel.getByRole('button', { name, exact: true }).click();
if (['New document', 'Primitives undo', 'Primitives redo'].includes(name))
await panel.getByRole('button', { name: 'Read primitives document', exact: true }).click();
await page.waitForFunction(() => window.__freecadWorkbenchState?.response && window.__freecadWorkbenchState.response !== window.circlePickBefore);
const reply = await page.evaluate(() => window.__freecadWorkbenchState.response);
assert.equal(reply.kind, 'result', JSON.stringify(reply)); return reply.result;
};
await panel.getByLabel('Primitives document', { exact: true }).fill('CirclePick');
await click('New document'); stage('browser-native-load-complete');
stage('browser-primitive-expressions-enter');
await checkPrimitiveExpressions({ page, panel, click, record, persist: expressionsOnly });
stage('browser-primitive-expressions-complete');
if (expressionsOnly) result.deferredCases.push('runCircleVertexPickingCases');
if (!expressionsOnly) {
stage('browser-edge-location-enter');
await checkEdgeLocation({ page, panel, click, record, result, directory });
stage('browser-edge-location-complete');
for (let i = 0; i < points.length; ++i) {
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Vertex');
await panel.getByLabel('Primitive object', { exact: true }).fill(`Point${i + 1}`);
for (let axis = 0; axis < 3; ++axis)
await panel.getByLabel(`Primitive ${['X', 'Y', 'Z'][axis]}`, { exact: true }).fill(String(points[i][axis]));
const state = await click('Create primitive');
assert.deepEqual(state.objects.find(o => o.name === `Point${i + 1}`).primitivePoint, points[i]);
}
const before = await click('Read primitives document');
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Circle');
await panel.getByLabel('Primitive object', { exact: true }).fill('PickedArc');
await panel.getByLabel('Circle definition', { exact: true }).selectOption('points');
const canvas = panel.locator('[aria-label="Primitives geometry"] canvas');
// Project fixture vertices using the viewport's initial camera solely to locate mouse clicks.
const pick = async (index, point) => {
const button = panel.getByRole('button', { name: `Pick circle point ${index + 1} from a vertex`, exact: true });
await button.click(); assert.equal(await button.getAttribute('aria-pressed'), 'true');
await canvas.scrollIntoViewIfNeeded();
const size = await canvas.boundingBox();
const position = await page.evaluate(async ({ point, width, height }) => {
const THREE = await import('/node_modules/three/build/three.module.js');
const center = new THREE.Vector3(0, 0, 5), radius = Math.hypot(20, 0, 10);
const camera = new THREE.PerspectiveCamera(45, width / height, radius / 10000, radius * 100);
camera.up.set(0, 0, 1);
camera.position.copy(center).add(new THREE.Vector3(1, -1, 0.8).normalize().multiplyScalar(radius * 1.7));
camera.lookAt(center); camera.updateMatrixWorld();
const projected = new THREE.Vector3(...point).project(camera);
return [(projected.x + 1) * width / 2, (1 - projected.y) * height / 2];
}, { point, width: size.width, height: size.height });
await canvas.click({ position: { x: position[0], y: position[1] } });
await page.waitForFunction(({ index, point }) => point.every((value, axis) =>
Number(document.querySelector(`[aria-label="Circle point ${index + 1} ${['X', 'Y', 'Z'][axis]}"]`).value) === value), { index, point });
assert.equal(await button.getAttribute('aria-pressed'), 'false');
assert.equal(await panel.getByLabel('Primitive type', { exact: true }).inputValue(), 'Circle');
assert.equal(await panel.getByLabel('Primitive object', { exact: true }).inputValue(), 'PickedArc');
};
for (let index = 0; index < 3; ++index) await pick(index, points[index]);
assert.deepEqual(await click('Read primitives document'), before);
const created = await click('Create primitive');
const check = state => {
const arc = state.objects.find(o => o.name === 'PickedArc');
assert.equal(arc?.type, 'Part::Circle'); assert.equal(arc.topology.valid, true);
assert.equal(arc.topology.Edge, 1); near(arc.parameters.Radius, 10);
near(arc.parameters.Angle2 - arc.parameters.Angle1, 180);
const line = arc.lines[0];
line[0].forEach((value, axis) => near(value, points[0][axis]));
line.at(-1).forEach((value, axis) => near(value, points[2][axis]));
assert.ok(line.some(point => Math.abs(point[2] - 10) < 0.1));
assert.deepEqual(state.objects.filter(o => o.name !== 'PickedArc'), before.objects);
return arc;
};
check(created); assert.equal(created.undoCount, before.undoCount + 1);
assert.deepEqual((await click('Primitives undo')).objects, before.objects);
check(await click('Primitives redo'));
record({ points, route: 'mounted viewport pointer events' }, { arc: check(created), undoCount: created.undoCount },
'Three native vertices are picked without changing object/type or document; OCCT creates the oriented arc with expected radius/endpoints and atomic undo/redo');
await panel.getByRole('button', { name: 'Pick circle point 1 from a vertex', exact: true }).click();
const cancelled = await click('Cancel'); check(cancelled); assert.equal(cancelled.undoCount, created.undoCount);
assert.equal(await panel.getByRole('button', { name: 'Pick circle point 1 from a vertex', exact: true }).getAttribute('aria-pressed'), 'false');
await panel.getByRole('button', { name: 'Save primitives document', exact: true }).click();
await panel.getByLabel('Saved primitives file').waitFor();
const bytes = await page.evaluate(async () => {
const { readDocumentFile } = await import('/src/storage.ts');
return Array.from(new Uint8Array(await readDocumentFile('Primitives-CirclePick.FCStd')));
});
assert.ok(bytes.length > 0); await page.reload();
await panel.getByLabel('Primitives document', { exact: true }).fill('CirclePick');
await panel.getByLabel('Primitives FCStd file', { exact: true }).setInputFiles({ name: 'picked.FCStd', mimeType: 'application/octet-stream', buffer: Buffer.from(bytes) });
await panel.locator('[data-primitive-object="PickedArc"]').waitFor();
check(await click('Read primitives document'));
for (const [label, viewport] of [['desktop', { width: 1440, height: 1000 }], ['mobile', { width: 390, height: 844 }]]) {
await page.setViewportSize(viewport);
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Circle');
await panel.getByLabel('Primitive object', { exact: true }).fill('PickedArc');
await panel.getByLabel('Circle definition', { exact: true }).selectOption('points');
if (label === 'mobile') await pick(1, points[1]);
const bounds = await panel.evaluate(node => ({ width: node.clientWidth, scroll: node.scrollWidth }));
assert.ok(bounds.scroll <= bounds.width + 1);
const png = await canvas.screenshot({ path: path.join(directory, `${label}-canvas.png`) });
const colors = await page.evaluate(async encoded => {
const image = new Image(); image.src = `data:image/png;base64,${encoded}`; await image.decode();
const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height;
const context = canvas.getContext('2d'); context.drawImage(image, 0, 0);
const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data, values = new Set();
for (let index = 0; index < pixels.length; index += 4) values.add(`${pixels[index]},${pixels[index + 1]},${pixels[index + 2]}`);
return values.size;
}, png.toString('base64'));
assert.ok(colors > 1, `Blank ${label} canvas`);
const screenshot = path.join(directory, `${label}.png`); await page.screenshot({ path: screenshot, fullPage: true });
result.evidenceFiles[screenshot] = createHash('sha256').update(fs.readFileSync(screenshot)).digest('hex');
}
record({ cancel: true, opfs: true, mobilePick: true }, { bytes: bytes.length },
'Cancel disarms vertex picking without native mutation; OPFS FCStd reload retains geometry, and desktop/mobile canvases render with mobile vertex picking');
stage('browser-surface-picking-enter');
await page.setViewportSize({ width: 1440, height: 1000 });
await panel.getByLabel('Primitives document', { exact: true }).fill('SurfacePick');
await click('New document');
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Plane');
await panel.getByLabel('Primitive Length', { exact: true }).fill('20');
await panel.getByLabel('Primitive Width', { exact: true }).fill('20');
await click('Create primitive');
const planeState = await click('Read primitives document');
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Circle');
await panel.getByLabel('Primitive object', { exact: true }).fill('SurfaceArc');
await panel.getByLabel('Circle definition', { exact: true }).selectOption('points');
const surfacePoints = [[5, 10, 0], [10, 15, 0], [15, 10, 0]], picked = [];
for (let index = 0; index < 3; ++index) {
if (index === 1) await page.setViewportSize({ width: 390, height: 844 });
if (index === 2) await page.setViewportSize({ width: 1440, height: 1000 });
const button = panel.getByRole('button', { name: `Pick circle point ${index + 1} on geometry`, exact: true });
await button.click();
await canvas.scrollIntoViewIfNeeded();
const size = await canvas.boundingBox();
const position = await page.evaluate(async ({ point, width, height }) => {
const THREE = await import('/node_modules/three/build/three.module.js');
const center = new THREE.Vector3(10, 10, 0), radius = Math.hypot(20, 20);
const camera = new THREE.PerspectiveCamera(45, width / height, radius / 10000, radius * 100);
camera.up.set(0, 0, 1);
camera.position.copy(center).add(new THREE.Vector3(1, -1, 0.8).normalize().multiplyScalar(radius * 1.7));
camera.lookAt(center); camera.updateMatrixWorld();
const projected = new THREE.Vector3(...point).project(camera);
return [(projected.x + 1) * width / 2, (1 - projected.y) * height / 2];
}, { point: surfacePoints[index], width: size.width, height: size.height });
await canvas.click({ position: { x: position[0], y: position[1] } });
await page.waitForFunction(index => document.querySelector(`[aria-label="Pick circle point ${index + 1} on geometry"]`)
?.getAttribute('aria-pressed') === 'false', index);
const point = await Promise.all(['X', 'Y', 'Z'].map(async axis => Number(await panel.getByLabel(`Circle point ${index + 1} ${axis}`, { exact: true }).inputValue())));
point.forEach((value, axis) => assert.ok(Math.abs(value - surfacePoints[index][axis]) < 0.1, JSON.stringify({ point, expected: surfacePoints[index] })));
picked.push(point);
assert.equal(await panel.getByLabel('Primitive object', { exact: true }).inputValue(), 'SurfaceArc');
assert.equal(await panel.getByLabel('Primitive type', { exact: true }).inputValue(), 'Circle');
}
assert.deepEqual(await click('Read primitives document'), planeState);
const surfaceState = await click('Create primitive');
const surfaceArc = surfaceState.objects.find(o => o.name === 'SurfaceArc');
assert.equal(surfaceArc.type, 'Part::Circle'); assert.equal(surfaceArc.topology.valid, true);
assert.equal(surfaceArc.topology.Edge, 1); assert.ok(Math.abs(surfaceArc.parameters.Radius - 5) < 0.1);
surfaceArc.lines[0][0].forEach((value, axis) => near(value, picked[0][axis]));
surfaceArc.lines[0].at(-1).forEach((value, axis) => near(value, picked[2][axis]));
assert.ok(surfaceArc.lines[0].some(point => Math.hypot(...point.map((value, axis) => value - picked[1][axis])) < 0.1));
assert.equal(surfaceState.undoCount, planeState.undoCount + 1);
assert.deepEqual((await click('Primitives undo')).objects, planeState.objects);
assert.deepEqual((await click('Primitives redo')).objects, surfaceState.objects);
const surfaceScreenshot = path.join(directory, 'surface-picking.png');
await canvas.screenshot({ path: surfaceScreenshot });
result.evidenceFiles[surfaceScreenshot] = createHash('sha256').update(fs.readFileSync(surfaceScreenshot)).digest('hex');
record({ surfacePoints, route: 'mounted desktop/mobile surface pointer events' }, { picked, arc: surfaceArc },
'Surface picks preserve the draft object and document; native OCCT creates an arc through the picked positions with analytic radius and atomic undo/redo');
stage('browser-subelement-vertices-enter');
await panel.getByLabel('Primitives document', { exact: true }).fill('SubelementPick');
await click('New document');
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Plane');
for (const field of ['Length', 'Width']) await panel.getByLabel(`Primitive ${field}`, { exact: true }).fill('20');
await panel.getByLabel('Primitive position Z', { exact: true }).fill('4');
await panel.getByLabel('Primitive axis X', { exact: true }).fill('1');
await panel.getByLabel('Primitive axis Z', { exact: true }).fill('0');
await panel.getByLabel('Primitive rotation angle', { exact: true }).fill('90');
await click('Create primitive');
const vertexState = await click('Read primitives document');
const plane = vertexState.objects.find(o => o.name === 'Plane');
assert.equal(plane.topology.Vertex, 4); assert.equal(plane.vertices.length, 4);
const corners = [[0, 0, 4], [20, 0, 4], [20, 0, 24], [0, 0, 24]];
for (const corner of corners) assert.ok(plane.vertices.some(point => point.every((value, axis) => Math.abs(value - corner[axis]) < 1e-6)));
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Circle');
await panel.getByLabel('Primitive object', { exact: true }).fill('CornerArc');
await panel.getByLabel('Circle definition', { exact: true }).selectOption('points');
const clickWorld = async (point, offset = 0) => {
await canvas.scrollIntoViewIfNeeded(); const size = await canvas.boundingBox();
const position = await page.evaluate(async ({ point, width, height }) => {
const THREE = await import('/node_modules/three/build/three.module.js');
const center = new THREE.Vector3(10, 0, 14), radius = Math.hypot(20, 20);
const camera = new THREE.PerspectiveCamera(45, width / height, radius / 10000, radius * 100);
camera.up.set(0, 0, 1);
camera.position.copy(center).add(new THREE.Vector3(1, -1, 0.8).normalize().multiplyScalar(radius * 1.7));
camera.lookAt(center); camera.updateMatrixWorld();
const p = new THREE.Vector3(...point).project(camera);
return [(p.x + 1) * width / 2, (1 - p.y) * height / 2];
}, { point, width: size.width, height: size.height });
await canvas.click({ position: { x: position[0] + offset, y: position[1] } });
};
for (let index = 0; index < 3; ++index) {
await page.setViewportSize(index === 1 ? { width: 390, height: 844 } : { width: 1440, height: 1000 });
const button = panel.getByRole('button', { name: `Pick circle point ${index + 1} from a vertex`, exact: true });
await button.click();
const beforePoint = await Promise.all(['X', 'Y', 'Z'].map(axis => panel.getByLabel(`Circle point ${index + 1} ${axis}`, { exact: true }).inputValue()));
await clickWorld([10, 0, 14]);
assert.equal(await button.getAttribute('aria-pressed'), 'true');
assert.deepEqual(await Promise.all(['X', 'Y', 'Z'].map(axis => panel.getByLabel(`Circle point ${index + 1} ${axis}`, { exact: true }).inputValue())), beforePoint);
await clickWorld(corners[index], 3);
await page.waitForFunction(index => document.querySelector(`[aria-label="Pick circle point ${index + 1} from a vertex"]`)
?.getAttribute('aria-pressed') === 'false', index);
for (const [axis, label] of ['X', 'Y', 'Z'].entries()) near(Number(await panel.getByLabel(`Circle point ${index + 1} ${label}`, { exact: true }).inputValue()), corners[index][axis]);
assert.equal(await panel.getByLabel('Primitive object', { exact: true }).inputValue(), 'CornerArc');
assert.equal(await panel.getByLabel('Primitive type', { exact: true }).inputValue(), 'Circle');
}
assert.deepEqual(await click('Read primitives document'), vertexState);
const cornerState = await click('Create primitive');
const cornerArc = cornerState.objects.find(o => o.name === 'CornerArc');
assert.equal(cornerArc.topology.valid, true); assert.equal(cornerArc.topology.Edge, 1);
near(cornerArc.parameters.Radius, Math.sqrt(200)); near(cornerArc.parameters.Angle2 - cornerArc.parameters.Angle1, 180);
cornerArc.lines[0][0].forEach((value, axis) => near(value, corners[0][axis]));
cornerArc.lines[0].at(-1).forEach((value, axis) => near(value, corners[2][axis]));
assert.equal(cornerState.undoCount, vertexState.undoCount + 1);
assert.deepEqual((await click('Primitives undo')).objects, vertexState.objects);
assert.deepEqual((await click('Primitives redo')).objects, cornerState.objects);
const vertexScreenshot = path.join(directory, 'subelement-vertices.png');
await canvas.screenshot({ path: vertexScreenshot });
result.evidenceFiles[vertexScreenshot] = createHash('sha256').update(fs.readFileSync(vertexScreenshot)).digest('hex');
record({ corners, placement: plane.primitivePlacement, pixelOffset: 3 }, { vertices: plane.vertices, arc: cornerArc },
'Desktop/mobile clicks snap to exact transformed OCCT subelement vertices; face interiors cannot satisfy the vertex gate; native arc geometry and atomic history are preserved');
stage('browser-subelement-vertices-complete');
stage('browser-location-picking-enter');
assert.deepEqual((await click('Primitives undo')).objects, vertexState.objects);
const locationBaseline = await click('Read primitives document');
await panel.getByLabel('Primitive type', { exact: true }).selectOption('Box');
await panel.getByLabel('Primitive object', { exact: true }).fill('LocatedBox');
for (const [field, value] of [['Length', 2], ['Width', 3], ['Height', 4]])
await panel.getByLabel(`Primitive ${field}`, { exact: true }).fill(String(value));
await panel.getByRole('button', { name: 'Reset primitive placement', exact: true }).click();
await panel.getByLabel('Primitive rotation angle', { exact: true }).fill('90');
const locationButton = panel.getByRole('button', { name: 'Pick primitive location', exact: true });
const placementFields = async key => Promise.all(['X', 'Y', 'Z'].map(async axis =>
Number(await panel.getByLabel(`Primitive ${key} ${axis}`, { exact: true }).inputValue())));
await locationButton.click();
let location;
for (const [label, viewport, target] of [
['desktop', { width: 1440, height: 1000 }, [5, 0, 10]],
['mobile', { width: 390, height: 844 }, [15, 0, 20]],
]) {
await page.setViewportSize(viewport);
await clickWorld(target);
location = await placementFields('position');
location.forEach((value, axis) => assert.ok(Math.abs(value - target[axis]) < 0.15));
(await placementFields('axis')).forEach((value, axis) => near(value, [0, -1, 0][axis]));
assert.equal(await panel.getByLabel('Primitive rotation angle', { exact: true }).inputValue(), '90');
assert.equal(await locationButton.getAttribute('aria-pressed'), 'true');
assert.equal(await panel.getByLabel('Primitive object', { exact: true }).inputValue(), 'LocatedBox');
assert.equal(await panel.getByLabel('Primitive type', { exact: true }).inputValue(), 'Box');
await canvas.click({ position: { x: 5, y: 5 } });
assert.deepEqual(await placementFields('position'), location);
assert.equal(await locationButton.getAttribute('aria-pressed'), 'true');
assert.deepEqual(await click('Read primitives document'), locationBaseline);
const screenshot = path.join(directory, `location-${label}.png`);
await page.screenshot({ path: screenshot, fullPage: true });
result.evidenceFiles[screenshot] = createHash('sha256').update(fs.readFileSync(screenshot)).digest('hex');
}
for (const [viewport, corner] of [
[{ width: 1440, height: 1000 }, corners[0]],
[{ width: 390, height: 844 }, corners[2]],
]) {
await page.setViewportSize(viewport);
for (const [axis, value] of [['X', 1], ['Y', 0], ['Z', 0]])
await panel.getByLabel(`Primitive axis ${axis}`, { exact: true }).fill(String(value));
await clickWorld(corner, 3);
location = await placementFields('position');
location.forEach((value, axis) => near(value, corner[axis]));
(await placementFields('axis')).forEach((value, axis) => near(value, [0, -1, 0][axis]));
assert.equal(await panel.getByLabel('Primitive rotation angle', { exact: true }).inputValue(), '90');
assert.equal(await panel.getByLabel('Primitive object', { exact: true }).inputValue(), 'LocatedBox');
assert.equal(await panel.getByLabel('Primitive type', { exact: true }).inputValue(), 'Box');
assert.equal(await locationButton.getAttribute('aria-pressed'), 'true');
assert.deepEqual(await click('Read primitives document'), locationBaseline);
}
record({ corners: [corners[0], corners[2]], pixelOffset: 3 }, { location, axis: await placementFields('axis') },
'Desktop/mobile location picks snap to transformed native vertices and replace a stale draft axis with the rotated plane normal without changing draft identity, angle, native objects or history');
await canvas.click({ button: 'right', position: { x: 5, y: 5 } });
assert.equal(await locationButton.getAttribute('aria-pressed'), 'false');
assert.deepEqual(await placementFields('position'), location);
await locationButton.click(); await page.keyboard.press('Escape');
assert.equal(await locationButton.getAttribute('aria-pressed'), 'false');
await locationButton.click(); await locationButton.click();
assert.equal(await locationButton.getAttribute('aria-pressed'), 'false');
await locationButton.click();
assert.deepEqual(await click('Cancel'), locationBaseline);
assert.equal(await locationButton.getAttribute('aria-pressed'), 'false');
assert.deepEqual(await placementFields('position'), [0, 0, 0]);
// Cancel resets dimensions, so restore the independent analytic fixture.
for (const [field, value] of [['Length', 2], ['Width', 3], ['Height', 4]])
await panel.getByLabel(`Primitive ${field}`, { exact: true }).fill(String(value));
await panel.getByLabel('Primitive rotation angle', { exact: true }).fill('90');
await locationButton.click(); await clickWorld([15, 0, 20]);
await clickWorld(corners[2], 3);
location = await placementFields('position');
const locatedState = await click('Create primitive');
const locatedBox = locatedState.objects.find(item => item.name === 'LocatedBox');
assert.equal(await locationButton.getAttribute('aria-pressed'), 'false');
assert.equal(locatedBox.type, 'Part::Box'); assert.equal(locatedBox.topology.valid, true);
near(locatedBox.volume, 24);
locatedBox.primitivePlacement.position.forEach((value, axis) => near(value, location[axis]));
locatedBox.primitivePlacement.axis.forEach((value, axis) => near(value, [0, -1, 0][axis]));
near(locatedBox.primitivePlacement.angle, 90);
for (const [axis, offsets] of [[0, [-4, 0]], [1, [0, 3]], [2, [0, 2]]]) {
near(Math.min(...locatedBox.vertices.map(point => point[axis])), location[axis] + offsets[0]);
near(Math.max(...locatedBox.vertices.map(point => point[axis])), location[axis] + offsets[1]);
}
assert.deepEqual(locatedState.objects.filter(item => item.name !== 'LocatedBox'), locationBaseline.objects);
assert.equal(locatedState.undoCount, locationBaseline.undoCount + 1);
assert.deepEqual((await click('Primitives undo')).objects, locationBaseline.objects);
assert.deepEqual((await click('Primitives redo')).objects, locatedState.objects);
record({ targets: [[5, 0, 10], [15, 0, 20], corners[2]], angle: 90, dimensions: [2, 3, 4] }, { location, box: locatedBox },
'Surface and planar vertex location picking copy world position and normal; misses and cancellation preserve native state; create at the native vertex yields analytic rotated OCCT vertices and atomic history');
stage('browser-location-picking-complete');
}
assert.deepEqual(errors, []);
stage('browser-complete'); result.status = 'not_implemented';
result.error = { code: 'implementation_required', message: result.remaining.join('; ') };
} catch (error) { result.error = { code: 'circle_vertex_picking_failed', message: error.stack || String(error) }; }
finally {
if (browser) await browser.close(); stage('browser-cleanup-complete');
fs.writeFileSync(path.join(directory, 'result.json'), JSON.stringify(result, null, 2));
}
return [result];
}