diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml new file mode 100644 index 0000000..d9e564b --- /dev/null +++ b/.github/workflows/code-style.yml @@ -0,0 +1,55 @@ +name: code-style + +# The ecosystem's code-style policy (SwiftJWW#9), following the reference +# implementation piloted in OCCTSwiftScripts#114/#115; see +# docs/code-style-policy-proposal-2026-08.md in the `ecosystem` repo for the +# full rationale. This repo is small enough to fully sweep into compliance in +# one PR and go straight to a blocking gate, rather than needing OCCTSwift's +# gradual "if you touch it, you fix it" exemption manifest. +# +# Pure text analysis (formatting + lint), no compiled output needed by any of +# the three checks, so this stays a macOS runner only because that's where +# swift-format and swiftlint are both readily available via Homebrew. +# +# swift-format and SwiftLint are blocking: formatting has no judgment call in +# it. The comment-ratio check is report-only by design (see the script's own +# header) and is not a gate; it always exits 0. + +on: + push: + paths: + - 'Sources/**' + - 'Tests/**' + - '.swift-format' + - '.swiftlint.yml' + - 'Scripts/comment-ratio-check.sh' + - '.github/workflows/code-style.yml' + pull_request: + paths: + - 'Sources/**' + - 'Tests/**' + - '.swift-format' + - '.swiftlint.yml' + - 'Scripts/comment-ratio-check.sh' + - '.github/workflows/code-style.yml' + workflow_dispatch: + +jobs: + code-style: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + + - name: Install swift-format and SwiftLint + run: brew install swift-format swiftlint + + - name: swift-format lint --strict + run: | + find Sources Tests -name '*.swift' -print0 \ + | xargs -0 swift-format lint --strict --configuration .swift-format + + - name: swiftlint lint --strict + run: swiftlint lint --strict --config .swiftlint.yml + + - name: Comment:code ratio (report-only, never fails) + run: Scripts/comment-ratio-check.sh diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..481e5cc --- /dev/null +++ b/.swift-format @@ -0,0 +1,75 @@ +{ + "version" : 1, + "lineLength" : 100, + "indentation" : { + "spaces" : 4 + }, + "tabWidth" : 4, + "maximumBlankLines" : 1, + "respectsExistingLineBreaks" : true, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineBreakBetweenDeclarationAttributes" : false, + "multiElementCollectionTrailingCommas" : true, + "prioritizeKeepingFunctionOutputTogether" : false, + "reflowMultilineStringLiterals" : "never", + "spacesAroundRangeFormationOperators" : false, + "spacesBeforeEndOfLineComments" : 2, + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "orderedImports" : { + "includeConditionalImports" : false + }, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "AvoidRetroactiveConformances" : true, + "BeginDocumentationCommentWithOneLineSummary" : true, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyLinesOpeningClosingBraces" : false, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : true + } +} diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..05c78fc --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,26 @@ +# Deliberately narrow: `only_rules`, not `disabled_rules`. SwiftLint's default +# rule set covers two kinds of territory this repo doesn't want it opining on: +# +# - Layout (colon/comma/opening_brace/line_length/...): swift-format already +# owns this; running both fights, since they can disagree on the same line. +# - Code-quality/complexity (identifier_name, cyclomatic_complexity, +# function_body_length, nesting, type_body_length, file_length, ...): a +# separate concern from code style, and one that overlaps the ecosystem's +# own code-structure policy (a repo that needs a structural pass runs one +# as its own scoped initiative, not as a side effect of a style-lint +# gate). This repo's dense binary-parsing code in particular leans on +# short, conventional locals (a, b, p, r, j, n) that identifier_name's +# default minimum length would flag as noise, not real problems. +# +# What's left is the one rule that catches something swift-format has no +# equivalent for: a doc comment not attached to any declaration. +# +# See docs/code-style-policy-proposal-2026-08.md in the ecosystem repo for +# the full rationale. + +only_rules: + - orphaned_doc_comment + +excluded: + - .build + - Tests diff --git a/Scripts/comment-ratio-check.sh b/Scripts/comment-ratio-check.sh new file mode 100755 index 0000000..b91bd2c --- /dev/null +++ b/Scripts/comment-ratio-check.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# comment-ratio-check.sh: flag Swift files where comment lines outnumber code +# lines, as a signal for a human to look, not a fail. +# +# Part of the ecosystem's code-style policy (SwiftJWW#9); see +# docs/code-style-policy-proposal-2026-08.md in the `ecosystem` repo for the +# full rationale. A high ratio is sometimes legitimate (a small function with +# several real caveats worth spelling out) and sometimes means a doc comment +# has drifted into restating design rationale that belongs in docs/ instead +# (see the proposal's own CurveAdaptors.md finding for what that looks like +# once it's had years to drift): this script surfaces the number, it doesn't +# judge which case it is. Report-only: always exits 0, for the same reason a +# codebase nobody has swept yet shouldn't fail every file above a threshold +# on day one: that punishes the report instead of the problem. +# +# Usage: +# Scripts/comment-ratio-check.sh # default threshold: 1.0 +# Scripts/comment-ratio-check.sh 1.5 # custom threshold +set -euo pipefail + +threshold="${1:-1.0}" + +[ -d Sources ] || { echo "FAIL: Sources/ not found; run from the repo root" >&2; exit 1; } + +flagged=0 + +while IFS= read -r -d '' file; do + comment=0 + code=0 + while IFS= read -r line; do + trimmed="${line#"${line%%[![:space:]]*}"}" + [ -z "$trimmed" ] && continue + case "$trimmed" in + //*) comment=$((comment + 1)) ;; + *) code=$((code + 1)) ;; + esac + done < "$file" + + [ "$code" -eq 0 ] && continue + + # POSIX sh has no float math; compare as comment*100 >= threshold*code so + # a fractional threshold (the common case: 1.0, 1.5) still works exactly. + threshold_x100=$(awk -v t="$threshold" 'BEGIN { printf "%d", t * 100 }') + if [ $((comment * 100)) -ge $((threshold_x100 * code)) ]; then + ratio=$(awk -v c="$comment" -v d="$code" 'BEGIN { printf "%.2f", c / d }') + printf ' %-55s comment=%-5d code=%-5d ratio=%s\n' "$file" "$comment" "$code" "$ratio" + flagged=$((flagged + 1)) + fi +done < <(find Sources -name '*.swift' -print0 | sort -z) + +if [ "$flagged" -eq 0 ]; then + echo "comment-ratio-check: no files at or above ${threshold}x comment:code (Sources/)" +else + echo "comment-ratio-check: $flagged file(s) at or above ${threshold}x comment:code (Sources/), not a failure, a signal to look" +fi + +exit 0 diff --git a/Sources/SwiftJWW/DXFWriter.swift b/Sources/SwiftJWW/DXFWriter.swift index ab2eaf6..e51a62d 100644 --- a/Sources/SwiftJWW/DXFWriter.swift +++ b/Sources/SwiftJWW/DXFWriter.swift @@ -1,14 +1,16 @@ import Foundation -/// Writes a ``JWW/Drawing`` as an ASCII **DXF** (an entities-only, version-agnostic DXF that AutoCAD / -/// LibreCAD / most CAD tools accept). Maps JWW entities to DXF: line→`LINE`, full circle→`CIRCLE`, -/// circular arc→`ARC`, elliptical arc→`ELLIPSE`, point→`POINT`, text→`TEXT`. +/// Writes a ``JWW/Drawing`` as an ASCII **DXF**. +/// +/// An entities-only, version-agnostic DXF that AutoCAD / LibreCAD / most CAD tools accept. Maps +/// JWW entities to DXF: line→`LINE`, full circle→`CIRCLE`, circular arc→`ARC`, +/// elliptical arc→`ELLIPSE`, point→`POINT`, text→`TEXT`. public enum DXFWriter { public static func string(_ dwg: JWW.Drawing) -> String { var s = "999\nSwiftJWW\n" s.reserveCapacity(dwg.entities.count * 120) - // BLOCKS section — one BLOCK per definition, named BLK. + // BLOCKS section: one BLOCK per definition, named BLK. if !dwg.blocks.isEmpty { s += "0\nSECTION\n2\nBLOCKS\n" for num in dwg.blocks.keys.sorted() { @@ -35,74 +37,128 @@ public enum DXFWriter { private static func emit(_ e: JWW.Entity, into s: inout String) { func p(_ code: Int, _ v: String) { s += "\(code)\n\(v)\n" } func num(_ v: Double) -> String { v.isFinite ? String(format: "%.6f", v) : "0.0" } - func normDeg(_ x: Double) -> Double { let a = x.truncatingRemainder(dividingBy: 360); return a < 0 ? a + 360 : a } + func normDeg(_ x: Double) -> Double { + let a = x.truncatingRemainder(dividingBy: 360) + return a < 0 ? a + 360 : a + } let deg = 180.0 / Double.pi switch e { - case let .line(a, b, layer, color): - p(0, "LINE"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(a.x)); p(20, num(a.y)); p(30, "0.0") - p(11, num(b.x)); p(21, num(b.y)); p(31, "0.0") + case .line(let a, let b, let layer, let color): + p(0, "LINE") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(a.x)) + p(20, num(a.y)) + p(30, "0.0") + p(11, num(b.x)) + p(21, num(b.y)) + p(31, "0.0") - case let .arc(c, r, start, sweep, tilt, ratio, full, layer, color): - if abs(ratio - 1) < 1e-9 { // circle / circular arc + case .arc( + let c, let r, let start, let sweep, let tilt, let ratio, let full, let layer, let color): + if abs(ratio - 1) < 1e-9 { // circle / circular arc if full || abs(abs(sweep) - 2 * .pi) < 1e-6 { - p(0, "CIRCLE"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(c.x)); p(20, num(c.y)); p(30, "0.0"); p(40, num(r)) + p(0, "CIRCLE") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(c.x)) + p(20, num(c.y)) + p(30, "0.0") + p(40, num(r)) } else { - p(0, "ARC"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(c.x)); p(20, num(c.y)); p(30, "0.0"); p(40, num(r)) + p(0, "ARC") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(c.x)) + p(20, num(c.y)) + p(30, "0.0") + p(40, num(r)) // JWW start/sweep are measured from the tilt axis; DXF wants absolute CCW angles in // [0,360). Add tilt, and order start→end CCW (so a negative sweep isn't drawn inverted). - let a0 = tilt + start, a1 = tilt + start + sweep - let lo = sweep >= 0 ? a0 : a1, hi = sweep >= 0 ? a1 : a0 - p(50, num(normDeg(lo * deg))); p(51, num(normDeg(hi * deg))) + let a0 = tilt + start + let a1 = tilt + start + sweep + let lo = sweep >= 0 ? a0 : a1 + let hi = sweep >= 0 ? a1 : a0 + p(50, num(normDeg(lo * deg))) + p(51, num(normDeg(hi * deg))) } - } else { // ellipse / elliptical arc - p(0, "ELLIPSE"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(c.x)); p(20, num(c.y)); p(30, "0.0") - p(11, num(cos(tilt) * r)); p(21, num(sin(tilt) * r)); p(31, "0.0") // major axis endpoint, rel. to center + } else { // ellipse / elliptical arc + p(0, "ELLIPSE") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(c.x)) + p(20, num(c.y)) + p(30, "0.0") + p(11, num(cos(tilt) * r)) + p(21, num(sin(tilt) * r)) + p(31, "0.0") // major axis endpoint, rel. to center p(40, num(ratio)) - p(41, num(full ? 0 : start)); p(42, num(full ? 2 * .pi : start + sweep)) + p(41, num(full ? 0 : start)) + p(42, num(full ? 2 * .pi : start + sweep)) } - case let .point(at, layer, color): - p(0, "POINT"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(at.x)); p(20, num(at.y)); p(30, "0.0") + case .point(let at, let layer, let color): + p(0, "POINT") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(at.x)) + p(20, num(at.y)) + p(30, "0.0") - case let .text(at, height, _, angleRad, string, layer, color): - p(0, "TEXT"); p(8, "\(layer)"); p(62, aci(color)) - p(10, num(at.x)); p(20, num(at.y)); p(30, "0.0") + case .text(let at, let height, _, let angleRad, let string, let layer, let color): + p(0, "TEXT") + p(8, "\(layer)") + p(62, aci(color)) + p(10, num(at.x)) + p(20, num(at.y)) + p(30, "0.0") p(40, num(height > 0 ? height : 2.5)) p(1, dxfText(string)) if abs(angleRad) > 1e-9 { p(50, num(angleRad * deg)) } - case let .insert(def, at, scaleX, scaleY, rotationRad, layer, color): - p(0, "INSERT"); p(8, "\(layer)"); p(62, aci(color)); p(2, "BLK\(def)") - p(10, num(at.x)); p(20, num(at.y)); p(30, "0.0") - p(41, num(scaleX == 0 ? 1 : scaleX)); p(42, num(scaleY == 0 ? 1 : scaleY)); p(43, "1.0") + case .insert(let def, let at, let scaleX, let scaleY, let rotationRad, let layer, let color): + p(0, "INSERT") + p(8, "\(layer)") + p(62, aci(color)) + p(2, "BLK\(def)") + p(10, num(at.x)) + p(20, num(at.y)) + p(30, "0.0") + p(41, num(scaleX == 0 ? 1 : scaleX)) + p(42, num(scaleY == 0 ? 1 : scaleY)) + p(43, "1.0") if abs(rotationRad) > 1e-9 { p(50, num(rotationRad * deg)) } - case let .dimension(parts, _): - for part in parts { emit(part, into: &s) } // decomposed: dimension line, text, witness lines, arrows + case .dimension(let parts, _): + // Decomposed: dimension line, text, witness lines, arrows. + for part in parts { emit(part, into: &s) } } } - /// JWW pen colour → AutoCAD Color Index. JWW colours are small integers; pass through, clamped to a - /// valid ACI (1…255), defaulting odd values to 7 (white/black). + /// JWW pen colour to AutoCAD Color Index. + /// + /// JWW colours are small integers; pass through, clamped to a valid ACI (1...255), defaulting + /// odd values to 7 (white/black). private static func aci(_ c: Int) -> String { (1...255).contains(c) ? "\(c)" : "7" } - /// Prepare a (Unicode) string for a DXF group-1 value: strip newlines, and escape every non-ASCII - /// character as the DXF `\U+XXXX` unicode escape. This renders correctly in AutoCAD / LibreCAD - /// regardless of the reader's assumed code page (an entities-only DXF carries no `$DWGCODEPAGE`). + /// Prepare a (Unicode) string for a DXF group-1 value. + /// + /// Strips newlines, and escapes every non-ASCII character as the DXF `\U+XXXX` unicode + /// escape. This renders correctly in AutoCAD / LibreCAD regardless of the reader's assumed + /// code page (an entities-only DXF carries no `$DWGCODEPAGE`). static func dxfText(_ s: String) -> String { var out = "" for u in s.unicodeScalars { - if u == "\n" || u == "\r" { out += " " } - else if u.value < 0x80 { out.unicodeScalars.append(u) } - else { out += String(format: "\\U+%04X", u.value) } + if u == "\n" || u == "\r" { + out += " " + } else if u.value < 0x80 { + out.unicodeScalars.append(u) + } else { + out += String(format: "\\U+%04X", u.value) + } } return out } diff --git a/Sources/SwiftJWW/JWW.swift b/Sources/SwiftJWW/JWW.swift index 5f0c9b2..989c5f9 100644 --- a/Sources/SwiftJWW/JWW.swift +++ b/Sources/SwiftJWW/JWW.swift @@ -1,13 +1,14 @@ import Foundation -/// A native-Swift reader for **JWW** — the native drawing format of **Jw_cad**, the free 2D CAD program -/// widely used in Japan. JWW is an MFC `CArchive`-serialized binary file: an 8-byte `JwwData.` magic, a -/// version, a large fixed (version-gated) document header, then an MFC object array of drawing entities. +/// A native-Swift reader for **JWW**, the native drawing format of **Jw_cad**, the free 2D CAD +/// program widely used in Japan. /// -/// `SwiftJWW` reads the geometry — lines, arcs/circles/ellipses, points, and text — into a neutral +/// JWW is an MFC `CArchive`-serialized binary file: an 8-byte `JwwData.` magic, a version, a large +/// fixed (version-gated) document header, then an MFC object array of drawing entities. +/// `SwiftJWW` reads the geometry (lines, arcs/circles/ellipses, points, and text) into a neutral /// ``Drawing``. It is a clean-room port of the documented JWW byte layout (LibreCAD's `jwwlib` -/// reverse-engineering + the published `jwdatafmt` spec). Block inserts and dimensions are recognised -/// but not yet expanded (see ``Entity``). +/// reverse-engineering, plus the published `jwdatafmt` spec). Block inserts and dimensions are +/// recognised but not yet expanded (see ``Entity``). /// /// ```swift /// let dwg = try JWW.read(contentsOf: url) @@ -17,26 +18,41 @@ public enum JWW { // MARK: Model - public struct Point: Equatable, Sendable { public var x: Double; public var y: Double } + public struct Point: Equatable, Sendable { + public var x: Double + public var y: Double + } - /// A drawing entity. Coordinates are in the drawing's own units (mm in real-world scale). + /// A drawing entity. + /// + /// Coordinates are in the drawing's own units (mm in real-world scale). public indirect enum Entity: Sendable { case line(a: Point, b: Point, layer: Int, color: Int) /// `start`/`sweep` in radians (CCW). `tilt` rotates the axis; `ratio` is the minor/major axis /// ratio (1 = circle). `full` marks a closed circle/ellipse. - case arc(center: Point, radius: Double, start: Double, sweep: Double, tilt: Double, ratio: Double, full: Bool, layer: Int, color: Int) + case arc( + center: Point, radius: Double, start: Double, sweep: Double, tilt: Double, + ratio: Double, full: Bool, layer: Int, color: Int) case point(at: Point, layer: Int, color: Int) /// `string` is decoded to Unicode at read time from the file's CP932 (Shift-JIS) bytes. - case text(at: Point, height: Double, width: Double, angleRad: Double, string: String, layer: Int, color: Int) - /// A block insertion: places block definition `def` (by number — see ``Drawing/blocks``) at + case text( + at: Point, height: Double, width: Double, angleRad: Double, string: String, layer: Int, + color: Int) + /// A block insertion: places block definition `def` (by number, see ``Drawing/blocks``) at /// `at`, scaled and rotated. - case insert(def: Int, at: Point, scaleX: Double, scaleY: Double, rotationRad: Double, layer: Int, color: Int) + case insert( + def: Int, at: Point, scaleX: Double, scaleY: Double, rotationRad: Double, layer: Int, + color: Int) /// A dimension, decomposed into its drawn parts (dimension line, value text, witness lines). case dimension(parts: [Entity], layer: Int) } /// A block definition: a named group of entities, referenced by ``Entity/insert(def:...)``. - public struct BlockDef: Sendable { public var number: Int; public var name: String; public var entities: [Entity] } + public struct BlockDef: Sendable { + public var number: Int + public var name: String + public var entities: [Entity] + } public struct Drawing: Sendable { public var version: Int @@ -53,15 +69,25 @@ public enum JWW { var lo = Point(x: .greatestFiniteMagnitude, y: .greatestFiniteMagnitude) var hi = Point(x: -.greatestFiniteMagnitude, y: -.greatestFiniteMagnitude) var any = false - func acc(_ p: Point) { any = true; lo.x = min(lo.x, p.x); lo.y = min(lo.y, p.y); hi.x = max(hi.x, p.x); hi.y = max(hi.y, p.y) } + func acc(_ p: Point) { + any = true + lo.x = min(lo.x, p.x) + lo.y = min(lo.y, p.y) + hi.x = max(hi.x, p.x) + hi.y = max(hi.y, p.y) + } func visit(_ e: Entity) { switch e { - case let .line(a, b, _, _): acc(a); acc(b) - case let .arc(c, r, _, _, _, _, _, _, _): acc(Point(x: c.x - r, y: c.y - r)); acc(Point(x: c.x + r, y: c.y + r)) - case let .point(p, _, _): acc(p) - case let .text(p, _, _, _, _, _, _): acc(p) - case let .insert(_, p, _, _, _, _, _): acc(p) - case let .dimension(parts, _): parts.forEach(visit) + case .line(let a, let b, _, _): + acc(a) + acc(b) + case .arc(let c, let r, _, _, _, _, _, _, _): + acc(Point(x: c.x - r, y: c.y - r)) + acc(Point(x: c.x + r, y: c.y + r)) + case .point(let p, _, _): acc(p) + case .text(let p, _, _, _, _, _, _): acc(p) + case .insert(_, let p, _, _, _, _, _): acc(p) + case .dimension(let parts, _): parts.forEach(visit) } } entities.forEach(visit) @@ -89,14 +115,18 @@ public enum JWW { return try r.parse() } - /// Decode JWW/CP932 (Shift-JIS) text bytes to a Swift String. Uses the system CoreFoundation - /// Windows-31J table on Apple platforms; falls back to `.shiftJIS` then a lossy UTF-8 decode. - /// (CP932 via CoreFoundation is reliable on Apple platforms; on Linux it may be unavailable.) + /// Decode JWW/CP932 (Shift-JIS) text bytes to a Swift String. + /// + /// Uses the system CoreFoundation Windows-31J table on Apple platforms; falls back to + /// `.shiftJIS` then a lossy UTF-8 decode. CP932 via CoreFoundation is reliable on Apple + /// platforms; on Linux it may be unavailable. public static func decodeCP932(_ bytes: [UInt8]) -> String { let data = Data(bytes) #if canImport(CoreFoundation) - let cp932 = String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.dosJapanese.rawValue))) - if let s = String(data: data, encoding: cp932) { return s } + let cp932 = String.Encoding( + rawValue: CFStringConvertEncodingToNSStringEncoding( + CFStringEncoding(CFStringEncodings.dosJapanese.rawValue))) + if let s = String(data: data, encoding: cp932) { return s } #endif return String(data: data, encoding: .shiftJIS) ?? String(decoding: bytes, as: UTF8.self) } diff --git a/Sources/SwiftJWW/Reader.swift b/Sources/SwiftJWW/Reader.swift index 308b4cd..9cc8efe 100644 --- a/Sources/SwiftJWW/Reader.swift +++ b/Sources/SwiftJWW/Reader.swift @@ -12,28 +12,46 @@ extension JWW { // MARK: primitives (LE) - mutating func u8() throws -> Int { try ensure(1); defer { p += 1 }; return Int(b[p]) } - mutating func u16() throws -> Int { try ensure(2); defer { p += 2 }; return Int(b[p]) | (Int(b[p + 1]) << 8) } + mutating func u8() throws -> Int { + try ensure(1) + defer { p += 1 } + return Int(b[p]) + } + mutating func u16() throws -> Int { + try ensure(2) + defer { p += 2 } + return Int(b[p]) | (Int(b[p + 1]) << 8) + } mutating func u32() throws -> Int { - try ensure(4); defer { p += 4 } + try ensure(4) + defer { p += 4 } return Int(b[p]) | (Int(b[p + 1]) << 8) | (Int(b[p + 2]) << 16) | (Int(b[p + 3]) << 24) } mutating func f64() throws -> Double { - try ensure(8); defer { p += 8 } + try ensure(8) + defer { p += 8 } var bits: UInt64 = 0 for k in 0..<8 { bits |= UInt64(b[p + k]) << (8 * k) } return Double(bitPattern: bits) } - mutating func skip(_ n: Int) throws { try ensure(n); p += n } - func ensure(_ n: Int) throws { guard n >= 0, p + n <= b.count else { throw JWW.Error.truncated } } + mutating func skip(_ n: Int) throws { + try ensure(n) + p += n + } + func ensure(_ n: Int) throws { + guard n >= 0, p + n <= b.count else { throw JWW.Error.truncated } + } var eof: Bool { p >= b.count } - /// A JWW length-prefixed byte string: `u8 len`, or if len==0xFF then `u16 len`; then `len` bytes. + /// A JWW length-prefixed byte string: `u8 len`, or if len==0xFF then `u16 len`, then `len` + /// bytes. + /// /// Returns the raw bytes (consumes the full declared length regardless). mutating func jwString() throws -> [UInt8] { let n0 = try u8() let n = n0 == 0xFF ? try u16() : n0 - try ensure(n); defer { p += n } + try ensure(n) + defer { p += n } return Array(b[p..

