374 lines
28 KiB
JavaScript
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];
|
|
}
|