partforge 0.133.0 → 0.133.1
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/AUTHORING-PARTS.md
CHANGED
|
@@ -319,7 +319,7 @@ and asks for the existing positional view argument
|
|
|
319
319
|
## Materials and appearance
|
|
320
320
|
|
|
321
321
|
A sub-part's `display` block says how it LOOKS. Appearance never changes
|
|
322
|
-
geometry, exports (other than
|
|
322
|
+
geometry, exports (other than export colours, below), `measure` or `verify`, and a
|
|
323
323
|
mistake in it never fails a build — lint warns and the viewer falls back.
|
|
324
324
|
|
|
325
325
|
```js
|
|
@@ -336,7 +336,7 @@ meta: { title: "…", units: "mm", environment: "studio" },
|
|
|
336
336
|
else the viewer's blue-grey — and **realistic mode shows it as a PLA print**
|
|
337
337
|
(`pla-print`'s finish and layer lines) in that same colour. So an untouched
|
|
338
338
|
part looks printed in realistic mode; name a material when it is made some
|
|
339
|
-
other way. The PLA look is realistic-only: the CAD view,
|
|
339
|
+
other way. The PLA look is realistic-only: the CAD view, export colours and
|
|
340
340
|
`declaresMaterials` are unaffected, and a part that names no material is not
|
|
341
341
|
treated as declaring one.
|
|
342
342
|
- `color` — the base colour (`0xRRGGBB`). With a preset it is the TINT. Presets
|
|
@@ -378,9 +378,11 @@ on oak shrinks the grain to a 0.2 mm tile, i.e. invisible noise):
|
|
|
378
378
|
|
|
379
379
|
Presets without a pattern ignore it.
|
|
380
380
|
|
|
381
|
-
**3MF export**
|
|
382
|
-
colour), so a multi-colour print opens in a slicer already split and
|
|
383
|
-
|
|
381
|
+
**3MF and STEP export** carry each sub-part's colour (its `color`, else its
|
|
382
|
+
preset's colour), so a multi-colour print opens in a slicer already split and
|
|
383
|
+
coloured, and each STEP body opens in a CAD tool in its own colour. A part with
|
|
384
|
+
no `color` or `material` exports a 3MF exactly as before; its STEP bodies take
|
|
385
|
+
the viewer's blue-grey.
|
|
384
386
|
|
|
385
387
|
| Preset | Name | Tintable | Use |
|
|
386
388
|
| --- | --- | --- | --- |
|
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}]` → `Promise<ArrayBuffer>` of a STEP assembly. B-rep class only. |
|
|
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. |
|
|
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
|
@@ -160,7 +160,7 @@ export const ROUTED_CAD_OPS = ["shell"];
|
|
|
160
160
|
* @property {(profile: number[][]|{outer:number[][],holes?:number[][][]}|{start:number[],segments:object[]}|Shape2D) => Shape2D} shape2d 2-D boolean value; one shared contour-storage implementation on both backends
|
|
161
161
|
* @property {(inputs: (Shape2D|number[][]|{start:number[],segments:object[]})[]) => Shape2D} hull convex hull of all inputs → a convex Shape2D (faceted; pure-JS monotone chain)
|
|
162
162
|
* @property {(inputs: (Shape2D|number[][]|{start:number[],segments:object[]})[]) => Shape2D} hullChain swept hull over an ordered sequence (≥2): union of hull([inᵢ,inᵢ₊₁])
|
|
163
|
-
* @property {(named:{name:string,solid:Solid}[]) => Promise<ArrayBuffer>} toSTEP OCCT only (Manifold throws KernelCapabilityError)
|
|
163
|
+
* @property {(named:{name:string,solid:Solid,color?:number|null}[]) => Promise<ArrayBuffer>} toSTEP OCCT only (Manifold throws KernelCapabilityError); `color` is 0xRRGGBB per body, absent = the viewer's no-material colour
|
|
164
164
|
* @property {(name: string) => Solid} import imported geometry declared in the part's imports field (registered pre-build by the framework via the underscore-prefixed `_registerImport`/`_importDigest`/`_acceptsStep`/`_acceptsMesh` side-channel, not a part author's calling surface)
|
|
165
165
|
* @property {(name:string) => void} [beginSubPart] open a per-sub-part solid-cache round (both backends)
|
|
166
166
|
* @property {() => void} [endSubPart] close the cache round (always pair with beginSubPart)
|
|
@@ -37,6 +37,7 @@ import { composePose, transformPositions, rotateNormals } from "./pose.js";
|
|
|
37
37
|
import { filterBrepEdges } from "./brep-edges.js";
|
|
38
38
|
import { meshToStl } from "./mesh-stl.js";
|
|
39
39
|
import { heightfieldMesh, hashGridData } from "./heightfield.js";
|
|
40
|
+
import { writeStep } from "./occt-step.js";
|
|
40
41
|
const MESH = { preview: { tolerance: 0.1, angularTolerance: 0.25 }, print: { tolerance: 0.01, angularTolerance: 0.1 } };
|
|
41
42
|
|
|
42
43
|
// Heightfield sew-time warning threshold, in triangles. Measured by the plan's
|
|
@@ -112,7 +113,7 @@ function stlBufferToShape(replicad, arrayBuffer) {
|
|
|
112
113
|
|
|
113
114
|
export function createOcctKernel(replicad) {
|
|
114
115
|
const { makeCylinder, makeBox, makeCircle, makeHelix, assembleWire, genericSweep,
|
|
115
|
-
loft, draw,
|
|
116
|
+
loft, draw, importSTEP, measureVolume, makeSphere, makeLine, Plane } = replicad;
|
|
116
117
|
|
|
117
118
|
// Feature-skip warnings: everything occt-repair (and roundAll) skips or rescues
|
|
118
119
|
// is recorded here as well as console.warned, and jobs.js drains it per
|
|
@@ -755,7 +756,10 @@ export function createOcctKernel(replicad) {
|
|
|
755
756
|
// region-in/region-out form directly. `_`-prefixed — not part of the public kernel
|
|
756
757
|
// surface.
|
|
757
758
|
_offsetRegions: offsetRegions,
|
|
758
|
-
|
|
759
|
+
// Our own writer, not replicad's exportSTEP, so colours come out right (see
|
|
760
|
+
// occt-step.js). `color` per body is optional; jobs.js passes the sub-part's
|
|
761
|
+
// printColor, the same colour its .3mf object gets.
|
|
762
|
+
toSTEP: async (named) => writeStep(replicad, named.map(({ name, solid, color }) => ({ name, shape: solid._mat()._s, color }))),
|
|
759
763
|
// Imported geometry, registered pre-build by the framework via `_registerImport`
|
|
760
764
|
// (ensureImports, Task 8). Every call clones the master shape — replicad ops
|
|
761
765
|
// consume their operands, and the master must never be handed out directly, or
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// STEP writer for the OCCT backend: replicad 0.23.1's `exportSTEP` (and the
|
|
2
|
+
// `createAssembly` it calls), changed only in how each body's colour is made.
|
|
3
|
+
//
|
|
4
|
+
// replicad gets colour wrong twice. A body with no colour is painted its default
|
|
5
|
+
// red, so every exported part opened red in a CAD tool. And a colour it is given
|
|
6
|
+
// is built with `Quantity_ColorRGBA(r/255, g/255, b/255, a)` — a LINEAR-RGB
|
|
7
|
+
// constructor — from an sRGB hex, while OCCT's STEP writer converts linear to
|
|
8
|
+
// sRGB on the way out, so each colour landed lighter than asked (#3366cc was
|
|
9
|
+
// written as #7caae7). Here every body gets a colour (the viewer's no-material
|
|
10
|
+
// drafting colour when it has none of its own), converted sRGB → linear with
|
|
11
|
+
// OCCT's own Convert_sRGB_To_LinearRGB — the exact inverse of the writer's
|
|
12
|
+
// conversion — before it reaches that constructor, so a body's COLOUR_RGB is
|
|
13
|
+
// the 0xRRGGBB the caller passed. (Building the colour as Quantity_TOC_sRGB
|
|
14
|
+
// directly would be tidier, but this WASM build does not bind that enum.)
|
|
15
|
+
//
|
|
16
|
+
// Everything else — the XCAF document, naming, the writer's modes and statics,
|
|
17
|
+
// 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. The
|
|
19
|
+
// document handle passed to Transfer is deliberately never freed: the document
|
|
20
|
+
// itself is owned by its WrappingObj (as replicad's AssemblyExporter owns it),
|
|
21
|
+
// and freeing the handle too would destroy it twice.
|
|
22
|
+
import { NO_MATERIAL_COLOR } from "../materials/resolve.js";
|
|
23
|
+
|
|
24
|
+
const isColor = (c) => Number.isInteger(c) && c >= 0 && c <= 0xffffff;
|
|
25
|
+
|
|
26
|
+
// 0xRRGGBB → sRGB channels in 0..1. A missing or unusable colour falls back to
|
|
27
|
+
// the no-material colour: appearance never fails an export.
|
|
28
|
+
function srgbChannels(color) {
|
|
29
|
+
const c = isColor(color) ? color : NO_MATERIAL_COLOR;
|
|
30
|
+
return [(c >> 16) & 255, (c >> 8) & 255, c & 255].map((v) => v / 255);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// bodies: [{ name, shape (a replicad Shape), color? (0xRRGGBB) }] → ArrayBuffer.
|
|
34
|
+
export function writeStep(replicad, bodies) {
|
|
35
|
+
const oc = replicad.getOC();
|
|
36
|
+
const r = replicad.GCWithScope();
|
|
37
|
+
const text = (s) => r(new oc.TCollection_ExtendedString_2(s, true));
|
|
38
|
+
|
|
39
|
+
const doc = new replicad.WrappingObj(new oc.TDocStd_Document(text("XmlOcaf")));
|
|
40
|
+
oc.XCAFDoc_ShapeTool.SetAutoNaming(false);
|
|
41
|
+
const mainLabel = r(doc.wrapped.Main());
|
|
42
|
+
const shapeTool = oc.XCAFDoc_DocumentTool.ShapeTool(mainLabel).get();
|
|
43
|
+
const colorTool = oc.XCAFDoc_DocumentTool.ColorTool(mainLabel).get();
|
|
44
|
+
for (const { name, shape, color } of bodies) {
|
|
45
|
+
const node = r(shapeTool.NewShape());
|
|
46
|
+
shapeTool.SetShape(node, shape.wrapped);
|
|
47
|
+
oc.TDataStd_Name.Set_1(node, text(name));
|
|
48
|
+
const [red, green, blue] = srgbChannels(color).map((v) => oc.Quantity_Color.Convert_sRGB_To_LinearRGB_1(v));
|
|
49
|
+
colorTool.SetColor_3(node, r(new oc.Quantity_ColorRGBA_5(red, green, blue, 1)), oc.XCAFDoc_ColorType.XCAFDoc_ColorSurf);
|
|
50
|
+
}
|
|
51
|
+
shapeTool.UpdateAssemblies();
|
|
52
|
+
|
|
53
|
+
const session = r(new oc.XSControl_WorkSession());
|
|
54
|
+
const writer = r(new oc.STEPCAFControl_Writer_2(r(new oc.Handle_XSControl_WorkSession_2(session)), false));
|
|
55
|
+
writer.SetColorMode(true);
|
|
56
|
+
writer.SetLayerMode(true);
|
|
57
|
+
writer.SetNameMode(true);
|
|
58
|
+
oc.Interface_Static.SetIVal("write.surfacecurve.mode", true);
|
|
59
|
+
oc.Interface_Static.SetIVal("write.precision.mode", 0);
|
|
60
|
+
oc.Interface_Static.SetIVal("write.step.assembly", 2);
|
|
61
|
+
oc.Interface_Static.SetIVal("write.step.schema", 5);
|
|
62
|
+
const progress = r(new oc.Message_ProgressRange_1());
|
|
63
|
+
writer.Transfer_1(new oc.Handle_TDocStd_Document_2(doc.wrapped), oc.STEPControl_StepModelType.STEPControl_AsIs, null, progress);
|
|
64
|
+
|
|
65
|
+
const filename = "partforge-export.step";
|
|
66
|
+
if (writer.Write(filename) !== oc.IFSelect_ReturnStatus.IFSelect_RetDone) throw new Error("STEP write failed");
|
|
67
|
+
const file = oc.FS.readFile(`/${filename}`);
|
|
68
|
+
oc.FS.unlink(`/${filename}`);
|
|
69
|
+
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
70
|
+
}
|
package/src/framework/jobs.js
CHANGED
|
@@ -439,7 +439,7 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
439
439
|
if (names.length === 0) throw new Error("no exportable parts selected");
|
|
440
440
|
const solids = names.map((name) => {
|
|
441
441
|
onProgress(`building ${label(name)}`);
|
|
442
|
-
return { name: exportName(name), solid: posed(name, "export", onProgress) };
|
|
442
|
+
return { name: exportName(name), solid: posed(name, "export", onProgress), color: printColor(part.parts[name]?.display) };
|
|
443
443
|
});
|
|
444
444
|
onProgress("writing STEP file");
|
|
445
445
|
const data = await kernel.toSTEP(solids);
|