@hatiolab/figure-model 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/v3-asset-types.d.ts +89 -16
- package/dist/v3-asset-types.d.ts.map +1 -1
- package/dist/v3-asset.d.ts.map +1 -1
- package/dist/v3-asset.js +34 -25
- package/dist/v3-asset.js.map +1 -1
- package/dist/v3-authoring-actions.d.ts +97 -0
- package/dist/v3-authoring-actions.d.ts.map +1 -0
- package/dist/v3-authoring-actions.js +384 -0
- package/dist/v3-authoring-actions.js.map +1 -0
- package/dist/v3-capabilities.d.ts.map +1 -1
- package/dist/v3-capabilities.js +4 -77
- package/dist/v3-capabilities.js.map +1 -1
- package/dist/v3-driver.d.ts +24 -1
- package/dist/v3-driver.d.ts.map +1 -1
- package/dist/v3-driver.js +68 -2
- package/dist/v3-driver.js.map +1 -1
- package/dist/v3-from-v2.d.ts +34 -1
- package/dist/v3-from-v2.d.ts.map +1 -1
- package/dist/v3-from-v2.js +676 -19
- package/dist/v3-from-v2.js.map +1 -1
- package/dist/v3-gate.d.ts +59 -6
- package/dist/v3-gate.d.ts.map +1 -1
- package/dist/v3-gate.js +310 -45
- package/dist/v3-gate.js.map +1 -1
- package/dist/v3-graph-types.d.ts +9 -1
- package/dist/v3-graph-types.d.ts.map +1 -1
- package/dist/v3-graph.d.ts +5 -3
- package/dist/v3-graph.d.ts.map +1 -1
- package/dist/v3-graph.js +93 -4
- package/dist/v3-graph.js.map +1 -1
- package/dist/v3-kernel-version.d.ts +1 -1
- package/dist/v3-kernel-version.js +1 -1
- package/dist/v3-mesh-compare.d.ts +2 -5
- package/dist/v3-mesh-compare.d.ts.map +1 -1
- package/dist/v3-mesh-compare.js +2 -145
- package/dist/v3-mesh-compare.js.map +1 -1
- package/dist/v3-shape-sampling.d.ts +14 -0
- package/dist/v3-shape-sampling.d.ts.map +1 -0
- package/dist/v3-shape-sampling.js +120 -0
- package/dist/v3-shape-sampling.js.map +1 -0
- package/dist/v3-surface.d.ts +15 -0
- package/dist/v3-surface.d.ts.map +1 -0
- package/dist/v3-surface.js +104 -0
- package/dist/v3-surface.js.map +1 -0
- package/docs/prototypes/v3-asset.schema.json +0 -8
- package/docs/v3-asset-persistence.md +1 -1
- package/docs/v3-legacy-risk-audit-2026-09-23.md +39 -0
- package/docs/v3-operator-contracts.md +2 -0
- package/docs/v3-shape-dimension-contract.md +111 -0
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Divisions per Bézier corner, the number things-scene's `roundedRect` asks `getPoints` for. */
|
|
2
|
+
const CORNER_DIVISIONS = 8;
|
|
3
|
+
/** Points on the outline of a rounded rectangle in the X–Z plane, centred, y = 0. */
|
|
4
|
+
export function roundedOutline(width, depth, roundX, roundZ) {
|
|
5
|
+
// Each semi-axis is bounded by its own half-section (shape-dimension contract §1).
|
|
6
|
+
const rx = Math.min(roundX, width / 2);
|
|
7
|
+
const rz = Math.min(roundZ, depth / 2);
|
|
8
|
+
const hw = width / 2, hd = depth / 2;
|
|
9
|
+
if (rx <= 0 && rz <= 0)
|
|
10
|
+
return [[-hw, 0, -hd], [hw, 0, -hd], [hw, 0, hd], [-hw, 0, hd]];
|
|
11
|
+
const pts = [];
|
|
12
|
+
// Corner k: quadratic Bézier from the end of one straight edge to the start of the next, control at the corner.
|
|
13
|
+
const corners = [
|
|
14
|
+
[[hw - rx, 0, -hd], [hw, 0, -hd], [hw, 0, -hd + rz]],
|
|
15
|
+
[[hw, 0, hd - rz], [hw, 0, hd], [hw - rx, 0, hd]],
|
|
16
|
+
[[-hw + rx, 0, hd], [-hw, 0, hd], [-hw, 0, hd - rz]],
|
|
17
|
+
[[-hw, 0, -hd + rz], [-hw, 0, -hd], [-hw + rx, 0, -hd]]
|
|
18
|
+
];
|
|
19
|
+
for (const [p0, c, p2] of corners) {
|
|
20
|
+
pts.push(p0);
|
|
21
|
+
for (let i = 1; i < CORNER_DIVISIONS; i++) {
|
|
22
|
+
const t = i / CORNER_DIVISIONS, a = (1 - t) * (1 - t), b = 2 * (1 - t) * t, d = t * t;
|
|
23
|
+
pts.push([a * p0[0] + b * c[0] + d * p2[0], 0, a * p0[2] + b * c[2] + d * p2[2]]);
|
|
24
|
+
}
|
|
25
|
+
pts.push(p2);
|
|
26
|
+
}
|
|
27
|
+
return pts;
|
|
28
|
+
}
|
|
29
|
+
export function lift(outline, y) {
|
|
30
|
+
return outline.map(([x, , z]) => [x, y, z]);
|
|
31
|
+
}
|
|
32
|
+
/** Points on an elliptic cylinder about local Y: both end rims. */
|
|
33
|
+
export function cylinderPoints(rx, rz, length, segments) {
|
|
34
|
+
const pts = [];
|
|
35
|
+
for (const y of [-length / 2, length / 2])
|
|
36
|
+
for (let i = 0; i < segments; i++) {
|
|
37
|
+
const a = (i / segments) * Math.PI * 2;
|
|
38
|
+
pts.push([rx * Math.cos(a), y, rz * Math.sin(a)]);
|
|
39
|
+
}
|
|
40
|
+
return pts;
|
|
41
|
+
}
|
|
42
|
+
/** Points on an ellipsoid: a fixed latitude–longitude grid. */
|
|
43
|
+
export function spherePoints(rx, ry, rz) {
|
|
44
|
+
const pts = [];
|
|
45
|
+
const rings = 6, around = 12;
|
|
46
|
+
for (let j = 1; j < rings; j++) {
|
|
47
|
+
const phi = (j / rings) * Math.PI;
|
|
48
|
+
for (let i = 0; i < around; i++) {
|
|
49
|
+
const th = (i / around) * Math.PI * 2;
|
|
50
|
+
pts.push([rx * Math.sin(phi) * Math.cos(th), ry * Math.cos(phi), rz * Math.sin(phi) * Math.sin(th)]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
pts.push([0, ry, 0], [0, -ry, 0]);
|
|
54
|
+
return pts;
|
|
55
|
+
}
|
|
56
|
+
export function boxPoints(w, h, d) {
|
|
57
|
+
const pts = [];
|
|
58
|
+
for (const sx of [-1, 1])
|
|
59
|
+
for (const sy of [-1, 1])
|
|
60
|
+
for (const sz of [-1, 1])
|
|
61
|
+
pts.push([(sx * w) / 2, (sy * h) / 2, (sz * d) / 2]);
|
|
62
|
+
return pts;
|
|
63
|
+
}
|
|
64
|
+
/** Surface points of a V3 shape in its own frame. */
|
|
65
|
+
export function v3ShapePoints(shape, segments) {
|
|
66
|
+
const d = shape.dimensions;
|
|
67
|
+
switch (shape.provider) {
|
|
68
|
+
case 'box':
|
|
69
|
+
return boxPoints(d.width, d.height, d.depth);
|
|
70
|
+
case 'cylinder':
|
|
71
|
+
return cylinderPoints(d.radius, d.radius, d.length, segments ?? 12);
|
|
72
|
+
case 'ellipticCylinder':
|
|
73
|
+
return cylinderPoints(d.radiusX, d.radiusZ, d.length, segments ?? 12);
|
|
74
|
+
case 'roundedBoxXZ': {
|
|
75
|
+
const outer = roundedOutline(d.width, d.depth, d.roundX, d.roundZ);
|
|
76
|
+
return [...lift(outer, -d.height / 2), ...lift(outer, d.height / 2)];
|
|
77
|
+
}
|
|
78
|
+
case 'hollowBoxXZ': {
|
|
79
|
+
const outer = roundedOutline(d.width, d.depth, d.roundX, d.roundZ);
|
|
80
|
+
const inner = roundedOutline(d.width - 2 * d.wallX, d.depth - 2 * d.wallZ, Math.max(d.roundX - d.wallX, 0), Math.max(d.roundZ - d.wallZ, 0));
|
|
81
|
+
return [...lift(outer, -d.height / 2), ...lift(outer, d.height / 2), ...lift(inner, d.height / 2), ...lift(inner, -d.height / 2 + d.floor)];
|
|
82
|
+
}
|
|
83
|
+
case 'frustum': {
|
|
84
|
+
const pts = [];
|
|
85
|
+
const seg = segments ?? 12;
|
|
86
|
+
for (const [y, r] of [[-d.height / 2, d.radiusBottom], [d.height / 2, d.radiusTop]])
|
|
87
|
+
for (let i = 0; i < seg; i++) {
|
|
88
|
+
const a = (i / seg) * Math.PI * 2;
|
|
89
|
+
pts.push([r * Math.cos(a), y, r * Math.sin(a)]);
|
|
90
|
+
}
|
|
91
|
+
return pts;
|
|
92
|
+
}
|
|
93
|
+
case 'sphere':
|
|
94
|
+
return spherePoints(d.radiusX, d.radiusY, d.radiusZ);
|
|
95
|
+
case 'roundedBox': {
|
|
96
|
+
const outer = roundedOutline(d.width, d.depth, d.round, d.round);
|
|
97
|
+
return [...lift(outer, -d.height / 2), ...lift(outer, d.height / 2)];
|
|
98
|
+
}
|
|
99
|
+
case 'hollowBox': {
|
|
100
|
+
const outer = roundedOutline(d.width, d.depth, d.round, d.round);
|
|
101
|
+
// The inner outline: each wall inset by its own thickness, corner reduced by the thickness on each axis.
|
|
102
|
+
const inner = roundedOutline(d.width - 2 * d.wallX, d.depth - 2 * d.wallZ, Math.max(d.round - d.wallX, 0), Math.max(d.round - d.wallZ, 0));
|
|
103
|
+
return [...lift(outer, -d.height / 2), ...lift(outer, d.height / 2), ...lift(inner, d.height / 2), ...lift(inner, -d.height / 2 + d.floor)];
|
|
104
|
+
}
|
|
105
|
+
case 'polygon': {
|
|
106
|
+
const profile = shape.profile ?? [];
|
|
107
|
+
return [...profile.map(q => [q.x, -d.height / 2, q.y]), ...profile.map(q => [q.x, d.height / 2, q.y])];
|
|
108
|
+
}
|
|
109
|
+
case 'portal':
|
|
110
|
+
return (shape.profile ?? []).flatMap(q => [[q.x, q.y, -d.depth / 2], [q.x, q.y, d.depth / 2]]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function transformPoints(points, r, t) {
|
|
114
|
+
return points.map(([x, y, z]) => [
|
|
115
|
+
r[0][0] * x + r[0][1] * y + r[0][2] * z + t[0],
|
|
116
|
+
r[1][0] * x + r[1][1] * y + r[1][2] * z + t[1],
|
|
117
|
+
r[2][0] * x + r[2][1] * y + r[2][2] * z + t[2]
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=v3-shape-sampling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-shape-sampling.js","sourceRoot":"","sources":["../src/v3-shape-sampling.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type Dynamic = any;
|
|
2
|
+
export interface V3SurfaceContext {
|
|
3
|
+
/** Every node output's type, from the compiled graph. */
|
|
4
|
+
types: Dynamic;
|
|
5
|
+
/** Which node writes each output. */
|
|
6
|
+
writers: Map<string, Dynamic>;
|
|
7
|
+
model: Dynamic;
|
|
8
|
+
assetFrame: string;
|
|
9
|
+
}
|
|
10
|
+
/** Check that a `{ placement, feature }` pair names a supported planar face, and say which one. */
|
|
11
|
+
export declare function resolveV3Surface(ctx: V3SurfaceContext, contact: Dynamic, path: string): Dynamic;
|
|
12
|
+
/** Where a resolved surface sits, and which way it faces, for one evaluation of the graph. */
|
|
13
|
+
export declare function evaluateV3Surface(values: Dynamic, s: Dynamic): Dynamic;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=v3-surface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-surface.d.ts","sourceRoot":"","sources":["../src/v3-surface.ts"],"names":[],"mappings":"AASA,KAAK,OAAO,GAAG,GAAG,CAAA;AAiBlB,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,KAAK,EAAE,OAAO,CAAA;IACd,qCAAqC;IACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,KAAK,EAAE,OAAO,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAgD/F;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAuBtE"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One reader for "this face of this part, in the asset frame". The capability bindings named their contacts this
|
|
3
|
+
* way first; the declared occupancy names the face it is mounted on the same way (V3 designer's ruling
|
|
4
|
+
* 2026-09-23), and neither writes the rule twice.
|
|
5
|
+
*
|
|
6
|
+
* A surface is a pair of graph outputs: an actual placement and a `feature@1` on the same shape. That pairing is
|
|
7
|
+
* what makes it a real face of a real drawn body, rather than any transform an author points at.
|
|
8
|
+
*/
|
|
9
|
+
import { V3ContractError as ContractError } from "./v3-contract.js";
|
|
10
|
+
const fail = (code, path, message) => {
|
|
11
|
+
throw new ContractError(code, path, message);
|
|
12
|
+
};
|
|
13
|
+
/** Outward normals in the shape's own frame, by feature name. */
|
|
14
|
+
const NORMALS = {
|
|
15
|
+
left: [-1, 0, 0],
|
|
16
|
+
right: [1, 0, 0],
|
|
17
|
+
bottom: [0, -1, 0],
|
|
18
|
+
top: [0, 1, 0],
|
|
19
|
+
back: [0, 0, -1],
|
|
20
|
+
front: [0, 0, 1],
|
|
21
|
+
start: [0, -1, 0],
|
|
22
|
+
end: [0, 1, 0]
|
|
23
|
+
};
|
|
24
|
+
/** Check that a `{ placement, feature }` pair names a supported planar face, and say which one. */
|
|
25
|
+
export function resolveV3Surface(ctx, contact, path) {
|
|
26
|
+
const { types, writers, model, assetFrame } = ctx;
|
|
27
|
+
if (!contact || typeof contact !== 'object' || Array.isArray(contact))
|
|
28
|
+
fail('SCHEMA', path, 'object required');
|
|
29
|
+
for (const k of ['placement', 'feature'])
|
|
30
|
+
if (!Object.hasOwn(contact, k))
|
|
31
|
+
fail('SCHEMA', path, `missing ${k}`);
|
|
32
|
+
for (const k of Object.keys(contact))
|
|
33
|
+
if (!['placement', 'feature'].includes(k))
|
|
34
|
+
fail('SCHEMA', path, `unknown ${k}`);
|
|
35
|
+
const typeOf = (ref) => {
|
|
36
|
+
if (typeof ref !== 'string' || !ref.trim())
|
|
37
|
+
fail('SCHEMA', path, 'nonempty string required');
|
|
38
|
+
if (!Object.hasOwn(types, ref))
|
|
39
|
+
fail('TARGET_ABSENT', path, ref);
|
|
40
|
+
return types[ref];
|
|
41
|
+
};
|
|
42
|
+
const placedType = typeOf(contact.placement), featureType = typeOf(contact.feature);
|
|
43
|
+
const place = writers.get(contact.placement), feature = writers.get(contact.feature);
|
|
44
|
+
let source = place, selection;
|
|
45
|
+
if (place?.op === 'select-item@1') {
|
|
46
|
+
const repeat = writers.get(place.args[0]), assembly = repeat?.op === 'repeat@1' ? writers.get(repeat.args[1]) : null;
|
|
47
|
+
if (assembly?.op !== 'assembly@1' || !assembly.args.includes(place.params.member))
|
|
48
|
+
fail('SURFACE_PROVENANCE', path, 'selected member must belong to the referenced repeat assembly');
|
|
49
|
+
source = writers.get(place.params.member);
|
|
50
|
+
if (source?.op !== 'member@1')
|
|
51
|
+
fail('SURFACE_PROVENANCE', path, 'selected source must be a declared template member');
|
|
52
|
+
if (model.inputs.some((i) => i.id === place.args[1] && i.role === 'state'))
|
|
53
|
+
fail('TARGET_STATE', path, 'binding target index must not change as animation state');
|
|
54
|
+
selection = { collection: place.args[0], index: place.args[1], member: place.params.member };
|
|
55
|
+
}
|
|
56
|
+
else if (place?.op !== 'place@1')
|
|
57
|
+
fail('SURFACE_PROVENANCE', path, 'actual placement or selected repeated member required');
|
|
58
|
+
if (feature?.op !== 'feature@1')
|
|
59
|
+
fail('SURFACE_PROVENANCE', path, 'provider feature required');
|
|
60
|
+
if (placedType.kind !== 'placed' || placedType.frame !== assetFrame || featureType.kind !== 'transform')
|
|
61
|
+
fail('FRAME_TYPE', path, 'contact must be in asset frame');
|
|
62
|
+
if (source.args[0] !== feature.args[0])
|
|
63
|
+
fail('SURFACE_OWNER', path, 'feature belongs to a different shape declaration');
|
|
64
|
+
const provider = types[feature.args[0]].provider, face = feature.params.name;
|
|
65
|
+
/*
|
|
66
|
+
Flat faces only. A rounded box is flat top and bottom whichever way its corners are rounded, so the per-axis
|
|
67
|
+
form (`rounded-box@2`, provider `roundedBoxXZ`) offers the same two faces as `rounded-box@1`; a hollow box
|
|
68
|
+
likewise. Everything else — a sphere, a frustum's slant, a cylinder's barrel — is not a plane to rest on.
|
|
69
|
+
*/
|
|
70
|
+
const allowed = provider === 'box'
|
|
71
|
+
? ['left', 'right', 'bottom', 'top', 'back', 'front']
|
|
72
|
+
: provider === 'roundedBox' || provider === 'roundedBoxXZ' || provider === 'hollowBox' || provider === 'hollowBoxXZ'
|
|
73
|
+
? ['bottom', 'top']
|
|
74
|
+
: provider === 'cylinder'
|
|
75
|
+
? ['start', 'end']
|
|
76
|
+
: [];
|
|
77
|
+
if (!allowed.includes(face))
|
|
78
|
+
fail('SURFACE_UNSUPPORTED', path, 'feature is not a supported planar surface');
|
|
79
|
+
return { ...contact, geometryId: selection ? null : place.id, selection, provider, face, frame: featureType.from, normal: NORMALS[face] };
|
|
80
|
+
}
|
|
81
|
+
/** Where a resolved surface sits, and which way it faces, for one evaluation of the graph. */
|
|
82
|
+
export function evaluateV3Surface(values, s) {
|
|
83
|
+
const placed = values[s.placement], local = values[s.feature], r = placed.pose.r;
|
|
84
|
+
const normal = r.map((row) => row.reduce((v, x, i) => v + x * s.normal[i], 0));
|
|
85
|
+
const t = r.map((row, j) => placed.pose.t[j] + row.reduce((v, x, i) => v + x * local.t[i], 0));
|
|
86
|
+
let geometryId = s.geometryId;
|
|
87
|
+
if (s.selection) {
|
|
88
|
+
const q = s.selection, item = values[q.collection].items.find((g) => g.index === values[q.index] && g.key === q.member);
|
|
89
|
+
if (!item)
|
|
90
|
+
fail('TARGET_ABSENT', s.placement, 'selected repeated item is absent');
|
|
91
|
+
geometryId = item.id;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
placement: s.placement,
|
|
95
|
+
feature: s.feature,
|
|
96
|
+
geometryId,
|
|
97
|
+
provider: s.provider,
|
|
98
|
+
face: s.face,
|
|
99
|
+
pose: { r: r.map((row) => [...row]), t },
|
|
100
|
+
normal,
|
|
101
|
+
provenance: 'verified-planar-surface'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=v3-surface.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v3-surface.js","sourceRoot":"","sources":["../src/v3-surface.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,14 +18,6 @@
|
|
|
18
18
|
"properties": {
|
|
19
19
|
"target": {"type": "string", "minLength": 1},
|
|
20
20
|
"segments": {"enum": [8,12,16,24,32,48]},
|
|
21
|
-
"legacyCapability": {
|
|
22
|
-
"type": "object", "additionalProperties": false, "required": ["roles", "accepts", "capacity"],
|
|
23
|
-
"properties": {
|
|
24
|
-
"roles": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
|
25
|
-
"accepts": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
|
26
|
-
"capacity": {"type": "integer", "minimum": 1, "maximum": 9007199254740991}
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
21
|
"material": {
|
|
30
22
|
"type": "object", "additionalProperties": false, "required": ["token"],
|
|
31
23
|
"properties": {
|
|
@@ -42,7 +42,7 @@ appearance는 모든 `place@1`/`member@1`에 정확히 하나씩 필요하다.
|
|
|
42
42
|
|
|
43
43
|
원기둥 분할 수는 현재 지원 preset으로 명시 저장한다. 사용자에게 분할 수 직접 입력 UI를 추가하지 않았다. 다른 provider에 임의의 segments를 붙이는 것은 거부한다. 재질은 token/preset/transparent/emissive 범위를 지원하며 미지원 필드를 조용히 버리지 않고 거부한다.
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
부품에 붙어 있던 구버전 capability 메타데이터는 정본에 자리를 두지 않는다(2026-09-23). 변환기는 그것을 `PART_CAPABILITY` 손실로 보고하고, 어느 자산도 그 값을 담지 않았다. 자리만 남겨 두면 검증된 binding과 구별되지 않으므로 없앴다. `slot`을 `seat`/`grip`으로 자동 해석하지도 않는다. 이 의미를 V3로 가져오려면 검증되는 binding으로 다시 저작한다.
|
|
46
46
|
|
|
47
47
|
## 검증 결과
|
|
48
48
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# V3의 V2 잔재 위험 점검
|
|
2
|
+
|
|
3
|
+
2026-09-23. figure-model, things-scene, things-factory 소스 정적 점검.
|
|
4
|
+
목표는 V2 재현이 아니라 V3의 독립적 저작·평가 계약이다. 표본 통과는 전체 범위 증명이 아니다.
|
|
5
|
+
이 문서는 전체 제품 전환 완료 선언이 아니다. 운영 DB, 배포, 설치 버전은 변경하지 않았다.
|
|
6
|
+
|
|
7
|
+
## 우선순위와 조치
|
|
8
|
+
|
|
9
|
+
| 우선순위 | 근거 | 위험 | 조치/완료 기준 |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| P1 | things-factory `figure-service/server/service/figure/figure-source.ts`, `figure-mutation.ts` | V3 저장은 별도 경로지만 기존 생성·수정·복원은 V2 parseSource를 사용한다. V3 데이터 전환 뒤 제품 경로가 깨질 수 있다. | 전환 전 저장·발행·복원·AI·썸네일 전체 V3 경로 시험. 이번 점검에서 운영 경로 전환하지 않음. |
|
|
12
|
+
| P1 | things-scene `src/figure/v3/figure-v3-rig.ts`의 clipOf | driver ID의 `/` 앞부분을 재생 그룹으로 해석한다. V3 타입은 ID 문자열 형식을 요구하지 않는다. 이름 변경이 동작을 바꿀 수 있다. | 명시적 재생 그룹 계약을 설계하고 소비처와 변환기를 함께 수정. ID는 불투명 식별자. 런타임에서 V2 이름을 추측하는 fallback은 최종 제거. |
|
|
13
|
+
| P2 | `src/v3-asset-types.ts`, `v3-asset.ts`, scene rig | legacyCapability가 V3 정본·평가·렌더러에 남아 있다. 검증된 binding과 혼동되고 유지 비용이 생긴다. | V3 binding으로 재저작하거나 변환 보고서에 보존. 정본에서 제거할 때 fixture·소비처 동시 수정. 단순 삭제로 의미 손실을 숨기지 않음. |
|
|
14
|
+
| P2 | `src/v3-gate.ts` → `v3-mesh-compare.ts` | 제품 검사기가 V2 비교용 코드를 의존한다. 비교용 변경이 제품 판정에 전파된다. | 이번 변경에서 V3 형상 유틸리티를 `v3-shape-sampling.ts`로 분리. migration이 유틸리티를 사용하고 역방향 의존을 금지하는 테스트 추가. |
|
|
15
|
+
| P2 | things-scene `src/figure/v3/figure-v3-real-object.ts` effectiveDepth | depth 생략 시 평면 크기 배율의 기하평균으로 높이를 결정하는 V2 정책이 V3 소비처에 남아 있다. | V3 설계 입력과 씬 크기 매핑 정책을 명시. 자동 기하평균은 V3 보편 규칙으로 취급하지 말 것. 정책 변경 시 기존 배치의 변화 검증. |
|
|
16
|
+
| P2 | `src/index.ts`, `v3-capabilities.ts`, `v3-asset.ts` | 공개 진입점에 V2 변환·비교 API가 섞이고 capability/placement 목록을 V2 types에서 가져온다. | 제품 공통 어휘와 구버전 전용 계약을 분리하고 V3 전용 진입점과 migration 진입점을 설계. 이번 점검에서는 package.json/exports를 변경하지 않음. |
|
|
17
|
+
|
|
18
|
+
## 유지할 것과 격리할 것
|
|
19
|
+
|
|
20
|
+
- min/max, 상태 의존 치수, 표시 여부, 구간별 매핑은 V3 자체 용도로 평가한다. V2가 도입 계기였다는 이유로 삭제하지 않는다.
|
|
21
|
+
- 변환기, V2 기준 구현, 비교 시험은 이행 도구다. 존재 자체가 문제가 아니라 제품 런타임의 역방향 의존이 문제다.
|
|
22
|
+
- 공유된 축/벡터 타입 자체는 호환성 실행 코드가 아니다. 다만 V3 정책을 V2 목록과 묶어서는 안 된다.
|
|
23
|
+
- 구면·타원·둥근 모서리의 정의는 독립적인 V3 형상 계약으로 설명한다. V2 화면 일치는 계약의 근거가 아니다.
|
|
24
|
+
- 실제 binding 의미를 검증하지 못한 legacy 메타데이터는 기능 구현으로 집계하지 않는다.
|
|
25
|
+
|
|
26
|
+
## 이번 변경의 한계
|
|
27
|
+
|
|
28
|
+
형상 샘플링의 파일 경계를 분리했을 뿐 연속 표면의 보수적 경계를 새로 증명하지 않았다.
|
|
29
|
+
유한 샘플의 min/max 또는 Hausdorff 결과를 연속 형상 전체의 보장으로 해석해서는 안 된다.
|
|
30
|
+
기존 API 재수출은 이행 도구를 깨뜨리지 않기 위해 유지했지만, V3 검사기의 내부 의존은 제거했다.
|
|
31
|
+
이 테스트는 알려진 migration/V2 실행 파일로의 정적 의존을 차단하며 모든 의미적 호환 잔재를 검출하지는 않는다.
|
|
32
|
+
|
|
33
|
+
## 후속 검증 기준
|
|
34
|
+
|
|
35
|
+
1. V2 fixture 없이 작성한 V3 자산으로 생성·편집·리사이즈·동작·저장·재열기 검증.
|
|
36
|
+
2. driver ID를 바꿔도 재생 그룹과 동작이 유지되는 테스트.
|
|
37
|
+
3. V3 정본/평가 결과에 legacyCapability가 없고 binding 의미가 명시되는 테스트.
|
|
38
|
+
4. 씬 크기 입력이 occupancy 수정과 독립적이며 높이 정책이 명시되는 테스트.
|
|
39
|
+
5. migration 도구를 제품 런타임 의존 그래프에서 제외한 빌드·실행 검증.
|
|
@@ -76,6 +76,8 @@ args 순서와 output 이름은 버전별 연산 계약이다. 전체 연산 cat
|
|
|
76
76
|
| mul@1 | a,b | value=a*b | overflow/nonfinite |
|
|
77
77
|
| div@1 | a,b | value=a/b | b=0, overflow/nonfinite |
|
|
78
78
|
| geomean@1 | a,b[,c] (동일 차원) | value=(a·b[·c])^(1/n) | 음수 입력 |
|
|
79
|
+
| at-least@1 | a,b (동일 차원) | value=flag(a≥b). **flag 는 치수·pose·occupancy 로 못 간다** — appearance.visibility 만 받는다 (V3 설계자 결정 2026-09-22) | 없음 |
|
|
80
|
+
| curve@1 | x, at₀,v₀, at₁,v₁, … (at 는 x 와 같은 차원, v 는 한 차원; 키 2개 이상) | value=키 사이는 직선, 키 밖은 끝 값 유지 (piecewise-linear; V3 설계자 승인 2026-09-22) | at 가 strictly increasing 이 아님 (index 로 보고) |
|
|
79
81
|
| sphere-shape@1 | radiusX,radiusY,radiusZ (길이) | shape(sphere) | 반지름≤0 |
|
|
80
82
|
| polygon-shape@1 | height, x0,z0, x1,z1, … (길이, 점 3개 이상) | shape(polygon; 단면은 X–Z, Y 로 민다) | height≤0, 단면이 한 축으로 퍼지지 않음 |
|
|
81
83
|
| frustum-shape@1 | radiusTop,radiusBottom,height (길이) | shape(frustum; 축은 Y, radiusTop 이 +Y) | height≤0, 반지름 음수, 둘 다 0 |
|
|
@@ -144,3 +144,114 @@ range, and says how it knows:
|
|
|
144
144
|
- Theme mapping and the colour cache (renderer work; ADR-0087 stage review ruling 4).
|
|
145
145
|
- Shape deformation beyond dimensions: bending, twisting, free-form profiles that change with state. Those would be
|
|
146
146
|
new providers with their own contracts.
|
|
147
|
+
|
|
148
|
+
## 4. 표시 여부 (visibility) — 안 보이는 것과 작은 것은 다르다
|
|
149
|
+
|
|
150
|
+
**V3 설계자 결정 2026-09-22.** V2 는 부품을 숨길 때 크기를 0 으로 만든다. V3 는 그 방법을 쓰지
|
|
151
|
+
않는다. 0 을 아주 작은 양수로 바꾸는 것도 쓰지 않는다 — 확대·그림자·포인터 선택에서 차이가
|
|
152
|
+
생기고, 「안 보임」이 「아주 작음」이 되어 버린다.
|
|
153
|
+
|
|
154
|
+
### 규칙
|
|
155
|
+
|
|
156
|
+
1. **형상의 치수는 언제나 양수다.** 이 원칙은 안 바뀐다.
|
|
157
|
+
2. **표시 여부는 치수와 분리된 상태 의존 속성이다.** `appearance.visibility` 가 graph 의 `flag`
|
|
158
|
+
값 하나를 가리킨다. 없으면 항상 보인다.
|
|
159
|
+
3. **`flag` 는 치수·pose·occupancy 로 흘러갈 수 없다.** 타입이 막는다. `at-least@1` 만 flag 를
|
|
160
|
+
만들고, `appearance.visibility` 만 flag 를 받는다.
|
|
161
|
+
4. **숨긴 부품도 id·부착 관계·동작 연결을 유지한다.** 평가에서 빠지지 않는다.
|
|
162
|
+
5. **숨긴 부품은 그리기·그림자·포인터 선택에서 빠진다.**
|
|
163
|
+
6. **선언한 occupancy 는 표시 여부로 줄거나 늘지 않는다.** 검사는 숨긴 부품도 그대로 잰다.
|
|
164
|
+
|
|
165
|
+
### 평가 계약 — 숨겨진 상태에서 무엇을 계산하는가
|
|
166
|
+
|
|
167
|
+
**숨긴 부품은 보일 때와 똑같은 치수를 계산한다.** 숨김 전용 계산 경로가 없다. 3 번 규칙이
|
|
168
|
+
이것을 보장한다 — flag 가 치수 식에 닿을 수 없으니, 치수는 표시 여부와 무관하게 같은 값이다.
|
|
169
|
+
그래서 「숨겨진 상태의 잘못된 치수」라는 상태가 존재할 수 없다.
|
|
170
|
+
|
|
171
|
+
읽는 쪽이 쓰는 두 가지 목록이 갈린다.
|
|
172
|
+
|
|
173
|
+
| 목록 | 무엇이 들어가는가 | 누가 쓰는가 |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `v3WorldBoxesOf` | 그리는 몸통만 (`visible === false` 제외) | renderer, V2 비교 |
|
|
176
|
+
| `v3OccupiedBoxesOf` | 놓인 몸통 전부 (숨긴 것 포함) | occupancy 검사 |
|
|
177
|
+
|
|
178
|
+
### 변환기가 V2 의 scale 0 을 옮기는 방법
|
|
179
|
+
|
|
180
|
+
채널이 **모든 변하는 축에서 0 으로 시작하고, 처음 0 이 아닌 key 부터 끝까지 정확히 1** 이면
|
|
181
|
+
(= 부품 제 크기) visibility 로 옮긴다. 부품은 제 치수를 계속 갖고, flag 가 그 key 의 `at` 에서
|
|
182
|
+
켜진다. 그 외 — 숨기면서 크기도 바꾸는 채널 — 은 `SCALE_ZERO` 로 거절한다.
|
|
183
|
+
|
|
184
|
+
이것은 **의도적 재저작이다**(`method: 're-authored'`, `RE_AUTHORED_VISIBILITY` note). 0 과
|
|
185
|
+
문턱 사이에서 V2 는 자라나는 부품을 그리고 V3 는 안 그린다. DRY_ROOM 에서 실측했다: 문턱은
|
|
186
|
+
파라미터 범위의 1 %, 그 밖 모든 상태에서 V2 와 같고 차이는 그 1 % 안에서만 난다.
|
|
187
|
+
|
|
188
|
+
## When `proven` may be said (added 2026-09-23, after two counterexamples from the V3 designer)
|
|
189
|
+
|
|
190
|
+
`proven` means the gate evaluated the corners of the state box and nothing else. That is only a proof when every
|
|
191
|
+
coordinate the figure draws takes its extreme at a corner, and the property that guarantees it is **multilinearity**:
|
|
192
|
+
degree one in each state input, holding the others fixed. A multilinear value stays multilinear however it is
|
|
193
|
+
combined, which is why it can be built up through a whole figure.
|
|
194
|
+
|
|
195
|
+
Every operator in the kernel is classified by name in `V3_STATE_EXTREME_BASIS`, with no pattern and no default, and
|
|
196
|
+
a test compares that list against `V3_OPERATORS`: a new operator is unclassified, and unclassified is `unknown`.
|
|
197
|
+
|
|
198
|
+
`multilinear`, under the conditions the gate checks: `add@1`; `mul@1` when no two arguments depend on the same state
|
|
199
|
+
input; `div@1` by a state-free denominator; `rigid@1` when no rotation angle depends on state; `axis-slide@1` when the
|
|
200
|
+
axis is state-free; `compose@1`, `attach@1` and `map-point@1` when every pose reaching them has a state-free rotation
|
|
201
|
+
— `compose(a, b).t` is `a.r·b.t + a.t`, so a rotation that moves multiplies a translation that moves, and
|
|
202
|
+
`attach(a, o, b)` is two compositions with `inverse(b).t = −b.rᵀ·b.t`, which is the same shape of product;
|
|
203
|
+
`place@1`, `member@1`, `assembly@1`, `repeat@1`, `select-item@1`, `point@1`, `between@1`, `box@1`, `cylinder@1`, the
|
|
204
|
+
layout operators, which refuse state outright; `feature@1`, whose pose is a linear combination of the shape's
|
|
205
|
+
dimensions with an axis fixed by the feature's name rather than its size; the shape providers, whose extents are
|
|
206
|
+
linear in their arguments; and `at-least@1`, whose flag no coordinate can read.
|
|
207
|
+
|
|
208
|
+
`monotone` — may stand as a final coordinate, may not be combined with anything else moving with the same input:
|
|
209
|
+
`min@1`, `max@1`, `curve@1`, `geomean@1`, each refusing arguments that share a state input.
|
|
210
|
+
|
|
211
|
+
`unknown` — nothing is claimed, and a state-dependent argument sends the asset to sampling: `axis-turn@1`, `sin@1`,
|
|
212
|
+
`distance@1`, and `polygon-shape@1`, whose world box is the largest of its vertex coordinates, so two vertices
|
|
213
|
+
carrying one input fold it the way `min@1` folds.
|
|
214
|
+
|
|
215
|
+
A value from anything outside the multilinear list is **bent** in each state input it depends on. A bent value is at best monotone in that
|
|
216
|
+
input, so it may stand as a final coordinate on its own, and it may not be combined with anything else that moves with
|
|
217
|
+
the same input. `min@1`, `max@1` and `curve@1` are refused outright when two of their own arguments share an input.
|
|
218
|
+
|
|
219
|
+
The two counterexamples that set this, both of which the gate had called `proven` with no violation:
|
|
220
|
+
|
|
221
|
+
| expression | widest at | the gate had said |
|
|
222
|
+
| --- | --- | --- |
|
|
223
|
+
| `10 + 100·min(q, 1−q)`, q in [0, 1] | q = ½, 20 mm outside | proven, 0 violations |
|
|
224
|
+
| `10 + 100·(√q − q)`, q in [0.01, 1], √ as `geomean(q, 1)` | q = ¼, 7.5 mm outside | proven, 0 violations |
|
|
225
|
+
|
|
226
|
+
A new operator is bent until someone shows it preserves multilinearity and adds it to the list. Refusing costs
|
|
227
|
+
coverage, not correctness: the asset falls back to `sampled`, which states its step and is not called a proof.
|
|
228
|
+
|
|
229
|
+
### Fitting the volume and touching the plane are two claims
|
|
230
|
+
|
|
231
|
+
`proven` is about one thing: every part stays inside the declared volume. Whether the figure touches the plane it
|
|
232
|
+
is mounted on is the *smallest* gap over its parts, and a smallest folds the way `min@1` folds. Two boxes at 20q
|
|
233
|
+
and 20(1−q) mm above the floor each touch it at one end of q and both float 10 mm in the middle, with every
|
|
234
|
+
coordinate linear in q (V3 designer's counterexample, 2026-09-23). The corners were extremal and the gate still
|
|
235
|
+
missed it.
|
|
236
|
+
|
|
237
|
+
Reading it as the lowest point of the figure was no better: a box hanging entirely below the plane passed, because
|
|
238
|
+
its lowest point was under zero, and a design that reaches under the plane on purpose could not pass at all. That is
|
|
239
|
+
a one-sided position test, not contact.
|
|
240
|
+
|
|
241
|
+
Two things then have to hold for that face to lie in the plane, and the first alone is not enough: a box stood on
|
|
242
|
+
its side, with its right-hand face declared as the mounting face, passed because that face's centre happened to sit
|
|
243
|
+
at Y = 0. A vertical face and a horizontal floor are not the same plane. So the check is the face's reference point
|
|
244
|
+
against the plane, within a distance tolerance, **and** its normal parallel to the plane's, within an angle
|
|
245
|
+
tolerance. Whether the normal points into the plane or away from it is a separate contract — which way up the figure
|
|
246
|
+
is mounted — and is not asked here.
|
|
247
|
+
|
|
248
|
+
The plane is Y = 0 in the asset frame, for a floor figure and a ceiling one alike, because the origin is where the
|
|
249
|
+
figure is fastened. It is not read off `occupancy.bounds.y.max`: that moved the thing the figure hangs from whenever
|
|
250
|
+
someone widened the volume it takes up.
|
|
251
|
+
|
|
252
|
+
So an asset that needs the contact **names the face that makes it** — a placement and a `feature@1` on the same
|
|
253
|
+
shape, read by `resolveV3Surface`, the same reader the capability bindings use. The check is that face against the
|
|
254
|
+
plane, both ways. There is no smallest over parts left to fold, and what remains to ask is whether the face moves: a
|
|
255
|
+
face whose placement does not follow state sits in the same place at every state, so one evaluation settles it, and
|
|
256
|
+
a face that moves sends the asset to sampling, because corner extremality says nothing about an equality holding in
|
|
257
|
+
between. An asset that names no mounting face is not asked to touch anything.
|