= 300 else { throw JWW.Error.unsupportedVersion(version) } + guard version == 230 || version >= 300 else { + throw JWW.Error.unsupportedVersion(version) + } - try skipString() // memo - try skip(4) // zumen - try skip(4) // writeGLay - for _ in 0..<16 { // 16 layer-groups - try skip(4); try skip(4); try skip(8); try skip(4) // glay, writeLay, scale(d), glayProtect - for _ in 0..<16 { try skip(4); try skip(4) } // 16 layers: lay, layProtect + try skipString() // memo + try skip(4) // zumen + try skip(4) // writeGLay + for _ in 0..<16 { // 16 layer-groups + try skip(4) + try skip(4) + try skip(8) + try skip(4) // glay, writeLay, scale(d), glayProtect + for _ in 0..<16 { + try skip(4) + try skip(4) + } // 16 layers: lay, layProtect } - try skip(14 * 4) // Dummy[14] - try skip(5 * 4) // Sunpou1..5 - try skip(4) // Dummy1 - try skip(4) // maxDrawWid - try skip(8 * 2) // PrtGenten x,y - try skip(8) // prtBairitsu - try skip(4) // prt90Kaiten - try skip(4) // memoriMode - try skip(8) // memoriHyoujiMin - try skip(8 * 2) // memoriX, memoriY - try skip(8 * 2) // memoriKijunTen x,y - for _ in 0..<(16 * 16) { try skipString() } // layer names - for _ in 0..<16 { try skipString() } // group names - try skip(8); try skip(8); try skip(4); try skip(8) // kageLevel, kageIdo, kage9_15Flg, kabeKageLevel - if version >= 300 { try skip(8); try skip(8) } // tenkuuZuLevel, tenkuuZuEnkoR - try skip(4) // mmTani3D - try skip(8); try skip(8 * 2) // bairitsu, genten x,y - try skip(8); try skip(8 * 2) // hanniBairitsu, hanniGenten x,y + try skip(14 * 4) // Dummy[14] + try skip(5 * 4) // Sunpou1..5 + try skip(4) // Dummy1 + try skip(4) // maxDrawWid + try skip(8 * 2) // PrtGenten x,y + try skip(8) // prtBairitsu + try skip(4) // prt90Kaiten + try skip(4) // memoriMode + try skip(8) // memoriHyoujiMin + try skip(8 * 2) // memoriX, memoriY + try skip(8 * 2) // memoriKijunTen x,y + for _ in 0..<(16 * 16) { try skipString() } // layer names + for _ in 0..<16 { try skipString() } // group names + try skip(8) + try skip(8) + try skip(4) + try skip(8) // kageLevel, kageIdo, kage9_15Flg, kabeKageLevel + if version >= 300 { + try skip(8) + try skip(8) + } // tenkuuZuLevel, tenkuuZuEnkoR + try skip(4) // mmTani3D + try skip(8) + try skip(8 * 2) // bairitsu, genten x,y + try skip(8) + try skip(8 * 2) // hanniBairitsu, hanniGenten x,y if version >= 300 { - for _ in 0..<8 { try skip(8); try skip(8); try skip(8); try skip(4) } // zoom 1..8: 3d + dword + for _ in 0..<8 { + try skip(8) + try skip(8) + try skip(8) + try skip(4) + } // zoom 1..8: 3d + dword } else { - for _ in 0..<4 { try skip(8); try skip(8); try skip(8) } // zoom 1..4: 3d + for _ in 0..<4 { + try skip(8) + try skip(8) + try skip(8) + } // zoom 1..4: 3d } if version >= 300 { - try skip(8 * 3); try skip(4); try skip(8 * 2); try skip(8); try skip(4) // dDm11-13, lnDm1, dDm21-22, mojiBG, nMojiBG + try skip(8 * 3) + try skip(4) + try skip(8 * 2) + try skip(8) + try skip(4) // dDm11-13, lnDm1, dDm21-22, mojiBG, nMojiBG } - for _ in 0..<10 { try skip(8) } // fukusen[0..9] - try skip(8) // ryoygawaFukusenTomeDe - for _ in 0..<10 { try skip(4); try skip(4) } // Pen[0..9] color, width - for _ in 0..<10 { try skip(4); try skip(4); try skip(8) } // PrtPen[0..9] color, width, tenHankei - for _ in 0..<8 { try skip(4 * 4) } // LType1 (i=2..9) - for _ in 0..<5 { try skip(5 * 4) } // LType2 (i=11..15) - for _ in 0..<4 { try skip(4 * 4) } // LType3 (i=16..19) - try skip(4 * 11) // 11 draw/print flags - try skip(4); try skip(4) // lnDrawTime, nEyeInit - try skip(4 * 3) // eye_H_Ichi 1,2,3 (DWORD) - try skip(8 * 5) // eye Z1,Y1,Z2,Y2,V3 (DOUBLE) - try skip(8 * 4) // senNagasa, boxX, boxY, enHankey - try skip(4); try skip(4) // solidNinniColor, solidColor + for _ in 0..<10 { try skip(8) } // fukusen[0..9] + try skip(8) // ryoygawaFukusenTomeDe + for _ in 0..<10 { + try skip(4) + try skip(4) + } // Pen[0..9] color, width + for _ in 0..<10 { + try skip(4) + try skip(4) + try skip(8) + } // PrtPen[0..9] color, width, tenHankei + for _ in 0..<8 { try skip(4 * 4) } // LType1 (i=2..9) + for _ in 0..<5 { try skip(5 * 4) } // LType2 (i=11..15) + for _ in 0..<4 { try skip(4 * 4) } // LType3 (i=16..19) + try skip(4 * 11) // 11 draw/print flags + try skip(4) + try skip(4) // lnDrawTime, nEyeInit + try skip(4 * 3) // eye_H_Ichi 1,2,3 (DWORD) + try skip(8 * 5) // eye Z1,Y1,Z2,Y2,V3 (DOUBLE) + try skip(8 * 4) // senNagasa, boxX, boxY, enHankey + try skip(4) + try skip(4) // solidNinniColor, solidColor if version >= 420 { - for _ in 0..<257 { try skip(4); try skip(4) } // SXF color display - for _ in 0..<257 { try skipString(); try skip(4); try skip(4); try skip(8) } // SXF color print - for _ in 0..<33 { try skip(4 * 4) } // SXF ltype pattern - for _ in 0..<33 { try skipString(); try skip(4); for _ in 0..<10 { try skip(8) } } // SXF ltype param + for _ in 0..<257 { + try skip(4) + try skip(4) + } // SXF color display + for _ in 0..<257 { + try skipString() + try skip(4) + try skip(4) + try skip(8) + } // SXF color print + for _ in 0..<33 { try skip(4 * 4) } // SXF ltype pattern + for _ in 0..<33 { + try skipString() + try skip(4) + for _ in 0..<10 { try skip(8) } + } // SXF ltype param } - for _ in 0..<10 { try skip(8 * 3); try skip(4) } // Moji[1..10] x,y,d, col - try skip(8 * 3) // mojiSizeX,Y,Kankaku - try skip(4); try skip(4) // mojiColor, mojiShu - try skip(8); try skip(8); try skip(4) // seiriGyouKan, seiriSuu, kijunZureOn - try skip(8 * 3); try skip(8 * 3) // kijunZureX[3], kijunZureY[3] + for _ in 0..<10 { + try skip(8 * 3) + try skip(4) + } // Moji[1..10] x,y,d, col + try skip(8 * 3) // mojiSizeX,Y,Kankaku + try skip(4) + try skip(4) // mojiColor, mojiShu + try skip(8) + try skip(8) + try skip(4) // seiriGyouKan, seiriSuu, kijunZureOn + try skip(8 * 3) + try skip(8 * 3) // kijunZureX[3], kijunZureY[3] } // MARK: entity base (CData) struct Base { var penStyle = 0, penColor = 0, layer = 0 } mutating func readBase() throws -> Base { - _ = try u32() // group + _ = try u32() // group let penStyle = try u8() let penColor = try u16() - if version >= 351 { _ = try u16() } // penWidth + if version >= 351 { _ = try u16() } // penWidth let layer = try u16() - _ = try u16() // glayer - _ = try u16() // flags + _ = try u16() // glayer + _ = try u16() // flags return Base(penStyle: penStyle, penColor: penColor, layer: layer) } // MARK: parse - enum Parsed { case entity(JWW.Entity), blockDef(JWW.BlockDef), solid, skip } + enum Parsed { + case entity(JWW.Entity) + case blockDef(JWW.BlockDef) + case solid, skip + } - /// MFC `ReadCount` — a WORD, or (if it equals 0xFFFF) a following DWORD. Used for the object-array - /// length and each block definition's member count. - mutating func readCount() throws -> Int { let w = try u16(); return w == 0xFFFF ? try u32() : w } + /// MFC `ReadCount`: a WORD, or (if it equals 0xFFFF) a following DWORD. + /// + /// Used for the object-array length and each block definition's member count. + mutating func readCount() throws -> Int { + let w = try u16() + return w == 0xFFFF ? try u32() : w + } mutating func parse() throws -> JWW.Drawing { try readHeader() @@ -139,10 +222,12 @@ extension JWW { var classMap: [Int: String] = [:] var mapIndex = 1 - _ = try readCount() // main object-array length (drive by EOF below) + _ = try readCount() // main object-array length (drive by EOF below) while !eof { let parsed: Parsed - do { parsed = try readObject(&classMap, &mapIndex) } catch JWW.Error.truncated { break } + do { parsed = try readObject(&classMap, &mapIndex) } catch JWW.Error.truncated { + break + } switch parsed { case .entity(let e): entities.append(e) @@ -165,52 +250,69 @@ extension JWW { /// Read one MFC object: resolve its class tag, then deserialize the entity. `CDataList` (a block /// definition) recursively reads its member objects; `CDataBlock` is a block insert; `CDataSunpou` /// is decomposed into its drawn parts. - mutating func readObject(_ classMap: inout [Int: String], _ mapIndex: inout Int) throws -> Parsed { + mutating func readObject(_ classMap: inout [Int: String], _ mapIndex: inout Int) throws + -> Parsed + { let tag = try u16() var j = 0 switch tag { - case 0x0000: return .skip // null object — no index consumed + case 0x0000: return .skip // null object, no index consumed case 0xFFFF: - _ = try u16() // schema - let len = try u16() // class-name length (WORD) - try ensure(len); let name = Array(b[p..

