Skip to content

Commit a49cd20

Browse files
authored
Merge pull request #230 from ducflair/jorgedanisc
Jorgedanisc
2 parents 85ba28c + c9337c0 commit a49cd20

20 files changed

Lines changed: 995 additions & 105 deletions

File tree

.documentation/SchemaUpdates.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ on the python side:
1717

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

20-
and in case you need to check the fbs schema or what changed (changes may be git staged): @duc/schema/duc.sql
20+
and in case you need to check the fbs schema or what changed (changes may be git staged): @duc/schema/duc.sql
21+
22+
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

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
1.66 MB
Binary file not shown.

packages/ducjs/src/restore/restoreElements.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,12 @@ const restoreElement = (
534534
: null;
535535

536536
// Create the base restored element
537+
// Skip sizeFromPoints override for block instance elements: their stored
538+
// width/height represents the total duplication grid, not single-cell dims.
539+
const isBlockInstanceElement = !!element.instanceId;
537540
const sizeFromPoints =
538541
!hasBindings &&
542+
!isBlockInstanceElement &&
539543
getSizeFromPoints(finalPoints.map(getScopedBezierPointFromDucPoint));
540544
let restoredElement = restoreElementWithProperties(
541545
element,
@@ -555,6 +559,7 @@ const restoreElement = (
555559
x: element.x,
556560
y: element.y,
557561
// Only calculate size from points if we don't have bindings
562+
// and the element is not a block instance member (dup array uses stored total dims)
558563
...(!sizeFromPoints
559564
? {}
560565
: {
@@ -764,7 +769,7 @@ const restoreElement = (
764769
modelType: isValidString(modelElement.modelType) || null,
765770
code: isValidString(modelElement.code) || null,
766771
fileIds: modelElement.fileIds || [],
767-
svgPath: modelElement.svgPath || null,
772+
thumbnail: modelElement.thumbnail instanceof Uint8Array ? modelElement.thumbnail : null,
768773
viewerState: (modelElement.viewerState || null) as Viewer3DState | null,
769774
},
770775
localState,

packages/ducjs/src/transform.ts

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,16 +301,110 @@ function fixElementFromRust(el: any): any {
301301
};
302302
}
303303

304+
function normalizeViewer3DStateForRust(vs: any): any {
305+
if (!vs || typeof vs !== "object") return vs;
306+
307+
const n = (v: any, fallback: number) =>
308+
typeof v === "number" && Number.isFinite(v) ? v : fallback;
309+
310+
const b = (v: any, fallback: boolean) =>
311+
typeof v === "boolean" ? v : fallback;
312+
313+
const s = (v: any, fallback: string) =>
314+
typeof v === "string" ? v : fallback;
315+
316+
const arr3 = (v: any, fallback: [number, number, number]): [number, number, number] =>
317+
Array.isArray(v) && v.length === 3 && v.every((x: any) => typeof x === "number")
318+
? (v as [number, number, number])
319+
: fallback;
320+
321+
const arr4 = (v: any, fallback: [number, number, number, number]): [number, number, number, number] =>
322+
Array.isArray(v) && v.length === 4 && v.every((x: any) => typeof x === "number")
323+
? (v as [number, number, number, number])
324+
: fallback;
325+
326+
const cam = vs.camera ?? {};
327+
const dsp = vs.display ?? {};
328+
const mat = vs.material ?? {};
329+
const clip = vs.clipping ?? {};
330+
const expl = vs.explode ?? {};
331+
const zeb = vs.zebra ?? {};
332+
333+
const normalizeClipPlane = (cp: any) => ({
334+
enabled: b(cp?.enabled, false),
335+
value: n(cp?.value, 0),
336+
normal: cp?.normal != null ? arr3(cp.normal, [0, 0, 0]) : null,
337+
});
338+
339+
return {
340+
camera: {
341+
control: s(cam.control, "orbit"),
342+
ortho: b(cam.ortho, true),
343+
up: s(cam.up, "Z"),
344+
position: arr3(cam.position, [0, 0, 0]),
345+
quaternion: arr4(cam.quaternion, [0, 0, 0, 1]),
346+
target: arr3(cam.target, [0, 0, 0]),
347+
zoom: n(cam.zoom, 1),
348+
panSpeed: n(cam.panSpeed, 1),
349+
rotateSpeed: n(cam.rotateSpeed, 1),
350+
zoomSpeed: n(cam.zoomSpeed, 1),
351+
holroyd: b(cam.holroyd, false),
352+
},
353+
display: {
354+
wireframe: b(dsp.wireframe, false),
355+
transparent: b(dsp.transparent, false),
356+
blackEdges: b(dsp.blackEdges, true),
357+
grid: dsp.grid ?? { type: "uniform", value: false },
358+
axesVisible: b(dsp.axesVisible, false),
359+
axesAtOrigin: b(dsp.axesAtOrigin, true),
360+
},
361+
material: {
362+
metalness: n(mat.metalness, 0.3),
363+
roughness: n(mat.roughness, 0.65),
364+
defaultOpacity: n(mat.defaultOpacity, 0.5),
365+
edgeColor: n(mat.edgeColor, 0x707070),
366+
ambientIntensity: n(mat.ambientIntensity, 1.0),
367+
directIntensity: n(mat.directIntensity, 1.1),
368+
},
369+
clipping: {
370+
x: normalizeClipPlane(clip.x),
371+
y: normalizeClipPlane(clip.y),
372+
z: normalizeClipPlane(clip.z),
373+
intersection: b(clip.intersection, false),
374+
showPlanes: b(clip.showPlanes, false),
375+
objectColorCaps: b(clip.objectColorCaps, false),
376+
},
377+
explode: {
378+
active: b(expl.active, false),
379+
value: n(expl.value, 0),
380+
},
381+
zebra: {
382+
active: b(zeb.active, false),
383+
stripeCount: toInteger(zeb.stripeCount, 6),
384+
stripeDirection: n(zeb.stripeDirection, 0),
385+
colorScheme: s(zeb.colorScheme, "blackwhite"),
386+
opacity: n(zeb.opacity, 1),
387+
mappingMode: s(zeb.mappingMode, "reflection"),
388+
},
389+
};
390+
}
391+
304392
function fixElementToRust(el: any): any {
305393
if (!el) return el;
306394

307395
fixStylesHatch(el, false);
308396
fixCustomDataToRust(el);
309-
if (el.type === "model" && el.viewerState?.display?.grid) {
310-
el.viewerState = {
311-
...el.viewerState,
312-
display: { ...el.viewerState.display, grid: fixViewer3DGridToRust(el.viewerState.display.grid) },
313-
};
397+
398+
if (el.type === "model") {
399+
if (el.viewerState) {
400+
el.viewerState = normalizeViewer3DStateForRust(el.viewerState);
401+
}
402+
if (el.viewerState?.display?.grid) {
403+
el.viewerState = {
404+
...el.viewerState,
405+
display: { ...el.viewerState.display, grid: fixViewer3DGridToRust(el.viewerState.display.grid) },
406+
};
407+
}
314408
}
315409

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

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

478+
if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
479+
return obj;
480+
}
481+
384482
if (typeof obj === "object") {
385483
const out: Record<string, any> = {};
386484
for (const key in obj) {

packages/ducjs/src/types/elements/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,8 +1180,8 @@ export type DucModelElement = _DucElementBase & {
11801180
/** Defines the source code of the model using build123d python code */
11811181
code: string | null;
11821182

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

11861186
/** Possibly connected external files, such as STEP, STL, DXF, etc. */
11871187
fileIds: ExternalFileId[];

packages/ducjs/src/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ export type ToolType =
171171
| "laser"
172172
| "table"
173173
| "doc"
174-
| "pdf";
174+
| "pdf"
175+
| "model";
175176

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

packages/ducjs/src/utils/elements/newElement.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export const newPdfElement = (currentScope: Scope, opts: ElementConstructorOpts)
398398
export const newModelElement = (currentScope: Scope, opts: ElementConstructorOpts): NonDeleted<DucModelElement> => ({
399399
modelType: null,
400400
code: null,
401-
svgPath: null,
401+
thumbnail: null,
402402
fileIds: [],
403403
viewerState: null,
404404
..._newElementBase<DucModelElement>("model", currentScope, opts),
@@ -420,6 +420,10 @@ const _deepCopyElement = (val: any, depth: number = 0) => {
420420
return val;
421421
}
422422

423+
if (ArrayBuffer.isView(val)) {
424+
return (val as any).slice(0);
425+
}
426+
423427
const objectType = Object.prototype.toString.call(val);
424428

425429
if (objectType === "[object Object]") {

packages/ducpdf/src/duc2pdf/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ web-sys = { version = "0.3", features = ["console"] }
3333
svgtypes = "0.12"
3434
serde = { version = "1.0", features = ["derive"] }
3535
serde_json = "1.0"
36+
serde-wasm-bindgen = "0.6"
3637
log = "0.4.22"
3738
console_log = "1.0"
3839

packages/ducpdf/src/duc2pdf/index.ts

Lines changed: 53 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function initWasmFromBinary(wasmBinary: BufferSource): Promise<void
3232
}
3333

3434
const requiredFunctions = [
35+
'convert_exported_data_to_pdf_wasm',
3536
'convert_duc_to_pdf_rs',
3637
'convert_duc_to_pdf_with_scale_wasm',
3738
'convert_duc_to_pdf_crop_wasm',
@@ -77,6 +78,7 @@ async function initWasm(): Promise<any> {
7778

7879
// Validate that required functions exist on the imported module
7980
const requiredFunctions = [
81+
'convert_exported_data_to_pdf_wasm',
8082
'convert_duc_to_pdf_rs',
8183
'convert_duc_to_pdf_with_scale_wasm',
8284
'convert_duc_to_pdf_crop_wasm',
@@ -191,16 +193,18 @@ export async function convertDucToPdf(
191193
let ducBytes = new Uint8Array(ducData);
192194
let viewBackgroundColor;
193195
let normalizedData: ExportedDataState | null = null;
196+
let rustPayload: unknown = null;
194197

195198
try {
196199
const latestBlob = new Blob([ducBytes]);
197200
const parsed = await parseDuc(latestBlob);
198201
if (parsed) {
199-
// Extract scope from parsed data - use localState.scope first, fallback to globalState.mainScope
200202
const scope = parsed?.localState?.scope || parsed?.globalState?.mainScope || 'mm';
201-
202-
// ensure that we are only working with mm on the pdf conversion logic
203-
const normalized: ExportedDataState = normalizeForSerializationScope(parsed as unknown as ExportedDataState, 'mm', scope);
203+
const normalized: ExportedDataState = normalizeForSerializationScope(
204+
parsed as unknown as ExportedDataState,
205+
'mm',
206+
scope,
207+
);
204208
normalized.localState.scope = 'mm';
205209
normalized.globalState.mainScope = 'mm';
206210

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

281-
if (options && (options.offsetX !== undefined || options.offsetY !== undefined)) {
285+
if (rustPayload) {
286+
const backgroundColor = options?.backgroundColor ? options.backgroundColor.trim() : viewBackgroundColor;
287+
result = wasm.convert_exported_data_to_pdf_wasm(
288+
rustPayload,
289+
options?.offsetX,
290+
options?.offsetY,
291+
typeof options?.width === 'number' ? options.width : undefined,
292+
typeof options?.height === 'number' ? options.height : undefined,
293+
backgroundColor === undefined ? undefined : backgroundColor,
294+
typeof options?.scale === 'number' ? options.scale : undefined,
295+
fontMap,
296+
);
297+
} else if (options && (options.offsetX !== undefined || options.offsetY !== undefined)) {
282298
// Use crop mode with offset
283299
const offsetX = options.offsetX || 0;
284300
const offsetY = options.offsetY || 0;
@@ -291,43 +307,43 @@ export async function convertDucToPdf(
291307
if (hasFonts) {
292308
result = scaleOption !== undefined
293309
? wasm.convert_duc_to_pdf_crop_with_fonts_scaled_wasm(
294-
ducBytes,
295-
offsetX,
296-
offsetY,
297-
widthOption,
298-
heightOption,
299-
backgroundOption,
300-
scaleOption,
301-
fontMap
302-
)
310+
ducBytes,
311+
offsetX,
312+
offsetY,
313+
widthOption,
314+
heightOption,
315+
backgroundOption,
316+
scaleOption,
317+
fontMap
318+
)
303319
: wasm.convert_duc_to_pdf_crop_with_fonts_wasm(
304-
ducBytes,
305-
offsetX,
306-
offsetY,
307-
widthOption,
308-
heightOption,
309-
backgroundOption,
310-
fontMap
311-
);
320+
ducBytes,
321+
offsetX,
322+
offsetY,
323+
widthOption,
324+
heightOption,
325+
backgroundOption,
326+
fontMap
327+
);
312328
} else {
313329
result = scaleOption !== undefined
314330
? wasm.convert_duc_to_pdf_crop_scaled_wasm(
315-
ducBytes,
316-
offsetX,
317-
offsetY,
318-
widthOption,
319-
heightOption,
320-
backgroundOption,
321-
scaleOption
322-
)
331+
ducBytes,
332+
offsetX,
333+
offsetY,
334+
widthOption,
335+
heightOption,
336+
backgroundOption,
337+
scaleOption
338+
)
323339
: wasm.convert_duc_to_pdf_crop_wasm(
324-
ducBytes,
325-
offsetX,
326-
offsetY,
327-
widthOption,
328-
heightOption,
329-
backgroundOption
330-
);
340+
ducBytes,
341+
offsetX,
342+
offsetY,
343+
widthOption,
344+
heightOption,
345+
backgroundOption
346+
);
331347
}
332348
} else {
333349
// Standard conversion

0 commit comments

Comments
 (0)