Skip to content

Commit 96ce4ba

Browse files
committed
Neptune_Render: fix DXF export border/width and paper design loading.
Export device border from spans and channel walls from channelWidth, harden border detection on re-import, and tolerate incomplete connection paths when loading legacy JSON.
1 parent a611968 commit 96ce4ba

4 files changed

Lines changed: 228 additions & 60 deletions

File tree

src/app/import/dxfDeviceModel.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ function isAxisAlignedRect(sketch: DxfSketch, tol = 0.5): boolean {
108108
return xs.size === 2 && ys.size === 2;
109109
}
110110

111+
function sketchRectAreaMm2(sketch: DxfSketch): number {
112+
const xs = sketch.lines.flatMap((l) => [l.a.x, l.b.x]);
113+
const ys = sketch.lines.flatMap((l) => [l.a.y, l.b.y]);
114+
if (!xs.length || !ys.length) return 0;
115+
return Math.max(0, Math.max(...xs) - Math.min(...xs)) * Math.max(0, Math.max(...ys) - Math.min(...ys));
116+
}
117+
118+
/**
119+
* Prefer an explicit *_border layer; otherwise the largest axis-aligned rectangle.
120+
* Prevents a thin channel rectangle from stealing the device outline on re-import.
121+
*/
122+
function pickDeviceBorderSketch(candidates: DxfSketch[]): DxfSketch | null {
123+
if (candidates.length === 0) return null;
124+
const named = candidates.filter((s) => /border/i.test(s.name));
125+
const pool = named.length > 0 ? named : candidates;
126+
return pool.reduce((best, sketch) => (sketchRectAreaMm2(sketch) > sketchRectAreaMm2(best) ? sketch : best));
127+
}
128+
111129
function computeBoundsFromSketches(sketches: DxfSketch[]): DxfDeviceModel["bounds"] {
112130
const bounds = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity };
113131
for (const sketch of sketches) {
@@ -243,13 +261,24 @@ export function parseDxfDocument(parsed: any, sourceName = "imported"): DxfDevic
243261
const minSketchZ = sketches.length > 0 ? Math.min(...sketches.map((s) => s.z)) : 0;
244262
let borderSketch: DxfSketch | null = null;
245263
const channelSketches: DxfSketch[] = [];
264+
const borderCandidates: DxfSketch[] = [];
246265
for (const sketch of sketches) {
247266
if (sketch.z <= minSketchZ + 0.01 && isAxisAlignedRect(sketch)) {
248-
borderSketch = sketch;
267+
borderCandidates.push(sketch);
249268
} else if (sketch.lines.length > 0 || sketch.circles.length > 0) {
250269
channelSketches.push(sketch);
251270
}
252271
}
272+
borderSketch = pickDeviceBorderSketch(borderCandidates);
273+
// Non-selected rectangles (e.g. closed pockets) still count as channel geometry.
274+
for (const sketch of borderCandidates) {
275+
if (borderSketch && sketch.name === borderSketch.name) {
276+
continue;
277+
}
278+
if (sketch.lines.length > 0 || sketch.circles.length > 0) {
279+
channelSketches.push(sketch);
280+
}
281+
}
253282
if (!borderSketch && sketches.length > 0) {
254283
borderSketch = sketches[0];
255284
}

src/app/manufacturing/dxfExport.ts

Lines changed: 186 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,42 @@ function emptyPlaceholderEntities(device: Device): string {
119119
);
120120
}
121121