JWW.Entity { let base = try readBase() let c = JWW.Point(x: try f64(), y: try f64()) - let r = try f64(), sa = try f64(), aa = try f64(), tilt = try f64(), ratio = try f64() + let r = try f64() + let sa = try f64() + let aa = try f64() + let tilt = try f64() + let ratio = try f64() let full = try u32() != 0 - return .arc(center: c, radius: r, start: sa, sweep: aa, tilt: tilt, ratio: ratio, full: full, layer: base.layer, color: base.penColor) + return .arc( + center: c, radius: r, start: sa, sweep: aa, tilt: tilt, ratio: ratio, full: full, + layer: base.layer, color: base.penColor) } mutating func readTen() throws -> JWW.Entity { let base = try readBase() let pt = JWW.Point(x: try f64(), y: try f64()) - _ = try u32() // kariten - if base.penStyle == 100 { _ = try u32(); _ = try f64(); _ = try f64() } + _ = try u32() // kariten + if base.penStyle == 100 { + _ = try u32() + _ = try f64() + _ = try f64() + } return .point(at: pt, layer: base.layer, color: base.penColor) } mutating func readMoji() throws -> JWW.Entity { let base = try readBase() let at = JWW.Point(x: try f64(), y: try f64()) - _ = try f64(); _ = try f64() // end x,y - _ = try u32() // mojiShu - let sx = try f64(), sy = try f64(); _ = try f64() + _ = try f64() + _ = try f64() // end x,y + _ = try u32() // mojiShu + let sx = try f64() + let sy = try f64() + _ = try f64() let ang = try f64() - _ = try jwString() // font name - let raw = try jwString() // text (CP932) — decode to Unicode now - return .text(at: at, height: sy, width: sx, angleRad: ang, string: JWW.decodeCP932(raw), layer: base.layer, color: base.penColor) + _ = try jwString() // font name + let raw = try jwString() // text (CP932): decode to Unicode now + return .text( + at: at, height: sy, width: sx, angleRad: ang, string: JWW.decodeCP932(raw), + layer: base.layer, color: base.penColor) } - /// Dimension = base + dimension line + value text, plus (v4.20+) a mode word, two witness lines, - /// and four arrow/reference points. Decomposed into its drawn parts. + /// Dimension = base + dimension line + value text, plus (v4.20+) a mode word, two witness + /// lines, and four arrow/reference points. + /// + /// Decomposed into its drawn parts. mutating func readSunpou() throws -> JWW.Entity { let base = try readBase() var parts: [JWW.Entity] = [] - parts.append(try readSen()) // dimension line - parts.append(try readMoji()) // value text + parts.append(try readSen()) // dimension line + parts.append(try readMoji()) // value text if version >= 420 { - _ = try u16() // SXF mode - parts.append(try readSen()); parts.append(try readSen()) // witness lines + _ = try u16() // SXF mode + parts.append(try readSen()) + parts.append(try readSen()) // witness lines for _ in 0..<4 { parts.append(try readTen()) } // arrows / reference points } return .dimension(parts: parts, layer: base.layer) diff --git a/Sources/jww2dxf/main.swift b/Sources/jww2dxf/main.swift index 34516ba..6bb21e4 100644 --- a/Sources/jww2dxf/main.swift +++ b/Sources/jww2dxf/main.swift @@ -7,7 +7,9 @@ guard args.count >= 2 else { exit(2) } let inURL = URL(fileURLWithPath: args[1]) -let outURL = args.count >= 3 ? URL(fileURLWithPath: args[2]) +let outURL = + args.count >= 3 + ? URL(fileURLWithPath: args[2]) : inURL.deletingPathExtension().appendingPathExtension("dxf") do { @@ -16,7 +18,9 @@ do { let c = dwg.counts var line = "\(inURL.lastPathComponent) → \(outURL.lastPathComponent) (v\(dwg.version); " line += "\(c.line) line, \(c.arc) arc, \(c.point) point, \(c.text) text" - if c.solid + c.block + c.dim > 0 { line += "; skipped \(c.solid) solid/\(c.block) block/\(c.dim) dim" } + if c.solid + c.block + c.dim > 0 { + line += "; skipped \(c.solid) solid/\(c.block) block/\(c.dim) dim" + } line += ")" if let b = dwg.bounds { line += String(format: " bbox %.2f×%.2f", b.max.x - b.min.x, b.max.y - b.min.y) diff --git a/Tests/SwiftJWWTests/SwiftJWWTests.swift b/Tests/SwiftJWWTests/SwiftJWWTests.swift index 2cfd0a2..589f820 100644 --- a/Tests/SwiftJWWTests/SwiftJWWTests.swift +++ b/Tests/SwiftJWWTests/SwiftJWWTests.swift @@ -1,5 +1,6 @@ -import Testing import Foundation +import Testing + @testable import SwiftJWW @Suite("JWW reading + DXF") @@ -10,9 +11,14 @@ struct SwiftJWWTests { struct Builder { var d = Data() mutating func u8(_ v: Int) { d.append(UInt8(v & 0xFF)) } - mutating func u16(_ v: Int) { d.append(UInt8(v & 0xFF)); d.append(UInt8((v >> 8) & 0xFF)) } + mutating func u16(_ v: Int) { + d.append(UInt8(v & 0xFF)) + d.append(UInt8((v >> 8) & 0xFF)) + } mutating func u32(_ v: Int) { for k in 0..<4 { d.append(UInt8((v >> (8 * k)) & 0xFF)) } } - mutating func f64(_ v: Double) { withUnsafeBytes(of: v.bitPattern.littleEndian) { d.append(contentsOf: $0) } } + mutating func f64(_ v: Double) { + withUnsafeBytes(of: v.bitPattern.littleEndian) { d.append(contentsOf: $0) } + } mutating func emptyStr() { u8(0) } mutating func zeros(_ n: Int) { d.append(Data(count: n)) } @@ -20,52 +26,162 @@ struct SwiftJWWTests { mutating func header() { d.append(contentsOf: Array("JwwData.".utf8)) u32(700) - emptyStr() // memo - u32(0); u32(0) // zumen, writeGLay - for _ in 0..<16 { zeros(4 + 4 + 8 + 4); for _ in 0..<16 { zeros(8) } } // layer-groups - zeros(14 * 4); zeros(5 * 4); zeros(4); zeros(4) // dummy, sunpou, dummy1, maxDrawWid - zeros(8 * 2); zeros(8); zeros(4); zeros(4); zeros(8); zeros(8 * 2); zeros(8 * 2) - for _ in 0..<(16 * 16) { emptyStr() }; for _ in 0..<16 { emptyStr() } // names - zeros(8); zeros(8); zeros(4); zeros(8) // kage... - zeros(8); zeros(8) // tenkuu (v>=300) - zeros(4); zeros(8); zeros(8 * 2); zeros(8); zeros(8 * 2) - for _ in 0..<8 { zeros(8 + 8 + 8 + 4) } // zoom (v>=300) - zeros(8 * 3); zeros(4); zeros(8 * 2); zeros(8); zeros(4) // dDm (v>=300) - zeros(8 * 10); zeros(8) // fukusen, ryo - for _ in 0..<10 { zeros(8) }; for _ in 0..<10 { zeros(16) } // Pen, PrtPen - for _ in 0..<8 { zeros(16) }; for _ in 0..<5 { zeros(20) }; for _ in 0..<4 { zeros(16) } // LType - zeros(4 * 11); zeros(8); zeros(4 * 3); zeros(8 * 5); zeros(8 * 4); zeros(8) // flags..solid - for _ in 0..<257 { zeros(8) } // SXF color display (v>=420) - for _ in 0..<257 { emptyStr(); zeros(16) } // SXF color print - for _ in 0..<33 { zeros(16) } // SXF ltype pattern - for _ in 0..<33 { emptyStr(); zeros(4 + 80) } // SXF ltype param - for _ in 0..<10 { zeros(8 * 3 + 4) } // Moji[1..10] - zeros(8 * 3); zeros(8); zeros(8); zeros(8); zeros(4); zeros(8 * 6) // moji write settings + emptyStr() // memo + u32(0) + u32(0) // zumen, writeGLay + for _ in 0..<16 { + zeros(4 + 4 + 8 + 4) + for _ in 0..<16 { zeros(8) } + } // layer-groups + zeros(14 * 4) + zeros(5 * 4) + zeros(4) + zeros(4) // dummy, sunpou, dummy1, maxDrawWid + zeros(8 * 2) + zeros(8) + zeros(4) + zeros(4) + zeros(8) + zeros(8 * 2) + zeros(8 * 2) + for _ in 0..<(16 * 16) { emptyStr() } + for _ in 0..<16 { emptyStr() } // names + zeros(8) + zeros(8) + zeros(4) + zeros(8) // kage... + zeros(8) + zeros(8) // tenkuu (v>=300) + zeros(4) + zeros(8) + zeros(8 * 2) + zeros(8) + zeros(8 * 2) + for _ in 0..<8 { zeros(8 + 8 + 8 + 4) } // zoom (v>=300) + zeros(8 * 3) + zeros(4) + zeros(8 * 2) + zeros(8) + zeros(4) // dDm (v>=300) + zeros(8 * 10) + zeros(8) // fukusen, ryo + for _ in 0..<10 { zeros(8) } + for _ in 0..<10 { zeros(16) } // Pen, PrtPen + for _ in 0..<8 { zeros(16) } + for _ in 0..<5 { zeros(20) } + for _ in 0..<4 { zeros(16) } // LType + zeros(4 * 11) + zeros(8) + zeros(4 * 3) + zeros(8 * 5) + zeros(8 * 4) + zeros(8) // flags..solid + for _ in 0..<257 { zeros(8) } // SXF color display (v>=420) + for _ in 0..<257 { + emptyStr() + zeros(16) + } // SXF color print + for _ in 0..<33 { zeros(16) } // SXF ltype pattern + for _ in 0..<33 { + emptyStr() + zeros(4 + 80) + } // SXF ltype param + for _ in 0..<10 { zeros(8 * 3 + 4) } // Moji[1..10] + zeros(8 * 3) + zeros(8) + zeros(8) + zeros(8) + zeros(4) + zeros(8 * 6) // moji write settings } mutating func base(penStyle: Int = 1, color: Int = 2, layer: Int = 0) { - u32(0); u8(penStyle); u16(color); u16(0); u16(layer); u16(0); u16(0) // v>=351 → penWidth present + u32(0) + u8(penStyle) + u16(color) + u16(0) + u16(layer) + u16(0) + u16(0) // v>=351 → penWidth present + } + mutating func str(_ s: String) { + let a = Array(s.utf8) + u8(a.count) + d.append(contentsOf: a) + } + mutating func classTag(_ name: String) { + u16(0xFFFF) + u16(0x2bc) + u16(name.utf8.count) + d.append(contentsOf: Array(name.utf8)) + } + // inline entity field groups (no object tag; used inside CDataSunpou) + mutating func senFields( + _ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, layer: Int = 0 + ) { + base(layer: layer) + f64(x1) + f64(y1) + f64(x2) + f64(y2) + } + mutating func mojiFields(_ text: String, layer: Int = 0) { + base(layer: layer) + f64(0) + f64(0) + f64(0) + f64(0) + u32(0) + f64(2) + f64(2) + f64(0) + f64(0) + emptyStr() + str(text) + } + mutating func tenFields(_ x: Double, _ y: Double, layer: Int = 0) { + base(layer: layer) + f64(x) + f64(y) + u32(0) } - mutating func str(_ s: String) { let a = Array(s.utf8); u8(a.count); d.append(contentsOf: a) } - mutating func classTag(_ name: String) { u16(0xFFFF); u16(0x2bc); u16(name.utf8.count); d.append(contentsOf: Array(name.utf8)) } - // inline entity field groups (no object tag — used inside CDataSunpou) - mutating func senFields(_ x1: Double, _ y1: Double, _ x2: Double, _ y2: Double, layer: Int = 0) { base(layer: layer); f64(x1); f64(y1); f64(x2); f64(y2) } - mutating func mojiFields(_ text: String, layer: Int = 0) { base(layer: layer); f64(0); f64(0); f64(0); f64(0); u32(0); f64(2); f64(2); f64(0); f64(0); emptyStr(); str(text) } - mutating func tenFields(_ x: Double, _ y: Double, layer: Int = 0) { base(layer: layer); f64(x); f64(y); u32(0) } } /// Build a JWW with 2 lines (one class def + one back-ref) and 1 arc. static func sample() -> Data { var b = Builder() b.header() - b.u16(3) // array preamble (non-0xFFFF) - // line 1 — new class CDataSen - b.u16(0xFFFF); b.u16(0x2bc); b.u16(8); b.d.append(contentsOf: Array("CDataSen".utf8)) - b.base(layer: 5); b.f64(0); b.f64(0); b.f64(10); b.f64(0) - // line 2 — back-ref (CDataSen class is map index 1) - b.u16(0x8001); b.base(layer: 5); b.f64(10); b.f64(0); b.f64(10); b.f64(5) - // arc — new class CDataEnko - b.u16(0xFFFF); b.u16(0x2bc); b.u16(9); b.d.append(contentsOf: Array("CDataEnko".utf8)) - b.base(layer: 7); b.f64(3); b.f64(4); b.f64(2); b.f64(0); b.f64(.pi); b.f64(0); b.f64(1); b.u32(0) + b.u16(3) // array preamble (non-0xFFFF) + // line 1: new class CDataSen + b.u16(0xFFFF) + b.u16(0x2bc) + b.u16(8) + b.d.append(contentsOf: Array("CDataSen".utf8)) + b.base(layer: 5) + b.f64(0) + b.f64(0) + b.f64(10) + b.f64(0) + // line 2: back-ref (CDataSen class is map index 1) + b.u16(0x8001) + b.base(layer: 5) + b.f64(10) + b.f64(0) + b.f64(10) + b.f64(5) + // arc: new class CDataEnko + b.u16(0xFFFF) + b.u16(0x2bc) + b.u16(9) + b.d.append(contentsOf: Array("CDataEnko".utf8)) + b.base(layer: 7) + b.f64(3) + b.f64(4) + b.f64(2) + b.f64(0) + b.f64(.pi) + b.f64(0) + b.f64(1) + b.u32(0) return b.d } @@ -75,40 +191,61 @@ struct SwiftJWWTests { #expect(dwg.version == 700) #expect(dwg.counts.line == 2) #expect(dwg.counts.arc == 1) - guard case let .line(a, bb, layer, _) = dwg.entities[0] else { Issue.record("not a line"); return } + guard case .line(let a, let bb, let layer, _) = dwg.entities[0] else { + Issue.record("not a line") + return + } #expect(a.x == 0 && bb.x == 10 && layer == 5) - guard case let .arc(c, r, _, sweep, _, ratio, _, _, _) = dwg.entities[2] else { Issue.record("not an arc"); return } + guard case .arc(let c, let r, _, let sweep, _, let ratio, _, _, _) = dwg.entities[2] else { + Issue.record("not an arc") + return + } #expect(c.x == 3 && c.y == 4 && r == 2 && ratio == 1 && abs(sweep - .pi) < 1e-9) let bounds = try #require(dwg.bounds) - #expect(bounds.max.x == 10) // line endpoints reach x=10 + #expect(bounds.max.x == 10) // line endpoints reach x=10 } @Test("DXF writer maps entities to LINE / CIRCLE / ARC / POINT / TEXT") func dxf() throws { - let dwg = JWW.Drawing(version: 700, entities: [ - .line(a: .init(x: 0, y: 0), b: .init(x: 1, y: 1), layer: 0, color: 2), - .arc(center: .init(x: 0, y: 0), radius: 5, start: 0, sweep: 2 * .pi, tilt: 0, ratio: 1, full: true, layer: 0, color: 1), - .arc(center: .init(x: 0, y: 0), radius: 5, start: 0, sweep: .pi, tilt: 0, ratio: 1, full: false, layer: 0, color: 1), - .point(at: .init(x: 2, y: 3), layer: 0, color: 1), - .text(at: .init(x: 0, y: 0), height: 2.5, width: 2.5, angleRad: 0, string: "AB", layer: 0, color: 7), - ], counts: .init()) + let dwg = JWW.Drawing( + version: 700, + entities: [ + .line(a: .init(x: 0, y: 0), b: .init(x: 1, y: 1), layer: 0, color: 2), + .arc( + center: .init(x: 0, y: 0), radius: 5, start: 0, sweep: 2 * .pi, tilt: 0, + ratio: 1, full: true, layer: 0, color: 1), + .arc( + center: .init(x: 0, y: 0), radius: 5, start: 0, sweep: .pi, tilt: 0, ratio: 1, + full: false, layer: 0, color: 1), + .point(at: .init(x: 2, y: 3), layer: 0, color: 1), + .text( + at: .init(x: 0, y: 0), height: 2.5, width: 2.5, angleRad: 0, string: "AB", + layer: 0, color: 7), + ], counts: .init()) let s = DXFWriter.string(dwg) - #expect(s.contains("\nLINE\n") && s.contains("\nCIRCLE\n") && s.contains("\nARC\n") && s.contains("\nPOINT\n") && s.contains("\nTEXT\n")) + #expect( + s.contains("\nLINE\n") && s.contains("\nCIRCLE\n") && s.contains("\nARC\n") + && s.contains("\nPOINT\n") && s.contains("\nTEXT\n")) #expect(s.hasSuffix("EOF\n")) - #expect(s.contains("\nAB\n")) // text payload + #expect(s.contains("\nAB\n")) // text payload } @Test("arc start/end include the tilt axis, normalized to [0,360)") func arcTilt() throws { // start=10°, sweep=40°, tilt=90° → DXF start=100°, end=140°. - let dwg = JWW.Drawing(version: 700, entities: [ - .arc(center: .init(x: 0, y: 0), radius: 1, start: 10 * .pi / 180, sweep: 40 * .pi / 180, - tilt: .pi / 2, ratio: 1, full: false, layer: 0, color: 1), - ], counts: .init()) + let dwg = JWW.Drawing( + version: 700, + entities: [ + .arc( + center: .init(x: 0, y: 0), radius: 1, start: 10 * .pi / 180, + sweep: 40 * .pi / 180, + tilt: .pi / 2, ratio: 1, full: false, layer: 0, color: 1) + ], counts: .init()) let s = DXFWriter.string(dwg) let lines = s.components(separatedBy: "\n") func after(_ code: String) -> Double? { - guard let i = lines.firstIndex(of: code) else { return nil }; return Double(lines[i + 1]) + guard let i = lines.firstIndex(of: code) else { return nil } + return Double(lines[i + 1]) } #expect(abs((after("50") ?? 0) - 100) < 1e-3) #expect(abs((after("51") ?? 0) - 140) < 1e-3) @@ -119,42 +256,76 @@ struct SwiftJWWTests { static func sampleWithBlocksAndDims() -> Data { var b = Builder() b.header() - b.u16(3) // main array preamble - b.classTag("CDataSen"); b.senFields(0, 0, 10, 0) // top-level line - b.classTag("CDataSunpou"); b.base(layer: 3) // dimension - b.senFields(0, 0, 10, 0); b.mojiFields("100") // dim line + value text - b.u16(0); b.senFields(0, 0, 0, 1); b.senFields(10, 0, 10, 1) // sxf mode + 2 witness lines - for _ in 0..<4 { b.tenFields(0, 0) } // 4 arrow/ref points - b.classTag("CDataBlock"); b.base(layer: 2) // block insert → def 7 - b.f64(5); b.f64(6); b.f64(1); b.f64(1); b.f64(0); b.u32(7) + b.u16(3) // main array preamble + b.classTag("CDataSen") + b.senFields(0, 0, 10, 0) // top-level line + b.classTag("CDataSunpou") + b.base(layer: 3) // dimension + b.senFields(0, 0, 10, 0) + b.mojiFields("100") // dim line + value text + b.u16(0) + b.senFields(0, 0, 0, 1) + b.senFields(10, 0, 10, 1) // sxf mode + 2 witness lines + for _ in 0..<4 { b.tenFields(0, 0) } // 4 arrow/ref points + b.classTag("CDataBlock") + b.base(layer: 2) // block insert → def 7 + b.f64(5) + b.f64(6) + b.f64(1) + b.f64(1) + b.f64(0) + b.u32(7) // block-definition list: count (skipped as a null tag), then one CDataList b.u16(1) - b.classTag("CDataList"); b.base(); b.u32(7); b.u32(0); b.u32(0); b.str("widget") - b.u16(1) // 1 member - b.classTag("CDataSen"); b.senFields(0, 0, 5, 5) // member line + b.classTag("CDataList") + b.base() + b.u32(7) + b.u32(0) + b.u32(0) + b.str("widget") + b.u16(1) // 1 member + b.classTag("CDataSen") + b.senFields(0, 0, 5, 5) // member line return b.d } @Test("captures block definitions, inserts, and dimensions") func blocksAndDims() throws { let dwg = try JWW.read(data: Self.sampleWithBlocksAndDims()) - #expect(dwg.counts.line == 1) // only the top-level line (dim parts + block members excluded) + // only the top-level line (dim parts + block members excluded) + #expect(dwg.counts.line == 1) #expect(dwg.counts.dim == 1) - #expect(dwg.counts.block == 1) // the insert + #expect(dwg.counts.block == 1) // the insert // block definition 7 captured with its single member line let def = try #require(dwg.blocks[7]) #expect(def.name == "widget" && def.entities.count == 1) // the insert references def 7 - guard let ins = dwg.entities.first(where: { if case .insert = $0 { return true } else { return false } }), - case let .insert(num, at, _, _, _, _, _) = ins else { Issue.record("no insert"); return } + guard + let ins = dwg.entities.first(where: { + if case .insert = $0 { return true } else { return false } + }), + case .insert(let num, let at, _, _, _, _, _) = ins + else { + Issue.record("no insert") + return + } #expect(num == 7 && at.x == 5 && at.y == 6) // the dimension decomposes into parts (line + text + witnesses + arrows) - guard let dim = dwg.entities.first(where: { if case .dimension = $0 { return true } else { return false } }), - case let .dimension(parts, _) = dim else { Issue.record("no dimension"); return } + guard + let dim = dwg.entities.first(where: { + if case .dimension = $0 { return true } else { return false } + }), + case .dimension(let parts, _) = dim + else { + Issue.record("no dimension") + return + } #expect(parts.count >= 3) // DXF carries a BLOCKS section + INSERT let dxf = DXFWriter.string(dwg) - #expect(dxf.contains("\nBLOCKS\n") && dxf.contains("\nBLOCK\n") && dxf.contains("\nINSERT\n") && dxf.contains("\nBLK7\n")) + #expect( + dxf.contains("\nBLOCKS\n") && dxf.contains("\nBLOCK\n") && dxf.contains("\nINSERT\n") + && dxf.contains("\nBLK7\n")) } @Test("looksLikeJWW + error on bad magic and empty") diff --git a/okf/index.md b/okf/index.md index 842edee..bc9efc2 100644 --- a/okf/index.md +++ b/okf/index.md @@ -9,8 +9,8 @@ timestamp: 2026-06-25 # SwiftJWW -A native-Swift reader for **JWW** — the native drawing format of Jw_cad, the free 2D CAD program -widely used in Japan — plus a **`jww2dxf`** command-line converter. JWW is an MFC `CArchive`-serialized +A native-Swift reader for **JWW** (the native drawing format of Jw_cad, the free 2D CAD program +widely used in Japan) plus a **`jww2dxf`** command-line converter. JWW is an MFC `CArchive`-serialized binary file; SwiftJWW reads the geometry (lines, arcs/circles/ellipses, points, text) and converts it to DXF. Pure Swift, no third-party dependencies; a clean-room port validated byte-for-byte against LibreCAD's `jwwlib`. @@ -18,7 +18,7 @@ LibreCAD's `jwwlib`. ## Role in the ecosystem - **Cluster:** kernel -- **Depends on:** nothing (leaf — pure Swift) +- **Depends on:** nothing (leaf, pure Swift) - **Feeds products:** 2D-drawing import for the OCCTSwift CAD I/O stack (e.g. OCCTSwiftIO's JWW path) ## Components @@ -37,3 +37,4 @@ See [`references/`](references/index.md) for the JWW format spec and reference r - [Search before building](policies/search-before-building.md) - [Code structure](policies/code-structure.md) - [Issue labels and project-board tracking](policies/issue-tracking.md) +- [Code style](policies/code-style.md) diff --git a/okf/policies/code-style.md b/okf/policies/code-style.md new file mode 100644 index 0000000..5d6afbc --- /dev/null +++ b/okf/policies/code-style.md @@ -0,0 +1,52 @@ +--- +type: policy +title: Code style +description: Swift naming/API shape follows the Swift API Design Guidelines, formatting follows Google's Swift Style Guide via swift-format, and doc comments stay terse; docs/ is the single source of truth for design rationale, not a second copy of it. +tags: [policy, style, swift, agents] +timestamp: 2026-08-12 +--- + +# Code style + +**Naming and API shape** follow the +[Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) as-is: +clarity at the point of use over brevity, methods without side effects read as noun phrases, +methods with side effects as imperative verbs, boolean properties/methods read as assertions. + +**Formatting and file layout** follow +[Google's Swift Style Guide](https://google.github.io/swift/), enforced by `swift-format` +(configured in `.swift-format`: 100-column limit, 4-space indent, the ecosystem's one deliberate +divergence from Google's own 2-space default, chosen to avoid a repo-wide reformat diff with no +readability gain). `swift-format lint --strict` is a blocking CI check +(`.github/workflows/code-style.yml`): formatting has no judgment call in it. + +**SwiftLint is scoped to `orphaned_doc_comment` only** (`.swiftlint.yml`), not its full default +rule set. SwiftLint's defaults duplicate `swift-format`'s formatting opinions (and can disagree +with them on the same line) and add a large, separate surface of code-quality/complexity opinions +(`identifier_name`, `cyclomatic_complexity`, `function_body_length`, `nesting`, ...) that overlap +the ecosystem's own [code-structure](code-structure.md) policy rather than this one; a repo that +needs a structural pass runs one as its own scoped initiative, not as a side effect of a +style-lint gate. `orphaned_doc_comment` is the one rule left that catches something +`swift-format` has no equivalent for. + +**Doc comments stay terse.** A `///` comment is a single-sentence summary plus only the +`Parameter`/`Returns`/`Throws` tags that add something the summary doesn't already say. +Design rationale, extended examples, and cross-references to prior issues belong in `docs/`, not +duplicated in source: `docs/` is the single source of truth for *why* and *how*, per +[GitLab's documentation style guide](https://docs.gitlab.com/development/documentation/styleguide/) +("share the link to the documentation instead of rephrasing the information"). + +**The comment:code ratio check is a nudge, not a gate.** `Scripts/comment-ratio-check.sh` flags +(never fails) a file whose comment lines outnumber its code lines: a high ratio is sometimes +legitimate, so it's a signal for review, not an automatic failure. + +Why: this repo is the ecosystem's second pilot for the wider code-style policy (after +`OCCTSwiftScripts`), chosen for the same reason: small enough (~699 Swift lines across ~6 files) +to sweep into full compliance in one PR rather than needing a gradual adoption mechanism. No +first-party C++ exists here, so `clang-format` is out of scope for this repo entirely. Full +research and rationale: +[`ecosystem` docs/code-style-policy-proposal-2026-08.md](https://github.com/SecondMouseAU/ecosystem/blob/main/docs/code-style-policy-proposal-2026-08.md). +Filed and tracked as [SwiftJWW#9](https://github.com/SecondMouseAU/SwiftJWW/issues/9). + +Ecosystem standard: see +[OKF-STANDARD.md](https://github.com/SecondMouseAU/ecosystem/blob/main/OKF-STANDARD.md).