partforge 0.133.1 → 0.133.3
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/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -296,7 +296,7 @@ above. All ops return a `Solid`.
|
|
|
296
296
|
| `vector2d(name, {shape?, width\|height\|fit, align?, valign?})` | A declared vector document → `Shape2D`. `name` = a declared name in the part's `vectors` field (`partforge-vector` JSON — authored by hand or ingested from an `.svg`; never raw `.svg`). With no `shape`, the document's own composition: every `"add"` shape unioned minus every `"subtract"` shape; `shape` selects one named shape's geometry whatever its role. Sizing follows the document's required `units`: `"artwork"` requires exactly one of `width`/`height`/`fit` in millimetres, `"mm"` places as authored (scale 1, no translate) and accepts a size option optionally; more than one is refused. Uniform scale in every case (`fit` = larger extent); `align`/`valign` position it, same as `text2d`, defaulting to centre/middle for `"artwork"` and to no translate for `"mm"`. **The composed call derives ONE transform from every region in the document**, `"add"` and `"subtract"` alike, so a size or align option cannot scale the subtracts relative to the adds; a `shape` call is measured against that shape alone. **Primitive contours (`circle`/`rect`/`polygon`) expand to ordinary path contours at the format boundary, in `vector-format.js`, so this op's kernel-facing contract is unchanged by them** — nothing below the loader learns primitives exist. Conformance: **both backends** (it lowers to `shape2d` + `union`, exactly like `text2d`). Parity: **identical across backends by construction** — the regions are curve-native (arcs/cubics), so there is no sampling step for the two backends to diverge over. |
|
|
297
297
|
| `hull(inputs[])` | Convex hull of all inputs (each a `Shape2D`, a curve contour, or an `[[x,y],…]` point list) → a convex `Shape2D`. Backend-agnostic: a pure-JS monotone-chain hull over the inputs' sampled points (curved inputs tessellated at a fixed LOD), lifted via `shape2d` (see the parity note below). Throws on an empty input array or a degenerate (collinear/point-count < 3) hull. |
|
|
298
298
|
| `hullChain(inputs[])` | Swept hull over an ordered sequence of ≥2 inputs (same input forms as `hull`): the union of `hull([inᵢ, inᵢ₊₁])` for each consecutive pair — e.g. a tapered link connecting a row of circles. Throws with fewer than 2 inputs. |
|
|
299
|
-
| `toSTEP(named[])` | `[{name, solid, color?}]` → `Promise<ArrayBuffer>` of a STEP assembly. B-rep class only. `color` (`0xRRGGBB`, optional) is the body's surface colour, written as that exact sRGB value; a body without one (or with an unusable one) is written in the viewer's no-material blue-grey (`0x9fb4cc`), never a kernel default. The framework passes each sub-part's display colour — the one its 3MF object gets. Additive: `CONTRACT_VERSION` stays 4, the `import` precedent. |
|
|
299
|
+
| `toSTEP(named[])` | `[{name, solid, color?}]` → `Promise<ArrayBuffer>` of a STEP assembly. B-rep class only. `color` (`0xRRGGBB`, optional) is the body's surface colour, written as that exact sRGB value; a body without one (or with an unusable one) is written in the viewer's no-material blue-grey (`0x9fb4cc`), never a kernel default. The framework passes each sub-part's display colour — the one its 3MF object gets. Every file is AP242, its header's `FILE_SCHEMA` and its body alike — the first export in a process included. Additive: `CONTRACT_VERSION` stays 4, the `import` precedent. |
|
|
300
300
|
| `import(name)` | Previously-registered imported geometry (STEP/STL/3MF, declared in the part's `imports` field) as an ordinary `Solid`. Required on both in-repo backends (Manifold accepts mesh formats, OCCT accepts STEP); a format the routed backend can't use throws lazily, at this call, not at registration. Fed by an underscore-prefixed side-channel, not part authors — see [Conformance classes](#conformance-classes). Additive: not in `OCCT_ONLY_OPS`; needed no `CONTRACT_VERSION` bump of its own (v3 came from the mesh fillet/chamfer change, not this op). |
|
|
301
301
|
|
|
302
302
|
`hull`/`hullChain` parity: point-list and curve-contour inputs hull bit-identically
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// STEP writer for the OCCT backend: replicad 0.23.1's `exportSTEP` (and the
|
|
2
|
-
// `createAssembly` it calls), changed
|
|
2
|
+
// `createAssembly` it calls), changed in how each body's colour is made and in
|
|
3
|
+
// when the writer's model is made (so every file's header names AP242; below).
|
|
3
4
|
//
|
|
4
5
|
// replicad gets colour wrong twice. A body with no colour is painted its default
|
|
5
6
|
// red, so every exported part opened red in a CAD tool. And a colour it is given
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
//
|
|
16
17
|
// Everything else — the XCAF document, naming, the writer's modes and statics,
|
|
17
18
|
// the FS round trip, and which native objects are handed to GC — is replicad's
|
|
18
|
-
// own, so this writes the same file replicad did apart from the colours
|
|
19
|
+
// own, so this writes the same file replicad did apart from the colours and the
|
|
20
|
+
// first file in a process, whose header replicad left saying AP214. The
|
|
19
21
|
// document handle passed to Transfer is deliberately never freed: the document
|
|
20
22
|
// itself is owned by its WrappingObj (as replicad's AssemblyExporter owns it),
|
|
21
23
|
// and freeing the handle too would destroy it twice.
|
|
@@ -51,14 +53,22 @@ export function writeStep(replicad, bodies) {
|
|
|
51
53
|
shapeTool.UpdateAssemblies();
|
|
52
54
|
|
|
53
55
|
const session = r(new oc.XSControl_WorkSession());
|
|
54
|
-
const
|
|
55
|
-
writer.
|
|
56
|
-
writer.SetLayerMode(true);
|
|
57
|
-
writer.SetNameMode(true);
|
|
56
|
+
const sessionHandle = r(new oc.Handle_XSControl_WorkSession_2(session));
|
|
57
|
+
const writer = r(new oc.STEPCAFControl_Writer_2(sessionHandle, false));
|
|
58
58
|
oc.Interface_Static.SetIVal("write.surfacecurve.mode", true);
|
|
59
59
|
oc.Interface_Static.SetIVal("write.precision.mode", 0);
|
|
60
60
|
oc.Interface_Static.SetIVal("write.step.assembly", 2);
|
|
61
61
|
oc.Interface_Static.SetIVal("write.step.schema", 5);
|
|
62
|
+
// The header's FILE_SCHEMA is fixed when the writer's model is made, which
|
|
63
|
+
// constructing the writer does — before the statics above are set. They cannot
|
|
64
|
+
// go first: the process's first STEP writer is what defines them, so until then
|
|
65
|
+
// SetIVal is a silent no-op. So the first file in a process said AP214 (OCCT's
|
|
66
|
+
// default) over an AP242 body. Re-initialising with a fresh model now stamps
|
|
67
|
+
// the header with the schema the body is written in, on every export.
|
|
68
|
+
writer.Init(sessionHandle, true);
|
|
69
|
+
writer.SetColorMode(true);
|
|
70
|
+
writer.SetLayerMode(true);
|
|
71
|
+
writer.SetNameMode(true);
|
|
62
72
|
const progress = r(new oc.Message_ProgressRange_1());
|
|
63
73
|
writer.Transfer_1(new oc.Handle_TDocStd_Document_2(doc.wrapped), oc.STEPControl_StepModelType.STEPControl_AsIs, null, progress);
|
|
64
74
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/framework/stale-touch-guard.js
|
|
2
|
+
// Keeps the camera controls from holding onto a finger that has already left
|
|
3
|
+
// the screen.
|
|
4
|
+
//
|
|
5
|
+
// OrbitControls tracks every pointer that goes down on the canvas and forgets
|
|
6
|
+
// one only on its pointerup or pointercancel. When the page is embedded in a
|
|
7
|
+
// host that runs its own gestures (a chat app's WKWebView, which scrolls the
|
|
8
|
+
// conversation on a vertical drag), the host can take a drag over and the page
|
|
9
|
+
// never hears that finger lift. The controls then keep it as down forever: the
|
|
10
|
+
// next one-finger drag reads as a two-finger pinch (it zooms), and with two
|
|
11
|
+
// stuck, no drag moves the camera at all.
|
|
12
|
+
//
|
|
13
|
+
// Two measures, both on the canvas's document and both passive:
|
|
14
|
+
//
|
|
15
|
+
// 1. Do-nothing touchstart/touchend/touchcancel listeners. WebKit on iOS hands
|
|
16
|
+
// a touch to the page only where the page listens for TOUCH events, and the
|
|
17
|
+
// controls listen for pointer events alone, so without these the host's
|
|
18
|
+
// gesture recognizers could claim a drag the page itself was handling.
|
|
19
|
+
//
|
|
20
|
+
// 2. Stale fingers are cancelled on arrival. A touch pointerdown marked
|
|
21
|
+
// isPrimary is the first finger of a brand-new gesture, so no other touch
|
|
22
|
+
// can really be down; every touch id still recorded as down on the canvas is
|
|
23
|
+
// sent the pointercancel it missed, from a capture listener on the document
|
|
24
|
+
// (which runs before the controls' own listener on the canvas). A second
|
|
25
|
+
// finger of the same gesture is never primary, so pinch and two-finger pan
|
|
26
|
+
// are untouched.
|
|
27
|
+
//
|
|
28
|
+
// The controls release pointer capture for the id they remove, and that call
|
|
29
|
+
// throws for an id the browser has already forgotten, aborting their cleanup
|
|
30
|
+
// half way (the move listeners and the gesture state would stay live). So the
|
|
31
|
+
// canvas's releasePointerCapture is wrapped to ignore that one failure.
|
|
32
|
+
const TOUCH_TYPES = ["touchstart", "touchend", "touchcancel"];
|
|
33
|
+
|
|
34
|
+
export function guardStaleTouches(element) {
|
|
35
|
+
const doc = element.ownerDocument;
|
|
36
|
+
if (!doc) return () => {};
|
|
37
|
+
const noop = () => {};
|
|
38
|
+
const touchesDown = new Set();
|
|
39
|
+
|
|
40
|
+
const release = element.releasePointerCapture;
|
|
41
|
+
if (typeof release === "function") {
|
|
42
|
+
element.releasePointerCapture = function releaseIfHeld(pointerId) {
|
|
43
|
+
try {
|
|
44
|
+
release.call(this, pointerId);
|
|
45
|
+
} catch {
|
|
46
|
+
// Already released: the finger is gone, which is all the caller wanted.
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const onDown = (event) => {
|
|
52
|
+
if (event.pointerType !== "touch") return;
|
|
53
|
+
if (event.isPrimary && touchesDown.size) {
|
|
54
|
+
for (const pointerId of [...touchesDown]) {
|
|
55
|
+
if (pointerId === event.pointerId) continue;
|
|
56
|
+
touchesDown.delete(pointerId);
|
|
57
|
+
element.dispatchEvent(new PointerEvent("pointercancel", { pointerId, pointerType: "touch", bubbles: true }));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (event.target === element) touchesDown.add(event.pointerId);
|
|
61
|
+
};
|
|
62
|
+
const onEnd = (event) => { touchesDown.delete(event.pointerId); };
|
|
63
|
+
|
|
64
|
+
for (const type of TOUCH_TYPES) doc.addEventListener(type, noop, { capture: true, passive: true });
|
|
65
|
+
doc.addEventListener("pointerdown", onDown, { capture: true, passive: true });
|
|
66
|
+
doc.addEventListener("pointerup", onEnd, { capture: true, passive: true });
|
|
67
|
+
doc.addEventListener("pointercancel", onEnd, { capture: true, passive: true });
|
|
68
|
+
|
|
69
|
+
return function dispose() {
|
|
70
|
+
for (const type of TOUCH_TYPES) doc.removeEventListener(type, noop, { capture: true });
|
|
71
|
+
doc.removeEventListener("pointerdown", onDown, { capture: true });
|
|
72
|
+
doc.removeEventListener("pointerup", onEnd, { capture: true });
|
|
73
|
+
doc.removeEventListener("pointercancel", onEnd, { capture: true });
|
|
74
|
+
if (typeof release === "function") element.releasePointerCapture = release;
|
|
75
|
+
touchesDown.clear();
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/framework/viewer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
3
|
+
import { guardStaleTouches } from "./stale-touch-guard.js";
|
|
3
4
|
import { toCreasedNormals } from "three/addons/utils/BufferGeometryUtils.js";
|
|
4
5
|
import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
|
|
5
6
|
import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
|
|
@@ -231,6 +232,9 @@ export function createViewer(container, part) {
|
|
|
231
232
|
|
|
232
233
|
const controls = new OrbitControls(activeCamera, renderer.domElement);
|
|
233
234
|
controls.enableDamping = true;
|
|
235
|
+
// A finger a host gesture swallowed would otherwise stay "down" in the
|
|
236
|
+
// controls forever (every later drag zooms) — see stale-touch-guard.js.
|
|
237
|
+
const disposeTouchGuard = guardStaleTouches(renderer.domElement);
|
|
234
238
|
|
|
235
239
|
// --- render on demand -------------------------------------------------------
|
|
236
240
|
// The loop ticks every animation frame (controls, tweens, frame listeners —
|
|
@@ -2608,6 +2612,7 @@ export function createViewer(container, part) {
|
|
|
2608
2612
|
canonicalCaptureHidden.clear();
|
|
2609
2613
|
camTween.cancel();
|
|
2610
2614
|
controls.dispose();
|
|
2615
|
+
disposeTouchGuard();
|
|
2611
2616
|
for (const t of flashTimers) clearTimeout(t);
|
|
2612
2617
|
flashTimers.clear();
|
|
2613
2618
|
// A dot whose timer was just cancelled — or one held indefinitely — still
|