Skip to content
Merged

Dev #231

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .documentation/SchemaUpdates.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ on the python side:

And then run the build (or test commands if available) for each from @duc/package.json

and in case you need to check the fbs schema or what changed (changes may be git staged): @duc/schema/duc.sql
and in case you need to check the fbs schema or what changed (changes may be git staged): @duc/schema/duc.sql

migrations for the .sql files may need to be created by running the following the folder and adding on top of the last one: schema/migrations
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified assets/testing/duc-files/universal.duc
Binary file not shown.
7 changes: 6 additions & 1 deletion packages/ducjs/src/restore/restoreElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,8 +534,12 @@ const restoreElement = (
: null;

// Create the base restored element
// Skip sizeFromPoints override for block instance elements: their stored
// width/height represents the total duplication grid, not single-cell dims.
const isBlockInstanceElement = !!element.instanceId;
const sizeFromPoints =
!hasBindings &&
!isBlockInstanceElement &&
getSizeFromPoints(finalPoints.map(getScopedBezierPointFromDucPoint));
let restoredElement = restoreElementWithProperties(
element,
Expand All @@ -555,6 +559,7 @@ const restoreElement = (
x: element.x,
y: element.y,
// Only calculate size from points if we don't have bindings
// and the element is not a block instance member (dup array uses stored total dims)
...(!sizeFromPoints
? {}
: {
Expand Down Expand Up @@ -764,7 +769,7 @@ const restoreElement = (
modelType: isValidString(modelElement.modelType) || null,
code: isValidString(modelElement.code) || null,
fileIds: modelElement.fileIds || [],
svgPath: modelElement.svgPath || null,
thumbnail: modelElement.thumbnail instanceof Uint8Array ? modelElement.thumbnail : null,
viewerState: (modelElement.viewerState || null) as Viewer3DState | null,
},
localState,
Expand Down
108 changes: 103 additions & 5 deletions packages/ducjs/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,110 @@ function fixElementFromRust(el: any): any {
};
}

function normalizeViewer3DStateForRust(vs: any): any {
if (!vs || typeof vs !== "object") return vs;

const n = (v: any, fallback: number) =>
typeof v === "number" && Number.isFinite(v) ? v : fallback;

const b = (v: any, fallback: boolean) =>
typeof v === "boolean" ? v : fallback;

const s = (v: any, fallback: string) =>
typeof v === "string" ? v : fallback;

const arr3 = (v: any, fallback: [number, number, number]): [number, number, number] =>
Array.isArray(v) && v.length === 3 && v.every((x: any) => typeof x === "number")
? (v as [number, number, number])
: fallback;

const arr4 = (v: any, fallback: [number, number, number, number]): [number, number, number, number] =>
Array.isArray(v) && v.length === 4 && v.every((x: any) => typeof x === "number")
? (v as [number, number, number, number])
: fallback;

const cam = vs.camera ?? {};
const dsp = vs.display ?? {};
const mat = vs.material ?? {};
const clip = vs.clipping ?? {};
const expl = vs.explode ?? {};
const zeb = vs.zebra ?? {};

const normalizeClipPlane = (cp: any) => ({
enabled: b(cp?.enabled, false),
value: n(cp?.value, 0),
normal: cp?.normal != null ? arr3(cp.normal, [0, 0, 0]) : null,
});

return {
camera: {
control: s(cam.control, "orbit"),
ortho: b(cam.ortho, true),
up: s(cam.up, "Z"),
position: arr3(cam.position, [0, 0, 0]),
quaternion: arr4(cam.quaternion, [0, 0, 0, 1]),
target: arr3(cam.target, [0, 0, 0]),
zoom: n(cam.zoom, 1),
panSpeed: n(cam.panSpeed, 1),
rotateSpeed: n(cam.rotateSpeed, 1),
zoomSpeed: n(cam.zoomSpeed, 1),
holroyd: b(cam.holroyd, false),
},
display: {
wireframe: b(dsp.wireframe, false),
transparent: b(dsp.transparent, false),
blackEdges: b(dsp.blackEdges, true),
grid: dsp.grid ?? { type: "uniform", value: false },
axesVisible: b(dsp.axesVisible, false),
axesAtOrigin: b(dsp.axesAtOrigin, true),
},
material: {
metalness: n(mat.metalness, 0.3),
roughness: n(mat.roughness, 0.65),
defaultOpacity: n(mat.defaultOpacity, 0.5),
edgeColor: n(mat.edgeColor, 0x707070),
ambientIntensity: n(mat.ambientIntensity, 1.0),
directIntensity: n(mat.directIntensity, 1.1),
},
clipping: {
x: normalizeClipPlane(clip.x),
y: normalizeClipPlane(clip.y),
z: normalizeClipPlane(clip.z),
intersection: b(clip.intersection, false),
showPlanes: b(clip.showPlanes, false),
objectColorCaps: b(clip.objectColorCaps, false),
},
explode: {
active: b(expl.active, false),
value: n(expl.value, 0),
},
zebra: {
active: b(zeb.active, false),
stripeCount: toInteger(zeb.stripeCount, 6),
stripeDirection: n(zeb.stripeDirection, 0),
colorScheme: s(zeb.colorScheme, "blackwhite"),
opacity: n(zeb.opacity, 1),
mappingMode: s(zeb.mappingMode, "reflection"),
},
};
}

function fixElementToRust(el: any): any {
if (!el) return el;

fixStylesHatch(el, false);
fixCustomDataToRust(el);
if (el.type === "model" && el.viewerState?.display?.grid) {
el.viewerState = {
...el.viewerState,
display: { ...el.viewerState.display, grid: fixViewer3DGridToRust(el.viewerState.display.grid) },
};

if (el.type === "model") {
if (el.viewerState) {
el.viewerState = normalizeViewer3DStateForRust(el.viewerState);
}
if (el.viewerState?.display?.grid) {
el.viewerState = {
...el.viewerState,
display: { ...el.viewerState.display, grid: fixViewer3DGridToRust(el.viewerState.display.grid) },
};
}
}

// Convert TypeScript DucLine tuples [start, end] → Rust structs { start, end }
Expand Down Expand Up @@ -381,6 +475,10 @@ function flattenPrecisionValues(obj: any): any {

if (Array.isArray(obj)) return obj.map(flattenPrecisionValues);

if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
return obj;
}

if (typeof obj === "object") {
const out: Record<string, any> = {};
for (const key in obj) {
Expand Down
4 changes: 2 additions & 2 deletions packages/ducjs/src/types/elements/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1180,8 +1180,8 @@ export type DucModelElement = _DucElementBase & {
/** Defines the source code of the model using build123d python code */
code: string | null;

/** The last known SVG path representation of the 3D model for quick rendering on the canvas */
svgPath: string | null;
/** The last known image thumbnail of the 3D model for quick rendering on the canvas */
thumbnail: Uint8Array | null;

/** Possibly connected external files, such as STEP, STL, DXF, etc. */
fileIds: ExternalFileId[];
Expand Down
3 changes: 2 additions & 1 deletion packages/ducjs/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export type ToolType =
| "laser"
| "table"
| "doc"
| "pdf";
| "pdf"
| "model";

export type ElementOrToolType = DucElementType | ToolType | "custom";

Expand Down
6 changes: 5 additions & 1 deletion packages/ducjs/src/utils/elements/newElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export const newPdfElement = (currentScope: Scope, opts: ElementConstructorOpts)
export const newModelElement = (currentScope: Scope, opts: ElementConstructorOpts): NonDeleted<DucModelElement> => ({
modelType: null,
code: null,
svgPath: null,
thumbnail: null,
fileIds: [],
viewerState: null,
..._newElementBase<DucModelElement>("model", currentScope, opts),
Expand All @@ -420,6 +420,10 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
return val;
}

if (ArrayBuffer.isView(val)) {
return (val as any).slice(0);
}

const objectType = Object.prototype.toString.call(val);

if (objectType === "[object Object]") {
Expand Down
1 change: 1 addition & 0 deletions packages/ducpdf/src/duc2pdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ web-sys = { version = "0.3", features = ["console"] }
svgtypes = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
log = "0.4.22"
console_log = "1.0"

Expand Down
90 changes: 53 additions & 37 deletions packages/ducpdf/src/duc2pdf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export async function initWasmFromBinary(wasmBinary: BufferSource): Promise<void
}

const requiredFunctions = [
'convert_exported_data_to_pdf_wasm',
'convert_duc_to_pdf_rs',
'convert_duc_to_pdf_with_scale_wasm',
'convert_duc_to_pdf_crop_wasm',
Expand Down Expand Up @@ -77,6 +78,7 @@ async function initWasm(): Promise<any> {

// Validate that required functions exist on the imported module
const requiredFunctions = [
'convert_exported_data_to_pdf_wasm',
'convert_duc_to_pdf_rs',
'convert_duc_to_pdf_with_scale_wasm',
'convert_duc_to_pdf_crop_wasm',
Expand Down Expand Up @@ -191,16 +193,18 @@ export async function convertDucToPdf(
let ducBytes = new Uint8Array(ducData);
let viewBackgroundColor;
let normalizedData: ExportedDataState | null = null;
let rustPayload: unknown = null;

try {
const latestBlob = new Blob([ducBytes]);
const parsed = await parseDuc(latestBlob);
if (parsed) {
// Extract scope from parsed data - use localState.scope first, fallback to globalState.mainScope
const scope = parsed?.localState?.scope || parsed?.globalState?.mainScope || 'mm';

// ensure that we are only working with mm on the pdf conversion logic
const normalized: ExportedDataState = normalizeForSerializationScope(parsed as unknown as ExportedDataState, 'mm', scope);
const normalized: ExportedDataState = normalizeForSerializationScope(
parsed as unknown as ExportedDataState,
'mm',
scope,
);
normalized.localState.scope = 'mm';
normalized.globalState.mainScope = 'mm';

Expand Down Expand Up @@ -278,7 +282,19 @@ export async function convertDucToPdf(
let result: Uint8Array;
const hasFonts = fontMap.size > 0;

if (options && (options.offsetX !== undefined || options.offsetY !== undefined)) {
if (rustPayload) {
const backgroundColor = options?.backgroundColor ? options.backgroundColor.trim() : viewBackgroundColor;
result = wasm.convert_exported_data_to_pdf_wasm(
rustPayload,
options?.offsetX,
options?.offsetY,
typeof options?.width === 'number' ? options.width : undefined,
typeof options?.height === 'number' ? options.height : undefined,
backgroundColor === undefined ? undefined : backgroundColor,
typeof options?.scale === 'number' ? options.scale : undefined,
fontMap,
);
} else if (options && (options.offsetX !== undefined || options.offsetY !== undefined)) {
// Use crop mode with offset
const offsetX = options.offsetX || 0;
const offsetY = options.offsetY || 0;
Expand All @@ -291,43 +307,43 @@ export async function convertDucToPdf(
if (hasFonts) {
result = scaleOption !== undefined
? wasm.convert_duc_to_pdf_crop_with_fonts_scaled_wasm(
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
scaleOption,
fontMap
)
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
scaleOption,
fontMap
)
: wasm.convert_duc_to_pdf_crop_with_fonts_wasm(
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
fontMap
);
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
fontMap
);
} else {
result = scaleOption !== undefined
? wasm.convert_duc_to_pdf_crop_scaled_wasm(
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
scaleOption
)
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption,
scaleOption
)
: wasm.convert_duc_to_pdf_crop_wasm(
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption
);
ducBytes,
offsetX,
offsetY,
widthOption,
heightOption,
backgroundOption
);
}
} else {
// Standard conversion
Expand Down
Loading
Loading