Skip to content

Commit 096d3e6

Browse files
committed
SVG import
1 parent f869971 commit 096d3e6

7 files changed

Lines changed: 2531 additions & 0 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Per-call options for the v1.0 SVG encoder. All fields optional;
3+
* caller-supplied values override the defaults in
4+
* {@link DEFAULT_SVG_EXPORT_OPTIONS}.
5+
*/
6+
export interface SVGExportOptions {
7+
8+
/**
9+
* Which world plane the SceneModel is projected onto when
10+
* flattening to 2D. SVG is inherently 2D so one world axis has to
11+
* be dropped; the choice depends on which way "up" points in your
12+
* scene:
13+
*
14+
* - `"XY"` (default) — drop world Z. Right for the typical
15+
* floor-plan / drawing case where Z is small or always 0.
16+
* - `"XZ"` — drop world Y. Right for Y-up architectural scenes
17+
* where you want a plan view from above.
18+
* - `"YZ"` — drop world X. Right for elevations / side views.
19+
*
20+
* The chosen plane's first axis maps to SVG X, the second maps
21+
* to SVG Y (with optional {@link flipY}).
22+
*/
23+
projectionPlane?: "XY" | "XZ" | "YZ";
24+
25+
/**
26+
* When `true` (default), the projected Y axis is negated so the
27+
* drawing reads right-way-up — world +Y appears at the TOP of
28+
* the SVG, matching how a viewer would expect to see it. Pass
29+
* `false` to honour SVG's native +Y-down convention without
30+
* flipping (typical when the source scene is already Y-down).
31+
*/
32+
flipY?: boolean;
33+
34+
/**
35+
* Multiplier from scene units into SVG user-space units. Default
36+
* `1`. For a millimetre scene meant to print 1:1 to user-units,
37+
* leave `1`; to render at 1:50 use `0.02`.
38+
*/
39+
scale?: number;
40+
41+
/**
42+
* Padding (in scene units, pre-{@link scale}) around the
43+
* SceneModel's AABB when computing the SVG `viewBox`. Default
44+
* `5`. Avoids edge clipping when strokes have visible width.
45+
*/
46+
padding?: number;
47+
48+
/**
49+
* Optional explicit `width` attribute on the root `<svg>`
50+
* element. When omitted, set to the viewBox width (so the SVG
51+
* renders at its native scale). Pass a unit string (`"800"`,
52+
* `"100%"`, `"210mm"`) to control downstream rendering.
53+
*/
54+
width?: string | number;
55+
56+
/**
57+
* Optional explicit `height` attribute. See {@link width}.
58+
*/
59+
height?: string | number;
60+
61+
/**
62+
* Optional background `<rect>` fill. When set, emits a rectangle
63+
* the size of the viewBox before any geometry, so the output
64+
* isn't transparent. `[r, g, b]` triplet (channels in `[0, 1]`).
65+
*/
66+
backgroundColor?: [number, number, number];
67+
68+
/**
69+
* Optional override for stroke colour. When set, every emitted
70+
* line / polygon outline uses this colour regardless of
71+
* per-mesh / per-material colour.
72+
*/
73+
strokeColor?: [number, number, number];
74+
75+
/**
76+
* Optional override for fill colour. When set, every emitted
77+
* polygon (triangle) uses this colour regardless of per-mesh
78+
* colour. Set to `"none"` (the string) to suppress fills
79+
* entirely and emit triangles as their outlines only.
80+
*/
81+
fillColor?: [number, number, number] | "none";
82+
83+
/**
84+
* Multiplier from `SceneMaterial.lineWidth` (which the SDK
85+
* treats as pixel units) into SVG `stroke-width` (user-space
86+
* units). Default `1`. Combined with {@link scale}, gives full
87+
* control over emitted line weights.
88+
*/
89+
strokeWidthScale?: number;
90+
91+
/**
92+
* Fallback stroke width applied when a mesh / material doesn't
93+
* declare one. Default `0.5`.
94+
*/
95+
defaultStrokeWidth?: number;
96+
97+
/**
98+
* When `true` (default), wraps the geometry inside a `<g
99+
* stroke-linecap="round" stroke-linejoin="round">` so all
100+
* strokes inherit those properties without being repeated on
101+
* every element.
102+
*/
103+
roundJoins?: boolean;
104+
105+
/**
106+
* When `true`, consecutive line-segment pairs that share an
107+
* endpoint coalesce into a single `<polyline>`. Smaller output
108+
* + faster browser parse for drawings with long connected runs;
109+
* adds a coalescing pass. Default `false` — emits one `<line>`
110+
* per segment for simpler round-tripping through other tools.
111+
*/
112+
coalescePolylines?: boolean;
113+
}
114+
115+
116+
/**
117+
* @private
118+
*/
119+
export const DEFAULT_SVG_EXPORT_OPTIONS: Required<
120+
Omit<SVGExportOptions, "width" | "height" | "backgroundColor" | "strokeColor" | "fillColor">
121+
> = {
122+
projectionPlane: "XY",
123+
flipY: true,
124+
scale: 1,
125+
padding: 5,
126+
strokeWidthScale: 1,
127+
defaultStrokeWidth: 0.5,
128+
roundJoins: true,
129+
coalescePolylines: false,
130+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {ModelExporter} from "../ModelExporter";
2+
import {encode as encode_1_0} from "./versions/v1_0/encode";
3+
4+
/**
5+
* Exports a {@link model!scene.SceneModel | SceneModel} as SVG
6+
* (Scalable Vector Graphics) text. Triangle meshes become
7+
* `<polygon>` elements, line meshes become `<line>` (or
8+
* `<polyline>` with coalescing enabled), point meshes become
9+
* `<circle r="...">`. One `<g id="...">` wrapper per SceneObject.
10+
*
11+
* SVG is inherently 2D so one world axis is dropped at export
12+
* time; `SVGExportOptions.projectionPlane` (default `"XY"`)
13+
* controls which. Pairs naturally with {@link SVGLoader} for
14+
* round-tripping.
15+
*
16+
* For detailed usage, refer to {@link svg | @xeokit/sdk/formats/svg}.
17+
*/
18+
export class SVGExporter extends ModelExporter {
19+
constructor() {
20+
super({
21+
format: "SVG",
22+
fileDataType: "text",
23+
encoders: {"1.0": encode_1_0},
24+
defaultVersion: "1.0",
25+
});
26+
}
27+
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/**
2+
* Per-call options for {@link SVGLoader.load}. All fields optional;
3+
* caller-supplied values override the defaults in
4+
* {@link DEFAULT_SVG_LOAD_OPTIONS}.
5+
*/
6+
export interface SVGLoadOptions {
7+
8+
/**
9+
* Multiplier from SVG user-space units (1 unit = the value of the
10+
* `width`/`height`/`viewBox` attributes) into the SceneModel's
11+
* coordinate system. Default `1` — leaves coordinates in SVG
12+
* native units. For a drawing meant to sit in a millimetre scene
13+
* with SVG authored at 1 unit = 1 mm, leave at `1`; for SVG
14+
* authored at 1 unit = 1 inch, use `25.4`.
15+
*/
16+
scale?: number;
17+
18+
/**
19+
* When `true` (default), Y coordinates are negated so the SVG
20+
* (which uses +Y-down) renders the right way up in a scene with
21+
* +Y-up. Pass `false` if the host scene is +Y-down, or if you
22+
* want the SVG to appear mirrored vertically for some other
23+
* reason.
24+
*/
25+
flipY?: boolean;
26+
27+
/**
28+
* Optional override for stroke colour. When set to an `[r, g, b]`
29+
* triplet (channels in `[0, 1]`), every emitted line mesh uses
30+
* this colour regardless of the SVG's own `stroke` attribute —
31+
* useful for "ink one colour" presentations.
32+
*
33+
* When **omitted** (the default), the loader honours per-element
34+
* stroke colours from the SVG `stroke` attribute, bucketing
35+
* emitted segments by colour.
36+
*/
37+
color?: [number, number, number];
38+
39+
/**
40+
* Optional override for fill colour. When set to an `[r, g, b]`
41+
* triplet, every emitted fill mesh uses this colour regardless
42+
* of the SVG's `fill` attribute.
43+
*/
44+
fillColor?: [number, number, number];
45+
46+
/**
47+
* Floor on the pixel width of emitted line meshes. Sub-pixel SVG
48+
* strokes would otherwise vanish under GPU rasterisation. Default
49+
* `1.0`.
50+
*/
51+
minLineWidth?: number;
52+
53+
/**
54+
* Multiplier from SVG `stroke-width` (user-space units) into the
55+
* pixel widths consumed by `SceneMaterialParams.lineWidth`.
56+
* Default `1.0`. SVGs that author hairlines at 0.25 user-units
57+
* benefit from raising this to ~3 so they meet {@link minLineWidth}
58+
* naturally rather than being clamped.
59+
*/
60+
lineWidthScale?: number;
61+
62+
/**
63+
* Steps used to tessellate each cubic / quadratic bezier segment
64+
* into line pieces. Higher = smoother curves, more line indices.
65+
* `16` is a reasonable default for hand-drawn / CAD-export SVGs;
66+
* raise to `32` for logos and icon sets dominated by curves.
67+
* Default `16`.
68+
*/
69+
bezierSteps?: number;
70+
71+
/**
72+
* Steps used to tessellate `<circle>` and `<ellipse>` elements,
73+
* and the elliptical-arc commands (`A`/`a`) in paths. `36` = 10°
74+
* facets, indistinguishable from smooth at typical zoom. Default
75+
* `36`.
76+
*/
77+
circleSteps?: number;
78+
79+
/**
80+
* When `true` (default), `<path>`, `<rect>`, `<circle>`,
81+
* `<ellipse>`, and `<polygon>` elements with a non-`none` `fill`
82+
* attribute are tessellated to triangles via earcut and emitted
83+
* as `TrianglesPrimitive` meshes. Pass `false` to skip fill
84+
* emission (strokes only).
85+
*/
86+
renderFills?: boolean;
87+
88+
/**
89+
* Z-offset applied to filled triangle meshes so they sit *behind*
90+
* stroked line meshes from the same drawing. Without this, the
91+
* filled region's depth-buffer write swallows stroke fragments
92+
* along the shared boundary (z-fighting). Default `-0.05` (in
93+
* scene units post-{@link scale}).
94+
*/
95+
fillZOffset?: number;
96+
97+
/**
98+
* How element ids on emitted SceneObjects are formed.
99+
*
100+
* - `"group"` (default) — one SceneObject per top-level `<g>`.
101+
* Elements outside any group go into a synthetic SceneObject
102+
* named `<sceneModelId>-root`. Best for hand-authored SVGs
103+
* where `<g id="walls">` etc. is the user's logical grouping.
104+
* - `"element"` — one SceneObject per geometric element. Best
105+
* for SVGs exported from CAD where individual entities should
106+
* be independently pickable and the file has no useful groups.
107+
*/
108+
objectIdStrategy?: "group" | "element";
109+
110+
/**
111+
* Default stroke colour applied when an element (and all its
112+
* ancestors) carry no `stroke` attribute. SVG itself defaults to
113+
* `none` for stroke — but the typical user expectation when
114+
* importing an SVG into a viewer is "show me the drawing", so
115+
* the loader defaults to black. Set to `null` to honour SVG's
116+
* own default (no stroke at all when unspecified).
117+
*/
118+
defaultStroke?: [number, number, number] | null;
119+
120+
/**
121+
* Default fill colour applied when an element (and all its
122+
* ancestors) carry no `fill` attribute. SVG itself defaults
123+
* `fill` to black; the loader follows that to match browser
124+
* rendering. Set to `null` to suppress fills on un-styled
125+
* elements.
126+
*/
127+
defaultFill?: [number, number, number] | null;
128+
129+
/**
130+
* Optional progress callback. Invoked once per top-level element
131+
* group after extraction with `(elementIndex, totalElements)`.
132+
*/
133+
onProgress?: (elementIndex: number, totalElements: number) => void;
134+
135+
/**
136+
* When `true` (default), `<text>` elements are rasterised onto an
137+
* in-memory canvas per label and emitted as textured triangle
138+
* quads — room labels, dimensions, drawing-title text, etc. Pass
139+
* `false` to skip text emission entirely.
140+
*
141+
* Each `<text>` becomes one quad regardless of how many `<tspan>`
142+
* children it contains; per-tspan letter-spacing is lost but the
143+
* overall label reads correctly. Hosts that need per-tspan
144+
* kerning precision should pre-rasterise externally.
145+
*/
146+
renderText?: boolean;
147+
148+
/**
149+
* Optional override for text fill colour. When set, every emitted
150+
* text quad rasterises with this colour regardless of the SVG's
151+
* own `fill` attribute / CSS class.
152+
*/
153+
textColor?: [number, number, number];
154+
155+
/**
156+
* Fallback fill colour when a `<text>` element resolves to no
157+
* `fill` (no attribute, no class declaration, parent style also
158+
* null). Default near-black `[0.05, 0.05, 0.05]`.
159+
*/
160+
textDefaultColor?: [number, number, number];
161+
162+
/**
163+
* Fallback CSS font face when a `<text>` element's class doesn't
164+
* specify `font-family`. Default
165+
* `"Arial, Helvetica, sans-serif"`.
166+
*/
167+
textFont?: string;
168+
169+
/**
170+
* Fallback font size (SVG user-units) when a `<text>` element
171+
* carries no `font-size` attribute or class. Default `12`.
172+
*/
173+
textDefaultSize?: number;
174+
175+
/**
176+
* Canvas pixels per SVG user-unit when rasterising text. Higher =
177+
* sharper text at the cost of texture memory. `4` reads cleanly
178+
* at typical drawing-zoom; raise to `8` for high-DPI displays,
179+
* drop to `2` for huge drawings with hundreds of labels. Default
180+
* `4`.
181+
*/
182+
textPxPerUnit?: number;
183+
}
184+
185+
186+
/**
187+
* @private
188+
*/
189+
export const DEFAULT_SVG_LOAD_OPTIONS: Required<
190+
Omit<SVGLoadOptions, "color" | "fillColor" | "onProgress" | "defaultStroke" | "defaultFill" | "textColor">
191+
> = {
192+
scale: 1,
193+
flipY: true,
194+
minLineWidth: 1.0,
195+
lineWidthScale: 1.0,
196+
bezierSteps: 16,
197+
circleSteps: 36,
198+
renderFills: true,
199+
fillZOffset: -0.05,
200+
objectIdStrategy: "group",
201+
renderText: true,
202+
textDefaultColor: [0.05, 0.05, 0.05],
203+
textFont: "Arial, Helvetica, sans-serif",
204+
textDefaultSize: 12,
205+
textPxPerUnit: 4,
206+
};

0 commit comments

Comments
 (0)