122+
/**
123+
* Emit a device-outline rectangle from device spans (canvas µm → DXF mm).
124+
* Always use params spans rather than re-emitting imported EDGE geometry, which
125+
* may still be in original DXF coordinates and would misalign with ports/channels.
126+
*/
127+
function exportDeviceBorderEntities(device: Device, layerName: string): string {
128+
const widthUm = device.getXSpan();
129+
const heightUm = device.getYSpan();
130+
if (!Number.isFinite(widthUm) || !Number.isFinite(heightUm) || widthUm <= 0 || heightUm <= 0) {
131+
return "";
132+
}
133+
const corners: Array<[number, number]> = [
134+
[0, 0],
135+
[widthUm, 0],
136+
[widthUm, heightUm],
137+
[0, heightUm],
138+
[0, 0]
139+
];
140+
let out = "";
141+
for (let i = 0; i < corners.length - 1; i++) {
142+
const a = canvasUmToDxfMm(corners[i][0], corners[i][1], heightUm);
143+
const b = canvasUmToDxfMm(corners[i + 1][0], corners[i + 1][1], heightUm);
144+
out += writeLine(
145+
{
146+
type: "LINE",
147+
vertices: [
148+
{ x: a.x, y: a.y, z: 0 },
149+
{ x: b.x, y: b.y, z: 0 }
150+
]
151+
},
152+
layerName + "_border"
153+
);
154+
}
155+
return out;
156+
}
157+
122158
function readCircleRadiusUm(feature: Feature): number | null {
123159
const candidates = ["portRadius", "valveRadius", "radius1", "radius"];
124160
for (const key of candidates) {
@@ -134,10 +170,98 @@ function readCircleRadiusUm(feature: Feature): number | null {
134170
return null;
135171
}
136172

173+
function tryGetNumber(feature: Feature, key: string): number | null {
174+
try {
175+
const value = Number(feature.getValue(key));
176+
return Number.isFinite(value) ? value : null;
177+
} catch (_err) {
178+
return null;
179+
}
180+
}
181+
182+
/** Normalize [x,y] or ["Point", x, y] canvas coordinates. */
183+
function asUmPoint(value: unknown): [number, number] | null {
184+
if (!Array.isArray(value) || value.length < 2) {
185+
return null;
186+
}
187+
if (value.length >= 3 && value[0] === "Point") {
188+
const x = Number(value[1]);
189+
const y = Number(value[2]);
190+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
191+
}
192+
const x = Number(value[0]);
193+
const y = Number(value[1]);
194+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
195+
}
196+
197+
/**
198+
* Export one channel segment as two parallel wall lines spaced by channelWidth.
199+
* Matches typical manufacturing DXF (and the original inlet-channel-outlet sketch):
200+
* width is the gap between walls — not a closed rectangle that import can mistake for the device border.
201+
*/
202+
function exportChannelSegmentOutline(
203+
p1: [number, number],
204+
p2: [number, number],
205+
channelWidthUm: number,
206+
deviceHeightUm: number,
207+
layer: string,
208+
z: number
209+
): string {
210+
if (!(channelWidthUm > 0)) {
211+
return "";
212+
}
213+
const dx = p2[0] - p1[0];
214+
const dy = p2[1] - p1[1];
215+
const len = Math.hypot(dx, dy);
216+
if (len < 1e-9) {
217+
return "";
218+
}
219+
const ux = dx / len;
220+
const uy = dy / len;
221+
const px = -uy;
222+
const py = ux;
223+
const half = channelWidthUm / 2;
224+
const wallA1: [number, number] = [p1[0] - px * half, p1[1] - py * half];
225+
const wallA2: [number, number] = [p2[0] - px * half, p2[1] - py * half];
226+
const wallB1: [number, number] = [p1[0] + px * half, p1[1] + py * half];
227+
const wallB2: [number, number] = [p2[0] + px * half, p2[1] + py * half];
228+
229+
const a1 = canvasUmToDxfMm(wallA1[0], wallA1[1], deviceHeightUm);
230+
const a2 = canvasUmToDxfMm(wallA2[0], wallA2[1], deviceHeightUm);
231+
const b1 = canvasUmToDxfMm(wallB1[0], wallB1[1], deviceHeightUm);
232+
const b2 = canvasUmToDxfMm(wallB2[0], wallB2[1], deviceHeightUm);
233+
234+
let out = "";
235+
out += writeLine(
236+
{
237+
type: "LINE",
238+
vertices: [
239+
{ x: a1.x, y: a1.y, z },
240+
{ x: a2.x, y: a2.y, z }
241+
]
242+
},
243+
layer
244+
);
245+
out += writeLine(
246+
{
247+
type: "LINE",
248+
vertices: [
249+
{ x: b1.x, y: b1.y, z },
250+
{ x: b2.x, y: b2.y, z }
251+
]
252+
},
253+
layer
254+
);
255+
return out;
256+
}
257+
137258
function isStructuredDesignFeature(type: string): boolean {
138259
return (
139260
type === "Port" ||
140261
type === "Connection" ||
262+
type === "Channel" ||
263+
type === "RoundedChannel" ||
264+
type === "RoundedChannelConnection" ||
141265
type === "BetterMixer" ||
142266
type === "Mixer" ||
143267
type === "CurvedMixer" ||
@@ -166,9 +290,13 @@ function exportFeatureEntities(
166290
const type = feature.getType();
167291
let entities = "";
168292

169-
// After DXF import + canvas edits, Port/Connection/etc. are the live geometry.
170-
// Skip stale EDGE/DxfSketch payloads when structured features exist.
171-
if (type === "EDGE" || type === "DxfSketch") {
293+
// Device outline is always synthesized from getXSpan()/getYSpan() below.
294+
// Skip EDGE (imported coords may not match canvas-space features) and skip
295+
// stale DxfSketch payloads when structured features exist.
296+
if (type === "EDGE") {
297+
return entities;
298+
}
299+
if (type === "DxfSketch") {
172300
if (!includeRawDxfObjects) {
173301
return entities;
174302
}
@@ -202,41 +330,55 @@ function exportFeatureEntities(
202330
return entities;
203331
}
204332

205-
if (type === "Connection") {
206-
let segments: Array<[[number, number], [number, number]]> | null = null;
207-
let heightUm = NaN;
208-
try {
209-
segments = feature.getValue("segments") as Array<[[number, number], [number, number]]>;
210-
} catch (_err) {
333+
if (type === "Connection" || type === "Channel" || type === "RoundedChannel" || type === "RoundedChannelConnection") {
334+
const channelWidthUm = tryGetNumber(feature, "channelWidth");
335+
if (channelWidthUm == null || !(channelWidthUm > 0)) {
211336
return entities;
212337
}
213-
try {
214-
heightUm = Number(feature.getValue("height"));
215-
} catch (_err) {
216-
heightUm = 250;
217-
}
218-
const z = Number.isFinite(heightUm) ? heightUm * UM_TO_MM : 0.25;
219-
if (!segments || !Array.isArray(segments)) {
220-
return entities;
338+
const z = 0;
339+
const channelLayer = layerName + "_channels";
340+
341+
let segments: Array<[[number, number], [number, number]]> = [];
342+
if (type === "Connection" || type === "RoundedChannelConnection") {
343+
try {
344+
const raw = feature.getValue("segments") as Array<[[number, number], [number, number]]>;
345+
if (Array.isArray(raw)) {
346+
segments = raw;
347+
}
348+
} catch (_err) {
349+
// Fall through to start/end if segments are missing.
350+
}
351+
if (!segments.length) {
352+
try {
353+
const start = asUmPoint(feature.getValue("start"));
354+
const end = asUmPoint(feature.getValue("end"));
355+
if (start && end) {
356+
segments = [[start, end]];
357+
}
358+
} catch (_err) {
359+
return entities;
360+
}
361+
}
362+
} else {
363+
let start: [number, number] | null = null;
364+
let end: [number, number] | null = null;
365+
try {
366+
start = asUmPoint(feature.getValue("start"));
367+
end = asUmPoint(feature.getValue("end"));
368+
} catch (_err) {
369+
return entities;
370+
}
371+
if (start && end) {
372+
segments = [[start, end]];
373+
}
221374
}
375+
222376
for (const seg of segments) {
223377
if (!seg || seg.length < 2) continue;
224-
const p1 = seg[0];
225-
const p2 = seg[1];
226-
if (!Array.isArray(p1) || !Array.isArray(p2)) continue;
227-
const a = canvasUmToDxfMm(p1[0], p1[1], deviceHeightUm);
228-
const b = canvasUmToDxfMm(p2[0], p2[1], deviceHeightUm);
229-
if (Math.hypot(a.x - b.x, a.y - b.y) < 1e-9) continue;
230-
entities += writeLine(
231-
{
232-
type: "LINE",
233-
vertices: [
234-
{ x: a.x, y: a.y, z },
235-
{ x: b.x, y: b.y, z }
236-
]
237-
},
238-
layerName + "_channels"
239-
);
378+
const p1 = asUmPoint(seg[0]);
379+
const p2 = asUmPoint(seg[1]);
380+
if (!p1 || !p2) continue;
381+
entities += exportChannelSegmentOutline(p1, p2, channelWidthUm, deviceHeightUm, channelLayer, z);
240382
}
241383
return entities;
242384
}
@@ -248,9 +390,7 @@ function exportFeatureEntities(
248390
const bendLength = Number(feature.getValue("bendLength"));
249391
const bendSpacing = Number(feature.getValue("bendSpacing"));
250392
const numberOfBends = Number(feature.getValue("numberOfBends"));
251-
const heightUm = Number(feature.getValue("height"));
252-
const z = Number.isFinite(heightUm) ? heightUm * UM_TO_MM : 0.25;
253-
if (!Array.isArray(position) || !Number.isFinite(channelWidth)) {
393+
if (!Array.isArray(position) || !Number.isFinite(channelWidth) || !(channelWidth > 0)) {
254394
return entities;
255395
}
256396
const segments = betterMixerCenterlineSegments({
@@ -261,18 +401,13 @@ function exportFeatureEntities(
261401
numberOfBends: Number.isFinite(numberOfBends) ? numberOfBends : 1
262402
});
263403
for (const seg of segments) {
264-
const a = canvasUmToDxfMm(seg[0][0], seg[0][1], deviceHeightUm);
265-
const b = canvasUmToDxfMm(seg[1][0], seg[1][1], deviceHeightUm);
266-
if (Math.hypot(a.x - b.x, a.y - b.y) < 1e-9) continue;
267-
entities += writeLine(
268-
{
269-
type: "LINE",
270-
vertices: [
271-
{ x: a.x, y: a.y, z },
272-
{ x: b.x, y: b.y, z }
273-
]
274-
},
275-
layerName + "_mixer"
404+
entities += exportChannelSegmentOutline(
405+
seg[0],
406+
seg[1],
407+
channelWidth,
408+
deviceHeightUm,
409+
layerName + "_mixer",
410+
0
276411
);
277412
}
278413
} catch (_err) {
@@ -297,6 +432,9 @@ function exportLayerEntities(device: Device, layer: Layer): string {
297432
console.warn("[DXF export] Skipping feature", key, err);
298433
}
299434
}
435+
// Always write the device border from current spans so CAD extents match the
436+
// canvas device size (JSON params.width/length), even when EDGE is skipped.
437+
entities += exportDeviceBorderEntities(device, layerName);
300438
if (!entities) {
301439
entities = emptyPlaceholderEntities(device);
302440
}

0 commit comments

Comments
 (0)